py_import_helper/utils/
formatting.rs1use super::parsing::custom_import_sort;
7use crate::types::{FormattingConfig, ImportStatement};
8use std::collections::{HashMap, HashSet};
9
10#[must_use]
12pub fn format_imports(imports: &[ImportStatement], config: &FormattingConfig) -> Vec<String> {
13 let mut package_imports: HashMap<String, Vec<&ImportStatement>> = HashMap::new();
14
15 for import in imports {
17 package_imports
18 .entry(import.package.clone())
19 .or_default()
20 .push(import);
21 }
22
23 let mut result = Vec::new();
24 let mut packages: Vec<_> = package_imports.keys().collect();
25 packages.sort();
26
27 for package in packages {
28 let imports_for_package = package_imports
29 .get(package)
30 .expect("BUG: package key must exist in HashMap");
31
32 if let Some(first) = imports_for_package.first() {
33 if imports_for_package.len() == 1 && first.items.is_empty() {
34 result.push(first.statement.clone());
36 } else {
37 result.extend(merge_package_imports(imports_for_package, config));
40 }
41 }
42 }
43
44 result
45}
46
47#[must_use]
49pub fn merge_package_imports(
50 imports: &[&ImportStatement],
51 config: &FormattingConfig,
52) -> Vec<String> {
53 let mut all_items = HashSet::new();
54 let package = &imports[0].package;
55
56 for import in imports {
58 all_items.extend(import.items.iter().cloned());
59 }
60
61 if all_items.is_empty() {
62 return imports.iter().map(|i| i.statement.clone()).collect();
64 }
65
66 let mut sorted_items: Vec<_> = all_items.into_iter().collect();
67 sorted_items.sort_by(|a, b| custom_import_sort(a, b));
68
69 let should_use_multiline = if config.force_multiline {
71 true
72 } else if config.force_single_line {
73 false
74 } else {
75 let total_chars = sorted_items.iter().map(String::len).sum::<usize>();
77 let import_line_length = "from ".len()
78 + package.len()
79 + " import ".len()
80 + total_chars
81 + (sorted_items.len() * 2);
82
83 sorted_items.len() >= config.multiline_threshold || import_line_length > config.line_length
84 };
85
86 if should_use_multiline {
87 let indent = " ".repeat(config.indent_size);
89 let mut result = vec![format!("from {} import (", package)];
90
91 for item in &sorted_items {
92 if config.use_trailing_comma {
93 result.push(format!("{}{},", indent, item));
94 } else {
95 result.push(format!("{}{}", indent, item));
96 }
97 }
98
99 result.push(")".to_string());
100 result
101 } else {
102 vec![format!(
104 "from {} import {}",
105 package,
106 sorted_items.join(", ")
107 )]
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::types::{ImportCategory, ImportType};
115
116 #[test]
117 fn test_merge_package_imports() {
118 let import1 = ImportStatement {
119 statement: "from typing import Any".to_string(),
120 category: ImportCategory::StandardLibrary,
121 import_type: ImportType::From,
122 package: "typing".to_string(),
123 items: vec!["Any".to_string()],
124 is_multiline: false,
125 };
126
127 let import2 = ImportStatement {
128 statement: "from typing import Optional".to_string(),
129 category: ImportCategory::StandardLibrary,
130 import_type: ImportType::From,
131 package: "typing".to_string(),
132 items: vec!["Optional".to_string()],
133 is_multiline: false,
134 };
135
136 let config = FormattingConfig::default();
137 let merged = merge_package_imports(&[&import1, &import2], &config);
138 assert_eq!(merged.len(), 1);
139 assert!(merged[0].contains("Any"));
140 assert!(merged[0].contains("Optional"));
141 }
142}