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_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            // The call site is the usage, so the edge must count in the walk.
110            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}