use crate::analysis::{ParsedFile, ParsedFunction};
use crate::model::{Finding, Severity};
pub(crate) const BINDING_LOCATION: &str = file!();
use super::super::common::import_alias_lookup;
pub(crate) fn alloc_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let go = function.go_evidence();
go.alloc_loops
.iter()
.map(|line| Finding {
rule_id: "allocation_churn_in_loop".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} allocates new objects inside a loop",
function.fingerprint.name
),
evidence: vec![
"make/new or buffer construction appears inside a loop".to_string(),
"repeated per-iteration allocation can create avoidable heap churn".to_string(),
],
})
.collect()
}
pub(crate) fn fmt_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let go = function.go_evidence();
go.fmt_loops
.iter()
.map(|line| Finding {
rule_id: "fmt_hot_path".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} formats strings with fmt inside a loop",
function.fingerprint.name
),
evidence: vec![
"fmt formatting call appears inside a loop".to_string(),
"fmt-heavy formatting in iterative paths can be expensive".to_string(),
],
})
.collect()
}
pub(crate) fn reflect_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let go = function.go_evidence();
go.reflect_loops
.iter()
.map(|line| Finding {
rule_id: "reflection_hot_path".to_string(),
severity: Severity::Warning,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} uses reflection inside a loop",
function.fingerprint.name
),
evidence: vec![
"reflect package call appears inside a loop".to_string(),
"reflection in hot paths often adds avoidable overhead".to_string(),
],
})
.collect()
}
pub(crate) fn concat_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let go = function.go_evidence();
go.concat_loops
.iter()
.map(|line| Finding {
rule_id: "string_concat_in_loop".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} concatenates strings inside a loop",
function.fingerprint.name
),
evidence: vec![
"loop-local string concatenation can create repeated allocations".to_string(),
],
})
.collect()
}
pub(crate) fn json_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let go = function.go_evidence();
go.json_loops
.iter()
.map(|line| Finding {
rule_id: "repeated_json_marshaling".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} marshals JSON inside a loop",
function.fingerprint.name
),
evidence: vec![
"encoding/json marshal call appears inside a loop".to_string(),
"repeated JSON serialization in iterative paths can become a hot allocation site"
.to_string(),
],
})
.collect()
}
pub(crate) fn db_findings(
file: &ParsedFile,
function: &ParsedFunction,
enable_go_semantic: bool,
) -> Vec<Finding> {
let go = function.go_evidence();
let mut findings = Vec::new();
let nested_loop_signal = enable_go_semantic && has_nested_loop_signal(&function.body_text);
for query_call in go.db_query_calls {
if query_call.in_loop && query_call.method_name != "Preload" {
let receiver = query_call.receiver.as_deref().unwrap_or("<unknown>");
findings.push(Finding {
rule_id: "n_plus_one_query".to_string(),
severity: if nested_loop_signal {
Severity::Error
} else {
Severity::Warning
},
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: query_call.line,
end_line: query_call.line,
message: format!(
"function {} issues a database-style query inside a loop",
function.fingerprint.name
),
evidence: vec![
format!("looped query method: {receiver}.{}", query_call.method_name),
"query execution inside loops often turns into N+1 access patterns".to_string(),
if nested_loop_signal {
"semantic correlation tied this query to nested loop structure, which makes multiplicative query growth more likely"
.to_string()
} else {
"no nested-loop correlation was required for the baseline warning".to_string()
},
],
});
}
let Some(query_text) = &query_call.query_text else {
continue;
};
let normalized = query_text.to_ascii_uppercase();
if normalized.starts_with("SELECT *") {
findings.push(Finding {
rule_id: "wide_select_query".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: query_call.line,
end_line: query_call.line,
message: format!(
"function {} issues a wide SELECT * query",
function.fingerprint.name
),
evidence: vec![format!("query text: {query_text}")],
});
}
if normalized.contains("LIKE '%")
|| normalized.contains(" ORDER BY ") && !normalized.contains(" LIMIT ")
{
findings.push(Finding {
rule_id: "likely_unindexed_query".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: query_call.line,
end_line: query_call.line,
message: format!(
"function {} uses a query shape that may bypass effective indexing",
function.fingerprint.name
),
evidence: vec![
format!("query text: {query_text}"),
"leading wildcard filters or ORDER BY without LIMIT often scale poorly"
.to_string(),
],
});
}
}
findings
}
pub(crate) fn n_squared_findings(
file: &ParsedFile,
function: &ParsedFunction,
enable_go_semantic: bool,
) -> Vec<Finding> {
if !enable_go_semantic || !has_nested_loop_signal(&function.body_text) {
return Vec::new();
}
let mut findings = nested_loop_alloc_findings(file, function);
findings.extend(nested_loop_concat_findings(file, function));
findings
}
fn nested_loop_alloc_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let go = function.go_evidence();
go.alloc_loops
.iter()
.map(|line| Finding {
rule_id: "likely_n_squared_allocation".to_string(),
severity: Severity::Warning,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} appears to allocate inside a nested loop",
function.fingerprint.name
),
evidence: vec![
format!("allocation observed inside a loop at line {line}"),
"nested loop structure was also detected in the same function body".to_string(),
"this is conservative: preallocation or small bounded inputs may still make the code acceptable"
.to_string(),
"consider preallocating outside the inner loop or flattening repeated traversal"
.to_string(),
],
})
.collect()
}
fn nested_loop_concat_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
if has_builder_usage(&function.body_text) {
return Vec::new();
}
let go = function.go_evidence();
go.concat_loops
.iter()
.map(|line| Finding {
rule_id: "likely_n_squared_string_concat".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: *line,
end_line: *line,
message: format!(
"function {} appears to concatenate strings inside a nested loop",
function.fingerprint.name
),
evidence: vec![
format!("string concatenation observed inside a loop at line {line}"),
"nested loop structure was also detected in the same function body".to_string(),
"no obvious strings.Builder or bytes.Buffer usage was detected nearby".to_string(),
"consider strings.Builder, bytes.Buffer, or a pre-sized byte slice for repeated inner-loop string assembly"
.to_string(),
],
})
.collect()
}
fn has_nested_loop_signal(body_text: &str) -> bool {
let mut brace_depth = 0usize;
let mut active_loop_exit_depths = Vec::new();
for raw_line in body_text.split('\n') {
let line = raw_line.split("//").next().unwrap_or("");
let closing_braces = line.chars().filter(|character| *character == '}').count();
for _ in 0..closing_braces {
brace_depth = brace_depth.saturating_sub(1);
while active_loop_exit_depths
.last()
.is_some_and(|exit_depth| *exit_depth > brace_depth)
{
active_loop_exit_depths.pop();
}
}
if contains_keyword(line, "for") {
if !active_loop_exit_depths.is_empty() {
return true;
}
let opening_braces = line.chars().filter(|character| *character == '{').count();
active_loop_exit_depths.push(brace_depth + opening_braces.max(1));
}
brace_depth += line.chars().filter(|character| *character == '{').count();
}
false
}
fn has_builder_usage(body_text: &str) -> bool {
let normalized = body_text.replace(char::is_whitespace, "");
normalized.contains("strings.Builder")
|| normalized.contains("bytes.Buffer")
|| normalized.contains("Builder{}")
|| normalized.contains("Buffer{}")
}
fn contains_keyword(line: &str, keyword: &str) -> bool {
let bytes = line.as_bytes();
let keyword_bytes = keyword.as_bytes();
if keyword_bytes.is_empty() || bytes.len() < keyword_bytes.len() {
return false;
}
for start in 0..=bytes.len() - keyword_bytes.len() {
if &bytes[start..start + keyword_bytes.len()] != keyword_bytes {
continue;
}
let left_ok =
start == 0 || !bytes[start - 1].is_ascii_alphanumeric() && bytes[start - 1] != b'_';
let right_index = start + keyword_bytes.len();
let right_ok = right_index == bytes.len()
|| !bytes[right_index].is_ascii_alphanumeric() && bytes[right_index] != b'_';
if left_ok && right_ok {
return true;
}
}
false
}
pub(crate) fn load_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
let import_aliases = import_alias_lookup(&file.imports);
function
.calls
.iter()
.filter_map(|call| {
let receiver = call.receiver.as_deref()?;
let import_path = import_aliases.get(receiver)?;
let evidence = match (import_path.as_str(), call.name.as_str()) {
("io", "ReadAll") | ("io/ioutil", "ReadAll") => Some(format!(
"{receiver}.{} reads the full stream into memory",
call.name
)),
("os", "ReadFile") => Some(format!(
"{receiver}.ReadFile loads the whole file before processing"
)),
_ => None,
}?;
Some(Finding {
rule_id: "full_dataset_load".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: call.line,
end_line: call.line,
message: format!(
"function {} loads an entire payload into memory",
function.fingerprint.name
),
evidence: vec![
format!("import alias {receiver} resolves to {import_path}"),
evidence,
],
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{contains_keyword, has_builder_usage, has_nested_loop_signal};
#[test]
fn detects_nested_loop_signal() {
let body = "{\nfor _, row := range rows {\n for _, cell := range row {\n _ = cell\n }\n}\n}";
assert!(has_nested_loop_signal(body));
}
#[test]
fn ignores_sequential_loops() {
let body = "{\nfor _, row := range rows {\n _ = row\n}\nfor _, cell := range cells {\n _ = cell\n}\n}";
assert!(!has_nested_loop_signal(body));
}
#[test]
fn detects_builder_usage_markers() {
assert!(has_builder_usage("{\nvar builder strings.Builder\n}\n"));
assert!(has_builder_usage("{\nvar buf bytes.Buffer\n}\n"));
}
#[test]
fn matches_keyword_boundaries() {
assert!(contains_keyword("for _, item := range items {", "for"));
assert!(!contains_keyword("formatValue()", "for"));
}
}