use std::collections::BTreeSet;
use std::path::Path;
use proc_macro2::LineColumn;
use syn::ExprPath;
use syn::ItemMod;
use syn::ItemUse;
use syn::spanned::Spanned;
use syn::visit;
use syn::visit::Visit;
use super::function_imports::ImportTarget;
use super::scan::InlineCallFindingInputs;
use super::scan::ScanFileContext;
use super::support;
use crate::config::DiagnosticCode;
use crate::fixes::imports::ImportGroup;
use crate::fixes::imports::UseFix;
use crate::reporting::Finding;
use crate::reporting::FixSupport;
use crate::reporting::Severity;
use crate::rust_syntax::PathAnchor;
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: LineColumn,
pub(super) leaf_start: LineColumn,
pub(super) full_span_start: LineColumn,
pub(super) full_span_end: LineColumn,
pub(super) import_target: ImportTarget,
}
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: &ItemMod) {
if node.content.is_some() {
self.inline_mod_depth += 1;
visit::visit_item_mod(self, node);
self.inline_mod_depth -= 1;
} else {
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 = support::offset(file_context.offsets, candidate.prefix_start);
let leaf_start_byte = support::offset(file_context.offsets, candidate.leaf_start);
let full_start_byte = support::offset(file_context.offsets, candidate.full_span_start);
let full_end_byte = support::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.import_target == ImportTarget::ParentModule {
(
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,
diagnostic_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,
fix_support: 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.import_target == ImportTarget::ParentModule {
"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.import_target == ImportTarget::ParentModule {
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 path_anchor = PathAnchor::first(&segments)?;
if !path_anchor.is_crate_relative() {
return None;
}
let leaf = segments.last()?;
if !support::is_snake_case_function_name(leaf) {
return None;
}
let absolute_segments = support::resolve_to_absolute(&segments, current_module_path)?;
if absolute_segments.is_empty() {
return None;
}
if support::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() || !support::leaf_is_module(source_root, &absolute_module) {
return None;
}
let module_name = segments[segments.len() - 2].clone();
if PathAnchor::from(module_name.as_str()).is_crate_relative() {
return None;
}
if !support::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 = support::shorten_module_path(current_module_path, module_segments);
let import_target = match shortened.as_slice() {
[segment] if PathAnchor::from(segment.as_str()) == PathAnchor::Super => {
ImportTarget::ParentModule
},
_ => ImportTarget::OtherModule,
};
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(),
import_target,
})
}