1use std::collections::{BTreeMap, BTreeSet};
10
11use crate::{lexical::binding_pattern_names, visit::immediate_children, Node, SNode};
12
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15pub enum NamespaceDemand {
16 Whole,
18 Members(BTreeSet<String>),
20}
21
22impl NamespaceDemand {
23 fn add_member(&mut self, member: &str) {
24 if let Self::Members(members) = self {
25 members.insert(member.to_string());
26 }
27 }
28
29 fn widen(&mut self) {
30 *self = Self::Whole;
31 }
32}
33
34pub fn namespace_import_demands(program: &[SNode]) -> BTreeMap<String, NamespaceDemand> {
42 let mut demands = BTreeMap::new();
43 let mut aliases = BTreeSet::new();
44
45 for node in program {
46 collect_imports(node, &mut aliases, &mut demands);
47 }
48
49 for alias in aliases {
50 let demand = demands
51 .get_mut(&alias)
52 .expect("namespace alias and demand are recorded together");
53 if matches!(demand, NamespaceDemand::Whole) {
54 continue;
55 }
56 if program
57 .iter()
58 .any(|node| conflicts_with_alias(node, &alias))
59 {
60 demand.widen();
61 continue;
62 }
63 for node in program {
64 analyze_node(node, &alias, demand);
65 if matches!(demand, NamespaceDemand::Whole) {
66 break;
67 }
68 }
69 }
70
71 demands
72}
73
74fn collect_imports(
75 node: &SNode,
76 aliases: &mut BTreeSet<String>,
77 demands: &mut BTreeMap<String, NamespaceDemand>,
78) {
79 if let Node::NamespaceImport { alias, is_pub, .. } = &node.node {
80 let first = aliases.insert(alias.clone());
81 let demand = if *is_pub || !first {
82 NamespaceDemand::Whole
83 } else {
84 NamespaceDemand::Members(BTreeSet::new())
85 };
86 demands
87 .entry(alias.clone())
88 .and_modify(NamespaceDemand::widen)
89 .or_insert(demand);
90 }
91 for child in immediate_children(node) {
92 collect_imports(child, aliases, demands);
93 }
94}
95
96fn conflicts_with_alias(node: &SNode, alias: &str) -> bool {
97 let conflicts = match &node.node {
98 Node::ImportDecl { .. } => true,
101 Node::SelectiveImport { names, .. } => names.iter().any(|name| name == alias),
102 Node::NamespaceImport { .. } => false,
103 Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. } => {
104 binding_pattern_names(pattern)
105 .iter()
106 .any(|name| name == alias)
107 }
108 Node::Pipeline { name, params, .. }
109 | Node::FnDecl { name, params, .. }
110 | Node::ToolDecl { name, params, .. } => {
111 name == alias || params.iter().any(|param| param.name == alias)
112 }
113 Node::OverrideDecl { name, params, .. } => {
114 name == alias || params.iter().any(|param| param == alias)
115 }
116 Node::Closure { params, .. } => params.iter().any(|param| param.name == alias),
117 Node::ForIn { pattern, .. } => binding_pattern_names(pattern)
118 .iter()
119 .any(|name| name == alias),
120 Node::TryCatch { error_var, .. } => error_var.as_deref() == Some(alias),
121 Node::Parallel { variable, .. } => variable.as_deref() == Some(alias),
122 Node::SelectExpr { cases, .. } => cases.iter().any(|case| case.variable == alias),
123 Node::EnumDecl { name, .. }
124 | Node::StructDecl { name, .. }
125 | Node::TypeDecl { name, .. }
126 | Node::InterfaceDecl { name, .. }
127 | Node::SkillDecl { name, .. } => name == alias,
128 Node::EvalPackDecl { binding_name, .. } => binding_name == alias,
129 _ => false,
130 };
131 conflicts
132 || immediate_children(node)
133 .into_iter()
134 .any(|child| conflicts_with_alias(child, alias))
135}
136
137fn analyze_node(node: &SNode, alias: &str, demand: &mut NamespaceDemand) {
138 if matches!(demand, NamespaceDemand::Whole) {
139 return;
140 }
141
142 match &node.node {
143 Node::NamespaceImport { .. } => {}
144 Node::PropertyAccess { object, property }
145 | Node::OptionalPropertyAccess { object, property }
146 if is_identifier(object, alias) =>
147 {
148 demand.add_member(property);
149 }
150 Node::MethodCall {
151 object,
152 method,
153 args,
154 }
155 | Node::OptionalMethodCall {
156 object,
157 method,
158 args,
159 } if is_identifier(object, alias) => {
160 demand.add_member(method);
161 for arg in args {
162 analyze_node(arg, alias, demand);
163 }
164 }
165 Node::Assignment { target, value, .. } => {
166 if contains_alias(target, alias) {
167 demand.widen();
168 } else {
169 analyze_node(value, alias, demand);
170 }
171 }
172 Node::Identifier(name) if name == alias => demand.widen(),
173 Node::FunctionCall { name, .. } if name == alias => demand.widen(),
174 Node::EnumConstruct { enum_name, .. } if enum_name == alias => demand.widen(),
175 Node::StructConstruct { struct_name, .. } if struct_name == alias => demand.widen(),
176 _ => {
177 for child in immediate_children(node) {
178 analyze_node(child, alias, demand);
179 if matches!(demand, NamespaceDemand::Whole) {
180 break;
181 }
182 }
183 }
184 }
185}
186
187fn is_identifier(node: &SNode, name: &str) -> bool {
188 matches!(&node.node, Node::Identifier(candidate) if candidate == name)
189}
190
191fn contains_alias(node: &SNode, alias: &str) -> bool {
192 match &node.node {
193 Node::Identifier(name) => name == alias,
194 Node::FunctionCall { name, .. } => {
195 name == alias
196 || immediate_children(node)
197 .into_iter()
198 .any(|child| contains_alias(child, alias))
199 }
200 Node::EnumConstruct { enum_name, .. } => {
201 enum_name == alias
202 || immediate_children(node)
203 .into_iter()
204 .any(|child| contains_alias(child, alias))
205 }
206 Node::StructConstruct { struct_name, .. } => {
207 struct_name == alias
208 || immediate_children(node)
209 .into_iter()
210 .any(|child| contains_alias(child, alias))
211 }
212 _ => immediate_children(node)
213 .into_iter()
214 .any(|child| contains_alias(child, alias)),
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::parse_source;
222
223 fn demand(source: &str, alias: &str) -> NamespaceDemand {
224 namespace_import_demands(&parse_source(source).expect("source parses"))[alias].clone()
225 }
226
227 fn members(names: &[&str]) -> NamespaceDemand {
228 NamespaceDemand::Members(names.iter().map(|name| (*name).to_string()).collect())
229 }
230
231 #[test]
232 fn collects_static_property_and_method_members() {
233 assert_eq!(
234 demand(
235 r#"
236 import * as ui from "./ui.harn"
237 const page = ui.page
238 ui.render(page)
239 ui?.close()
240 "#,
241 "ui",
242 ),
243 members(&["close", "page", "render"])
244 );
245 }
246
247 #[test]
248 fn unused_private_namespace_has_empty_member_demand() {
249 assert_eq!(
250 demand(r#"import * as ui from "./ui.harn""#, "ui"),
251 members(&[])
252 );
253 }
254
255 #[test]
256 fn alias_escape_and_dynamic_access_require_whole_namespace() {
257 for source in [
258 r#"import * as ui from "./ui.harn"
259 return ui"#,
260 r#"import * as ui from "./ui.harn"
261 const name = "page"
262 return ui[name]"#,
263 ] {
264 assert_eq!(demand(source, "ui"), NamespaceDemand::Whole);
265 }
266 }
267
268 #[test]
269 fn public_import_and_shadowing_ambiguity_require_whole_namespace() {
270 for source in [
271 r#"pub import * as ui from "./ui.harn""#,
272 r#"import * as ui from "./ui.harn"
273 fn render(ui) { return ui.page }
274 return ui.page"#,
275 r#"import * as ui from "./ui.harn"
276 import "./other.harn"
277 return ui.page"#,
278 ] {
279 assert_eq!(demand(source, "ui"), NamespaceDemand::Whole);
280 }
281 }
282
283 #[test]
284 fn assignment_through_namespace_requires_whole_namespace() {
285 assert_eq!(
286 demand(
287 r#"import * as ui from "./ui.harn"
288 ui.page = "replacement""#,
289 "ui",
290 ),
291 NamespaceDemand::Whole
292 );
293 }
294}