1use std::collections::{HashMap, hash_map::Entry};
2use wgsl_parse::{SyntaxNode, syntax::*};
3
4use crate::{
5 error::UsageError,
6 pass::{Imports, Visit, flatten_imports, imported_item_path},
7};
8
9#[derive(Clone)]
10pub struct Module {
11 pub syntax: TranslationUnit,
12 pub path: ModulePath,
13 pub imports: Imports,
14}
15
16impl Module {
17 pub fn new(path: ModulePath, syntax: TranslationUnit) -> Self {
18 let imports = flatten_imports(&syntax.imports, &path);
19 Self {
20 syntax,
21 path,
22 imports,
23 }
24 }
25}
26
27#[derive(Default, Clone, Debug)]
28pub struct UsedItems {
29 used_items: HashMap<ModulePath, HashMap<Ident, Visibility>>,
31}
32
33impl std::fmt::Display for UsedItems {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 use itertools::Itertools;
37 for (path, items) in self.iter() {
38 writeln!(
39 f,
40 "{path} -> {}",
41 items.keys().map(|ident| ident.to_string()).format(", ")
42 )?;
43 }
44 Ok(())
45 }
46}
47
48impl UsedItems {
49 pub fn new() -> Self {
50 Self {
51 used_items: Default::default(),
52 }
53 }
54
55 pub fn get_module(&self, path: &ModulePath) -> Option<&HashMap<Ident, Visibility>> {
56 self.used_items.get(path)
57 }
58
59 pub fn contains_module(&self, path: &ModulePath) -> bool {
60 self.used_items.contains_key(path)
61 }
62
63 pub fn get_ident(&self, path: &ModulePath, ident: &Ident) -> Option<Visibility> {
64 self.used_items
65 .get(path)
66 .and_then(|items| items.get(ident).copied())
67 }
68
69 pub fn get_name(&self, path: &ModulePath, name: &str) -> Option<(Ident, Visibility)> {
70 self.used_items.get(path).and_then(|items| {
71 items
72 .iter()
73 .find(|(ident, _vis)| &**ident.name() == name)
74 .map(|(ident, vis)| (ident.clone(), *vis))
75 })
76 }
77
78 pub fn insert_module(&mut self, path: ModulePath, idents: HashMap<Ident, Visibility>) -> bool {
80 match self.used_items.entry(path) {
81 Entry::Occupied(_) => false,
82 Entry::Vacant(entry) => {
83 entry.insert(idents);
84 true
85 }
86 }
87 }
88
89 pub fn insert_ident(&mut self, path: ModulePath, ident: Ident, visibility: Visibility) -> bool {
92 let entry = self.used_items.entry(path.clone()).or_default();
93
94 match entry.entry(ident) {
95 Entry::Occupied(mut entry) => {
96 if *entry.get() < visibility {
97 entry.insert(visibility);
98 }
99 false
100 }
101 Entry::Vacant(entry) => {
102 entry.insert(visibility);
103 true
104 }
105 }
106 }
107
108 pub fn remove_module(&mut self, path: &ModulePath) -> bool {
110 self.used_items.remove(path).is_some()
111 }
112
113 pub fn is_empty(&mut self) -> bool {
114 self.used_items.is_empty()
115 }
116
117 pub fn iter(&self) -> impl Iterator<Item = (&ModulePath, &HashMap<Ident, Visibility>)> {
118 self.used_items.iter()
119 }
120}
121
122pub fn module_usage_analysis(
128 module: &Module,
129 already_used: &mut UsedItems,
130 to_analyze: &mut UsedItems,
131 ignore_visibility: bool,
132) -> Result<(), UsageError> {
133 if already_used.contains_module(&module.path) {
134 return Ok(());
135 }
136
137 already_used.insert_module(module.path.clone(), Default::default());
138 let const_asserts = module
139 .syntax
140 .global_declarations
141 .iter()
142 .filter(|decl| decl.is_const_assert());
143
144 for decl in const_asserts {
145 decl_usage_analysis(module, decl, already_used, to_analyze, ignore_visibility)?;
146 }
147
148 Ok(())
149}
150
151pub fn usage_analysis(
165 module: &Module,
166 decl_name: &str,
167 min_vis: Visibility,
168 already_used: &mut UsedItems,
169 to_analyze: &mut UsedItems,
170 ignore_visibility: bool,
171) -> Result<(), UsageError> {
172 if let Some((decl_ident, decl_vis)) = already_used.get_name(&module.path, decl_name) {
173 if !ignore_visibility && decl_vis < min_vis {
174 return Err(UsageError::Visibility {
175 orig: None,
176 decl: (module.path.clone(), decl_ident),
177 min_vis,
178 decl_vis,
179 });
180 }
181 } else {
182 if let Some(decl) = module.syntax.global_declarations.iter().find(|decl| {
183 decl.ident()
184 .is_some_and(|ident| &**ident.name() == decl_name)
185 }) {
186 if !ignore_visibility && decl.visibility() < min_vis {
188 return Err(UsageError::Visibility {
189 orig: None,
190 decl: (
191 module.path.clone(),
192 decl.ident().unwrap(),
193 ),
194 min_vis,
195 decl_vis: decl.visibility(),
196 });
197 } else {
198 decl_usage_analysis(module, decl, already_used, to_analyze, ignore_visibility)?;
199 }
200 } else if let Some((decl_ident, item)) = module
201 .imports
202 .iter()
203 .find(|(ident, _)| *ident.name() == decl_name)
204 {
205 if !ignore_visibility && item.visibility < min_vis {
207 return Err(UsageError::Visibility {
208 orig: None,
209 decl: (module.path.clone(), decl_ident.clone()),
210 min_vis,
211 decl_vis: item.visibility,
212 });
213 } else {
214 to_analyze.insert_ident(item.path.clone(), item.ident.clone(), item.visibility);
215 }
216 } else {
217 return Err(UsageError::NotFound(
218 module.path.clone(),
219 decl_name.to_string(),
220 ));
221 }
222 }
223 Ok(())
224}
225
226fn decl_usage_analysis(
228 module: &Module,
229 decl: &GlobalDeclaration,
230 already_used: &mut UsedItems,
231 to_analyze: &mut UsedItems,
232 ignore_visibility: bool,
233) -> Result<(), UsageError> {
234 if decl.ident().is_some_and(|ident| {
235 !already_used.insert_ident(module.path.clone(), ident, decl.visibility())
236 }) {
237 return Ok(());
239 }
240
241 let mut res = Ok(());
243
244 Visit::<TypeExpression>::visit_rec(decl, &mut |ty_expr| {
245 if let Some((import_path, import_ident)) =
247 imported_item_path(ty_expr, &module.path, &module.imports)
248 {
249 let min_vis = match import_path.origin {
250 PathOrigin::Absolute | PathOrigin::Relative(_) => Visibility::Package,
251 PathOrigin::Package(_) => Visibility::Public,
252 };
253 if let Some((decl_ident, decl_vis)) =
254 already_used.get_name(&import_path, &ty_expr.ident.name())
255 {
256 if !ignore_visibility && decl_vis < min_vis {
257 res = Err(UsageError::Visibility {
258 orig: decl.ident().map(|ident| (module.path.clone(), ident)),
259 decl: (import_path, decl_ident),
260 min_vis,
261 decl_vis,
262 });
263 }
264 } else {
265 to_analyze.insert_ident(import_path, import_ident, min_vis);
266 }
267 }
268 else {
270 let decl = module
273 .syntax
274 .global_declarations
275 .iter()
276 .find(|decl| decl.ident().is_some_and(|ident| ident == ty_expr.ident));
277
278 if let Some(decl) = decl
279 && let Err(err) =
280 decl_usage_analysis(module, decl, already_used, to_analyze, ignore_visibility)
281 {
282 res = Err(err);
283 }
284 }
285 });
286
287 res
288}
289
290#[test]
291fn test_ignore_visibility() {
292 let module = Module::new(
293 "library".parse().unwrap(),
294 "private fn value() {}".parse().unwrap(),
295 );
296 for ignore_visibility in [false, true] {
297 assert_eq!(
298 usage_analysis(
299 &module,
300 "value",
301 Visibility::Public,
302 &mut UsedItems::new(),
303 &mut UsedItems::new(),
304 ignore_visibility,
305 )
306 .is_ok(),
307 ignore_visibility,
308 );
309 }
310}