Skip to main content

fluidattacks_blends_domain/stack/
imports.rs

1use alloc::collections::BTreeMap;
2use alloc::string::String;
3use alloc::vec::Vec;
4
5use crate::stack::module_path::normalize_module_path;
6use crate::stack::reexports::reexport_sites;
7use crate::stack::requires::require_sites;
8use crate::stack::ImportBinding;
9use crate::syntax::{SyntaxGraph, SyntaxNode};
10use crate::{Language, NodeId};
11
12/// Who can look up the name an [`ImportSite`] binds.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14#[non_exhaustive]
15pub enum BindingReach {
16    /// Code in the site's scope, as with an import.
17    Scope,
18    /// Only modules importing the site's module, as with a re-export.
19    Importers,
20}
21
22/// One import or re-export resolved to what it binds.
23#[derive(Clone, PartialEq, Eq, Debug)]
24#[non_exhaustive]
25pub struct ImportSite {
26    /// Id of the AST node holding the bound name.
27    pub node: NodeId,
28    /// Id of the scope the binding is visible in.
29    pub scope: NodeId,
30    /// The local name the import binds.
31    pub symbol: String,
32    /// What module (and symbol, if any) that name is bound to.
33    pub binding: ImportBinding,
34    /// Who can look that name up.
35    pub reach: BindingReach,
36}
37
38/// Every import and re-export site in `syntax`, for the languages this resolves imports for.
39///
40/// Returns an empty list for a `language` this has no import reader for.
41#[must_use]
42pub fn import_sites(syntax: &SyntaxGraph, language: Language) -> Vec<ImportSite> {
43    let read_binding = match language {
44        Language::Python => python_binding,
45        Language::JavaScript | Language::TypeScript => script_binding,
46        _ => return Vec::new(),
47    };
48    let bound_names: BTreeMap<NodeId, (NodeId, &str)> = syntax
49        .symbol_index
50        .iter()
51        .flat_map(|(symbol, scopes)| {
52            scopes.iter().flat_map(move |(scope, nodes)| {
53                nodes
54                    .iter()
55                    .map(move |node| (*node, (*scope, symbol.as_str())))
56            })
57        })
58        .collect();
59    let mut sites: Vec<ImportSite> = bound_names
60        .iter()
61        .filter_map(|(node, (scope, symbol))| {
62            let binding = read_binding(syntax.nodes.get(node)?, symbol)?;
63            Some(ImportSite {
64                node: *node,
65                scope: *scope,
66                symbol: String::from(*symbol),
67                binding,
68                reach: BindingReach::Scope,
69            })
70        })
71        .collect();
72    if matches!(language, Language::JavaScript | Language::TypeScript) {
73        sites.extend(require_sites(syntax, &bound_names));
74        sites.extend(reexport_sites(syntax));
75        sites.sort_by_key(|site| site.node);
76    }
77    sites
78}
79
80fn python_binding(node: &SyntaxNode, symbol: &str) -> Option<ImportBinding> {
81    let SyntaxNode::Import {
82        expression: Some(expression),
83        alias,
84        module_name,
85        ..
86    } = node
87    else {
88        return None;
89    };
90    let Some(module_name) = module_name else {
91        let module = if alias.is_some() {
92            expression.as_str()
93        } else {
94            symbol
95        };
96        return Some(module_binding(module, 0));
97    };
98    let module = module_name.trim_start_matches('.');
99    let imported = expression.rsplit('.').next().unwrap_or(expression);
100    Some(symbol_binding(
101        module,
102        level_of(module_name.len().saturating_sub(module.len())),
103        imported,
104        symbol,
105    ))
106}
107
108fn script_binding(node: &SyntaxNode, symbol: &str) -> Option<ImportBinding> {
109    match node {
110        SyntaxNode::Import {
111            alias: Some(_),
112            module_name: Some(specifier),
113            import_type,
114            ..
115        } => {
116            let (module, relative_level) = script_module(specifier);
117            match import_type.as_deref() {
118                None => Some(symbol_binding(&module, relative_level, "default", symbol)),
119                Some("namespace_import" | "import_require_clause") => {
120                    Some(module_binding(&module, relative_level))
121                }
122                Some(_) => None,
123            }
124        }
125        SyntaxNode::ModuleImport {
126            expression,
127            module_name: Some(specifier),
128            ..
129        } => {
130            let imported = expression
131                .strip_prefix(specifier.as_str())?
132                .strip_prefix('.')?;
133            let (module, relative_level) = script_module(specifier);
134            Some(symbol_binding(&module, relative_level, imported, symbol))
135        }
136        _ => None,
137    }
138}
139
140pub(super) fn script_module(specifier: &str) -> (String, u32) {
141    let mut segments = specifier.split('/').peekable();
142    let mut relative = false;
143    let mut parents = 0_usize;
144    while let Some(segment) = segments.next_if(|segment| matches!(*segment, "." | "..")) {
145        relative = true;
146        parents = parents.saturating_add(usize::from(segment == ".."));
147    }
148    let module = normalize_module_path(&segments.collect::<Vec<_>>().join("/"));
149    let relative_level = if relative {
150        level_of(parents.saturating_add(1))
151    } else {
152        0
153    };
154    (module, relative_level)
155}
156
157pub(super) fn module_binding(module: &str, relative_level: u32) -> ImportBinding {
158    ImportBinding {
159        module_stem: String::from(module),
160        source_symbol: None,
161        relative_level,
162        binds_module: true,
163    }
164}
165
166pub(super) fn symbol_binding(
167    module: &str,
168    relative_level: u32,
169    imported: &str,
170    symbol: &str,
171) -> ImportBinding {
172    ImportBinding {
173        module_stem: String::from(module),
174        source_symbol: (imported != symbol).then(|| String::from(imported)),
175        relative_level,
176        binds_module: false,
177    }
178}
179
180fn level_of(depth: usize) -> u32 {
181    u32::try_from(depth).unwrap_or(u32::MAX)
182}