Skip to main content

ward/detection/
versions.rs

1/// Extract Java toolchain version from build.gradle.kts content.
2pub fn extract_java_version(content: &str) -> Option<u8> {
3    // Pattern: jvmToolchain(21) or jvmToolchain { languageVersion.set(JavaLanguageVersion.of(21)) }
4    for line in content.lines() {
5        let trimmed = line.trim();
6
7        // jvmToolchain(21)
8        if trimmed.contains("jvmToolchain")
9            && let Some(num) = extract_number_from_parens(trimmed)
10        {
11            return Some(num);
12        }
13
14        // JavaLanguageVersion.of(21)
15        if let Some(pos) = trimmed.find("JavaLanguageVersion.of(") {
16            let after = &trimmed[pos + "JavaLanguageVersion.of(".len()..];
17            let num_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
18            if let Ok(num) = num_str.parse() {
19                return Some(num);
20            }
21        }
22
23        // sourceCompatibility = JavaVersion.VERSION_21
24        if trimmed.contains("sourceCompatibility")
25            && trimmed.contains("VERSION_")
26            && let Some(v) = trimmed.split("VERSION_").nth(1)
27        {
28            let cleaned: String = v.chars().take_while(|c| c.is_ascii_digit()).collect();
29            if let Ok(num) = cleaned.parse() {
30                return Some(num);
31            }
32        }
33    }
34
35    None
36}
37
38/// Extract Node.js version from package.json engines field.
39pub fn extract_node_version(content: &str) -> Option<String> {
40    if let Ok(json) = serde_json::from_str::<serde_json::Value>(content)
41        && let Some(engines) = json.get("engines")
42        && let Some(node) = engines.get("node")
43    {
44        return node.as_str().map(|s| s.to_string());
45    }
46    None
47}
48
49fn extract_number_from_parens(s: &str) -> Option<u8> {
50    if let Some(start) = s.find('(') {
51        let rest = &s[start + 1..];
52        if let Some(end) = rest.find(')') {
53            let num_str: String = rest[..end]
54                .trim()
55                .chars()
56                .take_while(|c| c.is_ascii_digit())
57                .collect();
58            return num_str.parse().ok();
59        }
60    }
61    None
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_jvm_toolchain_simple() {
70        assert_eq!(
71            extract_java_version("kotlin { jvmToolchain(21) }"),
72            Some(21)
73        );
74    }
75
76    #[test]
77    fn test_jvm_toolchain_17() {
78        assert_eq!(extract_java_version("    jvmToolchain(17)"), Some(17));
79    }
80
81    #[test]
82    fn test_java_language_version() {
83        let content = r#"
84            java {
85                toolchain {
86                    languageVersion.set(JavaLanguageVersion.of(21))
87                }
88            }
89        "#;
90        assert_eq!(extract_java_version(content), Some(21));
91    }
92
93    #[test]
94    fn test_source_compatibility() {
95        assert_eq!(
96            extract_java_version("sourceCompatibility = JavaVersion.VERSION_17"),
97            Some(17)
98        );
99    }
100
101    #[test]
102    fn test_no_version() {
103        assert_eq!(extract_java_version("plugins { id(\"java\") }"), None);
104    }
105
106    #[test]
107    fn test_node_version() {
108        let content = r#"{"engines": {"node": ">=20"}}"#;
109        assert_eq!(extract_node_version(content), Some(">=20".to_owned()));
110    }
111
112    #[test]
113    fn test_no_node_version() {
114        let content = r#"{"name": "test"}"#;
115        assert_eq!(extract_node_version(content), None);
116    }
117
118    #[test]
119    fn test_java_version_with_extra_whitespace() {
120        assert_eq!(extract_java_version("  jvmToolchain( 21 )  "), Some(21));
121    }
122
123    #[test]
124    fn test_multiple_patterns_first_wins() {
125        let content = "jvmToolchain(17)\nsourceCompatibility = JavaVersion.VERSION_21";
126        assert_eq!(extract_java_version(content), Some(17));
127    }
128
129    #[test]
130    fn test_node_version_range() {
131        let content = r#"{"engines": {"node": ">=18 <22"}}"#;
132        assert_eq!(extract_node_version(content), Some(">=18 <22".to_owned()));
133    }
134
135    #[test]
136    fn test_node_version_invalid_json() {
137        assert_eq!(extract_node_version("not json"), None);
138    }
139
140    #[test]
141    fn test_node_version_no_engines() {
142        assert_eq!(
143            extract_node_version(r#"{"name": "app", "version": "1.0"}"#),
144            None
145        );
146    }
147
148    #[test]
149    fn test_java_version_empty_input() {
150        assert_eq!(extract_java_version(""), None);
151    }
152}