Skip to main content

deps_composer/
ecosystem.rs

1//! Composer ecosystem implementation for deps-lsp.
2//!
3//! This module implements the `Ecosystem` trait for PHP/Composer projects,
4//! providing LSP functionality for `composer.json` files.
5
6use std::any::Any;
7use std::sync::Arc;
8use tower_lsp_server::ls_types::{CompletionItem, Position, Uri};
9
10use deps_core::{
11    Ecosystem, ParseResult as ParseResultTrait, Registry, Result, lsp_helpers::EcosystemFormatter,
12};
13
14use crate::formatter::ComposerFormatter;
15use crate::registry::PackagistRegistry;
16
17/// Composer ecosystem implementation.
18///
19/// Provides LSP functionality for composer.json files, including:
20/// - Dependency parsing with position tracking
21/// - Version information from Packagist registry
22/// - Inlay hints for latest versions
23/// - Hover tooltips with package metadata
24/// - Code actions for version updates
25/// - Diagnostics for unknown/abandoned packages
26pub struct ComposerEcosystem {
27    registry: Arc<PackagistRegistry>,
28    formatter: ComposerFormatter,
29}
30
31impl ComposerEcosystem {
32    /// Creates a new Composer ecosystem with the given HTTP cache.
33    pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
34        Self {
35            registry: Arc::new(PackagistRegistry::new(cache)),
36            formatter: ComposerFormatter,
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
56impl deps_core::ecosystem::private::Sealed for ComposerEcosystem {}
57
58impl Ecosystem for ComposerEcosystem {
59    fn id(&self) -> &'static str {
60        "composer"
61    }
62
63    fn display_name(&self) -> &'static str {
64        "Composer (PHP)"
65    }
66
67    fn manifest_filenames(&self) -> &[&'static str] {
68        &["composer.json"]
69    }
70
71    fn lockfile_filenames(&self) -> &[&'static str] {
72        &["composer.lock"]
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_composer_json(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(Arc::new(crate::lockfile::ComposerLockParser))
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                    self.complete_package_names(&prefix).await
112                }
113                CompletionContext::Version {
114                    package_name,
115                    prefix,
116                } => self.complete_versions(&package_name, &prefix).await,
117                CompletionContext::Feature { .. } => vec![],
118                CompletionContext::None => vec![],
119            }
120        })
121    }
122
123    fn as_any(&self) -> &dyn Any {
124        self
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use deps_core::EcosystemConfig;
132    use std::collections::HashMap;
133
134    #[test]
135    fn test_ecosystem_id() {
136        let cache = Arc::new(deps_core::HttpCache::new());
137        let ecosystem = ComposerEcosystem::new(cache);
138        assert_eq!(ecosystem.id(), "composer");
139    }
140
141    #[test]
142    fn test_ecosystem_manifest_filenames() {
143        let cache = Arc::new(deps_core::HttpCache::new());
144        let ecosystem = ComposerEcosystem::new(cache);
145        assert_eq!(ecosystem.manifest_filenames(), &["composer.json"]);
146    }
147
148    #[test]
149    fn test_ecosystem_lockfile_filenames() {
150        let cache = Arc::new(deps_core::HttpCache::new());
151        let ecosystem = ComposerEcosystem::new(cache);
152        assert_eq!(ecosystem.lockfile_filenames(), &["composer.lock"]);
153    }
154
155    #[test]
156    fn test_lockfile_provider_returns_some() {
157        let cache = Arc::new(deps_core::HttpCache::new());
158        let ecosystem = ComposerEcosystem::new(cache);
159        assert!(ecosystem.lockfile_provider().is_some());
160    }
161
162    #[tokio::test]
163    async fn test_parse_manifest_valid() {
164        let cache = Arc::new(deps_core::HttpCache::new());
165        let ecosystem = ComposerEcosystem::new(cache);
166        let uri = Uri::from_file_path("/test/composer.json").unwrap();
167
168        let content = r#"{"require": {"symfony/console": "^6.0"}}"#;
169        let result = ecosystem.parse_manifest(content, &uri).await;
170        assert!(result.is_ok());
171
172        let parse_result = result.unwrap();
173        assert_eq!(parse_result.dependencies().len(), 1);
174    }
175
176    #[tokio::test]
177    async fn test_parse_manifest_invalid() {
178        let cache = Arc::new(deps_core::HttpCache::new());
179        let ecosystem = ComposerEcosystem::new(cache);
180        let uri = Uri::from_file_path("/test/composer.json").unwrap();
181
182        let result = ecosystem.parse_manifest("{invalid json}", &uri).await;
183        assert!(result.is_err());
184    }
185
186    #[tokio::test]
187    async fn test_complete_package_names_short_prefix() {
188        let cache = Arc::new(deps_core::HttpCache::new());
189        let ecosystem = ComposerEcosystem::new(cache);
190
191        let results = ecosystem.complete_package_names("s").await;
192        assert!(results.is_empty());
193    }
194
195    #[tokio::test]
196    async fn test_generate_inlay_hints_empty() {
197        let cache = Arc::new(deps_core::HttpCache::new());
198        let ecosystem = ComposerEcosystem::new(cache);
199        let uri = Uri::from_file_path("/test/composer.json").unwrap();
200
201        let content = r#"{"require": {}}"#;
202        let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
203
204        let hints = ecosystem
205            .generate_inlay_hints(
206                parse_result.as_ref(),
207                &HashMap::new(),
208                &HashMap::new(),
209                deps_core::LoadingState::Loaded,
210                &EcosystemConfig::default(),
211            )
212            .await;
213
214        assert!(hints.is_empty());
215    }
216
217    #[tokio::test]
218    async fn test_generate_completions_no_context() {
219        let cache = Arc::new(deps_core::HttpCache::new());
220        let ecosystem = ComposerEcosystem::new(cache);
221        let uri = Uri::from_file_path("/test/composer.json").unwrap();
222
223        let content = r#"{"name": "test/project"}"#;
224        let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
225        let position = Position {
226            line: 0,
227            character: 0,
228        };
229
230        let completions = ecosystem
231            .generate_completions(parse_result.as_ref(), position, content)
232            .await;
233        assert!(completions.is_empty());
234    }
235}