use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use rustc_hash::FxHashMap;
use super::parent_boundary;
use super::parent_boundary::ParentBoundaryKey;
use super::validated_plan;
use super::validated_plan::ValidatedPubUsePlan;
use crate::fixes::imports::UseFix;
use crate::fixes::imports::ValidatedFixSet;
use crate::reporting::Report;
use crate::rust_syntax::FileModulePath;
use crate::rust_syntax::ModuleMap;
use crate::selection::Selection;
pub(crate) struct PubUseFixScan {
pub fixes: ValidatedFixSet,
pub applied: usize,
pub skipped: usize,
}
struct PubUseFixFact {
child_file: PathBuf,
child_line: usize,
child_item_name: String,
parent_module: PathBuf,
parent_line: usize,
child_module: String,
}
struct PubUseCandidate {
child_file: PathBuf,
child_line: usize,
child_module: String,
exported_name: String,
parent_module_path: Vec<String>,
target_item_path: Vec<String>,
}
struct PubUseAnalysis {
supported_plans: Vec<ValidatedPubUsePlan>,
skipped: usize,
}
enum CandidateScreening {
Accept(NarrowablePubUseCandidate),
Skip,
}
enum CandidateExportMatch {
Matches,
Mismatch,
}
struct BarePubKeyword {
start: usize,
end: usize,
}
enum ChildVisibilityState {
PlainPub(BarePubKeyword),
NarrowerThanPub,
}
struct NarrowablePubUseCandidate {
candidate: PubUseCandidate,
bare_pub_keyword: BarePubKeyword,
}
pub(crate) fn scan_selection(selection: &Selection, report: &Report) -> Result<PubUseFixScan> {
let mut fixes = Vec::new();
let facts = collect_pub_use_fix_facts(selection, report);
let analysis = analyze_pub_use_candidates(&facts)?;
let parent_fix_groups = group_parent_pub_use_plans(&analysis.supported_plans);
for plan in &analysis.supported_plans {
fixes.push(plan.child_narrowing.clone());
}
for (parent_boundary, exports) in parent_fix_groups {
let removal =
parent_boundary::build_parent_pub_use_edit_for_exports(&parent_boundary, &exports)?;
fixes.push(removal);
}
fixes.extend(validated_plan::rewrite_subtree_imports_for_plans(
&analysis.supported_plans,
)?);
let fixes = ValidatedFixSet::try_from(fixes)?;
Ok(PubUseFixScan {
fixes,
applied: analysis.supported_plans.len(),
skipped: analysis.skipped,
})
}
fn collect_pub_use_fix_facts(selection: &Selection, report: &Report) -> Vec<PubUseFixFact> {
let mut facts = Vec::new();
for fact in report.facts.pub_use_fix_facts.iter() {
let child_rel = normalize_rel_path(&fact.child_path);
let parent_rel = normalize_rel_path(&fact.parent_path);
facts.push(PubUseFixFact {
child_file: selection.analysis_root.join(&child_rel),
child_line: fact.child_line,
child_item_name: fact.child_item_name.clone(),
parent_module: selection.analysis_root.join(&parent_rel),
parent_line: fact.parent_line,
child_module: fact.child_module.clone(),
});
}
facts
}
fn analyze_pub_use_candidates(facts: &[PubUseFixFact]) -> Result<PubUseAnalysis> {
let mut supported_plans = Vec::new();
let mut skipped = 0usize;
let mut module_maps: FxHashMap<PathBuf, ModuleMap> = FxHashMap::default();
for fact in facts {
let child_source = fs::read_to_string(&fact.child_file)
.with_context(|| format!("failed to read {}", fact.child_file.display()))?;
let parent_source = fs::read_to_string(&fact.parent_module)
.with_context(|| format!("failed to read {}", fact.parent_module.display()))?;
let Some(parent_export) = parent_boundary::resolve_parent_pub_use_export(
&parent_source,
fact.parent_line,
&fact.child_module,
&fact.child_item_name,
)
.with_context(|| {
format!(
"failed to resolve exported item from {}:{}",
fact.parent_module.display(),
fact.parent_line
)
})?
else {
skipped += 1;
continue;
};
let source_root = validated_plan::find_source_root(&fact.parent_module)
.context("failed to determine src root for parent module")?;
let module_map = module_maps
.entry(source_root.clone())
.or_insert_with(|| ModuleMap::resolve(&source_root));
let parent_module_path = match module_map
.file_module_path(&source_root, &fact.parent_module)
.context("failed to determine parent module path")?
{
FileModulePath::Known(module_path) => module_path,
FileModulePath::SeveralParents => {
skipped += 1;
continue;
},
};
let mut target_item_path = parent_module_path.clone();
target_item_path.push(fact.child_module.clone());
target_item_path.push(fact.child_item_name.clone());
let parent_boundary = ParentBoundaryKey {
parent_module: fact.parent_module.clone(),
..parent_export.parent_boundary
};
let candidate = PubUseCandidate {
child_file: fact.child_file.clone(),
child_line: fact.child_line,
child_module: fact.child_module.clone(),
exported_name: parent_export.exported_name,
parent_module_path,
target_item_path,
};
match screen_candidate(candidate, &fact.child_item_name, &child_source)? {
CandidateScreening::Accept(narrowable) => {
supported_plans.push(build_validated_plan(narrowable, parent_boundary));
},
CandidateScreening::Skip => skipped += 1,
}
}
Ok(PubUseAnalysis {
supported_plans,
skipped,
})
}
fn screen_candidate(
candidate: PubUseCandidate,
child_item: &str,
child_source: &str,
) -> Result<CandidateScreening> {
let export_match = if candidate.exported_name == child_item {
CandidateExportMatch::Matches
} else {
CandidateExportMatch::Mismatch
};
let child_visibility = child_visibility_state(child_source, candidate.child_line)?;
Ok(match (export_match, child_visibility) {
(CandidateExportMatch::Matches, ChildVisibilityState::PlainPub(bare_pub_keyword)) => {
CandidateScreening::Accept(NarrowablePubUseCandidate {
candidate,
bare_pub_keyword,
})
},
_ => CandidateScreening::Skip,
})
}
fn build_validated_plan(
narrowable: NarrowablePubUseCandidate,
parent_boundary: ParentBoundaryKey,
) -> ValidatedPubUsePlan {
let NarrowablePubUseCandidate {
candidate,
bare_pub_keyword,
} = narrowable;
let child_narrowing = UseFix {
path: candidate.child_file.clone(),
start: bare_pub_keyword.start,
end: bare_pub_keyword.end,
replacement: "pub(super)".to_string(),
import_group: None,
};
ValidatedPubUsePlan {
parent_boundary,
child_file: candidate.child_file,
child_module: candidate.child_module,
exported_name: candidate.exported_name,
parent_module_path: candidate.parent_module_path,
target_item_path: candidate.target_item_path,
child_narrowing,
}
}
fn child_visibility_state(source: &str, line: usize) -> Result<ChildVisibilityState> {
let line_span = validated_plan::line_span(source, line)
.context("failed to compute child item line span")?;
Ok(
find_bare_pub_keyword(&source[line_span.0..line_span.1], line_span.0)
.map_or(ChildVisibilityState::NarrowerThanPub, |bare_pub_keyword| {
ChildVisibilityState::PlainPub(bare_pub_keyword)
}),
)
}
fn find_bare_pub_keyword(line_text: &str, line_start: usize) -> Option<BarePubKeyword> {
let mut search_from = 0usize;
while let Some(offset) = line_text[search_from..].find("pub") {
let start = search_from + offset;
let end = start + "pub".len();
let extends_a_longer_identifier = line_text[..start]
.chars()
.next_back()
.is_some_and(|character| character.is_alphanumeric() || character == '_');
let keyword_ends_here = line_text[end..]
.chars()
.next()
.is_none_or(char::is_whitespace);
let restriction_follows = line_text[end..].trim_start().starts_with('(');
if !extends_a_longer_identifier && keyword_ends_here && !restriction_follows {
return Some(BarePubKeyword {
start: line_start + start,
end: line_start + end,
});
}
search_from = end;
}
None
}
fn group_parent_pub_use_plans(
plans: &[ValidatedPubUsePlan],
) -> BTreeMap<ParentBoundaryKey, Vec<(String, String)>> {
let mut groups = BTreeMap::new();
for plan in plans {
groups
.entry(plan.parent_boundary.clone())
.or_insert_with(Vec::new)
.push((plan.child_module.clone(), plan.exported_name.clone()));
}
groups
}
fn normalize_rel_path(path: impl AsRef<Path>) -> String {
path.as_ref().to_string_lossy().replace('\\', "/")
}