ward/detection/
versions.rs1pub fn extract_java_version(content: &str) -> Option<u8> {
3 for line in content.lines() {
5 let trimmed = line.trim();
6
7 if trimmed.contains("jvmToolchain")
9 && let Some(num) = extract_number_from_parens(trimmed)
10 {
11 return Some(num);
12 }
13
14 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 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
38pub 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}