use crate::transform::truncate::NodeSpan;
use crate::{Language, Result, SkimError, TransformConfig};
use tree_sitter::{Node, Tree};
use super::minimal::{
adjust_range_for_line_removal, is_removable_comment, remove_ranges, trim_and_normalize,
MAX_AST_DEPTH, MAX_AST_NODES,
};
use super::{compute_line_map_from_removed_ranges, normalize_line_map_blanks};
struct NoiseWalkContext<'a> {
source: &'a str,
source_bytes: &'a [u8],
language: Language,
ranges: &'a mut Vec<(usize, usize)>,
node_count: &'a mut usize,
}
fn consume_trailing_whitespace(source: &[u8], end: usize) -> usize {
let mut pos = end;
while pos < source.len() && source[pos] == b' ' {
pos += 1;
}
pos
}
fn is_inline_modifier_kind(kind: &str) -> bool {
matches!(
kind,
"lifetime" | "mutable_specifier" | "visibility_modifier" | "readonly" | "abstract"
)
}
struct PseudoRules {
strip_kinds: &'static [&'static str],
strip_keywords: &'static [&'static str],
strip_semicolons: bool,
strip_self_param: bool,
}
fn get_pseudo_rules(language: Language) -> PseudoRules {
match language {
Language::TypeScript => PseudoRules {
strip_kinds: &[
"type_annotation",
"type_parameters",
"type_arguments",
"decorator",
"readonly",
"abstract",
],
strip_keywords: &["export"],
strip_semicolons: true,
strip_self_param: false,
},
Language::JavaScript => PseudoRules {
strip_kinds: &["decorator"],
strip_keywords: &["export"],
strip_semicolons: true,
strip_self_param: false,
},
Language::Python => PseudoRules {
strip_kinds: &["type", "return_type", "decorator"],
strip_keywords: &[],
strip_semicolons: false,
strip_self_param: true,
},
Language::Rust => PseudoRules {
strip_kinds: &[
"visibility_modifier",
"lifetime",
"type_parameters",
"where_clause",
"attribute_item",
"mutable_specifier",
],
strip_keywords: &[],
strip_semicolons: true,
strip_self_param: false,
},
Language::Go => PseudoRules {
strip_kinds: &[],
strip_keywords: &[],
strip_semicolons: false,
strip_self_param: false,
},
Language::Java => PseudoRules {
strip_kinds: &[
"marker_annotation",
"annotation",
"type_parameters",
"throws",
],
strip_keywords: &[
"public",
"private",
"protected",
"static",
"final",
"abstract",
],
strip_semicolons: true,
strip_self_param: false,
},
Language::C => PseudoRules {
strip_kinds: &[],
strip_keywords: &["static", "extern", "const", "volatile"],
strip_semicolons: true,
strip_self_param: false,
},
Language::Cpp => PseudoRules {
strip_kinds: &[],
strip_keywords: &[
"static", "extern", "const", "volatile", "virtual", "override", "final", "noexcept",
],
strip_semicolons: true,
strip_self_param: false,
},
Language::CSharp => PseudoRules {
strip_kinds: &["attribute_list", "type_parameter_list"],
strip_keywords: &[
"public",
"private",
"protected",
"internal",
"static",
"virtual",
"override",
"sealed",
"abstract",
],
strip_semicolons: true,
strip_self_param: false,
},
Language::Ruby => PseudoRules {
strip_kinds: &[],
strip_keywords: &["private", "protected", "public"],
strip_semicolons: false,
strip_self_param: false,
},
Language::Kotlin => PseudoRules {
strip_kinds: &["type_parameters", "annotation"],
strip_keywords: &[
"public",
"private",
"protected",
"internal",
"open",
"data",
"sealed",
"override",
"abstract",
],
strip_semicolons: false,
strip_self_param: false,
},
Language::Swift => PseudoRules {
strip_kinds: &["attribute", "type_parameters"],
strip_keywords: &[
"public",
"private",
"internal",
"fileprivate",
"open",
"static",
"override",
"final",
],
strip_semicolons: false,
strip_self_param: false,
},
Language::Sql => PseudoRules {
strip_kinds: &[],
strip_keywords: &[],
strip_semicolons: true,
strip_self_param: false,
},
_ => PseudoRules {
strip_kinds: &[],
strip_keywords: &[],
strip_semicolons: false,
strip_self_param: false,
},
}
}
#[cfg(test)]
pub(crate) fn transform_pseudo(
source: &str,
tree: &Tree,
language: Language,
config: &TransformConfig,
) -> Result<String> {
let (result, _spans) = transform_pseudo_with_spans(source, tree, language, config)?;
Ok(result)
}
pub(crate) fn transform_pseudo_with_spans(
source: &str,
tree: &Tree,
language: Language,
config: &TransformConfig,
) -> Result<(String, Vec<NodeSpan>)> {
let (text, spans, _line_map) =
transform_pseudo_with_spans_and_line_map(source, tree, language, config)?;
Ok((text, spans))
}
pub(crate) fn transform_pseudo_with_spans_and_line_map(
source: &str,
tree: &Tree,
language: Language,
_config: &TransformConfig,
) -> Result<(String, Vec<NodeSpan>, Vec<usize>)> {
let rules = get_pseudo_rules(language);
let mut ranges: Vec<(usize, usize)> = Vec::new();
let mut node_count: usize = 0;
let mut ctx = NoiseWalkContext {
source,
source_bytes: source.as_bytes(),
language,
ranges: &mut ranges,
node_count: &mut node_count,
};
collect_noise_ranges(tree.root_node(), &mut ctx, &rules, 0)?;
ctx.ranges.sort_unstable_by_key(|&(start, _)| start);
ctx.ranges.dedup();
let mut final_ranges: Vec<(usize, usize)> = ctx
.ranges
.iter()
.map(|&(start, end)| adjust_range_for_line_removal(source, start, end))
.collect();
final_ranges.sort_unstable_by_key(|&(start, _)| start);
let line_map_after_removal = compute_line_map_from_removed_ranges(source, &final_ranges);
let result = remove_ranges(source, &final_ranges)?;
let result = collapse_whitespace(&result);
let pre_normalized = result;
let result = trim_and_normalize(&pre_normalized);
let line_map = normalize_line_map_blanks(&pre_normalized, line_map_after_removal);
let line_count = result.lines().count();
let spans = vec![NodeSpan::new(0..line_count, "source_file")];
Ok((result, spans, line_map))
}
fn collapse_whitespace(source: &str) -> String {
let mut result = String::with_capacity(source.len());
for line in source.lines() {
let indent_len = line.len() - line.trim_start().len();
let content = line[indent_len..].trim_end();
result.push_str(&line[..indent_len]);
let mut prev_space = false;
let mut leading = true;
for ch in content.chars() {
if ch == ' ' {
if !prev_space && !leading {
result.push(ch);
}
prev_space = true;
} else {
leading = false;
result.push(ch);
prev_space = false;
}
}
result.push('\n');
}
result
}
fn handle_language_special_cases(node: Node, ctx: &mut NoiseWalkContext<'_>) -> Option<Result<()>> {
let kind = node.kind();
match ctx.language {
Language::Rust if matches!(kind, "function_item" | "function_signature_item") => {
strip_rust_return_type(node, ctx.ranges);
None }
Language::Cpp if kind == "access_specifier" => {
let start = node.start_byte();
let colon_end = node
.next_sibling()
.filter(|s| s.kind() == ":")
.map_or(node.end_byte(), |s| s.end_byte());
let end = consume_trailing_whitespace(ctx.source_bytes, colon_end);
ctx.ranges.push((start, end));
Some(Ok(())) }
Language::Cpp if kind == "template_parameter_list" => {
let template_start = node
.prev_sibling()
.filter(|s| s.kind() == "template")
.map_or(node.start_byte(), |s| s.start_byte());
let end = consume_trailing_whitespace(ctx.source_bytes, node.end_byte());
ctx.ranges.push((template_start, end));
Some(Ok(())) }
_ => None,
}
}
fn collect_noise_ranges(
node: Node,
ctx: &mut NoiseWalkContext<'_>,
rules: &PseudoRules,
depth: usize,
) -> Result<()> {
if depth > MAX_AST_DEPTH {
return Err(SkimError::ParseError(format!(
"Maximum AST depth exceeded: {} (possible malicious input)",
MAX_AST_DEPTH
)));
}
*ctx.node_count += 1;
if *ctx.node_count > MAX_AST_NODES {
return Err(SkimError::ParseError(format!(
"Too many AST nodes: {} (max: {}). Possible malicious input.",
*ctx.node_count, MAX_AST_NODES
)));
}
let kind = node.kind();
if is_removable_comment(node, ctx.source, ctx.language) {
ctx.ranges.push((node.start_byte(), node.end_byte()));
return Ok(()); }
if rules.strip_kinds.contains(&kind) {
let start = node.start_byte();
let end = node.end_byte();
let adjusted_start = adjust_type_start(ctx.language, kind, ctx.source_bytes, start);
let end = if is_inline_modifier_kind(kind) {
consume_trailing_whitespace(ctx.source_bytes, end)
} else {
end
};
ctx.ranges.push((adjusted_start, end));
return Ok(()); }
if node.child_count() == 0 {
let text = node.utf8_text(ctx.source_bytes).unwrap_or("");
if rules.strip_keywords.contains(&text) {
let end = consume_trailing_whitespace(ctx.source_bytes, node.end_byte());
ctx.ranges.push((node.start_byte(), end));
return Ok(());
}
}
if rules.strip_semicolons && kind == ";" {
let is_for_loop = node
.parent()
.map(|p| {
matches!(
p.kind(),
"for_statement" | "for_in_statement" | "for_of_statement"
)
})
.unwrap_or(false);
if !is_for_loop {
ctx.ranges.push((node.start_byte(), node.end_byte()));
return Ok(());
}
}
if rules.strip_self_param && kind == "parameters" {
strip_python_self_param(node, ctx.source_bytes, ctx.ranges);
}
if let Some(result) = handle_language_special_cases(node, ctx) {
return result;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_noise_ranges(child, ctx, rules, depth + 1)?;
}
Ok(())
}
fn adjust_type_start(language: Language, kind: &str, source: &[u8], start: usize) -> usize {
match (language, kind) {
(Language::Python, "type" | "return_type") => {
const SEPARATORS: &[&[u8]] = &[b" -> ", b"-> ", b"->", b": ", b":"];
let prefix = source.get(start.saturating_sub(4)..start).unwrap_or(b"");
for sep in SEPARATORS {
if prefix.ends_with(sep) {
return start.saturating_sub(sep.len());
}
}
start
}
_ => start,
}
}
fn strip_python_self_param(
params_node: Node,
source_bytes: &[u8],
ranges: &mut Vec<(usize, usize)>,
) {
let mut cursor = params_node.walk();
let children: Vec<_> = params_node.children(&mut cursor).collect();
for (i, child) in children.iter().enumerate() {
let kind = child.kind();
if kind == "(" || kind == "," {
continue;
}
let is_self_or_cls = match kind {
"identifier" => matches!(child.utf8_text(source_bytes).unwrap_or(""), "self" | "cls"),
"typed_parameter" | "default_parameter" => {
let mut inner_cursor = child.walk();
let found = child
.children(&mut inner_cursor)
.next()
.and_then(|first_child| first_child.utf8_text(source_bytes).ok())
.is_some_and(|t| matches!(t, "self" | "cls"));
found
}
_ => false,
};
if is_self_or_cls {
let start = child.start_byte();
let end = extend_past_trailing_comma(child.end_byte(), &children, i, source_bytes);
ranges.push((start, end));
}
break; }
}
fn extend_past_trailing_comma(
end: usize,
children: &[Node],
index: usize,
source_bytes: &[u8],
) -> usize {
if let Some(next) = children.get(index + 1) {
if next.kind() == "," {
let comma_end = next.end_byte();
if comma_end < source_bytes.len() && source_bytes[comma_end] == b' ' {
return comma_end + 1;
}
return comma_end;
}
}
end
}
fn strip_rust_return_type(function_node: Node, ranges: &mut Vec<(usize, usize)>) {
let mut cursor = function_node.walk();
let children: Vec<_> = function_node.children(&mut cursor).collect();
for (i, child) in children.iter().enumerate() {
if child.kind() == "->" {
let end = if let Some(type_node) = children.get(i + 1) {
if type_node.kind() != "block" {
type_node.end_byte()
} else {
child.end_byte()
}
} else {
child.end_byte()
};
let start = child.start_byte().saturating_sub(1);
ranges.push((start, end));
return;
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::{Mode, Parser, TransformConfig};
fn transform(source: &str, language: Language) -> String {
let mut parser = Parser::new(language).unwrap();
let tree = parser.parse(source).unwrap();
let config = TransformConfig::with_mode(Mode::Pseudo);
transform_pseudo(source, &tree, language, &config).unwrap()
}
#[test]
fn test_typescript_pseudo_strips_type_annotations() {
let source = "function add(a: number, b: number): number {\n return a + b;\n}\n";
let result = transform(source, Language::TypeScript);
assert!(
!result.contains(": number"),
"type annotations should be stripped"
);
assert!(
result.contains("function add(a, b)"),
"function name and params preserved"
);
assert!(result.contains("return a + b"), "logic preserved");
}
#[test]
fn test_typescript_pseudo_strips_export() {
let source =
"export function greet(name: string): string {\n return `Hello, ${name}!`;\n}\n";
let result = transform(source, Language::TypeScript);
assert!(
!result.contains("export"),
"export keyword should be stripped"
);
assert!(
result.contains("function greet(name)"),
"function signature preserved without types"
);
}
#[test]
fn test_typescript_pseudo_strips_type_parameters() {
let source = "function identity<T>(value: T): T {\n return value;\n}\n";
let result = transform(source, Language::TypeScript);
assert!(
!result.contains("<T>"),
"type parameters should be stripped"
);
assert!(
result.contains("function identity(value)"),
"function preserved"
);
}
#[test]
fn test_typescript_pseudo_preserves_for_loop_semicolons() {
let source = "function loop() {\n for (let i = 0; i < 10; i++) {\n console.log(i);\n }\n}\n";
let result = transform(source, Language::TypeScript);
assert!(result.contains("i < 10"), "for-loop condition preserved");
}
#[test]
fn test_javascript_pseudo_strips_export_and_semicolons() {
let source = "export function add(x, y) {\n return x + y;\n}\n";
let result = transform(source, Language::JavaScript);
assert!(!result.contains("export"), "export should be stripped");
assert!(result.contains("function add(x, y)"), "function preserved");
assert!(result.contains("return x + y"), "logic preserved");
}
#[test]
fn test_python_pseudo_strips_type_hints() {
let source =
"def calculate_sum(a: int, b: int) -> int:\n result = a + b\n return result\n";
let result = transform(source, Language::Python);
assert!(
!result.contains(": int"),
"type annotations should be stripped"
);
assert!(!result.contains("-> int"), "return type should be stripped");
assert!(
result.contains("def calculate_sum(a, b)"),
"function signature preserved"
);
assert!(result.contains("return result"), "logic preserved");
}
#[test]
fn test_python_pseudo_strips_self_param() {
let source =
"class Calculator:\n def add(self, x: int, y: int) -> int:\n return x + y\n";
let result = transform(source, Language::Python);
assert!(!result.contains("self"), "self param should be stripped");
assert!(
result.contains("def add(x, y)"),
"method params preserved without self/types"
);
}
#[test]
fn test_python_pseudo_strips_decorators() {
let source = "@staticmethod\ndef helper() -> None:\n pass\n";
let result = transform(source, Language::Python);
assert!(
!result.contains("@staticmethod"),
"decorator should be stripped"
);
assert!(result.contains("def helper()"), "function preserved");
}
#[test]
fn test_rust_pseudo_strips_visibility() {
let source = "pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("pub "),
"visibility modifier should be stripped"
);
assert!(result.contains("fn add"), "function preserved");
}
#[test]
fn test_rust_pseudo_strips_lifetimes_and_type_params() {
let source = "pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n if x.len() > y.len() { x } else { y }\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("<'a>"),
"type parameters should be stripped"
);
}
#[test]
fn test_rust_pseudo_strips_attributes() {
let source = "#[derive(Debug)]\npub struct Point {\n pub x: i32,\n pub y: i32,\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("#[derive(Debug)]"),
"attribute should be stripped"
);
assert!(result.contains("struct Point"), "struct preserved");
}
#[test]
fn test_rust_pseudo_strips_where_clause() {
let source =
"fn process<T>(value: T) where T: Clone + Debug {\n println!(\"{:?}\", value);\n}\n";
let result = transform(source, Language::Rust);
assert!(!result.contains("where"), "where clause should be stripped");
assert!(result.contains("fn process"), "function preserved");
}
#[test]
fn test_rust_pseudo_strips_return_type() {
let source = "pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("-> i32"),
"return type should be stripped, got: {result}"
);
assert!(result.contains("fn add"), "function preserved");
}
#[test]
fn test_java_pseudo_strips_visibility() {
let source = "public class Simple {\n private int value;\n public int add(int a, int b) {\n return a + b;\n }\n}\n";
let result = transform(source, Language::Java);
assert!(
!result.contains("public "),
"public modifier should be stripped"
);
assert!(
!result.contains("private "),
"private modifier should be stripped"
);
assert!(result.contains("class Simple"), "class preserved");
assert!(result.contains("int add(int a, int b)"), "method preserved");
}
#[test]
fn test_java_pseudo_strips_annotations() {
let source = "@Override\npublic String toString() {\n return \"hello\";\n}\n";
let result = transform(source, Language::Java);
assert!(
!result.contains("@Override"),
"annotation should be stripped"
);
assert!(result.contains("String toString()"), "method preserved");
}
#[test]
fn test_c_pseudo_strips_qualifiers() {
let source = "static const int MAX = 100;\n";
let result = transform(source, Language::C);
assert!(!result.contains("static"), "static should be stripped");
assert!(!result.contains("const"), "const should be stripped");
assert!(result.contains("int MAX = 100"), "declaration preserved");
}
#[test]
fn test_c_pseudo_strips_semicolons() {
let source = "int add(int a, int b) {\n return a + b;\n}\n";
let result = transform(source, Language::C);
assert!(result.contains("return a + b"), "logic preserved");
}
#[test]
fn test_cpp_pseudo_strips_access_specifiers() {
let source = "class Foo {\npublic:\n int bar();\nprivate:\n int baz_;\n};\n";
let result = transform(source, Language::Cpp);
assert!(
!result.contains("public:"),
"access specifier should be stripped"
);
assert!(
!result.contains("private:"),
"access specifier should be stripped"
);
}
#[test]
fn test_cpp_pseudo_strips_virtual_override() {
let source = "class Shape {\npublic:\n virtual double area() const = 0;\n virtual ~Shape() = default;\n};\n";
let result = transform(source, Language::Cpp);
assert!(!result.contains("virtual"), "virtual should be stripped");
}
#[test]
fn test_collapse_whitespace_basic() {
let result = collapse_whitespace(" pub fn add() {}\n");
assert_eq!(result, " pub fn add() {}\n");
}
#[test]
fn test_collapse_whitespace_preserves_indentation() {
let result = collapse_whitespace(" let x = 1\n");
assert_eq!(result, " let x = 1\n");
}
#[test]
fn test_pseudo_respects_max_ast_nodes() {
let mut source = String::new();
for i in 0..4500 {
source.push_str("x = ");
for j in 0..20 {
if j > 0 {
source.push_str(" + ");
}
source.push_str(&(i * 20 + j).to_string());
}
source.push('\n');
}
let mut parser = Parser::new(Language::Python).unwrap();
let tree = parser.parse(&source).unwrap();
let config = TransformConfig::with_mode(Mode::Pseudo);
let result = transform_pseudo(&source, &tree, Language::Python, &config);
assert!(
result.is_err(),
"Expected error when exceeding MAX_AST_NODES"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Too many AST nodes"),
"Expected 'Too many AST nodes' error, got: {}",
err_msg
);
}
#[test]
fn test_pseudo_empty_input() {
let result = transform("", Language::TypeScript);
assert_eq!(result, "", "empty input should produce empty output");
}
#[test]
fn test_pseudo_overlapping_comment_and_noise_range() {
let source =
"@staticmethod # old helper\ndef helper(self, x: int) -> int:\n return x\n";
let result = transform(source, Language::Python);
assert!(
!result.contains("@staticmethod"),
"decorator should be stripped, got: {result}"
);
assert!(
!result.contains("# old helper"),
"inline comment should be stripped, got: {result}"
);
assert!(
!result.contains(": int"),
"type annotations should be stripped, got: {result}"
);
assert!(
result.contains("def helper(x)"),
"function preserved without self/types, got: {result}"
);
assert!(result.contains("return x"), "logic preserved");
}
#[test]
fn test_pseudo_markdown_passthrough() {
let source = "# Heading\n\nSome **bold** text.\n";
let config = TransformConfig::with_mode(Mode::Pseudo);
let (result, has_errors) = Language::Markdown
.transform_source(source, &config)
.unwrap();
assert_eq!(
result, source,
"Markdown should pass through unchanged in pseudo mode"
);
assert!(!has_errors, "passthrough should not report parse errors");
}
#[test]
fn test_python_pseudo_no_arrow_residue() {
let source = "def calculate_sum(a: int, b: int) -> int:\n return a + b\n";
let result = transform(source, Language::Python);
assert!(
!result.contains("->"),
"return type arrow should be fully stripped, got: {result}"
);
assert!(
result.contains("def calculate_sum(a, b):"),
"function signature should be clean, got: {result}"
);
}
#[test]
fn test_cpp_pseudo_no_orphaned_colon() {
let source = "class Foo {\npublic:\n int bar();\nprivate:\n int baz_;\n};\n";
let result = transform(source, Language::Cpp);
assert!(
!result.contains("public"),
"access specifier keyword should be stripped, got: {result}"
);
assert!(
!result.lines().any(|l| l.trim() == ":"),
"orphaned colon should not remain, got: {result}"
);
assert!(
result.contains("int bar()"),
"member declarations preserved, got: {result}"
);
}
#[test]
fn test_cpp_pseudo_no_orphaned_template() {
let source = "template<typename T>\nclass Container {\npublic:\n T value;\n};\n";
let result = transform(source, Language::Cpp);
assert!(
!result.contains("template"),
"template keyword should be stripped along with parameter list, got: {result}"
);
assert!(
result.contains("class Container"),
"class declaration preserved, got: {result}"
);
}
#[test]
fn test_rust_pseudo_trait_return_type() {
let source =
"pub trait Compute {\n fn compute(&self, value: i32) -> i32;\n fn reset(&mut self);\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("-> i32"),
"trait method return type should be stripped, got: {result}"
);
assert!(
result.contains("fn compute"),
"trait method name preserved, got: {result}"
);
}
#[test]
fn test_rust_pseudo_lifetime_no_space() {
let source = "pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n if x.len() > y.len() { x } else { y }\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("& str"),
"lifetime removal should not leave extra space in references, got: {result}"
);
assert!(
result.contains("&str"),
"reference types should be clean, got: {result}"
);
}
#[test]
fn test_typescript_pseudo_no_leading_space() {
let source = "export function add(a: number, b: number): number {\n return a + b;\n}\n";
let result = transform(source, Language::TypeScript);
assert!(
!result.starts_with(' '),
"output should not start with a leading space, got: {result}"
);
assert!(
result.contains("function add(a, b)"),
"function signature clean after export removal, got: {result}"
);
}
#[test]
fn test_java_pseudo_no_leading_spaces() {
let source = "public class Simple {\n private int value;\n public static final int MAX = 100;\n public int add(int a, int b) {\n return a + b;\n }\n}\n";
let result = transform(source, Language::Java);
assert!(
result.contains("class Simple"),
"class name preserved, got: {result}"
);
for line in result.lines() {
if line.is_empty() {
continue;
}
let indent = line.len() - line.trim_start().len();
assert!(
indent == 0 || indent == 4 || indent == 8,
"expected indentation of 0, 4, or 8 spaces but got {} for line: {:?}, full output: {result}",
indent,
line
);
}
}
#[test]
fn test_c_pseudo_const_no_space() {
let source = "const char* greeting = \"hello\";\n";
let result = transform(source, Language::C);
assert!(
!result.starts_with(' '),
"const removal should not leave leading space, got: {result}"
);
assert!(
result.contains("char* greeting"),
"declaration preserved after const removal, got: {result}"
);
}
#[test]
fn test_python_pseudo_multiple_return_types() {
let source = "def foo(x: int) -> str:\n return str(x)\n\ndef bar(y: str) -> int:\n return int(y)\n";
let result = transform(source, Language::Python);
assert!(
!result.contains("->"),
"all return type arrows should be stripped, got: {result}"
);
assert!(
result.contains("def foo(x):"),
"first function clean, got: {result}"
);
assert!(
result.contains("def bar(y):"),
"second function clean, got: {result}"
);
}
#[test]
fn test_consume_trailing_whitespace_basic() {
let source = b"pub fn add()";
assert_eq!(consume_trailing_whitespace(source, 3), 4);
}
#[test]
fn test_consume_trailing_whitespace_multiple_spaces() {
let source = b"pub fn add()";
assert_eq!(consume_trailing_whitespace(source, 3), 6);
}
#[test]
fn test_consume_trailing_whitespace_no_spaces() {
let source = b"pubfn";
assert_eq!(consume_trailing_whitespace(source, 3), 3);
}
#[test]
fn test_consume_trailing_whitespace_at_end() {
let source = b"pub";
assert_eq!(consume_trailing_whitespace(source, 3), 3);
}
#[test]
fn test_consume_trailing_whitespace_stops_at_newline() {
let source = b"pub \nfn";
assert_eq!(consume_trailing_whitespace(source, 3), 4);
}
#[test]
fn test_is_inline_modifier_kind_positives() {
assert!(is_inline_modifier_kind("lifetime"));
assert!(is_inline_modifier_kind("mutable_specifier"));
assert!(is_inline_modifier_kind("visibility_modifier"));
assert!(is_inline_modifier_kind("readonly"));
assert!(is_inline_modifier_kind("abstract"));
}
#[test]
fn test_is_inline_modifier_kind_negatives() {
assert!(!is_inline_modifier_kind("type_annotation"));
assert!(!is_inline_modifier_kind("decorator"));
assert!(!is_inline_modifier_kind("identifier"));
assert!(!is_inline_modifier_kind("function_item"));
assert!(!is_inline_modifier_kind(""));
}
#[test]
fn test_python_arrow_in_string_literal_preserved() {
let source = "def describe():\n return \"maps A -> B\"\n";
let result = transform(source, Language::Python);
assert!(
result.contains("->"),
"arrow inside string literal should be preserved, got: {result}"
);
assert!(
result.contains("\"maps A -> B\""),
"string content should be unchanged, got: {result}"
);
}
#[test]
fn test_cpp_pseudo_strips_template_function() {
let source = "template<typename T>\nT max_val(T a, T b) {\n return a > b ? a : b;\n}\n";
let result = transform(source, Language::Cpp);
assert!(
!result.contains("template"),
"template keyword should be stripped from function, got: {result}"
);
assert!(
!result.contains("<typename T>"),
"template parameter list should be stripped from function, got: {result}"
);
assert!(
result.contains("max_val"),
"function name preserved, got: {result}"
);
}
#[test]
fn test_rust_special_case_continues_recursion_into_body() {
let source =
"pub fn update(&mut self, value: i32) -> bool {\n self.val = value;\n true\n}\n";
let result = transform(source, Language::Rust);
assert!(
!result.contains("pub "),
"pub should be stripped via child recursion, got: {result}"
);
assert!(
!result.contains("mut "),
"mut should be stripped via child recursion, got: {result}"
);
assert!(
!result.contains("-> bool"),
"return type should be stripped by special case, got: {result}"
);
assert!(
result.contains("self.val = value"),
"function body should be preserved (recursion continued), got: {result}"
);
}
#[test]
fn test_cpp_access_specifier_skips_recursion() {
let source = "class Widget {\npublic:\n void draw();\nprotected:\n int x_;\n};\n";
let result = transform(source, Language::Cpp);
assert!(
!result.contains("public"),
"public access specifier fully stripped, got: {result}"
);
assert!(
!result.contains("protected"),
"protected access specifier fully stripped, got: {result}"
);
assert!(
!result.lines().any(|l| l.trim() == ":"),
"no orphaned colons, got: {result}"
);
assert!(
result.contains("void draw()"),
"member declarations preserved, got: {result}"
);
}
#[test]
fn test_cpp_template_parameter_list_skips_recursion() {
let source =
"template<typename K, typename V>\nclass Map {\npublic:\n V get(K key);\n};\n";
let result = transform(source, Language::Cpp);
assert!(
!result.contains("template"),
"template keyword stripped, got: {result}"
);
assert!(
!result.contains("<typename"),
"template parameters stripped, got: {result}"
);
assert!(
result.contains("class Map"),
"class declaration preserved, got: {result}"
);
}
#[test]
fn test_collapse_whitespace_preserves_indent_when_modifier_stripped() {
let result = collapse_whitespace(" fn add() {}\n");
assert_eq!(result, " fn add() {}\n", "normal 4-space indent");
let result = collapse_whitespace(" fn add() {}\n");
assert_eq!(
result, " fn add() {}\n",
"5-space indent preserved as indentation"
);
}
#[test]
fn test_collapse_whitespace_empty_lines() {
let result = collapse_whitespace("line one\n\nline two\n");
assert_eq!(result, "line one\n\nline two\n");
}
#[test]
fn test_collapse_whitespace_whitespace_only_lines() {
let result = collapse_whitespace(" \n \n\n");
assert_eq!(result, " \n \n\n");
}
#[test]
fn test_collapse_whitespace_multiline_mixed_patterns() {
let input = "fn foo() {\n let x = 1\n\n return x\n}\n";
let result = collapse_whitespace(input);
assert_eq!(result, "fn foo() {\n let x = 1\n\n return x\n}\n");
}
#[test]
fn test_collapse_whitespace_trailing_spaces_trimmed() {
let result = collapse_whitespace("fn foo() \n");
assert_eq!(result, "fn foo()\n", "trailing spaces should be trimmed");
}
#[test]
fn test_collapse_whitespace_leading_spaces_become_indent() {
let result = collapse_whitespace(" function add()\n");
assert_eq!(
result, " function add()\n",
"single leading space is part of indent"
);
let result = collapse_whitespace(" function add()\n");
assert_eq!(
result, " function add()\n",
"two leading spaces treated as indentation"
);
}
}