use std::path::Path;
use anyhow::Result;
use syn::Item;
use super::boundary;
use super::boundary::ParentBoundary;
use super::exports;
use super::exports::ParentFacadeExports;
use crate::compiler::settings::DriverSettings;
use crate::compiler::source_cache;
use crate::compiler::source_cache::ExtractedPaths;
use crate::compiler::source_cache::PathOrigin;
use crate::compiler::source_cache::SourceCache;
use crate::compiler::source_cache::UseRename;
use crate::rust_syntax::MODULE_PATH_SEPARATOR;
use crate::rust_syntax::PATH_KEYWORD_CRATE;
use crate::rust_syntax::PATH_KEYWORD_SELF;
use crate::rust_syntax::PATH_KEYWORD_SUPER;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParentFacadeUsage {
Unused,
UsedInsideParentSubtreeByRelativeImport,
UsedInsideParentSubtreeByRelativePath,
UsedInsideParentSubtreeByCrateImport,
UsedInsideParentSubtreeByCratePath,
UsedOutsideParentSubtree,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParentFacadeReferenceUsage {
None,
Import(PathOrigin),
DirectPath(PathOrigin),
}
pub(super) fn scan_facade_usage(
source_cache: &SourceCache,
settings: &DriverSettings,
source_root: &Path,
parent_boundary: &ParentBoundary,
exported_names: &ParentFacadeExports,
) -> Result<ParentFacadeUsage> {
let mut usage = ParentFacadeUsage::Unused;
for source_path in source_cache.source_files_under(source_root) {
if source_path == parent_boundary.boundary_file {
continue;
}
let Some(current_module_path) =
source_cache::module_path_from_source_file(source_root, source_path)
else {
continue;
};
let Some(extracted) = source_cache.extracted_paths(source_path) else {
continue;
};
match source_references_parent_export(
extracted,
¤t_module_path,
&parent_boundary.module_path,
&exported_names.explicit,
) {
ParentFacadeReferenceUsage::None => {},
ParentFacadeReferenceUsage::Import(PathOrigin::Relative) => {
if matches!(usage, ParentFacadeUsage::Unused)
&& source_path.starts_with(&parent_boundary.subtree_root)
{
usage = ParentFacadeUsage::UsedInsideParentSubtreeByRelativeImport;
} else if !source_path.starts_with(&parent_boundary.subtree_root) {
usage = ParentFacadeUsage::UsedOutsideParentSubtree;
break;
}
},
ParentFacadeReferenceUsage::Import(PathOrigin::Crate) => {
if matches!(usage, ParentFacadeUsage::Unused)
&& source_path.starts_with(&parent_boundary.subtree_root)
{
usage = ParentFacadeUsage::UsedInsideParentSubtreeByCrateImport;
} else if !source_path.starts_with(&parent_boundary.subtree_root) {
usage = ParentFacadeUsage::UsedOutsideParentSubtree;
break;
}
},
ParentFacadeReferenceUsage::DirectPath(PathOrigin::Relative) => {
if source_path.starts_with(&parent_boundary.subtree_root) {
usage = ParentFacadeUsage::UsedInsideParentSubtreeByRelativePath;
} else {
usage = ParentFacadeUsage::UsedOutsideParentSubtree;
break;
}
},
ParentFacadeReferenceUsage::DirectPath(PathOrigin::Crate) => {
if source_path.starts_with(&parent_boundary.subtree_root) {
usage = ParentFacadeUsage::UsedInsideParentSubtreeByCratePath;
} else {
usage = ParentFacadeUsage::UsedOutsideParentSubtree;
break;
}
},
}
}
if !matches!(usage, ParentFacadeUsage::UsedOutsideParentSubtree)
&& workspace_source_mentions_parent_export_literal(
source_cache,
settings,
parent_boundary,
&exported_names.explicit,
)?
{
usage = ParentFacadeUsage::UsedOutsideParentSubtree;
}
Ok(usage)
}
pub fn workspace_source_mentions_parent_export_literal(
source_cache: &SourceCache,
settings: &DriverSettings,
parent_boundary: &ParentBoundary,
exported_names: &[String],
) -> Result<bool> {
if settings.config_root == settings.package_root {
return Ok(false);
}
if parent_boundary.module_path.is_empty() {
return Ok(false);
}
let module_prefix = format!(
"{PATH_KEYWORD_CRATE}{MODULE_PATH_SEPARATOR}{}",
parent_boundary.module_path.join(MODULE_PATH_SEPARATOR)
);
let findings_root = settings
.findings_dir
.parent()
.map_or_else(|| settings.findings_dir.clone(), Path::to_path_buf);
for file in source_cache.source_files_under(&settings.config_root) {
if file.starts_with(&settings.package_root)
|| file.starts_with(&settings.findings_dir)
|| file.starts_with(&findings_root)
{
continue;
}
let source = source_cache.read_source(file)?;
if exported_names.iter().any(|name| {
let pattern = format!("{module_prefix}::{name}");
source.contains(&pattern)
}) {
return Ok(true);
}
}
Ok(false)
}
pub fn source_references_parent_export(
extracted: &ExtractedPaths,
current_module_path: &[String],
module_path: &[String],
exported_names: &[String],
) -> ParentFacadeReferenceUsage {
for (raw, origin) in &extracted.expr_paths {
if matching_origin_indexed(
raw,
*origin,
current_module_path,
module_path,
exported_names,
)
.is_some()
{
return ParentFacadeReferenceUsage::DirectPath(*origin);
}
if let Some(resolved) = resolve_alias_expr_path(raw, &extracted.use_renames)
&& matching_origin_indexed(
&resolved,
*origin,
current_module_path,
module_path,
exported_names,
)
.is_some()
{
return ParentFacadeReferenceUsage::DirectPath(*origin);
}
}
let mut import_usage = ParentFacadeReferenceUsage::None;
for (raw, origin) in &extracted.use_paths {
if matching_origin_indexed(
raw,
*origin,
current_module_path,
module_path,
exported_names,
)
.is_some()
{
import_usage =
merge_reference_usage(import_usage, ParentFacadeReferenceUsage::Import(*origin));
}
}
import_usage
}
fn resolve_alias_expr_path(raw: &[String], renames: &[UseRename]) -> Option<Vec<String>> {
let first = raw.first()?;
let rename = renames.iter().find(|rename| rename.alias == *first)?;
let mut resolved = rename.original_path.clone();
resolved.extend(raw[1..].iter().cloned());
Some(resolved)
}
fn matching_origin_indexed(
raw: &[String],
origin: PathOrigin,
current_module_path: &[String],
module_path: &[String],
exported_names: &[String],
) -> Option<PathOrigin> {
resolve_module_relative_paths(raw, current_module_path)
.into_iter()
.find(|segments| {
segments.len() == module_path.len() + 1
&& segments[..module_path.len()] == *module_path
&& exported_names
.iter()
.any(|name| name == &segments[module_path.len()])
})
.map(|_| origin)
}
pub(super) fn resolve_module_relative_paths(
raw: &[String],
current_module_path: &[String],
) -> Vec<Vec<String>> {
if raw.is_empty() {
return Vec::new();
}
if raw.first().map(String::as_str) == Some(PATH_KEYWORD_CRATE) {
return vec![raw[1..].to_vec()];
}
if raw.first().map(String::as_str) == Some(PATH_KEYWORD_SELF) {
let mut resolved = current_module_path.to_vec();
resolved.extend(raw[1..].iter().cloned());
return vec![resolved];
}
if raw.first().map(String::as_str) == Some(PATH_KEYWORD_SUPER) {
let mut index = 0usize;
let mut resolved = current_module_path.to_vec();
while raw
.get(index)
.is_some_and(|segment| segment == PATH_KEYWORD_SUPER)
{
if resolved.pop().is_none() {
return Vec::new();
}
index += 1;
}
if raw
.get(index)
.is_some_and(|segment| segment == PATH_KEYWORD_SELF)
{
index += 1;
}
resolved.extend(raw[index..].iter().cloned());
return vec![resolved];
}
(0..=current_module_path.len())
.map(|prefix_len| {
let mut resolved = current_module_path[..prefix_len].to_vec();
resolved.extend(raw.iter().cloned());
resolved
})
.collect()
}
pub(super) const fn merge_reference_usage(
current: ParentFacadeReferenceUsage,
next: ParentFacadeReferenceUsage,
) -> ParentFacadeReferenceUsage {
match (current, next) {
(ParentFacadeReferenceUsage::DirectPath(PathOrigin::Relative), _)
| (_, ParentFacadeReferenceUsage::DirectPath(PathOrigin::Relative)) => {
ParentFacadeReferenceUsage::DirectPath(PathOrigin::Relative)
},
(ParentFacadeReferenceUsage::Import(PathOrigin::Relative), _)
| (_, ParentFacadeReferenceUsage::Import(PathOrigin::Relative)) => {
ParentFacadeReferenceUsage::Import(PathOrigin::Relative)
},
(ParentFacadeReferenceUsage::DirectPath(PathOrigin::Crate), _)
| (_, ParentFacadeReferenceUsage::DirectPath(PathOrigin::Crate)) => {
ParentFacadeReferenceUsage::DirectPath(PathOrigin::Crate)
},
(ParentFacadeReferenceUsage::Import(PathOrigin::Crate), _)
| (_, ParentFacadeReferenceUsage::Import(PathOrigin::Crate)) => {
ParentFacadeReferenceUsage::Import(PathOrigin::Crate)
},
_ => ParentFacadeReferenceUsage::None,
}
}
pub fn public_reexport_exists_outside_parent(
source_cache: &SourceCache,
settings: &DriverSettings,
source_root: &Path,
child_file: &Path,
item_name: &str,
) -> Result<bool> {
let Some(parent_boundary) = boundary::parent_boundary_for_child(source_root, child_file) else {
return Ok(false);
};
let Some(child_module_path) =
source_cache::module_path_from_source_file(source_root, child_file)
else {
return Ok(false);
};
for source_file in source_cache.source_files_under(source_root) {
if source_file.starts_with(&parent_boundary.subtree_root) {
continue;
}
let Some(file) = source_cache.parsed_file(source_file) else {
continue;
};
let Some(current_module_path) =
source_cache::module_path_from_source_file(source_root, source_file)
else {
continue;
};
for item in &file.items {
let Item::Use(item_use) = item else {
continue;
};
let Some(_) = exports::parent_facade_visibility(&item_use.vis) else {
continue;
};
let mut paths = Vec::new();
source_cache::flatten_use_tree(Vec::new(), &item_use.tree, &mut paths);
for path in paths {
for resolved in resolve_module_relative_paths(&path, ¤t_module_path) {
if resolved.len() != child_module_path.len() + 1 {
continue;
}
if resolved[..child_module_path.len()] == *child_module_path
&& resolved[child_module_path.len()] == item_name
{
return Ok(true);
}
}
}
}
}
if settings.config_root != settings.package_root {
let module_prefix = format!(
"{PATH_KEYWORD_CRATE}{MODULE_PATH_SEPARATOR}{}",
child_module_path.join(MODULE_PATH_SEPARATOR)
);
let findings_root = settings
.findings_dir
.parent()
.map_or_else(|| settings.findings_dir.clone(), Path::to_path_buf);
for file in source_cache.source_files_under(&settings.config_root) {
if file.starts_with(&settings.package_root)
|| file.starts_with(&settings.findings_dir)
|| file.starts_with(&findings_root)
{
continue;
}
let source = source_cache.read_source(file)?;
let pattern = format!("{module_prefix}::{item_name}");
if source.contains(&pattern) {
return Ok(true);
}
}
}
Ok(false)
}