use ra_ap_syntax::{
ast::{self, HasModuleItem, HasName, HasVisibility, Item},
ted::{self, Position},
AstNode, Edition, Parse, SourceFile, SyntaxKind, SyntaxNode,
};
use std::env;
use std::io::Read;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <n>", args[0]);
std::process::exit(1);
}
let n: usize = args[1]
.parse()
.expect("Please provide a valid number in the set (2015, 2018, 2021, 2024).");
let edition = match n {
2015 => Edition::Edition2015,
2018 => Edition::Edition2018,
2021 => Edition::Edition2021,
2024 => Edition::Edition2024,
_ => panic!("nsupported or invalid Rust edition {n}"),
};
let content = read_stdin()?;
let instrumented = instrument_code(edition, &content);
print!("{instrumented}");
Ok(())
}
#[cfg_attr(not(feature = "internal_docs"), doc(hidden))]
fn parse_attr(attr: &str) -> Option<ra_ap_syntax::SyntaxNode> {
let parse: Parse<ast::SourceFile> = SourceFile::parse(attr, Edition::Edition2021);
parse
.tree()
.syntax()
.first_child()
.map(|node| node.clone_for_update())
}
#[cfg_attr(not(feature = "internal_docs"), doc(hidden))]
fn find_best_import_position(tree: &ast::SourceFile) -> (Position, bool) {
let item = tree
.items()
.filter(|item| !matches!(item, Item::Use(_)))
.filter(|item| {
!item.syntax().children_with_tokens().any(|token| {
token.kind() == SyntaxKind::COMMENT && token.to_string().starts_with("/*[toml]")
})
})
.take(1)
.next();
(
Position::before(item.expect("Could not unwrap item").syntax()),
true,
)
}
#[cfg_attr(not(feature = "internal_docs"), doc(hidden))]
fn instrument_code(edition: Edition, source: &str) -> String {
let parse = SourceFile::parse(source, edition);
let tree = parse.tree().clone_for_update();
let imports = ["use thag_profiler::{enable_profiling, end, profile, profiled};"];
for import_text in imports {
if !source.contains(import_text) {
let parse = SourceFile::parse(import_text, Edition::Edition2021);
if let Some(import_node) = parse
.tree()
.syntax()
.first_child()
.map(|n| n.clone_for_update())
{
let (pos, insert_nl) = find_best_import_position(&tree);
ted::insert(pos, &import_node);
if insert_nl {
let newline = ast::make::tokens::single_newline();
let (pos, _) = find_best_import_position(&tree);
ted::insert(pos, newline);
}
}
}
}
let newline = ast::make::tokens::single_newline();
let (pos, _) = find_best_import_position(&tree);
ted::insert(pos, newline);
for node in tree.syntax().descendants() {
if let Some(function) = ast::Fn::cast(node.clone()) {
if function.const_token().is_some() {
continue;
}
let fn_name = function.name().map(|n| n.text().to_string());
let attr_texts = if fn_name.as_deref() == Some("main") {
vec!["#[enable_profiling]"]
} else {
vec!["#[profiled]"]
};
let fn_token = function.fn_token().expect("Function token is None");
let maybe_visibility = function.visibility();
let maybe_async_token = function.async_token();
let maybe_unsafe_token = function.unsafe_token();
let target_token = if let Some(visibility) = maybe_visibility {
visibility.pub_token().unwrap_or_else(|| {
maybe_async_token.unwrap_or_else(|| maybe_unsafe_token.unwrap_or(fn_token))
})
} else if let Some(async_token) = maybe_async_token {
async_token
} else if let Some(unsafe_token) = maybe_unsafe_token {
unsafe_token
} else {
fn_token
};
let function_syntax: &SyntaxNode = function.syntax();
if function.body().is_some()
&& !function_syntax.descendants_with_tokens().any(|it| {
let text = it.to_string();
text.starts_with("#[profiled")
|| text.starts_with("#[thag_profiler::profiled")
|| text.starts_with("#[enable_profiling")
|| text.starts_with("#[thag_profiler::enable_profiling")
|| text.starts_with("#[test")
|| text.starts_with("profile!")
|| text.starts_with("enable_profiling")
})
{
let indent = function
.syntax()
.prev_sibling_or_token()
.and_then(|t| {
if t.kind() == SyntaxKind::WHITESPACE {
let s = t.to_string();
let new_indent = s
.rmatch_indices('\n')
.next()
.map_or_else(|| s.clone(), |(i, _)| (s[i..]).to_string());
Some(new_indent)
} else {
None
}
})
.unwrap_or_default();
for attr_text in &attr_texts {
let attr_node = parse_attr(&format!("{indent}{attr_text}"))
.expect("Failed to parse attribute");
ted::insert(Position::before(&target_token), &attr_node);
}
}
}
}
tree.syntax().to_string()
}
#[cfg_attr(not(feature = "internal_docs"), doc(hidden))]
fn read_stdin() -> std::io::Result<String> {
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instrument_no_duplicate_imports() {
let input = r#"
use some_crate::something;
use thag_proc_macros::enable_profiling;
fn foo() {}"#;
let output = instrument_code(Edition::Edition2021, input);
assert_eq!(
output
.matches("use thag_proc_macros::enable_profiling")
.count(),
1,
"should not duplicate existing import"
);
}
#[test]
fn test_instrument_basic_function_instrumentation() {
let input = "fn foo() {}";
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("#[profiled]\nfn foo()"));
}
#[test]
fn test_instrument_main_function_special_handling() {
let input = "fn main() {}";
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("#[enable_profiling]\nfn main()"));
}
#[test]
fn test_instrument_preserves_indentation() {
let input = r#"
impl Foo {
fn bar() {}
}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains(" #[profiled]\n fn bar()"));
}
#[test]
fn test_instrument_multiple_attributes() {
let input = r#"#[allow(dead_code)]
fn main() {}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("#[allow(dead_code)]\n#[enable_profiling]\nfn main()"));
}
#[test]
fn test_instrument_nested_functions() {
let input = r#"
fn outer() {
fn inner() {}
}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("#[profiled]\nfn outer()"));
assert!(output.contains(" #[profiled]\n fn inner()"));
}
#[test]
fn test_instrument_impl_block_functions() {
let input = r#"
impl Foo {
fn method1(&self) {}
fn method2(&self) {}
}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains(" #[profiled]\n fn method1"));
assert!(output.contains(" #[profiled]\n fn method2"));
}
#[test]
fn test_instrument_preserves_file_start() {
let input = "// Copyright notice\n\nfn foo() {}";
let output = instrument_code(Edition::Edition2021, input);
assert!(output.starts_with("// Copyright notice\n"));
}
#[test]
fn test_instrument_trait_impl_functions() {
let input = r#"
impl SomeTrait for Foo {
fn required_method(&self) {}
}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains(" #[profiled]\n fn required_method"));
}
#[test]
fn test_instrument_async_functions() {
let input = "async fn async_foo() {}";
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("#[profiled]\nasync fn async_foo()"));
}
#[test]
fn test_instrument_generic_functions() {
let input = "fn generic<T: Display>(value: T) {}";
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("#[profiled]\nfn generic<T: Display>"));
}
#[test]
fn test_instrument_doc_comments_preserved() {
let input = r#"
/// Doc comment
fn documented() {}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("/// Doc comment\n#[profiled]\nfn documented()"));
}
#[test]
fn test_instrument_complex_spacing() {
let input = r#"
use std::fmt;
// Some comment
fn foo() {}
fn bar() {}"#;
let output = instrument_code(Edition::Edition2021, input);
assert!(output.contains("}\n\n#[profiled]\nfn bar()"));
}
}