Skip to main content

blast_radius/parse/
mod.rs

1use 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}