use super::*;
pub(super) fn slice_map_findings(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
findings.extend(copy_append_idiom(file, function, lines));
findings.extend(map_delete_loop(file, function, lines));
findings.extend(sort_slice_vs_sort(file, function, lines));
findings.extend(range_string_by_index(file, function, lines));
findings.extend(map_lookup_double(file, function, lines));
findings.extend(slice_grow_without_cap_hint(file, function, lines));
findings.extend(interface_slice_alloc(file, function, lines));
findings.extend(map_of_slices_prealloc(file, function, lines));
findings.extend(clear_map_go121(file, function, lines));
findings.extend(unnecessary_slice_copy(file, function, lines));
findings.extend(three_index_slice_for_append(file, function, lines));
findings.extend(range_copy_large_struct(file, function, lines));
findings.extend(dense_int_set_map(file, function, lines));
findings
}
fn copy_append_idiom(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for bl in lines {
if bl.text.contains("append([]") && bl.text.contains("(nil),") && bl.text.contains("...)") {
findings.push(Finding {
rule_id: "copy_append_idiom_waste".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} clones a slice via append(nil, src...)",
function.fingerprint.name
),
evidence: vec![
format!("append([]T(nil), src...) at line {}", bl.line),
"make(len(src)) + copy allocates the exact size once".into(),
],
});
}
}
findings
}
fn map_delete_loop(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for (i, bl) in lines.iter().enumerate() {
if bl.in_loop
&& bl.text.contains("delete(")
&& let Some(prev) = lines.get(i.wrapping_sub(1))
&& prev.text.contains("for")
&& prev.text.contains("range")
{
findings.push(Finding {
rule_id: "map_delete_in_loop_vs_new_map".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} deletes map entries in a loop",
function.fingerprint.name
),
evidence: vec![
format!("delete() in range loop at line {}", bl.line),
"creating a new map is O(1) vs O(n) iterative delete".into(),
],
});
}
}
findings
}
fn sort_slice_vs_sort(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for alias in import_aliases_for(file, "sort") {
for bl in lines {
if bl.text.contains(&format!("{alias}.Sort("))
&& (bl.text.contains("StringSlice")
|| bl.text.contains("IntSlice")
|| bl.text.contains("Float64Slice"))
{
findings.push(Finding {
rule_id: "sort_slice_vs_sort_sort".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} uses sort.Sort with type adapter",
function.fingerprint.name
),
evidence: vec![
format!("{}.Sort with type adapter at line {}", alias, bl.line),
"slices.Sort (Go 1.21+) avoids interface dispatch overhead".into(),
],
});
}
}
}
findings
}
fn range_string_by_index(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for bl in lines {
if bl.text.contains("i := 0; i < len(") && bl.text.contains("); i++") {
let next_lines: Vec<&BodyLine> = lines
.iter()
.filter(|l| l.line > bl.line && l.line <= bl.line + 3)
.collect();
for nl in &next_lines {
if nl.text.contains("[i]") && !nl.text.contains("[]byte") {
findings.push(Finding {
rule_id: "range_over_string_by_index".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} iterates string by byte index",
function.fingerprint.name
),
evidence: vec![
format!("index-based string iteration at line {}", bl.line),
"for _, r := range s correctly handles multi-byte runes".into(),
],
});
break;
}
}
}
}
findings
}
fn map_lookup_double(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for (i, bl) in lines.iter().enumerate() {
if !(bl.text.contains(", ok :=") || bl.text.contains(", ok =")) {
continue;
}
let Some(start) = bl.text.find('[') else {
continue;
};
let Some(end) = bl.text[start..].find(']') else {
continue;
};
let end = start + end;
let key_expr = bl.text[start + 1..end].trim();
let map_expr = bl.text[..start]
.split_whitespace()
.last()
.unwrap_or("")
.trim_start_matches("if");
if map_expr.is_empty() || key_expr.is_empty() {
continue;
}
for next in lines.iter().skip(i + 1).take(5) {
if next.text.contains(&format!("{map_expr}[{key_expr}]")) {
findings.push(Finding {
rule_id: "map_lookup_double_access".into(),
severity: Severity::Warning,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: next.line,
message: format!(
"function {} performs double map lookup for same key",
function.fingerprint.name
),
evidence: vec![
format!(
"first lookup at line {}, second at line {}",
bl.line, next.line
),
"v, ok := m[k] does one lookup instead of two".into(),
],
});
break;
}
}
}
findings
}
fn interface_slice_alloc(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for bl in lines {
if (bl.text.contains("[]interface{}") || bl.text.contains("[]any{"))
&& (bl.text.contains("make(") || bl.text.contains(":=") || bl.text.contains("var "))
{
findings.push(Finding {
rule_id: "interface_slice_allocation".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} uses []interface{{}} or []any for homogeneous data",
function.fingerprint.name
),
evidence: vec![
format!("interface slice at line {}", bl.line),
"typed slices or generics avoid heap escape per element".into(),
],
});
}
}
findings
}
fn slice_grow_without_cap_hint(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for bl in lines {
if !bl.in_loop || !bl.text.contains("append(") {
continue;
}
let Some(target) = bl
.text
.split("append(")
.nth(1)
.and_then(|suffix| suffix.split(',').next())
.map(str::trim)
else {
continue;
};
if !is_identifier_name(target) {
continue;
}
let has_zero_cap_init = lines.iter().any(|line| {
line.line < bl.line
&& (line.text.starts_with(&format!("var {target} []"))
|| line.text.starts_with(&format!("{target} := []"))
|| (line.text.starts_with(&format!("{target} := make([]"))
&& line.text.contains(", 0)")
&& !line.text.contains(", 0,")))
});
if has_zero_cap_init {
findings.push(Finding {
rule_id: "slice_grow_without_cap_hint".into(),
severity: Severity::Warning,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} appends to {} in a loop without a capacity hint",
function.fingerprint.name, target
),
evidence: vec![
format!("append({target}, ...) inside loop at line {}", bl.line),
"make([]T, 0, len(source)) avoids repeated grow-and-copy cycles".into(),
],
});
}
}
findings
}
fn map_of_slices_prealloc(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for bl in lines {
if bl.in_loop && bl.text.contains("] = append(") && bl.text.contains('[') {
findings.push(Finding {
rule_id: "map_of_slices_prealloc".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} appends to a map-of-slices entry without preallocating",
function.fingerprint.name
),
evidence: vec![
format!("map[key] = append(map[key], ...) at line {}", bl.line),
"pre-sizing inner slices avoids repeated header writes and backing-array churn"
.into(),
],
});
}
}
findings
}
fn clear_map_go121(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for (i, bl) in lines.iter().enumerate() {
if bl.in_loop
&& bl.text.contains("delete(")
&& let Some(prev) = lines.get(i.wrapping_sub(1))
&& prev.text.contains("for")
&& prev.text.contains("range")
{
findings.push(Finding {
rule_id: "clear_map_go121".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: prev.line,
end_line: bl.line,
message: format!(
"function {} clears a map with per-key delete calls",
function.fingerprint.name
),
evidence: vec![
format!(
"range/delete clear pattern at lines {}-{}",
prev.line, bl.line
),
"clear(m) (Go 1.21+) resets the map in a single runtime call".into(),
],
});
}
}
findings
}
fn unnecessary_slice_copy(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for (index, bl) in lines.iter().enumerate() {
let Some((left, right)) = split_assignment(&bl.text) else {
continue;
};
if !(right.contains("slices.Clone(")
|| (right.contains("append([]") && right.contains("(nil),") && right.contains("...)")))
{
continue;
}
let binding = left
.trim()
.trim_start_matches("var ")
.split(',')
.next()
.unwrap_or("")
.split_whitespace()
.next()
.unwrap_or("")
.trim();
if !is_identifier_name(binding) {
continue;
}
let mutated = lines.iter().skip(index + 1).take(8).any(|line| {
line.text.contains(&format!("append({binding},"))
|| line.text.contains(&format!("{binding}["))
|| line.text.contains(&format!("{binding} ="))
});
if !mutated {
findings.push(Finding {
rule_id: "unnecessary_slice_copy_for_readonly".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} clones a slice that appears to be read-only",
function.fingerprint.name
),
evidence: vec![
format!(
"read-only slice clone assigned to {binding} at line {}",
bl.line
),
"reusing the original slice avoids an extra allocation and full copy".into(),
],
});
}
}
findings
}
fn three_index_slice_for_append(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for (index, bl) in lines.iter().enumerate() {
let Some((left, right)) = split_assignment(&bl.text) else {
continue;
};
if right.trim_start().starts_with("[]") || !right.contains('[') || !right.contains(']') {
continue;
}
let slice_part = right
.split('[')
.nth(1)
.and_then(|suffix| suffix.split(']').next())
.unwrap_or("");
if slice_part.matches(':').count() != 1 {
continue;
}
let binding = left
.trim()
.trim_start_matches("var ")
.split(',')
.next()
.unwrap_or("")
.split_whitespace()
.next()
.unwrap_or("")
.trim();
if !is_identifier_name(binding) {
continue;
}
if let Some(next) = lines
.iter()
.skip(index + 1)
.take(5)
.find(|line| line.text.contains(&format!("append({binding},")))
{
findings.push(Finding {
rule_id: "three_index_slice_for_append_safety".into(),
severity: Severity::Warning,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: next.line,
message: format!(
"function {} appends to a subslice without capping its capacity",
function.fingerprint.name
),
evidence: vec![
format!(
"subslice assignment at line {}, append at line {}",
bl.line, next.line
),
"use original[a:b:b] so append cannot overwrite the parent slice".into(),
],
});
}
}
findings
}
fn range_copy_large_struct(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
let signature_has_large_struct_slice = function.signature_text.contains("[]struct{")
|| file.go_structs().iter().any(|go_struct| {
go_struct.fields.len() >= 4
&& function
.signature_text
.contains(&format!("[]{}", go_struct.name))
});
if !signature_has_large_struct_slice {
return findings;
}
for bl in lines {
if bl.text.starts_with("for _, ")
&& bl.text.contains(":= range")
&& !bl.text.contains("string")
&& !bl.text.contains("int")
&& !bl.text.contains("byte")
&& let Some(slice_name) = bl
.text
.split("range ")
.nth(1)
.map(|s| s.trim().trim_end_matches(" {").trim())
&& !slice_name.contains("map[")
&& !slice_name.contains("chan ")
{
let name_lower = slice_name.to_lowercase();
if name_lower.ends_with("items")
|| name_lower.ends_with("records")
|| name_lower.ends_with("entries")
|| name_lower.ends_with("objects")
|| name_lower.ends_with("models")
|| name_lower.ends_with("rows")
|| name_lower.ends_with("users")
|| name_lower.ends_with("events")
|| name_lower.ends_with("results")
|| name_lower.ends_with("nodes")
{
findings.push(Finding {
rule_id: "range_copy_large_struct".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} may copy large structs in range loop",
function.fingerprint.name
),
evidence: vec![
format!("for _, v := range {} at line {}", slice_name, bl.line),
"use index access for i := range s {{ v := &s[i] }} to avoid copies".into(),
],
});
}
}
}
findings
}
fn dense_int_set_map(
file: &ParsedFile,
function: &ParsedFunction,
lines: &[BodyLine],
) -> Vec<Finding> {
let mut findings = Vec::new();
for bl in lines {
let lower = bl.text.to_lowercase();
if (bl.text.contains("map[int]bool") || bl.text.contains("map[int]struct{}"))
&& (lower.contains("seen") || lower.contains("set") || lower.contains("visited"))
{
findings.push(Finding {
rule_id: "unnecessary_map_for_set_of_ints".into(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(function.fingerprint.name.clone()),
start_line: bl.line,
end_line: bl.line,
message: format!(
"function {} uses a map as a dense integer set",
function.fingerprint.name
),
evidence: vec![
format!("map[int]set pattern at line {}", bl.line),
"for dense ranges, []bool or a bitset uses far less memory than a map".into(),
],
});
}
}
findings
}