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 = "python")]
17mod python;
18#[cfg(feature = "python")]
19pub(crate) use python::parse_python_module;
20
21#[cfg(feature = "rust")]
22mod rust_lang;
23#[cfg(feature = "rust")]
24pub(crate) use rust_lang::parse_rust_module;
25
26pub fn parse_module(path: &Path) -> Result<ModuleFacts> {
27    let source = fs::read_to_string(path)
28        .with_context(|| format!("failed to read source file {}", path.display()))?;
29
30    crate::language::adapter_for(path).parse(path, &source)
31}
32
33#[cfg(test)]
34mod tests {
35    use std::fs;
36
37    use tempfile::tempdir;
38
39    use super::parse_module;
40
41    #[test]
42    fn parses_js_files_with_jsx() {
43        let dir = tempdir().unwrap();
44        let path = dir.path().join("renderAvatar.js");
45        fs::write(
46            &path,
47            r#"
48import Avatar from '@mui/material/Avatar';
49
50export function renderAvatar(params) {
51  if (params.value == null) {
52    return '';
53  }
54
55  return <Avatar>{params.value.name}</Avatar>;
56}
57"#,
58        )
59        .unwrap();
60
61        let facts = parse_module(&path).unwrap();
62        assert_eq!(facts.imports.len(), 1);
63        assert!(
64            facts
65                .exports
66                .iter()
67                .any(|export| export.exported == "renderAvatar")
68        );
69    }
70
71    #[test]
72    fn parses_dynamic_imports_as_used_namespace_imports() {
73        let dir = tempdir().unwrap();
74        let path = dir.path().join("routes.tsx");
75        fs::write(
76            &path,
77            r#"
78import { lazy } from 'react';
79
80const ChatsPage = lazy(() =>
81  import("@/pages/chats-page").then((m) => ({ default: m.ChatsPage }))
82);
83const Settings = lazy(() => import(`./settings-page`));
84
85export const routes = [ChatsPage, Settings];
86"#,
87        )
88        .unwrap();
89
90        let facts = parse_module(&path).unwrap();
91        for source in ["@/pages/chats-page", "./settings-page"] {
92            let import = facts
93                .imports
94                .iter()
95                .find(|import| import.source == source)
96                .expect("dynamic import should be collected");
97            assert_eq!(import.kind, super::ImportKind::Dynamic);
98            assert_eq!(import.imported, super::ImportTarget::Namespace);
99            // The call site is the usage, so the edge must count in the walk.
100            assert!(facts.used_locals.contains(&import.local));
101        }
102    }
103
104    #[test]
105    fn parses_modern_module_extensions() {
106        let dir = tempdir().unwrap();
107
108        let mjs_path = dir.path().join("widget.mjs");
109        fs::write(&mjs_path, "export const widget = <div />;").unwrap();
110        parse_module(&mjs_path).unwrap();
111
112        let cts_path = dir.path().join("server.cts");
113        fs::write(&cts_path, "export const server = 1;").unwrap();
114        parse_module(&cts_path).unwrap();
115    }
116
117    #[cfg(feature = "python")]
118    #[test]
119    fn parses_python_imports_and_exports() {
120        let dir = tempdir().unwrap();
121        let path = dir.path().join("email.py");
122        fs::write(
123            &path,
124            r#"
125from ..models import User
126from . import formatting
127
128DEFAULT_TEMPLATE = "welcome"
129
130def send_email(user: User) -> str:
131    return formatting.format_subject(user.email, DEFAULT_TEMPLATE)
132"#,
133        )
134        .unwrap();
135
136        let facts = parse_module(&path).unwrap();
137        assert!(
138            facts
139                .exports
140                .iter()
141                .any(|export| export.exported == "send_email")
142        );
143        assert!(
144            facts
145                .imports
146                .iter()
147                .any(|import| import.source == "..models" && import.local == "User")
148        );
149        assert!(
150            facts
151                .imports
152                .iter()
153                .any(|import| import.source == ".formatting" && import.local == "formatting")
154        );
155    }
156
157    #[cfg(feature = "rust")]
158    #[test]
159    fn parses_rust_imports_exports_and_reexports() {
160        let dir = tempdir().unwrap();
161        let path = dir.path().join("lib.rs");
162        fs::write(
163            &path,
164            r#"
165pub mod services;
166
167use crate::models::User;
168pub use crate::services::email::send_email;
169
170pub struct App;
171"#,
172        )
173        .unwrap();
174
175        let facts = parse_module(&path).unwrap();
176        assert!(facts.exports.iter().any(|export| export.exported == "App"));
177        assert!(
178            facts
179                .imports
180                .iter()
181                .any(|import| import.source == "mod:services")
182        );
183        assert!(
184            facts
185                .imports
186                .iter()
187                .any(|import| import.source == "crate::models" && import.local == "User")
188        );
189        assert!(
190            facts
191                .reexports
192                .iter()
193                .any(|reexport| reexport.source == "crate::services::email"
194                    && reexport.exported == "send_email")
195        );
196    }
197
198    #[cfg(feature = "vue")]
199    #[test]
200    fn parses_vue_script_imports_and_default_export() {
201        let dir = tempdir().unwrap();
202        let path = dir.path().join("Button.vue");
203        fs::write(
204            &path,
205            r#"
206<script setup lang="ts">
207import { formatLabel } from './shared'
208const label = formatLabel('save')
209</script>
210<template><button>{{ label }}</button></template>
211"#,
212        )
213        .unwrap();
214
215        let facts = parse_module(&path).unwrap();
216        assert!(
217            facts
218                .imports
219                .iter()
220                .any(|import| import.source == "./shared" && import.local == "formatLabel")
221        );
222        assert!(
223            facts
224                .exports
225                .iter()
226                .any(|export| export.exported == "default")
227        );
228    }
229
230    #[cfg(feature = "svelte")]
231    #[test]
232    fn parses_svelte_script_imports_and_default_export() {
233        let dir = tempdir().unwrap();
234        let path = dir.path().join("Card.svelte");
235        fs::write(
236            &path,
237            r#"
238<script lang="ts">
239  import Button from './Button.vue'
240  export let title = 'Settings'
241</script>
242<Button />
243"#,
244        )
245        .unwrap();
246
247        let facts = parse_module(&path).unwrap();
248        assert!(
249            facts
250                .imports
251                .iter()
252                .any(|import| import.source == "./Button.vue" && import.local == "Button")
253        );
254        assert!(
255            facts
256                .exports
257                .iter()
258                .any(|export| export.exported == "default")
259        );
260    }
261}