Skip to main content

tl_package/
outdated.rs

1use crate::lockfile::LockFile;
2use crate::manifest::{DepSourceKind, DependencySpec, Manifest};
3
4/// Information about one outdated dependency.
5#[derive(Debug, Clone)]
6pub struct OutdatedInfo {
7    pub name: String,
8    pub current: String,
9    /// Latest version matching the manifest's version requirement.
10    pub latest_matching: Option<String>,
11    /// Latest version available in the registry (may be outside the requirement).
12    pub latest_available: Option<String>,
13    /// Source kind (for git/path deps, version columns don't apply).
14    pub source_kind: DepSourceKind,
15}
16
17impl OutdatedInfo {
18    pub fn is_up_to_date(&self) -> bool {
19        match (&self.latest_matching, &self.latest_available) {
20            (Some(matching), _) => matching == &self.current,
21            _ => true,
22        }
23    }
24}
25
26/// Check which dependencies are outdated.
27/// Accepts a version lookup closure for testability without a live registry.
28///
29/// `version_lookup(name)` should return a list of available version strings.
30pub fn check_outdated_with_versions(
31    manifest: &Manifest,
32    lock: &LockFile,
33    version_lookup: &dyn Fn(&str) -> Result<Vec<String>, String>,
34) -> Result<Vec<OutdatedInfo>, String> {
35    let mut results = Vec::new();
36
37    for (name, spec) in &manifest.dependencies {
38        let source_kind = spec.source_kind();
39
40        // Find current locked version
41        let current = match lock.find(name) {
42            Some(locked) => locked.version.clone(),
43            None => continue, // not installed yet
44        };
45
46        if source_kind != DepSourceKind::Registry {
47            // For git/path deps, we can't query for newer versions
48            results.push(OutdatedInfo {
49                name: name.clone(),
50                current,
51                latest_matching: None,
52                latest_available: None,
53                source_kind,
54            });
55            continue;
56        }
57
58        // Get the version requirement from the spec
59        let version_req_str = match spec {
60            DependencySpec::Simple(req) => req.clone(),
61            DependencySpec::Detailed(d) => d.version.clone().unwrap_or_else(|| "*".into()),
62        };
63
64        // Query available versions
65        match version_lookup(name) {
66            Ok(versions) => {
67                let req = crate::version::VersionReq::parse(&version_req_str)?;
68
69                // Find latest matching version
70                let mut matching_versions: Vec<crate::version::Version> = versions
71                    .iter()
72                    .filter_map(|v| crate::version::Version::parse(v).ok())
73                    .filter(|v| req.matches(v))
74                    .collect();
75                matching_versions.sort();
76                let latest_matching = matching_versions.last().map(|v| v.to_string());
77
78                // Find latest available version (any)
79                let mut all_versions: Vec<crate::version::Version> = versions
80                    .iter()
81                    .filter_map(|v| crate::version::Version::parse(v).ok())
82                    .collect();
83                all_versions.sort();
84                let latest_available = all_versions.last().map(|v| v.to_string());
85
86                results.push(OutdatedInfo {
87                    name: name.clone(),
88                    current,
89                    latest_matching,
90                    latest_available,
91                    source_kind,
92                });
93            }
94            Err(_) => {
95                // Registry unreachable — still report with no version info
96                results.push(OutdatedInfo {
97                    name: name.clone(),
98                    current,
99                    latest_matching: None,
100                    latest_available: None,
101                    source_kind,
102                });
103            }
104        }
105    }
106
107    Ok(results)
108}
109
110/// Check outdated dependencies using the package registry.
111#[cfg(feature = "registry")]
112pub fn check_outdated(manifest: &Manifest, lock: &LockFile) -> Result<Vec<OutdatedInfo>, String> {
113    check_outdated_with_versions(manifest, lock, &|name| {
114        let info = crate::registry_client::get_package_info(name)?;
115        Ok(info.versions.iter().map(|v| v.version.clone()).collect())
116    })
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::lockfile::LockedPackage;
123    use crate::manifest::{DetailedDep, ProjectConfig};
124    use std::collections::BTreeMap;
125
126    fn test_manifest(deps: Vec<(&str, DependencySpec)>) -> Manifest {
127        let mut dependencies = BTreeMap::new();
128        for (name, spec) in deps {
129            dependencies.insert(name.to_string(), spec);
130        }
131        Manifest {
132            project: ProjectConfig {
133                name: "test".into(),
134                version: "0.1.0".into(),
135                edition: None,
136                authors: None,
137                description: None,
138                entry: None,
139            },
140            dependencies,
141        }
142    }
143
144    #[test]
145    fn test_outdated_newer_available() {
146        let manifest = test_manifest(vec![("utils", DependencySpec::Simple("^1.0".into()))]);
147        let lock = LockFile {
148            packages: vec![LockedPackage::new(
149                "utils",
150                "1.0.0",
151                "registry+http://localhost@1.0.0".into(),
152            )],
153        };
154
155        let results = check_outdated_with_versions(&manifest, &lock, &|_name| {
156            Ok(vec!["1.0.0".into(), "1.3.0".into(), "2.0.0".into()])
157        })
158        .unwrap();
159
160        assert_eq!(results.len(), 1);
161        assert_eq!(results[0].name, "utils");
162        assert_eq!(results[0].current, "1.0.0");
163        assert_eq!(results[0].latest_matching, Some("1.3.0".into()));
164        assert_eq!(results[0].latest_available, Some("2.0.0".into()));
165        assert!(!results[0].is_up_to_date());
166    }
167
168    #[test]
169    fn test_outdated_up_to_date() {
170        let manifest = test_manifest(vec![("helpers", DependencySpec::Simple("^2.0".into()))]);
171        let lock = LockFile {
172            packages: vec![LockedPackage::new(
173                "helpers",
174                "2.1.0",
175                "registry+http://localhost@2.1.0".into(),
176            )],
177        };
178
179        let results = check_outdated_with_versions(&manifest, &lock, &|_name| {
180            Ok(vec!["2.0.0".into(), "2.1.0".into()])
181        })
182        .unwrap();
183
184        assert_eq!(results.len(), 1);
185        assert!(results[0].is_up_to_date());
186    }
187
188    #[test]
189    fn test_outdated_git_dep() {
190        let manifest = test_manifest(vec![(
191            "mylib",
192            DependencySpec::Detailed(DetailedDep {
193                version: None,
194                git: Some("https://github.com/user/mylib.git".into()),
195                branch: None,
196                tag: None,
197                rev: None,
198                path: None,
199            }),
200        )]);
201        let lock = LockFile {
202            packages: vec![LockedPackage::new(
203                "mylib",
204                "1.0.0",
205                LockedPackage::git_source("https://github.com/user/mylib.git", "abc123"),
206            )],
207        };
208
209        let results = check_outdated_with_versions(&manifest, &lock, &|_| {
210            panic!("should not query registry for git deps");
211        })
212        .unwrap();
213
214        assert_eq!(results.len(), 1);
215        assert_eq!(results[0].source_kind, DepSourceKind::Git);
216        assert!(results[0].is_up_to_date()); // git deps always "up to date" by this metric
217    }
218}