Skip to main content

normalize_languages/
external_packages.rs

1//! External package resolution - shared types only.
2//!
3//! Language-specific resolution has been moved to individual language modules:
4//! - Python: python.rs
5//! - Go: go.rs
6//! - Rust: rust.rs
7//! - JavaScript/TypeScript/Deno: ecmascript.rs
8//! - Java: java.rs
9//! - C/C++: c_cpp.rs
10//!
11//! This module contains:
12//! - ResolvedPackage: Common result type for package resolution
13//! - Global cache: ~/.cache/moss/ for indexed packages
14//! - PackageIndex: SQLite-backed package/symbol index
15
16use std::path::PathBuf;
17
18// =============================================================================
19// Shared Types
20// =============================================================================
21
22/// Result of resolving an external package
23#[derive(Debug, Clone)]
24pub struct ResolvedPackage {
25    /// Path to the package source
26    pub path: PathBuf,
27    /// Package name as imported
28    pub name: String,
29    /// Whether this is a namespace package (no __init__.py)
30    pub is_namespace: bool,
31}
32
33// =============================================================================
34// Global Cache
35// =============================================================================
36
37/// Get the global moss cache directory (~/.cache/moss/).
38pub fn get_global_cache_dir() -> Option<PathBuf> {
39    let cache_base = if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
40        PathBuf::from(xdg)
41    } else if let Ok(home) = std::env::var("HOME") {
42        PathBuf::from(home).join(".cache")
43    } else if let Ok(home) = std::env::var("USERPROFILE") {
44        PathBuf::from(home).join(".cache")
45    } else {
46        return None;
47    };
48
49    let moss_cache = cache_base.join("moss");
50    if !moss_cache.exists() {
51        std::fs::create_dir_all(&moss_cache).ok()?;
52    }
53
54    Some(moss_cache)
55}
56
57/// Get the path to the unified global package index database.
58pub fn get_global_packages_db() -> Option<PathBuf> {
59    let cache = get_global_cache_dir()?;
60    Some(cache.join("packages.db"))
61}
62
63/// Compare version strings semver-style.
64pub fn version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
65    let a_parts: Vec<u32> = a.split('.').filter_map(|p| p.parse().ok()).collect();
66    let b_parts: Vec<u32> = b.split('.').filter_map(|p| p.parse().ok()).collect();
67
68    for (ap, bp) in a_parts.iter().zip(b_parts.iter()) {
69        match ap.cmp(bp) {
70            std::cmp::Ordering::Equal => continue,
71            other => return other,
72        }
73    }
74    a_parts.len().cmp(&b_parts.len())
75}
76
77// =============================================================================
78// Global Package Index Database
79// =============================================================================
80
81use libsql::{Connection, Database, params};
82
83/// Parsed version as (major, minor).
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Version {
86    pub major: u32,
87    pub minor: u32,
88}
89
90impl Version {
91    pub fn parse(s: &str) -> Option<Version> {
92        let parts: Vec<&str> = s.split('.').collect();
93        if parts.len() >= 2 {
94            Some(Version {
95                major: parts[0].parse().ok()?,
96                minor: parts[1].parse().ok()?,
97            })
98        } else {
99            None
100        }
101    }
102
103    pub fn in_range(&self, min: Version, max: Option<Version>) -> bool {
104        if *self < min {
105            return false;
106        }
107        if let Some(max) = max {
108            if *self > max {
109                return false;
110            }
111        }
112        true
113    }
114}
115
116impl PartialOrd for Version {
117    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
118        Some(self.cmp(other))
119    }
120}
121
122impl Ord for Version {
123    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
124        match self.major.cmp(&other.major) {
125            std::cmp::Ordering::Equal => self.minor.cmp(&other.minor),
126            ord => ord,
127        }
128    }
129}
130
131/// A package record in the index.
132#[derive(Debug, Clone)]
133pub struct PackageRecord {
134    pub id: i64,
135    pub language: String,
136    pub name: String,
137    pub path: String,
138    pub min_major: u32,
139    pub min_minor: u32,
140    pub max_major: Option<u32>,
141    pub max_minor: Option<u32>,
142}
143
144impl PackageRecord {
145    pub fn min_version(&self) -> Version {
146        Version {
147            major: self.min_major,
148            minor: self.min_minor,
149        }
150    }
151
152    pub fn max_version(&self) -> Option<Version> {
153        match (self.max_major, self.max_minor) {
154            (Some(major), Some(minor)) => Some(Version { major, minor }),
155            _ => None,
156        }
157    }
158}
159
160/// A symbol record in the index.
161#[derive(Debug, Clone)]
162pub struct SymbolRecord {
163    pub id: i64,
164    pub package_id: i64,
165    pub name: String,
166    pub kind: String,
167    pub signature: String,
168    pub line: u32,
169}
170
171/// Global package index backed by libSQL.
172pub struct PackageIndex {
173    conn: Connection,
174    #[allow(dead_code)]
175    db: Database,
176}
177
178impl PackageIndex {
179    pub async fn open() -> Result<Self, libsql::Error> {
180        let db_path = get_global_packages_db().ok_or_else(|| {
181            libsql::Error::SqliteFailure(1, "Cannot determine cache directory".into())
182        })?;
183
184        let db = libsql::Builder::new_local(db_path).build().await?;
185        let conn = db.connect()?;
186        let index = PackageIndex { conn, db };
187        index.init_schema().await?;
188        Ok(index)
189    }
190
191    pub async fn open_in_memory() -> Result<Self, libsql::Error> {
192        let db = libsql::Builder::new_local(":memory:").build().await?;
193        let conn = db.connect()?;
194        let index = PackageIndex { conn, db };
195        index.init_schema().await?;
196        Ok(index)
197    }
198
199    async fn init_schema(&self) -> Result<(), libsql::Error> {
200        self.conn
201            .execute(
202                "CREATE TABLE IF NOT EXISTS packages (
203                id INTEGER PRIMARY KEY,
204                language TEXT NOT NULL,
205                name TEXT NOT NULL,
206                path TEXT NOT NULL,
207                min_major INTEGER NOT NULL,
208                min_minor INTEGER NOT NULL,
209                max_major INTEGER,
210                max_minor INTEGER,
211                indexed_at INTEGER NOT NULL
212            )",
213                (),
214            )
215            .await?;
216
217        self.conn
218            .execute(
219                "CREATE INDEX IF NOT EXISTS idx_packages_lang_name ON packages(language, name)",
220                (),
221            )
222            .await?;
223
224        self.conn
225            .execute(
226                "CREATE TABLE IF NOT EXISTS symbols (
227                id INTEGER PRIMARY KEY,
228                package_id INTEGER NOT NULL,
229                name TEXT NOT NULL,
230                kind TEXT NOT NULL,
231                signature TEXT NOT NULL,
232                line INTEGER NOT NULL,
233                FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE
234            )",
235                (),
236            )
237            .await?;
238
239        self.conn
240            .execute(
241                "CREATE INDEX IF NOT EXISTS idx_symbols_package ON symbols(package_id)",
242                (),
243            )
244            .await?;
245        self.conn
246            .execute(
247                "CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)",
248                (),
249            )
250            .await?;
251
252        Ok(())
253    }
254
255    pub async fn insert_package(
256        &self,
257        language: &str,
258        name: &str,
259        path: &str,
260        min_version: Version,
261        max_version: Option<Version>,
262    ) -> Result<i64, libsql::Error> {
263        let now = std::time::SystemTime::now()
264            .duration_since(std::time::UNIX_EPOCH)
265            .unwrap_or_default()
266            .as_secs() as i64;
267
268        self.conn.execute(
269            "INSERT INTO packages (language, name, path, min_major, min_minor, max_major, max_minor, indexed_at)
270             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
271            params![
272                language,
273                name,
274                path,
275                min_version.major,
276                min_version.minor,
277                max_version.map(|v| v.major),
278                max_version.map(|v| v.minor),
279                now,
280            ],
281        ).await?;
282        Ok(self.conn.last_insert_rowid())
283    }
284
285    pub async fn insert_symbol(
286        &self,
287        package_id: i64,
288        name: &str,
289        kind: &str,
290        signature: &str,
291        line: u32,
292    ) -> Result<i64, libsql::Error> {
293        self.conn
294            .execute(
295                "INSERT INTO symbols (package_id, name, kind, signature, line)
296             VALUES (?1, ?2, ?3, ?4, ?5)",
297                params![package_id, name, kind, signature, line],
298            )
299            .await?;
300        Ok(self.conn.last_insert_rowid())
301    }
302
303    pub async fn find_package(
304        &self,
305        language: &str,
306        name: &str,
307        version: Option<Version>,
308    ) -> Result<Option<PackageRecord>, libsql::Error> {
309        let mut rows = self
310            .conn
311            .query(
312                "SELECT id, language, name, path, min_major, min_minor, max_major, max_minor
313             FROM packages WHERE language = ?1 AND name = ?2",
314                params![language, name],
315            )
316            .await?;
317
318        let mut packages = Vec::new();
319        while let Some(row) = rows.next().await? {
320            packages.push(PackageRecord {
321                id: row.get(0)?,
322                language: row.get(1)?,
323                name: row.get(2)?,
324                path: row.get(3)?,
325                min_major: row.get(4)?,
326                min_minor: row.get(5)?,
327                max_major: row.get(6)?,
328                max_minor: row.get(7)?,
329            });
330        }
331
332        if let Some(ver) = version {
333            for pkg in packages {
334                if ver.in_range(pkg.min_version(), pkg.max_version()) {
335                    return Ok(Some(pkg));
336                }
337            }
338            Ok(None)
339        } else {
340            Ok(packages.into_iter().next())
341        }
342    }
343
344    pub async fn get_symbols(&self, package_id: i64) -> Result<Vec<SymbolRecord>, libsql::Error> {
345        let mut rows = self
346            .conn
347            .query(
348                "SELECT id, package_id, name, kind, signature, line
349             FROM symbols WHERE package_id = ?1 ORDER BY line",
350                params![package_id],
351            )
352            .await?;
353
354        let mut symbols = Vec::new();
355        while let Some(row) = rows.next().await? {
356            symbols.push(SymbolRecord {
357                id: row.get(0)?,
358                package_id: row.get(1)?,
359                name: row.get(2)?,
360                kind: row.get(3)?,
361                signature: row.get(4)?,
362                line: row.get(5)?,
363            });
364        }
365
366        Ok(symbols)
367    }
368
369    pub async fn find_symbol(
370        &self,
371        language: &str,
372        symbol_name: &str,
373        version: Option<Version>,
374    ) -> Result<Vec<(PackageRecord, SymbolRecord)>, libsql::Error> {
375        let mut rows = self.conn.query(
376            "SELECT p.id, p.language, p.name, p.path, p.min_major, p.min_minor, p.max_major, p.max_minor,
377                    s.id, s.package_id, s.name, s.kind, s.signature, s.line
378             FROM symbols s
379             JOIN packages p ON s.package_id = p.id
380             WHERE p.language = ?1 AND s.name = ?2",
381            params![language, symbol_name],
382        ).await?;
383
384        let mut results = Vec::new();
385        while let Some(row) = rows.next().await? {
386            results.push((
387                PackageRecord {
388                    id: row.get(0)?,
389                    language: row.get(1)?,
390                    name: row.get(2)?,
391                    path: row.get(3)?,
392                    min_major: row.get(4)?,
393                    min_minor: row.get(5)?,
394                    max_major: row.get(6)?,
395                    max_minor: row.get(7)?,
396                },
397                SymbolRecord {
398                    id: row.get(8)?,
399                    package_id: row.get(9)?,
400                    name: row.get(10)?,
401                    kind: row.get(11)?,
402                    signature: row.get(12)?,
403                    line: row.get(13)?,
404                },
405            ));
406        }
407
408        if let Some(ver) = version {
409            Ok(results
410                .into_iter()
411                .filter(|(pkg, _)| ver.in_range(pkg.min_version(), pkg.max_version()))
412                .collect())
413        } else {
414            Ok(results)
415        }
416    }
417
418    pub async fn is_indexed(&self, language: &str, name: &str) -> Result<bool, libsql::Error> {
419        let mut rows = self
420            .conn
421            .query(
422                "SELECT COUNT(*) FROM packages WHERE language = ?1 AND name = ?2",
423                params![language, name],
424            )
425            .await?;
426
427        if let Some(row) = rows.next().await? {
428            let count: i64 = row.get(0)?;
429            Ok(count > 0)
430        } else {
431            Ok(false)
432        }
433    }
434
435    pub async fn delete_package(&self, package_id: i64) -> Result<(), libsql::Error> {
436        self.conn
437            .execute(
438                "DELETE FROM symbols WHERE package_id = ?1",
439                params![package_id],
440            )
441            .await?;
442        self.conn
443            .execute("DELETE FROM packages WHERE id = ?1", params![package_id])
444            .await?;
445        Ok(())
446    }
447
448    pub async fn clear(&self) -> Result<(), libsql::Error> {
449        self.conn.execute("DELETE FROM symbols", ()).await?;
450        self.conn.execute("DELETE FROM packages", ()).await?;
451        Ok(())
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn test_version_parsing() {
461        assert_eq!(
462            Version::parse("3.11"),
463            Some(Version {
464                major: 3,
465                minor: 11
466            })
467        );
468        assert_eq!(
469            Version::parse("1.21"),
470            Some(Version {
471                major: 1,
472                minor: 21
473            })
474        );
475        assert_eq!(Version::parse("invalid"), None);
476    }
477
478    #[test]
479    fn test_version_comparison() {
480        let v1 = Version {
481            major: 3,
482            minor: 10,
483        };
484        let v2 = Version {
485            major: 3,
486            minor: 11,
487        };
488        let v3 = Version { major: 4, minor: 0 };
489
490        assert!(v1 < v2);
491        assert!(v2 < v3);
492        assert!(v1.in_range(v1, Some(v2)));
493        assert!(!v3.in_range(v1, Some(v2)));
494    }
495
496    #[tokio::test]
497    async fn test_package_index() {
498        let index = PackageIndex::open_in_memory().await.unwrap();
499
500        // Insert a package
501        let pkg_id = index
502            .insert_package(
503                "python",
504                "requests",
505                "/path/to/requests",
506                Version { major: 3, minor: 8 },
507                None,
508            )
509            .await
510            .unwrap();
511
512        // Insert a symbol
513        index
514            .insert_symbol(pkg_id, "get", "function", "def get(url)", 10)
515            .await
516            .unwrap();
517
518        // Find the package
519        let found = index
520            .find_package("python", "requests", None)
521            .await
522            .unwrap();
523        assert!(found.is_some());
524        assert_eq!(found.unwrap().name, "requests");
525
526        // Find the symbol
527        let symbols = index.get_symbols(pkg_id).await.unwrap();
528        assert_eq!(symbols.len(), 1);
529        assert_eq!(symbols[0].name, "get");
530
531        // Check indexed
532        assert!(index.is_indexed("python", "requests").await.unwrap());
533        assert!(!index.is_indexed("python", "nonexistent").await.unwrap());
534    }
535}