#![expect(
clippy::wildcard_enum_match_arm,
reason = "Hundreds of SyntaxKind variants, matching on '_' is the best option"
)]
use std::{
io,
path::{Path as StdPath, PathBuf},
};
use compact_str::CompactString;
use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
use ra_ap_syntax::{
AstNode, AstToken, Edition, NodeOrToken, SourceFile, SyntaxKind, SyntaxNode, SyntaxToken,
WalkEvent,
ast::{
Attr, Comment, CommentShape, ExternCrate, HasAttrs, HasModuleItem, HasName, MacroCall,
MacroRules, Meta, Module, Path, String as AstString, TokenTree, Use, UseTree,
},
};
use rustc_hash::FxHashSet;
use crate::util::read_to_string;
#[derive(Debug, Default)]
pub struct ParsedSource {
pub imports: FxHashSet<CompactString>,
pub paths: FxHashSet<PathBuf>,
pub is_empty: bool,
pub has_tests: bool,
pub has_doctests: bool,
}
impl ParsedSource {
pub fn from_path(path: &StdPath, is_entry_point: bool) -> io::Result<Self> {
let source = read_to_string(path)?;
Ok(SourceParser::parse(&source, Some(path), is_entry_point))
}
pub fn from_str(source: &str, path: &StdPath) -> Self {
SourceParser::parse(source, Some(path), true)
}
#[cfg(test)]
pub fn from_path_test(source: &str, path: &StdPath, is_entry_point: bool) -> Self {
SourceParser::parse(source, Some(path), is_entry_point)
}
}
struct SourceParser {
result: ParsedSource,
parent: Option<PathBuf>,
module: Option<PathBuf>,
markdown: String,
}
impl SourceParser {
fn new(path: Option<&StdPath>, is_entry_point: bool) -> Self {
let parent = path.and_then(|path| path.parent()).map(StdPath::to_path_buf);
let module = path.and_then(|path| {
let parent = path.parent()?;
if is_entry_point {
return Some(parent.to_path_buf());
}
match path.file_stem().and_then(|stem| stem.to_str()) {
Some("mod") | None => Some(parent.to_path_buf()),
Some(stem) => Some(parent.join(stem)),
}
});
Self { result: ParsedSource::default(), parent, module, markdown: String::new() }
}
fn parse(source: &str, path: Option<&StdPath>, is_entry_point: bool) -> ParsedSource {
let mut parser = Self::new(path, is_entry_point);
let tree = SourceFile::parse(source, Edition::CURRENT);
let tree = tree.tree();
parser.result.is_empty = tree.items().next().is_none();
parser.walk(tree.syntax());
parser.parse_markdown();
parser.result
}
fn parse_markdown(&mut self) {
if self.markdown.is_empty() {
return;
}
let mut current: Option<String> = None;
let markdown = std::mem::take(&mut self.markdown);
for event in Parser::new(&markdown) {
match event {
Event::Start(Tag::CodeBlock(kind)) => {
let (is_rust, is_executable) = match kind {
CodeBlockKind::Indented => (true, true),
CodeBlockKind::Fenced(info) => {
if info.is_empty() {
(true, true)
} else {
let mut is_rust = false;
let mut has_ignore = false;
for tag in info.split(',').map(str::trim) {
match tag {
"ignore" => {
is_rust = true;
has_ignore = true;
}
"rust" | "no_run" | "should_panic" | "compile_fail" => {
is_rust = true;
}
_ => {}
}
}
(is_rust, is_rust && !has_ignore)
}
}
};
if is_rust {
current = Some(String::new());
}
if is_executable {
self.result.has_doctests = true;
}
}
Event::End(TagEnd::CodeBlock) => {
if let Some(code) = current.take()
&& !code.trim().is_empty()
{
let snippet = SourceFile::parse(&code, Edition::CURRENT);
self.walk(snippet.tree().syntax());
}
}
Event::Text(text) => {
if let Some(code) = &mut current {
for line in text.lines() {
if !code.is_empty() {
code.push('\n');
}
let stripped =
line.strip_prefix('#').map_or(line, |rest| rest.trim_start());
code.push_str(stripped);
}
}
}
_ => {}
}
}
}
fn walk(&mut self, root: &SyntaxNode) {
let mut use_depth = 0u32;
for event in root.preorder_with_tokens() {
match event {
WalkEvent::Enter(element) => {
if let NodeOrToken::Node(node) = &element
&& node.kind() == SyntaxKind::USE
{
use_depth += 1;
}
self.visit(element, use_depth > 0);
}
WalkEvent::Leave(NodeOrToken::Node(node)) if node.kind() == SyntaxKind::USE => {
use_depth -= 1;
}
WalkEvent::Leave(_) => {}
}
}
}
fn visit(&mut self, element: NodeOrToken<SyntaxNode, SyntaxToken>, in_use: bool) {
match element {
NodeOrToken::Node(node) => self.visit_node(&node, in_use),
NodeOrToken::Token(token) => self.visit_token(token),
}
}
fn visit_node(&mut self, node: &SyntaxNode, in_use: bool) {
match node.kind() {
SyntaxKind::USE => self.visit_use(node),
SyntaxKind::EXTERN_CRATE => self.visit_extern_crate(node),
SyntaxKind::PATH if !in_use => self.visit_path(node),
SyntaxKind::MACRO_CALL => self.visit_macro_call(node),
SyntaxKind::MACRO_RULES => self.visit_macro_rules(node),
SyntaxKind::ATTR => self.visit_attribute(node),
SyntaxKind::MODULE => self.visit_module(node),
_ => {}
}
}
fn visit_token(&mut self, token: SyntaxToken) {
if token.kind() == SyntaxKind::COMMENT {
self.visit_comment(token);
}
}
fn visit_use(&mut self, node: &SyntaxNode) {
if let Some(use_tree) = Use::cast(node.clone()).and_then(|use_item| use_item.use_tree()) {
self.collect_use_tree(&use_tree);
}
}
fn visit_extern_crate(&mut self, node: &SyntaxNode) {
if let Some(name_ref) =
ExternCrate::cast(node.clone()).and_then(|extern_crate| extern_crate.name_ref())
{
self.add_import(name_ref.text().as_ref());
}
}
fn visit_path(&mut self, node: &SyntaxNode) {
if node.parent().is_some_and(|parent| parent.kind() == SyntaxKind::PATH) {
return;
}
if let Some(path) = Path::cast(node.clone()) {
self.collect_path(&path);
}
}
fn visit_macro_call(&mut self, node: &SyntaxNode) {
let Some(macro_call) = MacroCall::cast(node.clone()) else { return };
if let Some(path) = macro_call.path() {
self.collect_path(&path);
}
if let Some(token_tree) = macro_call.token_tree() {
self.collect_token_tree(&token_tree);
}
self.collect_macro_call(¯o_call);
}
fn visit_macro_rules(&mut self, node: &SyntaxNode) {
if let Some(token_tree) =
MacroRules::cast(node.clone()).and_then(|macro_rules| macro_rules.token_tree())
{
self.collect_token_tree(&token_tree);
}
}
fn visit_attribute(&mut self, node: &SyntaxNode) {
let Some(attr) = Attr::cast(node.clone()) else { return };
let Some(meta) = attr.meta() else { return };
match &meta {
Meta::CfgMeta(_) | Meta::CfgAttrMeta(_) => {
let has_test_token = meta
.syntax()
.descendants_with_tokens()
.filter_map(NodeOrToken::into_token)
.any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "test");
if has_test_token {
self.result.has_tests = true;
}
}
_ if meta.path().is_some_and(|path| path.to_string() == "test") => {
self.result.has_tests = true;
}
_ => {}
}
self.collect_meta_imports(&meta);
}
fn collect_meta_imports(&mut self, meta: &Meta) {
match meta {
Meta::TokenTreeMeta(tt_meta) => {
if let Some(token_tree) = tt_meta.token_tree() {
self.collect_token_tree(&token_tree);
if tt_meta.path().is_some_and(|path| path.to_string() == "serde") {
self.collect_serde_attribute(&token_tree);
}
}
}
Meta::CfgAttrMeta(cfg_attr_meta) => {
for inner in cfg_attr_meta.metas() {
self.collect_meta_imports(&inner);
}
}
Meta::UnsafeMeta(unsafe_meta) => {
if let Some(inner) = unsafe_meta.meta() {
self.collect_meta_imports(&inner);
}
}
_ => {}
}
}
fn visit_module(&mut self, node: &SyntaxNode) {
if node
.ancestors()
.any(|ancestor| ancestor.kind() == SyntaxKind::MODULE && ancestor != *node)
{
return;
}
let Some(module) = Module::cast(node.clone()) else { return };
let module_dir = self.module.clone();
let path_dir = self.parent.clone();
self.collect_module(&module, module_dir.as_deref(), path_dir.as_deref());
}
fn visit_comment(&mut self, token: SyntaxToken) {
let Some(comment) = Comment::cast(token) else { return };
let Some((text, _)) = comment.doc_comment() else { return };
for line in text.lines() {
let line = match comment.kind().shape {
CommentShape::Line => line.trim_start(),
CommentShape::Block => line.trim_start().trim_start_matches('*').trim_start(),
};
self.markdown.push_str(line);
self.markdown.push('\n');
}
self.markdown.push('\n');
}
fn collect_token_tree(&mut self, token_tree: &TokenTree) {
let tokens: Vec<_> = token_tree
.syntax()
.descendants_with_tokens()
.filter_map(NodeOrToken::into_token)
.filter(|token| !token.kind().is_trivia())
.collect();
let needs_reparse = tokens.iter().any(|token| {
matches!(token.kind(), SyntaxKind::USE_KW | SyntaxKind::EXTERN_KW | SyntaxKind::MOD_KW)
});
if needs_reparse {
let text = token_tree.syntax().text().to_string();
let text = text
.strip_prefix(['{', '(', '['])
.and_then(|text| text.strip_suffix(['}', ')', ']']))
.unwrap_or(&text);
if !text.is_empty() {
let parsed = SourceFile::parse(text, Edition::CURRENT);
self.walk(parsed.tree().syntax());
}
}
if tokens.iter().any(|token| token.kind() == SyntaxKind::IDENT && token.text() == "serde") {
self.collect_serde_attribute(token_tree);
}
self.collect_path_imports(&tokens);
}
fn collect_path_imports(&mut self, tokens: &[SyntaxToken]) {
for (index, token) in tokens.iter().enumerate() {
if token.kind() == SyntaxKind::COLON2
|| (token.kind() == SyntaxKind::COLON
&& tokens.get(index + 1).is_some_and(|next| next.kind() == SyntaxKind::COLON))
{
self.collect_path_import(tokens, index);
}
}
}
fn collect_path_import(&mut self, tokens: &[SyntaxToken], index: usize) {
let prev = tokens.get(index.wrapping_sub(1));
if let Some(prev) = prev.filter(|token| token.kind() == SyntaxKind::IDENT) {
let before_prev = tokens.get(index.wrapping_sub(2));
let is_continuation = before_prev.is_some_and(|token| {
token.kind() == SyntaxKind::COLON2
|| (token.kind() == SyntaxKind::COLON
&& tokens
.get(index.wrapping_sub(3))
.is_some_and(|token| token.kind() == SyntaxKind::COLON))
});
if !is_continuation {
self.add_import(prev.text());
}
} else {
let next_index =
if tokens[index].kind() == SyntaxKind::COLON2 { index + 1 } else { index + 2 };
if let Some(next) =
tokens.get(next_index).filter(|token| token.kind() == SyntaxKind::IDENT)
{
self.add_import(next.text());
}
}
}
fn collect_use_tree(&mut self, tree: &UseTree) {
if let Some(path) = tree.path() {
if let Some(name_ref) = path.segments().next().and_then(|segment| segment.name_ref()) {
self.add_import(name_ref.text().as_ref());
}
return;
}
if let Some(use_tree_list) = tree.use_tree_list() {
for subtree in use_tree_list.use_trees() {
self.collect_use_tree(&subtree);
}
}
}
fn collect_path(&mut self, path: &Path) {
if path.qualifier().is_some()
&& path.segment().is_some()
&& let Some(name_ref) = path.first_segment().and_then(|segment| segment.name_ref())
{
self.add_import(name_ref.text().as_ref());
}
}
fn collect_serde_attribute(&mut self, token_tree: &TokenTree) {
let tokens: Vec<_> = token_tree
.syntax()
.descendants_with_tokens()
.filter_map(NodeOrToken::into_token)
.filter(|token| !token.kind().is_trivia())
.collect();
for window in tokens.windows(3) {
let [key, eq, string] = window else { continue };
if key.kind() != SyntaxKind::CRATE_KW
&& (key.kind() != SyntaxKind::IDENT || !Self::is_serde_attribute_key(key.text()))
{
continue;
}
if eq.kind() != SyntaxKind::EQ {
continue;
}
if let Some(string) = AstString::cast(string.clone())
&& let Ok(string) = string.value()
&& let Some(import) = string.split("::").find(|s| !s.is_empty())
{
self.add_import(import);
}
}
}
fn is_serde_attribute_key(key: &str) -> bool {
matches!(key, "with" | "deserialize_with" | "serialize_with" | "crate" | "remote")
}
fn collect_macro_call(&mut self, macro_call: &MacroCall) {
let Some(parent) = &self.parent else { return };
let Some(token_tree) = macro_call.token_tree() else { return };
for token in token_tree
.syntax()
.descendants_with_tokens()
.filter_map(NodeOrToken::into_token)
.filter(|token| token.kind() == SyntaxKind::STRING)
.filter_map(AstString::cast)
{
let Ok(value) = token.value() else { continue };
if !value.ends_with(".rs") {
continue;
}
self.result.paths.insert(parent.join(value.as_ref()));
}
}
fn collect_module(
&mut self,
module: &Module,
module_dir: Option<&StdPath>,
path_dir: Option<&StdPath>,
) {
let Some(ident) = module.name() else { return };
let text = ident.text();
let name = text.strip_prefix("r#").unwrap_or(text);
let paths: Vec<_> =
module.attrs().flat_map(|attr| Self::extract_path_attr(&attr)).collect();
if let Some(item_list) = module.item_list() {
self.add_module_path(name, module_dir);
for path in &paths {
if StdPath::new(path)
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("rs"))
{
self.add_explicit_path(path, path_dir);
}
}
let subdir = paths.first().map_or(name, String::as_str);
let subdir = subdir.strip_suffix(".rs").unwrap_or(subdir);
let next_module_dir = module_dir.unwrap_or_else(|| StdPath::new("")).join(subdir);
let next_path_dir = path_dir.unwrap_or_else(|| StdPath::new("")).join(subdir);
for item in item_list.items() {
if let Some(child) = Module::cast(item.syntax().clone()) {
self.collect_module(&child, Some(&next_module_dir), Some(&next_path_dir));
}
}
return;
}
for path in &paths {
self.add_explicit_path(path, path_dir);
}
let has_path_attr = module.attrs().any(|attr| {
let Some(Meta::KeyValueMeta(meta)) = attr.meta() else { return false };
meta.path().is_some_and(|path| path.to_string() == "path") && meta.expr().is_some()
});
if !has_path_attr {
self.add_module_path(name, module_dir);
}
}
fn extract_path_attr(attr: &Attr) -> Vec<String> {
let mut paths = vec![];
let mut tokens = attr
.syntax()
.descendants_with_tokens()
.filter_map(NodeOrToken::into_token)
.filter(|token| !token.kind().is_trivia())
.peekable();
while let Some(token) = tokens.next() {
if token.kind() == SyntaxKind::IDENT
&& token.text() == "path"
&& tokens.next_if(|next| next.kind() == SyntaxKind::EQ).is_some()
&& let Some(value) = tokens.next_if(|next| next.kind() == SyntaxKind::STRING)
&& let Some(string) = AstString::cast(value)
&& let Ok(value) = string.value()
{
paths.push(value.to_string());
}
}
paths
}
fn add_import(&mut self, import: &str) {
if import.is_empty() {
return;
}
if Self::is_known_import(import) {
return;
}
let clean = import.strip_prefix("r#").unwrap_or(import);
if !clean.as_bytes().first().is_some_and(|byte| byte.is_ascii_lowercase() || *byte == b'_')
{
return;
}
self.result.imports.insert(clean.into());
}
fn is_known_import(import: &str) -> bool {
matches!(import, "crate" | "super" | "self" | "std")
}
fn add_module_path(&mut self, name: &str, dir: Option<&StdPath>) {
let Some(module) = &self.module else { return };
let name = name.strip_prefix("r#").unwrap_or(name);
let base = dir.map_or_else(|| module.clone(), |dir| module.join(dir));
self.result.paths.insert(base.join(format!("{name}.rs")));
self.result.paths.insert(base.join(name).join("mod.rs"));
}
fn add_explicit_path(&mut self, path: &str, dir: Option<&StdPath>) {
let Some(parent) = &self.parent else { return };
let base = dir.unwrap_or(parent);
self.result.paths.insert(base.join(path));
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn collects_imports_from_doc_rust_block() {
let source = r#"
/// Parses URLs.
///
/// ```rust
/// # use url::Url;
/// let url = Url::parse("https://example.com").unwrap();
/// println!("{}", url);
/// ```
fn demo() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["url".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn collects_imports_from_doc_block_with_attribute() {
let source = r"
/// ```rust
/// # use async_trait::async_trait;
/// #[async_trait]
/// trait HttpClient {
/// async fn send(request: Request);
/// }
/// ```
fn example() {}
";
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["async_trait".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn collects_imports_from_statement_based_doctest() {
let source = r#"
/// ```rust
/// let value = serde_json::json!({"key": "value"});
/// let serialized = serde_json::to_string(&value).unwrap();
/// ```
fn example() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["serde_json".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn non_executable_doctest_ignore() {
let source = r#"
/// ```ignore
/// # use url::Url;
/// let url = Url::parse("https://example.com").unwrap();
/// ```
fn demo() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["url".into()]));
assert!(!parsed.has_doctests);
}
#[test]
fn doctest_no_run() {
let source = r#"
/// ```no_run
/// # use url::Url;
/// let url = Url::parse("https://example.com").unwrap();
/// ```
fn demo() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["url".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn doctest_compile_fail() {
let source = r#"
/// ```compile_fail
/// # use url::Url;
/// let url = Url::parse("https://example.com").unwrap();
/// ```
fn demo() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["url".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn executable_doctest_should_panic() {
let source = r#"
/// ```should_panic
/// # use url::Url;
/// let url = Url::parse("https://example.com").unwrap();
/// ```
fn demo() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["url".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn executable_doctest_empty_fence() {
let source = r#"
/// ```
/// # use url::Url;
/// let url = Url::parse("https://example.com").unwrap();
/// ```
fn demo() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(parsed.imports, FxHashSet::from_iter(["url".into()]));
assert!(parsed.has_doctests);
}
#[test]
fn collects_paths_modules() {
let source = r"
mod foo;
pub mod bar;
mod r#box;
mod inline {
mod child;
}
mod a { mod b { mod c { mod d; } } }
";
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("foo.rs"),
PathBuf::from("foo/mod.rs"),
PathBuf::from("bar.rs"),
PathBuf::from("bar/mod.rs"),
PathBuf::from("box.rs"),
PathBuf::from("box/mod.rs"),
PathBuf::from("inline.rs"),
PathBuf::from("inline/mod.rs"),
PathBuf::from("inline/child.rs"),
PathBuf::from("inline/child/mod.rs"),
PathBuf::from("a.rs"),
PathBuf::from("a/mod.rs"),
PathBuf::from("a/b.rs"),
PathBuf::from("a/b/mod.rs"),
PathBuf::from("a/b/c.rs"),
PathBuf::from("a/b/c/mod.rs"),
PathBuf::from("a/b/c/d.rs"),
PathBuf::from("a/b/c/d/mod.rs"),
])
);
}
#[test]
fn collects_paths_path() {
let source = r#"
#[path = "custom/path.rs"]
mod foo;
#[path = "../sibling/mod.rs"]
mod sibling;
#[path = "implementations"]
mod impls {
mod bar;
}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("custom/path.rs"),
PathBuf::from("../sibling/mod.rs"),
PathBuf::from("impls.rs"),
PathBuf::from("impls/mod.rs"),
PathBuf::from("implementations/bar.rs"),
PathBuf::from("implementations/bar/mod.rs"),
])
);
}
#[test]
fn collects_paths_cfg() {
let source = r#"
#[cfg_attr(feature = "foo", path = "foo_impl.rs")]
mod impl_module;
#[cfg_attr(feature = "v1", path = "v1.rs")]
#[cfg_attr(feature = "v2", path = "v2.rs")]
mod versioned;
#[cfg(feature = "serde")]
mod serde_support;
#[cfg(test)]
mod tests {
mod fixtures;
}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("foo_impl.rs"),
PathBuf::from("impl_module.rs"),
PathBuf::from("impl_module/mod.rs"),
PathBuf::from("v1.rs"),
PathBuf::from("v2.rs"),
PathBuf::from("versioned.rs"),
PathBuf::from("versioned/mod.rs"),
PathBuf::from("serde_support.rs"),
PathBuf::from("serde_support/mod.rs"),
PathBuf::from("tests.rs"),
PathBuf::from("tests/mod.rs"),
PathBuf::from("tests/fixtures.rs"),
PathBuf::from("tests/fixtures/mod.rs"),
])
);
}
#[test]
fn collects_paths_macros() {
let source = r##"
include!("generated/code.rs");
include!(r#"path/to/file.rs"#);
const SOURCE: &str = include_str!("./minicore.rs");
const DATA: &[u8] = include_bytes!("data.rs");
fake_macro!("some/path.rs", "other_arg");
foo::bar::baz!("nested/call.rs");
"##;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("generated/code.rs"),
PathBuf::from("path/to/file.rs"),
PathBuf::from("./minicore.rs"),
PathBuf::from("data.rs"),
PathBuf::from("some/path.rs"),
PathBuf::from("nested/call.rs"),
])
);
}
#[test]
fn collects_paths_macro() {
let source = r#"
m! {
mod foo;
mod bar;
}
outer! {
inner! {
mod nested;
}
}
macro_rules! root {
() => {
pub mod de;
};
}
n! {
#[path = "custom.rs"]
mod explicit;
}
o! {
mod inline {
mod child;
}
}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("foo.rs"),
PathBuf::from("foo/mod.rs"),
PathBuf::from("bar.rs"),
PathBuf::from("bar/mod.rs"),
PathBuf::from("nested.rs"),
PathBuf::from("nested/mod.rs"),
PathBuf::from("de.rs"),
PathBuf::from("de/mod.rs"),
PathBuf::from("custom.rs"),
PathBuf::from("inline.rs"),
PathBuf::from("inline/mod.rs"),
PathBuf::from("inline/child.rs"),
PathBuf::from("inline/child/mod.rs"),
])
);
}
#[test]
fn collects_paths_expanded() {
let source = r#"
mod normalize {
#[path = "tests.rs"]
mod tests {
#[path = "and-n-others.rs"]
mod and_n_others {
fn test() {}
}
#[path = "basic.rs"]
mod basic {
fn test() {}
}
}
}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("normalize.rs"),
PathBuf::from("normalize/mod.rs"),
PathBuf::from("normalize/tests.rs"),
PathBuf::from("normalize/tests/mod.rs"),
PathBuf::from("normalize/tests/and_n_others.rs"),
PathBuf::from("normalize/tests/and_n_others/mod.rs"),
PathBuf::from("normalize/tests/and-n-others.rs"),
PathBuf::from("normalize/tests/basic.rs"),
PathBuf::from("normalize/tests/basic/mod.rs"),
])
);
}
#[test]
fn collects_paths_nested_attr() {
let parsed = ParsedSource::from_path_test(
r#"
#[cfg(windows)]
#[path = "windows/sys.rs"]
mod imp;
#[cfg(not(windows))]
#[path = "windows/stub.rs"]
mod imp;
"#,
Path::new("signal/windows.rs"),
false,
);
assert_eq!(
parsed.paths,
FxHashSet::from_iter([
PathBuf::from("signal/windows/sys.rs"),
PathBuf::from("signal/windows/stub.rs"),
])
);
}
#[test]
fn detects_normal_test() {
let source = r"
#[test]
fn my_test() {}
";
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert!(parsed.has_tests);
}
#[test]
fn detects_cfg_test_module() {
let source = r"
#[cfg(test)]
mod tests {
fn my_test() {}
}
";
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert!(parsed.has_tests);
}
#[test]
fn detects_test_behind_non_test_cfg() {
let source = r"
#[test]
#[cfg(any(coverage, coverage_nightly))]
fn my_test() {}
";
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert!(parsed.has_tests);
}
#[test]
fn detects_test_behind_feature_cfg() {
let source = r#"
#[test]
#[cfg(feature = "memory")]
fn my_test() {}
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert!(parsed.has_tests);
}
#[test]
fn collects_imports_inside_cfg_attr() {
let source = r#"
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Repro;
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert!(parsed.imports.contains("serde"));
}
#[test]
fn collects_imports_inside_nested_cfg_attr() {
let source = r#"
#[cfg_attr(feature = "a", cfg_attr(feature = "b", derive(serde::Serialize)))]
pub struct Repro;
"#;
let parsed = ParsedSource::from_str(source, Path::new("lib.rs"));
assert!(parsed.imports.contains("serde"));
}
}