deps_composer/
ecosystem.rs1use 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
17pub struct ComposerEcosystem {
27 registry: Arc<PackagistRegistry>,
28 formatter: ComposerFormatter,
29}
30
31impl ComposerEcosystem {
32 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}