Skip to content

Commit 7f73cbf

Browse files
committed
Add distill_external_module logic to handle namespace packages
1 parent 70223f2 commit 7f73cbf

File tree

2 files changed

+79
-23
lines changed

2 files changed

+79
-23
lines changed

rust/src/graph/builder/mod.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use cache::{ImportsCache, load_cache};
1717

1818
mod utils;
1919
use utils::{
20-
ResolvedImport, is_internal, is_package, path_to_module_name, resolve_external_module,
20+
ResolvedImport, distill_external_module, is_internal, is_package, path_to_module_name,
2121
resolve_internal_module, resolve_relative_import,
2222
};
2323

@@ -28,7 +28,7 @@ pub struct PackageSpec {
2828
}
2929

3030
pub fn build_graph(
31-
package: &PackageSpec,
31+
package: &PackageSpec, // TODO(peter) Support multiple packages
3232
include_external_packages: bool,
3333
exclude_type_checking_imports: bool,
3434
cache_dir: Option<&PathBuf>,
@@ -48,6 +48,7 @@ pub fn build_graph(
4848
&parsed_modules,
4949
include_external_packages,
5050
exclude_type_checking_imports,
51+
&HashSet::from([package.name.to_owned()]),
5152
);
5253

5354
Ok(assemble_graph(&imports_by_module, &package.name))
@@ -269,6 +270,7 @@ fn resolve_imports(
269270
parsed_modules: &[ParsedModule],
270271
include_external_packages: bool,
271272
exclude_type_checking_imports: bool,
273+
packages: &HashSet<String>,
272274
) -> HashMap<String, HashSet<ResolvedImport>> {
273275
let all_modules: HashSet<String> = parsed_modules
274276
.iter()
@@ -304,14 +306,17 @@ fn resolve_imports(
304306
line_contents: imported_object.line_contents.clone(),
305307
});
306308
} else if include_external_packages {
307-
// It's an external module and we're including them
308-
let external_module = resolve_external_module(&absolute_import_name);
309-
resolved_imports.insert(ResolvedImport {
310-
importer: parsed_module.module.name.to_string(),
311-
imported: external_module,
312-
line_number: imported_object.line_number,
313-
line_contents: imported_object.line_contents.clone(),
314-
});
309+
// Try to resolve as an external module
310+
if let Some(external_module) =
311+
distill_external_module(&absolute_import_name, packages)
312+
{
313+
resolved_imports.insert(ResolvedImport {
314+
importer: parsed_module.module.name.to_string(),
315+
imported: external_module,
316+
line_number: imported_object.line_number,
317+
line_contents: imported_object.line_contents.clone(),
318+
});
319+
}
315320
}
316321
}
317322

rust/src/graph/builder/utils.rs

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ pub fn is_package(module_path: &Path) -> bool {
2020
.unwrap_or(false)
2121
}
2222

23+
/// Check if a module is a descendant of another module.
24+
pub fn is_descendant(module_name: &str, potential_ancestor: &str) -> bool {
25+
module_name.starts_with(&format!("{}.", potential_ancestor))
26+
}
27+
2328
/// Check if module is internal
2429
pub fn is_internal(module_name: &str, package: &str) -> bool {
25-
if module_name == package || module_name.starts_with(&format!("{}.", package)) {
26-
return true;
27-
}
28-
false
30+
module_name == package || is_descendant(module_name, package)
2931
}
3032

3133
/// Convert module path to module name
@@ -105,13 +107,62 @@ pub fn resolve_internal_module(
105107
None
106108
}
107109

108-
/// Get external module name
109-
pub fn resolve_external_module(module_name: &str) -> String {
110-
// For simplicity, just return the root module for external imports
111-
// This matches the basic behavior from _distill_external_module
112-
module_name
113-
.split('.')
114-
.next()
115-
.unwrap_or(module_name)
116-
.to_string()
110+
/// Given a module that we already know is external, turn it into a module to add to the graph.
111+
///
112+
/// The 'distillation' process involves removing any unwanted subpackages. For example,
113+
/// django.models.db should be turned into simply django.
114+
///
115+
/// The process is more complex for potential namespace packages, as it's not possible to
116+
/// determine the portion package simply from name. Rather than adding the overhead of a
117+
/// filesystem read, we just get the shallowest component that does not clash with an internal
118+
/// module namespace. Take, for example, foo.blue.alpha.one. If one of the found
119+
/// packages is foo.blue.beta, the module will be distilled to foo.blue.alpha.
120+
/// Alternatively, if the found package is foo.green, the distilled module will
121+
/// be foo.blue.
122+
///
123+
/// Returns None if the module is a parent of one of the internal packages (doesn't make sense,
124+
/// probably an import of a namespace package).
125+
pub fn distill_external_module(
126+
module_name: &str,
127+
found_package_names: &HashSet<String>,
128+
) -> Option<String> {
129+
let module_root = module_name.split('.').next().unwrap();
130+
131+
for found_package in found_package_names {
132+
// If it's a module that is a parent of the package, return None
133+
// as it doesn't make sense and is probably an import of a namespace package.
134+
if is_descendant(found_package, module_name) {
135+
return None;
136+
}
137+
}
138+
139+
// If it shares a namespace with an internal module, get the shallowest component that does
140+
// not clash with an internal module namespace.
141+
let mut candidate_portions: Vec<String> = Vec::new();
142+
let mut sorted_found_packages: Vec<&String> = found_package_names.iter().collect();
143+
sorted_found_packages.sort();
144+
sorted_found_packages.reverse();
145+
146+
for found_package in sorted_found_packages {
147+
if is_descendant(found_package, module_root) {
148+
let mut internal_components: Vec<&str> = found_package.split('.').collect();
149+
let mut external_components: Vec<&str> = module_name.split('.').collect();
150+
let mut external_namespace_components: Vec<&str> = vec![];
151+
while external_components[0] == internal_components[0] {
152+
external_namespace_components.push(external_components.remove(0));
153+
internal_components.remove(0);
154+
}
155+
external_namespace_components.push(external_components[0]);
156+
candidate_portions.push(external_namespace_components.join("."));
157+
}
158+
}
159+
160+
if !candidate_portions.is_empty() {
161+
// If multiple internal modules share a namespace with this module, use the deepest one
162+
// as we know that that will be a namespace too.
163+
candidate_portions.sort_by_key(|portion| portion.split('.').count());
164+
Some(candidate_portions.last().unwrap().clone())
165+
} else {
166+
Some(module_root.to_string())
167+
}
117168
}

0 commit comments

Comments
 (0)