blast_radius/parse/
mod.rs1use std::fs;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5mod facts;
6pub use facts::*;
7
8mod javascript;
9pub(crate) use javascript::parse_javascript_module;
10
11#[cfg(any(feature = "vue", feature = "svelte"))]
12mod component;
13#[cfg(any(feature = "vue", feature = "svelte"))]
14pub(crate) use component::parse_component_module;
15
16#[cfg(feature = "java")]
17mod java;
18#[cfg(feature = "java")]
19pub(crate) use java::parse_java_module;
20
21#[cfg(feature = "ruby")]
22mod ruby;
23#[cfg(feature = "ruby")]
24pub(crate) use ruby::parse_ruby_module;
25
26#[cfg(feature = "python")]
27mod python;
28#[cfg(feature = "python")]
29pub(crate) use python::parse_python_module;
30
31#[cfg(feature = "rust")]
32mod rust_lang;
33#[cfg(feature = "rust")]
34pub(crate) use rust_lang::parse_rust_module;
35
36pub fn parse_module(path: &Path) -> Result<ModuleFacts> {
37 let source = fs::read_to_string(path)
38 .with_context(|| format!("failed to read source file {}", path.display()))?;
39
40 crate::language::adapter_for(path).parse(path, &source)
41}
42
43#[cfg(test)]
44mod tests {
45 use std::fs;
46
47 use tempfile::tempdir;
48
49 use super::parse_module;
50
51 #[test]
52 fn parses_js_files_with_jsx() {
53 let dir = tempdir().unwrap();
54 let path = dir.path().join("renderAvatar.js");
55 fs::write(
56 &path,
57 r#"
58import Avatar from '@mui/material/Avatar';
59
60export function renderAvatar(params) {
61 if (params.value == null) {
62 return '';
63 }
64
65 return <Avatar>{params.value.name}</Avatar>;
66}
67"#,
68 )
69 .unwrap();
70
71 let facts = parse_module(&path).unwrap();
72 assert_eq!(facts.imports.len(), 1);
73 assert!(
74 facts
75 .exports
76 .iter()
77 .any(|export| export.exported == "renderAvatar")
78 );
79 }
80
81 #[test]
82 fn parses_dynamic_imports_as_used_namespace_imports() {
83 let dir = tempdir().unwrap();
84 let path = dir.path().join("routes.tsx");
85 fs::write(
86 &path,
87 r#"
88import { lazy } from 'react';
89
90const ChatsPage = lazy(() =>
91 import("@/pages/chats-page").then((m) => ({ default: m.ChatsPage }))
92);
93const Settings = lazy(() => import(`./settings-page`));
94
95export const routes = [ChatsPage, Settings];
96"#,
97 )
98 .unwrap();
99
100 let facts = parse_module(&path).unwrap();
101 for source in ["@/pages/chats-page", "./settings-page"] {
102 let import = facts
103 .imports
104 .iter()
105 .find(|import| import.source == source)
106 .expect("dynamic import should be collected");
107 assert_eq!(import.kind, super::ImportKind::Dynamic);
108 assert_eq!(import.imported, super::ImportTarget::Namespace);
109 assert!(facts.used_locals.contains(&import.local));
111 }
112 }
113
114 #[test]
115 fn parses_modern_module_extensions() {
116 let dir = tempdir().unwrap();
117
118 let mjs_path = dir.path().join("widget.mjs");
119 fs::write(&mjs_path, "export const widget = <div />;").unwrap();
120 parse_module(&mjs_path).unwrap();
121
122 let cts_path = dir.path().join("server.cts");
123 fs::write(&cts_path, "export const server = 1;").unwrap();
124 parse_module(&cts_path).unwrap();
125 }
126
127 #[cfg(feature = "python")]
128 #[test]
129 fn parses_python_imports_and_exports() {
130 let dir = tempdir().unwrap();
131 let path = dir.path().join("email.py");
132 fs::write(
133 &path,
134 r#"
135from ..models import User
136from . import formatting
137
138DEFAULT_TEMPLATE = "welcome"
139
140def send_email(user: User) -> str:
141 return formatting.format_subject(user.email, DEFAULT_TEMPLATE)
142"#,
143 )
144 .unwrap();
145
146 let facts = parse_module(&path).unwrap();
147 assert!(
148 facts
149 .exports
150 .iter()
151 .any(|export| export.exported == "send_email")
152 );
153 assert!(
154 facts
155 .imports
156 .iter()
157 .any(|import| import.source == "..models" && import.local == "User")
158 );
159 assert!(
160 facts
161 .imports
162 .iter()
163 .any(|import| import.source == ".formatting" && import.local == "formatting")
164 );
165 }
166
167 #[cfg(feature = "rust")]
168 #[test]
169 fn parses_rust_imports_exports_and_reexports() {
170 let dir = tempdir().unwrap();
171 let path = dir.path().join("lib.rs");
172 fs::write(
173 &path,
174 r#"
175pub mod services;
176
177use crate::models::User;
178pub use crate::services::email::send_email;
179
180pub struct App;
181"#,
182 )
183 .unwrap();
184
185 let facts = parse_module(&path).unwrap();
186 assert!(facts.exports.iter().any(|export| export.exported == "App"));
187 assert!(
188 facts
189 .imports
190 .iter()
191 .any(|import| import.source == "mod:services")
192 );
193 assert!(
194 facts
195 .imports
196 .iter()
197 .any(|import| import.source == "crate::models" && import.local == "User")
198 );
199 assert!(
200 facts
201 .reexports
202 .iter()
203 .any(|reexport| reexport.source == "crate::services::email"
204 && reexport.exported == "send_email")
205 );
206 }
207
208 #[cfg(feature = "vue")]
209 #[test]
210 fn parses_vue_script_imports_and_default_export() {
211 let dir = tempdir().unwrap();
212 let path = dir.path().join("Button.vue");
213 fs::write(
214 &path,
215 r#"
216<script setup lang="ts">
217import { formatLabel } from './shared'
218const label = formatLabel('save')
219</script>
220<template><button>{{ label }}</button></template>
221"#,
222 )
223 .unwrap();
224
225 let facts = parse_module(&path).unwrap();
226 assert!(
227 facts
228 .imports
229 .iter()
230 .any(|import| import.source == "./shared" && import.local == "formatLabel")
231 );
232 assert!(
233 facts
234 .exports
235 .iter()
236 .any(|export| export.exported == "default")
237 );
238 }
239
240 #[cfg(feature = "svelte")]
241 #[test]
242 fn parses_svelte_script_imports_and_default_export() {
243 let dir = tempdir().unwrap();
244 let path = dir.path().join("Card.svelte");
245 fs::write(
246 &path,
247 r#"
248<script lang="ts">
249 import Button from './Button.vue'
250 export let title = 'Settings'
251</script>
252<Button />
253"#,
254 )
255 .unwrap();
256
257 let facts = parse_module(&path).unwrap();
258 assert!(
259 facts
260 .imports
261 .iter()
262 .any(|import| import.source == "./Button.vue" && import.local == "Button")
263 );
264 assert!(
265 facts
266 .exports
267 .iter()
268 .any(|export| export.exported == "default")
269 );
270 }
271
272 #[cfg(feature = "ruby")]
273 #[test]
274 fn parses_ruby_requires_and_exports() {
275 let dir = tempdir().unwrap();
276 let path = dir.path().join("email_service.rb");
277 fs::write(
278 &path,
279 r#"
280require_relative "../models/user"
281
282class EmailService
283 def self.send_email(email)
284 end
285end
286"#,
287 )
288 .unwrap();
289
290 let facts = parse_module(&path).unwrap();
291 assert!(
292 facts
293 .imports
294 .iter()
295 .any(|import| import.source == "../models/user")
296 );
297 assert!(
298 facts
299 .exports
300 .iter()
301 .any(|export| export.exported == "EmailService")
302 );
303 assert!(
304 facts
305 .exports
306 .iter()
307 .any(|export| export.exported == "send_email")
308 );
309 }
310
311 #[cfg(feature = "java")]
312 #[test]
313 fn parses_java_imports_and_exports() {
314 let dir = tempdir().unwrap();
315 let path = dir.path().join("EmailService.java");
316 fs::write(
317 &path,
318 r#"
319package com.example.service;
320
321import com.example.model.User;
322
323public class EmailService {}
324"#,
325 )
326 .unwrap();
327
328 let facts = parse_module(&path).unwrap();
329 assert!(
330 facts
331 .imports
332 .iter()
333 .any(|import| import.source == "com.example.model.User" && import.local == "User")
334 );
335 assert!(
336 facts
337 .exports
338 .iter()
339 .any(|export| export.exported == "EmailService")
340 );
341 }
342}