#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use crate::*;
#[doc(hidden)]
pub trait Alterator
where
Self: Checker,
{
#[must_use]
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
Self::get_default(node, code, span, field_name, children)
}
#[must_use]
fn get_text_span(node: &Node, code: &[u8], span: bool, text: bool) -> (String, Span) {
let text = if text {
String::from_utf8_lossy(&code[node.start_byte()..node.end_byte()]).into_owned()
} else {
String::new()
};
if span {
let (spos_row, spos_column) = node.start_position();
let (epos_row, epos_column) = node.end_position();
(
text,
Some((spos_row + 1, spos_column + 1, epos_row + 1, epos_column + 1)),
)
} else {
(text, None)
}
}
#[must_use]
fn get_default(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
let (text, span) = Self::get_text_span(node, code, span, node.child_count() == 0);
AstNode::with_field_name(node.kind(), text, span, field_name, children)
}
#[must_use]
fn get_ast_node(
node: &Node,
code: &[u8],
span: bool,
comment: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> Option<AstNode> {
if comment && Self::is_comment(node) {
None
} else {
Some(Self::alterate(node, code, span, field_name, children))
}
}
}
impl Alterator for PreprocCode {}
impl Alterator for CcommentCode {}
impl Alterator for CppCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
mut children: Vec<AstNode>,
) -> AstNode {
match Cpp::from(node.kind_id()) {
Cpp::StringLiteral | Cpp::CharLiteral => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
Cpp::PreprocDef | Cpp::PreprocFunctionDef | Cpp::PreprocCall => {
if let Some(last) = children.last()
&& last.r#type == "\n"
{
children.pop();
}
Self::get_default(node, code, span, field_name, children)
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for PythonCode {}
impl Alterator for JavaCode {}
impl Alterator for KotlinCode {}
impl Alterator for CsharpCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Csharp::from(node.kind_id()) {
Csharp::StringLiteral
| Csharp::VerbatimStringLiteral
| Csharp::RawStringLiteral
| Csharp::InterpolatedStringExpression
| Csharp::CharacterLiteral => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for GoCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Go::from(node.kind_id()) {
Go::InterpretedStringLiteral | Go::RawStringLiteral | Go::RuneLiteral => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for LuaCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Lua::from(node.kind_id()) {
Lua::String => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for MozjsCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Mozjs::from(node.kind_id()) {
Mozjs::String | Mozjs::String2 => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for JavascriptCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Javascript::from(node.kind_id()) {
Javascript::String | Javascript::String2 => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for TypescriptCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Typescript::from(node.kind_id()) {
Typescript::String | Typescript::String2 => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for TsxCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Tsx::from(node.kind_id()) {
Tsx::String | Tsx::String2 | Tsx::String3 => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for RustCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Rust::from(node.kind_id()) {
Rust::StringLiteral | Rust::CharLiteral => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for PerlCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Perl::from(node.kind_id()) {
Perl::StringSingleQuoted
| Perl::StringDoubleQuoted
| Perl::StringQQuoted
| Perl::StringQqQuoted
| Perl::BacktickQuoted
| Perl::CommandQxQuoted => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for BashCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Bash::from(node.kind_id()) {
Bash::String | Bash::RawString | Bash::AnsiCString | Bash::TranslatedString => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for TclCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Tcl::from(node.kind_id()) {
Tcl::QuotedWord | Tcl::BracedWord | Tcl::BracedWordSimple => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for PhpCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Php::from(node.kind_id()) {
Php::String
| Php::String2
| Php::String3
| Php::EncapsedString
| Php::Heredoc
| Php::Nowdoc
| Php::ShellCommandExpression => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for RubyCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Ruby::from(node.kind_id()) {
Ruby::String
| Ruby::ChainedString
| Ruby::BareString
| Ruby::Subshell
| Ruby::Regex
| Ruby::HeredocBody
| Ruby::StringArray
| Ruby::SymbolArray
| Ruby::DelimitedSymbol
| Ruby::SimpleSymbol
| Ruby::Character => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for ElixirCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Elixir::from(node.kind_id()) {
Elixir::String | Elixir::Charlist | Elixir::Sigil => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
impl Alterator for GroovyCode {
fn alterate(
node: &Node,
code: &[u8],
span: bool,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> AstNode {
match Groovy::from(node.kind_id()) {
Groovy::StringLiteral
| Groovy::StringFragment
| Groovy::StringFragment2
| Groovy::StringFragment3
| Groovy::StringFragment4
| Groovy::StringFragment5 => {
let (text, span) = Self::get_text_span(node, code, span, true);
AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
}
_ => Self::get_default(node, code, span, field_name, children),
}
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use std::path::PathBuf;
use crate::{CppCode, CppParser, ParserTrait};
use super::*;
#[test]
fn get_text_span_non_utf8_uses_replacement_char() {
let code = b"char c = '\xff';";
let path = PathBuf::from("test.c");
let parser = CppParser::new(code.to_vec(), &path, None);
let root = parser.get_root();
let (text, _) = CppCode::get_text_span(&root, code, false, true);
assert!(
text.contains('\u{FFFD}'),
"expected U+FFFD replacement char for non-UTF-8 source, got: {text:?}"
);
}
fn collect_nodes_by_kind<'a>(node: &'a AstNode, target_kind: &str, out: &mut Vec<&'a AstNode>) {
if node.r#type == target_kind {
out.push(node);
}
for child in &node.children {
collect_nodes_by_kind(child, target_kind, out);
}
}
fn build_ast<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
let path = PathBuf::from(filename);
let parser = P::new(code.to_vec(), &path, None);
let cfg = crate::AstCfg {
id: String::new(),
comment: false,
span: false,
};
let resp = crate::AstCallback::call(cfg, &parser);
resp.root.expect("parser should produce a root AST node")
}
fn assert_strings_flattened(root: &AstNode) {
let mut strings = Vec::new();
collect_nodes_by_kind(root, "string", &mut strings);
assert!(
!strings.is_empty(),
"expected at least one 'string' node in the AST"
);
for node in &strings {
assert!(
node.children.is_empty(),
"string node should be flattened (no children), got {} children; value={:?}",
node.children.len(),
node.value,
);
assert!(
!node.value.is_empty(),
"flattened string node should have non-empty text value"
);
}
}
#[test]
fn javascript_string_nodes_all_flattened() {
let code = br#"
const a = 'single';
const b = "double";
const obj = {"key": 1};
import "module";
"#;
let root = build_ast::<crate::JavascriptParser>(code, "test.js");
assert_strings_flattened(&root);
}
#[test]
fn typescript_string_nodes_all_flattened() {
let code = br#"
const a: string = 'single';
const b: string = "double";
const obj: Record<string, number> = {"key": 1};
import "module";
"#;
let root = build_ast::<crate::TypescriptParser>(code, "test.ts");
assert_strings_flattened(&root);
}
#[test]
fn tsx_string_nodes_all_flattened() {
let code = br#"
const a = 'single';
const b = "double";
const el = <div className="cls">{"text"}</div>;
"#;
let root = build_ast::<crate::TsxParser>(code, "test.tsx");
assert_strings_flattened(&root);
}
#[test]
fn php_string_like_nodes_all_flattened() {
let code = br#"<?php
$single = 'single';
$double = "double";
$cmd = `ls`;
$here = <<<EOT
some text
EOT;
$now = <<<'EOT'
literal
EOT;
"#;
let root = build_ast::<crate::PhpParser>(code, "test.php");
assert_strings_flattened(&root);
let mut shells = Vec::new();
collect_nodes_by_kind(&root, "shell_command_expression", &mut shells);
assert!(
!shells.is_empty(),
"expected at least one shell_command_expression node"
);
for node in &shells {
assert!(
node.children.is_empty(),
"shell_command_expression should be flattened; got {} children",
node.children.len()
);
assert!(
node.value.contains("ls"),
"flattened backtick literal should preserve text; got {:?}",
node.value
);
}
}
#[test]
fn groovy_string_literal_preserved_verbatim() {
let code = br#"
class A {
String single = 'hello'
String double = "world"
char ch = 'x'
}
"#;
let root = build_ast::<crate::GroovyParser>(code, "test.groovy");
let mut strings = Vec::new();
collect_nodes_by_kind(&root, "string_literal", &mut strings);
collect_nodes_by_kind(&root, "character_literal", &mut strings);
assert!(
!strings.is_empty(),
"expected at least one string/character literal in the AST"
);
for node in &strings {
assert!(
node.children.is_empty(),
"string-like node should be flattened (no children); got {} children, value={:?}",
node.children.len(),
node.value,
);
assert!(
!node.value.is_empty(),
"flattened string-like node should have non-empty text value"
);
}
}
#[test]
fn groovy_multiline_string_fragment_preserves_newlines() {
let code = b"def s = \"\"\"first line\nsecond line\nthird line\"\"\"";
let root = build_ast::<crate::GroovyParser>(code, "test.groovy");
let mut strings = Vec::new();
collect_nodes_by_kind(&root, "string_literal", &mut strings);
assert!(
!strings.is_empty(),
"expected at least one string_literal node for the triple-quoted body"
);
let any_with_newline = strings.iter().any(|n| n.value.contains('\n'));
assert!(
any_with_newline,
"expected the flattened string_literal to keep its embedded newlines; got values: {:?}",
strings.iter().map(|n| &n.value).collect::<Vec<_>>()
);
}
}