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_modern_module_extensions() {
83 let dir = tempdir().unwrap();
84
85 let mjs_path = dir.path().join("widget.mjs");
86 fs::write(&mjs_path, "export const widget = <div />;").unwrap();
87 parse_module(&mjs_path).unwrap();
88
89 let cts_path = dir.path().join("server.cts");
90 fs::write(&cts_path, "export const server = 1;").unwrap();
91 parse_module(&cts_path).unwrap();
92 }
93
94 #[cfg(feature = "python")]
95 #[test]
96 fn parses_python_imports_and_exports() {
97 let dir = tempdir().unwrap();
98 let path = dir.path().join("email.py");
99 fs::write(
100 &path,
101 r#"
102from ..models import User
103from . import formatting
104
105DEFAULT_TEMPLATE = "welcome"
106
107def send_email(user: User) -> str:
108 return formatting.format_subject(user.email, DEFAULT_TEMPLATE)
109"#,
110 )
111 .unwrap();
112
113 let facts = parse_module(&path).unwrap();
114 assert!(
115 facts
116 .exports
117 .iter()
118 .any(|export| export.exported == "send_email")
119 );
120 assert!(
121 facts
122 .imports
123 .iter()
124 .any(|import| import.source == "..models" && import.local == "User")
125 );
126 assert!(
127 facts
128 .imports
129 .iter()
130 .any(|import| import.source == ".formatting" && import.local == "formatting")
131 );
132 }
133
134 #[cfg(feature = "rust")]
135 #[test]
136 fn parses_rust_imports_exports_and_reexports() {
137 let dir = tempdir().unwrap();
138 let path = dir.path().join("lib.rs");
139 fs::write(
140 &path,
141 r#"
142pub mod services;
143
144use crate::models::User;
145pub use crate::services::email::send_email;
146
147pub struct App;
148"#,
149 )
150 .unwrap();
151
152 let facts = parse_module(&path).unwrap();
153 assert!(facts.exports.iter().any(|export| export.exported == "App"));
154 assert!(
155 facts
156 .imports
157 .iter()
158 .any(|import| import.source == "mod:services")
159 );
160 assert!(
161 facts
162 .imports
163 .iter()
164 .any(|import| import.source == "crate::models" && import.local == "User")
165 );
166 assert!(
167 facts
168 .reexports
169 .iter()
170 .any(|reexport| reexport.source == "crate::services::email"
171 && reexport.exported == "send_email")
172 );
173 }
174
175 #[cfg(feature = "vue")]
176 #[test]
177 fn parses_vue_script_imports_and_default_export() {
178 let dir = tempdir().unwrap();
179 let path = dir.path().join("Button.vue");
180 fs::write(
181 &path,
182 r#"
183<script setup lang="ts">
184import { formatLabel } from './shared'
185const label = formatLabel('save')
186</script>
187<template><button>{{ label }}</button></template>
188"#,
189 )
190 .unwrap();
191
192 let facts = parse_module(&path).unwrap();
193 assert!(
194 facts
195 .imports
196 .iter()
197 .any(|import| import.source == "./shared" && import.local == "formatLabel")
198 );
199 assert!(
200 facts
201 .exports
202 .iter()
203 .any(|export| export.exported == "default")
204 );
205 }
206
207 #[cfg(feature = "svelte")]
208 #[test]
209 fn parses_svelte_script_imports_and_default_export() {
210 let dir = tempdir().unwrap();
211 let path = dir.path().join("Card.svelte");
212 fs::write(
213 &path,
214 r#"
215<script lang="ts">
216 import Button from './Button.vue'
217 export let title = 'Settings'
218</script>
219<Button />
220"#,
221 )
222 .unwrap();
223
224 let facts = parse_module(&path).unwrap();
225 assert!(
226 facts
227 .imports
228 .iter()
229 .any(|import| import.source == "./Button.vue" && import.local == "Button")
230 );
231 assert!(
232 facts
233 .exports
234 .iter()
235 .any(|export| export.exported == "default")
236 );
237 }
238
239 #[cfg(feature = "ruby")]
240 #[test]
241 fn parses_ruby_requires_and_exports() {
242 let dir = tempdir().unwrap();
243 let path = dir.path().join("email_service.rb");
244 fs::write(
245 &path,
246 r#"
247require_relative "../models/user"
248
249class EmailService
250 def self.send_email(email)
251 end
252end
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 == "../models/user")
263 );
264 assert!(
265 facts
266 .exports
267 .iter()
268 .any(|export| export.exported == "EmailService")
269 );
270 assert!(
271 facts
272 .exports
273 .iter()
274 .any(|export| export.exported == "send_email")
275 );
276 }
277
278 #[cfg(feature = "java")]
279 #[test]
280 fn parses_java_imports_and_exports() {
281 let dir = tempdir().unwrap();
282 let path = dir.path().join("EmailService.java");
283 fs::write(
284 &path,
285 r#"
286package com.example.service;
287
288import com.example.model.User;
289
290public class EmailService {}
291"#,
292 )
293 .unwrap();
294
295 let facts = parse_module(&path).unwrap();
296 assert!(
297 facts
298 .imports
299 .iter()
300 .any(|import| import.source == "com.example.model.User" && import.local == "User")
301 );
302 assert!(
303 facts
304 .exports
305 .iter()
306 .any(|export| export.exported == "EmailService")
307 );
308 }
309}