use std::collections::HashMap;
use std::sync::Arc;
use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::is_test_function;
use rustc_hir::{Item, ItemKind, Mod};
use rustc_lint::{LateContext, LintContext};
use rustc_span::hygiene::ExpnKind;
use rustc_span::source_map::SourceMap;
use rustc_span::{BytePos, SourceFile, Span, Symbol};
use super::cfg_test::cfg_predicate_mentions_test;
use super::config::{InlineStyle, InlineTestFootprint};
use super::{INLINE_TEST_FOOTPRINT, paths};
struct TestItem {
span: Span,
module_name: Option<Symbol>,
}
#[derive(Default)]
struct FileAcc {
production_count: usize,
test_items: Vec<TestItem>,
inline_test_lines: usize,
file: Option<Arc<SourceFile>>,
}
pub(super) fn run(state: &InlineTestFootprint, cx: &LateContext<'_>) {
if paths::is_separate_test_target(cx) {
return;
}
let mut files: HashMap<BytePos, FileAcc> = HashMap::new();
walk(cx, cx.tcx.hir_root_module(), &mut files);
for acc in files.values() {
emit_inline_style(state, cx, acc);
}
}
fn walk(cx: &LateContext<'_>, module: &Mod<'_>, files: &mut HashMap<BytePos, FileAcc>) {
for item_id in module.item_ids {
classify(cx, cx.tcx.hir_item(*item_id), files);
}
}
fn classify(cx: &LateContext<'_>, item: &Item<'_>, files: &mut HashMap<BytePos, FileAcc>) {
let expn_kind = item.span.ctxt().outer_expn_data().kind;
if matches!(expn_kind, ExpnKind::AstPass(_) | ExpnKind::Desugaring(_)) {
return;
}
let cfg_test = cfg_predicate_mentions_test(cx.tcx, item.hir_id());
let is_test = cfg_test
|| (matches!(item.kind, ItemKind::Fn { .. })
&& is_test_function(cx.tcx, item.owner_id.def_id));
if matches!(expn_kind, ExpnKind::Macro(..)) {
if !is_test {
record_production(cx, item.span.source_callsite(), files);
}
return;
}
if let ItemKind::Mod(ident, module) = &item.kind {
match (cfg_test, is_external_module(cx, item.span, module)) {
(true, false) => {
record_test(cx, item.span, Some(ident.name), files);
}
(true, true) => {}
(false, _) => {
record_production(cx, item.span, files);
walk(cx, module, files);
}
}
return;
}
if is_test {
record_test(cx, item.span, None, files);
} else {
record_production(cx, item.span, files);
}
}
fn is_external_module(cx: &LateContext<'_>, declaration: Span, module: &Mod<'_>) -> bool {
let source_map = cx.sess().source_map();
source_map.lookup_source_file(declaration.lo()).start_pos
!= source_map
.lookup_source_file(module.spans.inner_span.lo())
.start_pos
}
fn record_production(cx: &LateContext<'_>, span: Span, files: &mut HashMap<BytePos, FileAcc>) {
acc_for(cx.sess().source_map(), span, files).production_count += 1;
}
fn record_test(
cx: &LateContext<'_>,
span: Span,
module_name: Option<Symbol>,
files: &mut HashMap<BytePos, FileAcc>,
) {
let source_map = cx.sess().source_map();
let lines = line_count(source_map, span);
let acc = acc_for(source_map, span, files);
acc.test_items.push(TestItem { span, module_name });
acc.inline_test_lines += lines;
}
fn acc_for<'a>(
source_map: &SourceMap,
span: Span,
files: &'a mut HashMap<BytePos, FileAcc>,
) -> &'a mut FileAcc {
let file = source_map.lookup_source_file(span.lo());
let acc = files.entry(file.start_pos).or_default();
if acc.file.is_none() {
acc.file = Some(file);
}
acc
}
fn line_count(source_map: &SourceMap, span: Span) -> usize {
source_map
.span_to_lines(span)
.map(|file_lines| file_lines.lines.len())
.unwrap_or(0)
}
fn emit_inline_style(state: &InlineTestFootprint, cx: &LateContext<'_>, acc: &FileAcc) {
if acc.test_items.is_empty() || acc.production_count == 0 {
return;
}
let Some(file) = &acc.file else {
return;
};
match state.inline_style {
InlineStyle::ExternalOnly => {
for item in &acc.test_items {
span_lint_and_help(
cx,
INLINE_TEST_FOOTPRINT,
item.span,
"inline test code should live in an external module",
None,
help_extract(file, item.module_name),
);
}
}
InlineStyle::ExternalWhenLong => {
let file_lines = file.count_lines();
let over_absolute = acc.inline_test_lines > state.inline_max_lines;
let over_fraction = state.inline_max_fraction_of_file.is_some_and(|cap| {
file_lines > 0 && (acc.inline_test_lines as f32 / file_lines as f32) > cap
});
if !over_absolute && !over_fraction {
return;
}
let message = if over_absolute {
format!(
"inline test code spans {} lines, over the limit of {}",
acc.inline_test_lines, state.inline_max_lines,
)
} else {
format!(
"inline test code is {} of {} lines in this file, over the configured fraction",
acc.inline_test_lines, file_lines,
)
};
span_lint_and_help(
cx,
INLINE_TEST_FOOTPRINT,
union_span(acc.test_items.iter().map(|item| item.span)),
message,
None,
help_extract(file, common_module_name(&acc.test_items)),
);
}
}
}
fn common_module_name(items: &[TestItem]) -> Option<Symbol> {
let mut names = items.iter().map(|item| item.module_name);
let first = names.next()??;
names.all(|name| name == Some(first)).then_some(first)
}
fn help_extract(file: &SourceFile, module_name: Option<Symbol>) -> String {
let name = module_name.map_or_else(|| "tests".to_owned(), |name| name.to_string());
match paths::real_path(file).and_then(|path| paths::canonical_target(&path, &name)) {
Some(target) => format!(
"move the inline test code into a separate file (e.g. `{}`) and replace it with an \
external `mod {name};` declaration",
target.display(),
),
None => format!(
"move the inline test code into a separate file and replace it with an external \
`mod {name};` declaration",
),
}
}
fn union_span(spans: impl IntoIterator<Item = Span>) -> Span {
let mut spans = spans.into_iter();
let first = spans
.next()
.expect("caller guarantees a non-empty iterator");
spans.fold(first, |union, span| union.to(span))
}