Skip to content

Commit c29737f

Browse files
authored
Merge pull request #673 from gauge-sh/interface-visibility
Support 'visibility' in interfaces
2 parents 1036d5c + f961487 commit c29737f

File tree

13 files changed

+74
-19
lines changed

13 files changed

+74
-19
lines changed

public/tach-domain-toml-schema.json

+8
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@
250250
"items": { "type": "string" },
251251
"description": "List of regex patterns that match modules which adopt this interface"
252252
},
253+
"visibility": {
254+
"type": "array",
255+
"items": {
256+
"type": "string"
257+
},
258+
"default": null,
259+
"description": "List of visibility patterns"
260+
},
253261
"data_types": {
254262
"type": "string",
255263
"default": "all",

public/tach-toml-schema.json

+8
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@
180180
"items": { "type": "string" },
181181
"description": "List of regex patterns that match modules which adopt this interface"
182182
},
183+
"visibility": {
184+
"type": "array",
185+
"items": {
186+
"type": "string"
187+
},
188+
"default": null,
189+
"description": "List of visibility patterns"
190+
},
183191
"data_types": {
184192
"type": "string",
185193
"default": "all",

python/tach/extension.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class InterfaceConfig:
127127
expose: list[str]
128128
# 'from' in tach.toml
129129
from_modules: list[str]
130+
visibility: list[str] | None
130131
data_types: InterfaceDataTypes
131132

132133
CacheBackend = Literal["disk"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from module1.api import something

python/tests/example/many_features/other_src_root/module5/tach.domain.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[root]
2-
depends_on = []
2+
depends_on = ["//module1.api"]
33
layer = "mid"
44

55
[[interfaces]]

python/tests/example/many_features/real_src/module1/tach.domain.toml

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,19 @@ layer = "hightest"
1010
[[modules]]
1111
path = "api"
1212
depends_on = ["**"]
13-
visibility = ["<domain_root>"]
13+
visibility = ["<domain_root>", "//module5"]
1414
layer = "mid"
1515

16+
[[interfaces]]
17+
expose = [".*"]
18+
from = ["api"]
19+
visibility = ["<domain_root>"]
20+
21+
[[interfaces]]
22+
expose = ["MyAPI"]
23+
from = ["api"]
24+
visibility = ["//module5"]
25+
1626
[[modules]]
1727
path = "submodule1"
1828
depends_on = []

python/tests/test_check.py

+6
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ def test_many_features_example_dir(example_dir, capfd):
315315
"module3.anything",
316316
"public interface",
317317
),
318+
(
319+
"[FAIL]",
320+
"other_src_root/module5/__init__.py",
321+
"module1.api.something",
322+
"public interface",
323+
),
318324
]
319325

