tokmd_analysis_imports/
lib.rs1#![forbid(unsafe_code)]
8
9pub fn supports_language(lang: &str) -> bool {
11 matches!(
12 lang.to_ascii_lowercase().as_str(),
13 "rust" | "javascript" | "typescript" | "python" | "go"
14 )
15}
16
17pub fn parse_imports<S: AsRef<str>>(lang: &str, lines: &[S]) -> Vec<String> {
19 match lang.to_ascii_lowercase().as_str() {
20 "rust" => parse_rust_imports(lines),
21 "javascript" | "typescript" => parse_js_imports(lines),
22 "python" => parse_py_imports(lines),
23 "go" => parse_go_imports(lines),
24 _ => Vec::new(),
25 }
26}
27
28pub fn normalize_import_target(target: &str) -> String {
32 let trimmed = target.trim();
33 if trimmed.starts_with('.') {
34 return "local".to_string();
35 }
36 let trimmed = trimmed.trim_matches('"').trim_matches('\'');
37 trimmed
38 .split(['/', ':', '.'])
39 .next()
40 .unwrap_or(trimmed)
41 .to_string()
42}
43
44fn parse_rust_imports<S: AsRef<str>>(lines: &[S]) -> Vec<String> {
45 let mut imports = Vec::new();
46 for line in lines {
47 let trimmed = line.as_ref().trim();
48 if trimmed.starts_with("use ")
49 && let Some(rest) = trimmed.strip_prefix("use ")
50 {
51 let rest = rest.trim_end_matches(';').trim();
52 let target = rest.split("::").next().unwrap_or(rest).to_string();
53 imports.push(target);
54 } else if trimmed.starts_with("mod ")
55 && let Some(rest) = trimmed.strip_prefix("mod ")
56 {
57 let target = rest.trim_end_matches(';').trim().to_string();
58 imports.push(target);
59 }
60 }
61 imports
62}
63
64fn parse_js_imports<S: AsRef<str>>(lines: &[S]) -> Vec<String> {
65 let mut imports = Vec::new();
66 for line in lines {
67 let trimmed = line.as_ref().trim();
68 if trimmed.starts_with("import ")
69 && let Some(target) = extract_quoted(trimmed)
70 {
71 imports.push(target);
72 }
73 if let Some(idx) = trimmed.find("require(")
74 && let Some(target) = extract_quoted(&trimmed[idx..])
75 {
76 imports.push(target);
77 }
78 }
79 imports
80}
81
82fn parse_py_imports<S: AsRef<str>>(lines: &[S]) -> Vec<String> {
83 let mut imports = Vec::new();
84 for line in lines {
85 let trimmed = line.as_ref().trim();
86 if trimmed.starts_with("import ")
87 && let Some(rest) = trimmed.strip_prefix("import ")
88 {
89 let target = rest.split_whitespace().next().unwrap_or(rest).to_string();
90 imports.push(target);
91 } else if trimmed.starts_with("from ")
92 && let Some(rest) = trimmed.strip_prefix("from ")
93 {
94 let target = rest.split_whitespace().next().unwrap_or(rest).to_string();
95 imports.push(target);
96 }
97 }
98 imports
99}
100
101fn parse_go_imports<S: AsRef<str>>(lines: &[S]) -> Vec<String> {
102 let mut imports = Vec::new();
103 let mut in_block = false;
104 for line in lines {
105 let trimmed = line.as_ref().trim();
106 if trimmed.starts_with("import (") {
107 in_block = true;
108 continue;
109 }
110 if in_block {
111 if trimmed.starts_with(')') {
112 in_block = false;
113 continue;
114 }
115 if let Some(target) = extract_quoted(trimmed) {
116 imports.push(target);
117 }
118 continue;
119 }
120 if trimmed.starts_with("import ")
121 && let Some(target) = extract_quoted(trimmed)
122 {
123 imports.push(target);
124 }
125 }
126 imports
127}
128
129fn extract_quoted(text: &str) -> Option<String> {
130 let mut chars = text.chars();
131 let mut quote = None;
132 for ch in chars.by_ref() {
133 if ch == '"' || ch == '\'' {
134 quote = Some(ch);
135 break;
136 }
137 }
138 let quote = quote?;
139 let mut out = String::new();
140 for ch in chars {
141 if ch == quote {
142 break;
143 }
144 out.push(ch);
145 }
146 if out.is_empty() { None } else { Some(out) }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
156 fn test_supports_known_languages() {
157 assert!(supports_language("Rust"));
158 assert!(supports_language("rust"));
159 assert!(supports_language("JavaScript"));
160 assert!(supports_language("TypeScript"));
161 assert!(supports_language("Python"));
162 assert!(supports_language("Go"));
163 }
164
165 #[test]
166 fn test_unsupported_languages() {
167 assert!(!supports_language("Java"));
168 assert!(!supports_language("C"));
169 assert!(!supports_language("C++"));
170 assert!(!supports_language("Ruby"));
171 assert!(!supports_language(""));
172 }
173
174 #[test]
177 fn test_normalize_relative_to_local() {
178 assert_eq!(normalize_import_target("./utils"), "local");
179 assert_eq!(normalize_import_target("../lib"), "local");
180 assert_eq!(normalize_import_target("."), "local");
181 }
182
183 #[test]
184 fn test_normalize_npm_scoped_package() {
185 assert_eq!(normalize_import_target("react/dom"), "react");
186 assert_eq!(normalize_import_target("lodash/fp"), "lodash");
187 }
188
189 #[test]
190 fn test_normalize_rust_crate() {
191 assert_eq!(normalize_import_target("std::collections"), "std");
192 assert_eq!(normalize_import_target("serde::Deserialize"), "serde");
193 }
194
195 #[test]
196 fn test_normalize_python_dotted() {
197 assert_eq!(normalize_import_target("os.path"), "os");
198 assert_eq!(normalize_import_target("collections.abc"), "collections");
199 }
200
201 #[test]
202 fn test_normalize_strips_quotes() {
203 assert_eq!(normalize_import_target("\"react\""), "react");
204 assert_eq!(normalize_import_target("'lodash'"), "lodash");
205 }
206
207 #[test]
208 fn test_normalize_trims_whitespace() {
209 assert_eq!(normalize_import_target(" react "), "react");
210 }
211
212 #[test]
215 fn test_parse_rust_use_statement() {
216 let lines = ["use std::collections::HashMap;", "use serde::Deserialize;"];
217 let imports = parse_imports("Rust", &lines);
218 assert_eq!(imports, vec!["std", "serde"]);
219 }
220
221 #[test]
222 fn test_parse_rust_mod_statement() {
223 let lines = ["mod utils;", "mod tests;"];
224 let imports = parse_imports("Rust", &lines);
225 assert_eq!(imports, vec!["utils", "tests"]);
226 }
227
228 #[test]
229 fn test_parse_rust_mixed() {
230 let lines = [
231 "use anyhow::Result;",
232 "mod config;",
233 "fn main() {}",
234 "use tokei::Languages;",
235 ];
236 let imports = parse_imports("rust", &lines);
237 assert_eq!(imports, vec!["anyhow", "config", "tokei"]);
238 }
239
240 #[test]
241 fn test_parse_rust_ignores_non_import_lines() {
242 let lines = ["fn main() {}", "let x = 42;", "// use fake;"];
243 let imports = parse_imports("Rust", &lines);
244 assert!(imports.is_empty());
245 }
246
247 #[test]
250 fn test_parse_js_import_from() {
251 let lines = [
252 "import React from 'react';",
253 "import { useState } from \"react\";",
254 ];
255 let imports = parse_imports("JavaScript", &lines);
256 assert_eq!(imports, vec!["react", "react"]);
257 }
258
259 #[test]
260 fn test_parse_js_require() {
261 let lines = [
262 "const fs = require('fs');",
263 "const path = require(\"path\");",
264 ];
265 let imports = parse_imports("JavaScript", &lines);
266 assert_eq!(imports, vec!["fs", "path"]);
267 }
268
269 #[test]
270 fn test_parse_ts_imports() {
271 let lines = ["import type { Foo } from 'bar';"];
272 let imports = parse_imports("TypeScript", &lines);
273 assert_eq!(imports, vec!["bar"]);
274 }
275
276 #[test]
279 fn test_parse_python_import() {
280 let lines = ["import os", "import sys"];
281 let imports = parse_imports("Python", &lines);
282 assert_eq!(imports, vec!["os", "sys"]);
283 }
284
285 #[test]
286 fn test_parse_python_from_import() {
287 let lines = [
288 "from pathlib import Path",
289 "from collections import defaultdict",
290 ];
291 let imports = parse_imports("Python", &lines);
292 assert_eq!(imports, vec!["pathlib", "collections"]);
293 }
294
295 #[test]
296 fn test_parse_python_ignores_comments() {
297 let lines = ["# import fake", "import os"];
298 let imports = parse_imports("Python", &lines);
299 assert_eq!(imports, vec!["os"]);
300 }
301
302 #[test]
305 fn test_parse_go_single_import() {
306 let lines = ["import \"fmt\""];
307 let imports = parse_imports("Go", &lines);
308 assert_eq!(imports, vec!["fmt"]);
309 }
310
311 #[test]
312 fn test_parse_go_block_import() {
313 let lines = ["import (", "\t\"fmt\"", "\t\"os\"", ")"];
314 let imports = parse_imports("Go", &lines);
315 assert_eq!(imports, vec!["fmt", "os"]);
316 }
317
318 #[test]
319 fn test_parse_go_std_and_external() {
320 let lines = ["import (", "\t\"fmt\"", "\t\"github.com/pkg/errors\"", ")"];
321 let imports = parse_imports("Go", &lines);
322 assert_eq!(imports, vec!["fmt", "github.com/pkg/errors"]);
323 }
324
325 #[test]
328 fn test_parse_unsupported_language_returns_empty() {
329 let lines = ["#include <stdio.h>"];
330 let imports = parse_imports("C", &lines);
331 assert!(imports.is_empty());
332 }
333
334 #[test]
337 fn test_parse_empty_input() {
338 let lines: Vec<&str> = vec![];
339 let imports = parse_imports("Rust", &lines);
340 assert!(imports.is_empty());
341 }
342
343 #[test]
346 fn test_extract_quoted_double() {
347 assert_eq!(extract_quoted("from \"hello\""), Some("hello".to_string()));
348 }
349
350 #[test]
351 fn test_extract_quoted_single() {
352 assert_eq!(extract_quoted("from 'world'"), Some("world".to_string()));
353 }
354
355 #[test]
356 fn test_extract_quoted_empty_string() {
357 assert_eq!(extract_quoted("\"\""), None);
358 }
359
360 #[test]
361 fn test_extract_quoted_no_quotes() {
362 assert_eq!(extract_quoted("no quotes here"), None);
363 }
364}