mod comments;
mod context;
mod errors;
mod general;
mod performance;
#[cfg(test)]
mod tests;
use std::path::Path;
use anyhow::{Context, Result, anyhow};
use tree_sitter::{Node, Parser};
use crate::analysis::go::fingerprint::build_function_fingerprint;
use crate::analysis::{ParsedFile, ParsedFunction};
use self::comments::extract_doc_comment;
use self::context::{
collect_busy_wait_lines, collect_context_factory_calls, collect_goroutine_in_loop_lines,
collect_goroutine_launch_lines, collect_goroutine_without_shutdown_lines,
collect_mutex_lock_in_loop_lines, collect_sleep_in_loop_lines,
function_has_context_parameter,
};
use self::errors::{
collect_dropped_error_lines, collect_errorf_calls, collect_panic_on_error_lines,
};
use self::general::{
build_test_function_summary, collect_calls, collect_imports, collect_local_string_literals,
collect_package_string_literals, collect_struct_tags, collect_symbols, count_descendants,
extract_receiver_type, find_package_name,
};
use self::performance::{
collect_allocation_in_loop_lines, collect_db_query_calls, collect_fmt_in_loop_lines,
collect_json_marshal_in_loop_lines, collect_reflection_in_loop_lines,
collect_string_concat_in_loop_lines,
};
pub(super) fn parse_file(path: &Path, source: &str) -> Result<ParsedFile> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.map_err(|error| anyhow!(error.to_string()))
.context("failed to configure Go parser")?;
let tree = parser
.parse(source, None)
.ok_or_else(|| anyhow!("tree-sitter returned no parse tree"))?;
let root = tree.root_node();
let package_name = find_package_name(root, source);
let is_test_file = path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.ends_with("_test.go"));
let imports = collect_imports(root, source);
let package_string_literals = collect_package_string_literals(root, source);
let struct_tags = collect_struct_tags(root, source);
let symbols = collect_symbols(root, source);
let functions = collect_functions(root, source, &imports, is_test_file);
Ok(ParsedFile {
path: path.to_path_buf(),
package_name,
is_test_file,
syntax_error: root.has_error(),
byte_size: source.len(),
package_string_literals,
struct_tags,
functions,
imports,
symbols,
})
}
fn collect_functions(
root: Node<'_>,
source: &str,
imports: &[crate::analysis::ImportSpec],
is_test_file: bool,
) -> Vec<ParsedFunction> {
let mut functions = Vec::new();
visit_for_functions(root, source, imports, is_test_file, &mut functions);
functions.sort_by(|left, right| {
left.fingerprint
.start_line
.cmp(&right.fingerprint.start_line)
.then(left.fingerprint.name.cmp(&right.fingerprint.name))
});
functions
}
fn visit_for_functions(
node: Node<'_>,
source: &str,
imports: &[crate::analysis::ImportSpec],
is_test_file: bool,
functions: &mut Vec<ParsedFunction>,
) {
if matches!(node.kind(), "function_declaration" | "method_declaration") {
if let Some(parsed_function) = parse_function_node(node, source, imports, is_test_file) {
functions.push(parsed_function);
}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
visit_for_functions(child, source, imports, is_test_file, functions);
}
}
fn parse_function_node(
node: Node<'_>,
source: &str,
imports: &[crate::analysis::ImportSpec],
is_test_file: bool,
) -> Option<ParsedFunction> {
let body_node = node.child_by_field_name("body")?;
let calls = collect_calls(body_node, source);
let local_string_literals = collect_local_string_literals(body_node, source);
let type_assertion_count = count_descendants(body_node, "type_assertion_expression");
let has_context_parameter = function_has_context_parameter(node, source, imports);
let doc_comment = extract_doc_comment(source, node.start_position().row);
let function_name = source.get(node.child_by_field_name("name")?.byte_range())?.to_string();
let test_summary = build_test_function_summary(
&function_name,
body_node,
source,
&calls,
is_test_file,
);
let dropped_error_lines = collect_dropped_error_lines(body_node, source);
let panic_on_error_lines = collect_panic_on_error_lines(body_node, source);
let errorf_calls = collect_errorf_calls(body_node, source);
let context_factory_calls = collect_context_factory_calls(body_node, source, imports);
let goroutine_launch_lines = collect_goroutine_launch_lines(body_node);
let goroutine_in_loop_lines = collect_goroutine_in_loop_lines(body_node);
let goroutine_without_shutdown_lines =
collect_goroutine_without_shutdown_lines(body_node, source);
let sleep_in_loop_lines = collect_sleep_in_loop_lines(body_node, source, imports);
let busy_wait_lines = collect_busy_wait_lines(body_node, source);
let mutex_lock_in_loop_lines = collect_mutex_lock_in_loop_lines(body_node, source);
let allocation_in_loop_lines = collect_allocation_in_loop_lines(body_node, source, imports);
let fmt_in_loop_lines = collect_fmt_in_loop_lines(body_node, source, imports);
let reflection_in_loop_lines = collect_reflection_in_loop_lines(body_node, source, imports);
let string_concat_in_loop_lines = collect_string_concat_in_loop_lines(body_node, source);
let json_marshal_in_loop_lines =
collect_json_marshal_in_loop_lines(body_node, source, imports);
let db_query_calls = collect_db_query_calls(body_node, source);
let receiver_type = node
.child_by_field_name("receiver")
.and_then(|receiver| extract_receiver_type(receiver, source));
let fingerprint = build_function_fingerprint(
node,
source,
receiver_type,
type_assertion_count,
calls.len(),
)?;
Some(ParsedFunction {
fingerprint,
calls,
has_context_parameter,
doc_comment,
local_string_literals,
test_summary,
dropped_error_lines,
panic_on_error_lines,
errorf_calls,
context_factory_calls,
goroutine_launch_lines,
goroutine_in_loop_lines,
goroutine_without_shutdown_lines,
sleep_in_loop_lines,
busy_wait_lines,
mutex_lock_in_loop_lines,
allocation_in_loop_lines,
fmt_in_loop_lines,
reflection_in_loop_lines,
string_concat_in_loop_lines,
json_marshal_in_loop_lines,
db_query_calls,
})
}