use std::collections::BTreeSet;
use std::path::Path;
use syn::ExprPath;
use syn::ItemUse;
use syn::spanned::Spanned;
use syn::visit::Visit;
use super::InlineCallFindingInputs;
use super::ScanFileContext;
use super::shared;
use crate::config::DiagnosticCode;
use crate::diagnostics::Finding;
use crate::diagnostics::Severity;
use crate::fix_support::FixSupport;
use crate::imports::ImportGroup;
use crate::imports::UseFix;
pub(super) struct InlineCallCandidate {
pub(super) function_name: String,
pub(super) module_name: String,
pub(super) module_path: String,
pub(super) absolute_module: Vec<String>,
pub(super) prefix_start: proc_macro2::LineColumn,
pub(super) leaf_start: proc_macro2::LineColumn,
pub(super) full_span_start: proc_macro2::LineColumn,
pub(super) full_span_end: proc_macro2::LineColumn,
pub(super) is_parent_module: bool,
}
pub(super) struct InlineCallDetector<'a> {
pub(super) source_root: &'a Path,
pub(super) current_module_path: &'a [String],
pub(super) declared_modules: &'a BTreeSet<String>,
pub(super) candidates: Vec<InlineCallCandidate>,
pub(super) inline_mod_depth: usize,
}
impl Visit<'_> for InlineCallDetector<'_> {
fn visit_item_use(&mut self, _: &ItemUse) {}
fn visit_item_mod(&mut self, node: &syn::ItemMod) {
if node.content.is_some() {
self.inline_mod_depth += 1;
syn::visit::visit_item_mod(self, node);
self.inline_mod_depth -= 1;
} else {
syn::visit::visit_item_mod(self, node);
}
}
fn visit_expr_path(&mut self, node: &ExprPath) {
if self.inline_mod_depth > 0 || node.qself.is_some() {
return;
}
if let Some(candidate) = analyze_inline_call(
self.source_root,
self.current_module_path,
self.declared_modules,
node,
) {
self.candidates.push(candidate);
}
}
}
pub(super) fn build_inline_call_findings_and_fixes(
file_context: &ScanFileContext<'_>,
inline_inputs: &InlineCallFindingInputs<'_>,
) -> (Vec<Finding>, Vec<UseFix>) {
let display_path = file_context.display_path();
let mut findings = Vec::new();
let mut fixes = Vec::new();
let mut inserted_modules: BTreeSet<Vec<String>> = BTreeSet::new();
for candidate in inline_inputs.candidates {
let prefix_start_byte = shared::offset(file_context.offsets, candidate.prefix_start);
let leaf_start_byte = shared::offset(file_context.offsets, candidate.leaf_start);
let full_start_byte = shared::offset(file_context.offsets, candidate.full_span_start);
let full_end_byte = shared::offset(file_context.offsets, candidate.full_span_end);
let source_line = file_context
.text
.lines()
.nth(candidate.full_span_start.line.saturating_sub(1))
.unwrap_or_default()
.to_string();
let full_path_text = file_context
.text
.get(full_start_byte..full_end_byte)
.unwrap_or_default()
.to_string();
let (message, suggestion) = if candidate.is_parent_module {
(
format!(
"use `super::{}` instead of the fully-qualified path",
candidate.function_name
),
Some(format!(
"rewrite the call as `super::{}`",
candidate.function_name
)),
)
} else {
(
format!(
"import the module `{}` instead of using the fully-qualified path for `{}`",
candidate.module_name, candidate.function_name
),
Some(format!(
"add `use {};` and call `{}::{}`",
candidate.module_path, candidate.module_name, candidate.function_name
)),
)
};
findings.push(Finding {
severity: Severity::Warning,
code: DiagnosticCode::PreferModuleImport,
path: display_path.clone(),
line: candidate.full_span_start.line,
column: candidate.full_span_start.column + 1,
highlight_len: full_path_text.len().max(1),
source_line,
item: None,
message,
suggestion,
fixability: FixSupport::PreferModuleImport,
related: None,
});
let group = Some(ImportGroup {
bare_name: candidate.module_name.clone(),
full_path: candidate.absolute_module.join("::"),
});
let call_prefix = if candidate.is_parent_module {
"super::".to_string()
} else {
format!("{}::", candidate.module_name)
};
fixes.push(UseFix {
path: file_context.path.to_path_buf(),
start: prefix_start_byte,
end: leaf_start_byte,
replacement: call_prefix,
import_group: group.clone(),
});
if candidate.is_parent_module {
continue;
}
if inline_inputs
.will_import_modules
.contains(&candidate.absolute_module)
{
continue;
}
if !inserted_modules.insert(candidate.absolute_module.clone()) {
continue;
}
fixes.push(UseFix {
path: file_context.path.to_path_buf(),
start: inline_inputs.file_insertion_offset,
end: inline_inputs.file_insertion_offset,
replacement: format!("use {};\n", candidate.module_path),
import_group: group,
});
}
(findings, fixes)
}
fn analyze_inline_call(
source_root: &Path,
current_module_path: &[String],
declared_modules: &BTreeSet<String>,
node: &ExprPath,
) -> Option<InlineCallCandidate> {
let path = &node.path;
let segments: Vec<String> = path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect();
if segments.len() < 3 {
return None;
}
let first = segments.first()?;
if first != "crate" && first != "super" {
return None;
}
let leaf = segments.last()?;
if !shared::is_snake_case_function_name(leaf) {
return None;
}
let absolute_segments = shared::resolve_to_absolute(&segments, current_module_path)?;
if absolute_segments.is_empty() {
return None;
}
if shared::leaf_is_module(source_root, &absolute_segments) {
return None;
}
let absolute_module = absolute_segments[..absolute_segments.len() - 1].to_vec();
if absolute_module.is_empty() || !shared::leaf_is_module(source_root, &absolute_module) {
return None;
}
let module_name = segments[segments.len() - 2].clone();
if module_name == "super" || module_name == "crate" {
return None;
}
if !shared::is_snake_case_module_name(&module_name) {
return None;
}
if declared_modules.contains(&module_name) {
return None;
}
let module_segments = &segments[..segments.len() - 1];
let shortened = shared::shorten_module_path(current_module_path, module_segments);
let is_parent_module = shortened.as_slice() == ["super"];
let module_path = shortened.join("::");
let first_seg = path.segments.first()?;
let leaf_seg = path.segments.last()?;
Some(InlineCallCandidate {
function_name: leaf.clone(),
module_name,
module_path,
absolute_module,
prefix_start: first_seg.ident.span().start(),
leaf_start: leaf_seg.ident.span().start(),
full_span_start: path.span().start(),
full_span_end: path.span().end(),
is_parent_module,
})
}