gruel_air/sema/visibility.rs
1//! Visibility checking for module system.
2//!
3//! This module implements the visibility rules defined in ADR-0026:
4//! - `pub` items are always accessible
5//! - Private items are accessible if the files are in the same directory module
6
7use std::path::{Path, PathBuf};
8
9use gruel_error::{CompileError, CompileResult, ErrorKind};
10use gruel_span::FileId;
11
12use crate::types::EnumId;
13
14use super::Sema;
15
16impl Sema<'_> {
17 /// Check if the accessing file can see a private item from the target file.
18 ///
19 /// Visibility rules (per ADR-0026):
20 /// - `pub` items are always accessible
21 /// - Private items are accessible if the files are in the same directory module
22 ///
23 /// Directory module membership includes:
24 /// - Files directly in the directory (e.g., `utils/strings.gruel` is in `utils`)
25 /// - Facade files for the directory (e.g., `_utils.gruel` is in `utils` module)
26 ///
27 /// Returns true if the item is accessible.
28 pub(crate) fn is_accessible(
29 &self,
30 accessing_file_id: FileId,
31 target_file_id: FileId,
32 is_pub: bool,
33 ) -> bool {
34 // Public items are always accessible
35 if is_pub {
36 return true;
37 }
38
39 // Get paths for both files
40 let accessing_path = self.get_file_path(accessing_file_id);
41 let target_path = self.get_file_path(target_file_id);
42
43 // If we can't determine the paths, be permissive (for single-file mode or tests)
44 match (accessing_path, target_path) {
45 (Some(acc), Some(tgt)) => {
46 // Get the "module identity" for each file.
47 // For a regular file like `utils/strings.gruel`, the module is `utils/`
48 // For a facade file like `_utils.gruel`, the module is `utils/` (the directory it represents)
49 let acc_module = get_module_identity(Path::new(acc));
50 let tgt_module = get_module_identity(Path::new(tgt));
51
52 acc_module == tgt_module
53 }
54 // If either path is unknown, allow access (e.g., synthetic types, single-file mode)
55 _ => true,
56 }
57 }
58
59 /// Resolve an enum type through a module reference.
60 ///
61 /// Used for qualified enum paths like `module.EnumName::Variant` in match patterns.
62 /// Checks visibility: private enums are only accessible from the same directory.
63 pub fn resolve_enum_through_module(
64 &self,
65 _module_ref: gruel_rir::InstRef,
66 type_name: lasso::Spur,
67 span: gruel_span::Span,
68 ) -> CompileResult<EnumId> {
69 let type_name_str = self.interner.resolve(&type_name);
70
71 // Try to find the enum globally
72 let enum_id = self.enums.get(&type_name).copied().ok_or_else(|| {
73 CompileError::new(ErrorKind::UnknownEnumType(type_name_str.to_string()), span)
74 })?;
75
76 // Check visibility
77 let enum_def = self.type_pool.enum_def(enum_id);
78 let accessing_file_id = span.file_id;
79 let target_file_id = enum_def.file_id;
80
81 if !self.is_accessible(accessing_file_id, target_file_id, enum_def.is_pub) {
82 return Err(CompileError::new(
83 ErrorKind::PrivateMemberAccess {
84 item_kind: "enum".to_string(),
85 name: type_name_str.to_string(),
86 },
87 span,
88 ));
89 }
90
91 Ok(enum_id)
92 }
93}
94
95/// Get the module identity for a file path.
96///
97/// - For regular files: returns the parent directory
98/// - For facade files (`_foo.gruel`): returns the corresponding directory (`foo/`)
99///
100/// This allows facade files to be treated as part of their corresponding directory module.
101pub(crate) fn get_module_identity(path: &Path) -> Option<PathBuf> {
102 let parent = path.parent()?;
103 let file_stem = path.file_stem()?.to_str()?;
104
105 // Check if this is a facade file (starts with underscore)
106 if let Some(module_name) = file_stem.strip_prefix('_') {
107 // Facade file: _utils.gruel -> parent/utils
108 // Strip the leading underscore
109 Some(parent.join(module_name))
110 } else {
111 // Regular file: the module is just the parent directory
112 Some(parent.to_path_buf())
113 }
114}