320326
expected_dependencies = [

src/checks/interface.rs

+21-17
Original file line numberDiff line numberDiff line change
@@ -56,35 +56,35 @@ impl<'a> InterfaceChecker<'a> {
5656
Ok(self)
5757
}
5858

59-
fn check_member(&self, member: &str, module_path: &str) -> InterfaceCheckResult {
59+
fn check_member(
60+
&self,
61+
member: &str,
62+
definition_module: &str,
63+
usage_module: &str,
64+
) -> InterfaceCheckResult {
6065
if member.is_empty() {
6166
return InterfaceCheckResult::TopLevelModule;
6267
}
6368

64-
let matching_interfaces = self.interfaces.get_interfaces(module_path);
69+
let matching_interfaces = self.interfaces.get_interfaces(definition_module);
6570

6671
if matching_interfaces.is_empty() {
6772
return InterfaceCheckResult::NoInterfaces;
6873
}
6974

70-
let mut is_exposed = false;
7175
for interface in matching_interfaces {
72-
if interface.expose.iter().any(|re| re.is_match(member)) {
73-
is_exposed = true;
76+
if interface.is_exposed_to(member, usage_module) {
77+
return InterfaceCheckResult::Exposed {
78+
type_check_result: self
79+
.type_check_cache
80+
.as_ref()
81+
.map(|cache| cache.get_result(member))
82+
.unwrap_or(TypeCheckResult::Unknown),
83+
};
7484
}
7585
}
7686

77-
if !is_exposed {
78-
return InterfaceCheckResult::NotExposed;
79-
}
80-
81-
InterfaceCheckResult::Exposed {
82-
type_check_result: self
83-
.type_check_cache
84-
.as_ref()
85-
.map(|cache| cache.get_result(member))
86-
.unwrap_or(TypeCheckResult::Unknown),
87-
}
87+
InterfaceCheckResult::NotExposed
8888
}
8989

9090
fn check_interfaces(
@@ -113,7 +113,11 @@ impl<'a> InterfaceChecker<'a> {
113113
.strip_prefix(&dependency_module_config.path)
114114
.and_then(|s| s.strip_prefix('.'))
115115
.unwrap_or("");
116-
let check_result = self.check_member(import_member, &dependency_module_config.path);
116+
let check_result = self.check_member(
117+
import_member,
118+
&dependency_module_config.path,
119+
&file_module.module_config().path,
120+
);
117121
match check_result {
118122
InterfaceCheckResult::NotExposed => Ok(vec![Diagnostic::new_located_error(
119123
file_module.relative_file_path().to_path_buf(),

src/config/domain.rs

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ impl Resolvable<InterfaceConfig> for InterfaceConfig {
182182
_ => format!("{}.{}", location.mod_path, mod_path),
183183
})
184184
.collect(),
185+
visibility: self.visibility.clone().map(|vis| vis.resolve(location)),
185186
data_types: self.data_types.clone(),
186187
}
187188
}

src/config/interfaces.rs

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub struct InterfaceConfig {
4343
skip_serializing_if = "is_default_from_modules"
4444
)]
4545
pub from_modules: Vec<String>,
46+
#[serde(default)]
47+
pub visibility: Option<Vec<String>>,
4648
#[serde(default, skip_serializing_if = "InterfaceDataTypes::is_default")]
4749
pub data_types: InterfaceDataTypes,
4850
}

src/interfaces/compiled.rs

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use regex::Regex;
44
#[derive(Debug, Clone)]
55
pub struct CompiledInterface {
66
pub from_modules: Vec<Regex>,
7+
pub visibility: Option<Vec<String>>,
78
pub expose: Vec<Regex>,
89
pub data_types: InterfaceDataTypes,
910
}
@@ -19,6 +20,16 @@ impl CompiledInterface {
1920
self.expose.iter().any(|regex| regex.is_match(member_name))
2021
}
2122

23+
pub fn is_visible_to(&self, module_path: &str) -> bool {
24+
self.visibility.as_ref().map_or(true, |visibility| {
25+
visibility.iter().any(|v| v == module_path)
26+
})
27+
}
28+
29+
pub fn is_exposed_to(&self, member: &str, module_path: &str) -> bool {
30+
self.matches_member(member) && self.is_visible_to(module_path)
31+
}
32+
2233
pub fn should_type_check(&self, module_path: &str) -> bool {
2334
self.data_types != InterfaceDataTypes::All && self.matches_module(module_path)
2435
}
@@ -45,6 +56,7 @@ impl<'a> CompiledInterfaces {
4556
.iter()
4657
.map(|pattern| Regex::new(&format!("^{}$", pattern)).unwrap())
4758
.collect(),
59+
visibility: interface.visibility.clone(),
4860
})
4961
.collect();
5062

src/interfaces/data_types.rs

+1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ mod tests {
333333
InterfaceConfig {
334334
expose: vec![".*".to_string()],
335335
from_modules: vec!["my_module".to_string()],
336+
visibility: None,
336337
data_types: InterfaceDataTypes::Primitive,
337338
}
338339
}

src/parsing/config.rs

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ fn migrate_strict_mode_to_interfaces(filepath: &Path, config: &mut ProjectConfig
7171
interfaces.push(InterfaceConfig {
7272
expose: interface_members,
7373
from_modules: vec![module.path.clone()],
74+
visibility: None,
7475
data_types: InterfaceDataTypes::All,
7576
});
7677
}

0 commit comments

Comments
 (0)