use crate::types::{FunctionUsedName, MemberAssignment, PropertyValue};
pub fn preprocess_regex_literals(expr: &str) -> String {
let chars: Vec<char> = expr.chars().collect();
let mut result = String::with_capacity(expr.len());
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if ch == '"' || ch == '\'' {
result.push(ch);
let delim = ch;
i += 1;
while i < chars.len() {
let c = chars[i];
result.push(c);
if c == '\\' && i + 1 < chars.len() {
i += 1;
result.push(chars[i]);
} else if c == delim {
break;
}
i += 1;
}
i += 1;
continue;
}
if ch == '/' {
let prev = result.trim_end().chars().last();
let is_division = match prev {
Some(c) => c.is_alphanumeric() || c == '_' || c == ')' || c == ']',
None => false,
};
if !is_division {
i += 1;
let mut in_class = false;
while i < chars.len() {
match chars[i] {
'\\' => {
i += 1; }
'[' if !in_class => {
in_class = true;
}
']' if in_class => {
in_class = false;
}
'/' if !in_class => {
i += 1; break;
}
_ => {}
}
i += 1;
}
while i < chars.len() && chars[i].is_alphabetic() {
i += 1;
}
result.push(' '); continue;
}
}
result.push(ch);
i += 1;
}
result
}
pub fn preprocess_template_literals(expr: &str) -> String {
let mut result = String::with_capacity(expr.len());
let mut in_template = false;
let mut in_interp = false;
let mut interp_depth = 0u32;
let chars: Vec<char> = expr.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if in_interp {
match ch {
'{' => {
interp_depth += 1;
result.push(ch);
}
'}' => {
if interp_depth == 0 {
in_interp = false;
result.push(' '); } else {
interp_depth -= 1;
result.push(ch);
}
}
_ => result.push(ch),
}
} else if in_template {
match ch {
'`' => in_template = false,
'$' if i + 1 < chars.len() && chars[i + 1] == '{' => {
in_interp = true;
interp_depth = 0;
result.push(' '); i += 2; continue;
}
_ => {} }
} else {
match ch {
'`' => in_template = true,
_ => result.push(ch),
}
}
i += 1;
}
result
}
pub fn collect_names_from_expression(expr: &str) -> Vec<FunctionUsedName> {
let preprocessed = preprocess_template_literals(expr);
let mut result = Vec::new();
let tokens = tokenize_idents(&preprocessed);
let mut i = 0;
let mut prev_tok: &str = "";
while i < tokens.len() {
let tok = &tokens[i];
if !is_identifier(tok) {
prev_tok = tok.as_str();
i += 1;
if tok == ")" || tok == "]" {
i = skip_chain_tokens(&tokens, i);
}
continue;
}
if tok == "instanceof" {
prev_tok = "instanceof";
i += 1; if i < tokens.len() && is_identifier(&tokens[i]) {
i += 1; }
continue;
}
if is_js_keyword(tok) {
prev_tok = tok.as_str();
i += 1;
continue;
}
if (prev_tok == "{" || prev_tok == ",") && i + 1 < tokens.len() && tokens[i + 1] == ":" {
prev_tok = ":";
i += 2; continue;
}
if i + 1 < tokens.len() && tokens[i + 1] == "(" {
prev_tok = "id";
i += 2;
continue;
}
let is_dot_chain = i + 2 < tokens.len() && tokens[i + 1] == "." && is_identifier(&tokens[i + 2]);
let is_opt_chain =
i + 3 < tokens.len() && tokens[i + 1] == "?" && tokens[i + 2] == "." && is_identifier(&tokens[i + 3]);
if is_dot_chain || is_opt_chain {
let member = if is_dot_chain {
tokens[i + 2].clone()
} else {
tokens[i + 3].clone()
};
result.push(FunctionUsedName {
name: tok.clone(),
accessed_item: Some(member),
line: 0,
});
prev_tok = "id";
i += if is_dot_chain { 3 } else { 4 };
'chain: loop {
while (i + 1 < tokens.len() && tokens[i] == "." && is_identifier(&tokens[i + 1]))
|| (i + 2 < tokens.len()
&& tokens[i] == "?"
&& tokens[i + 1] == "."
&& is_identifier(&tokens[i + 2]))
{
i += if tokens[i] == "." { 2 } else { 3 };
}
if i < tokens.len() && (tokens[i] == "[" || tokens[i] == "(") {
let (opener, closer) = if tokens[i] == "[" { ("[", "]") } else { ("(", ")") };
let mut depth = 1usize;
i += 1;
while i < tokens.len() && depth > 0 {
if tokens[i] == opener {
depth += 1;
} else if tokens[i] == closer {
depth -= 1;
}
i += 1;
}
} else {
break 'chain;
}
}
} else {
result.push(FunctionUsedName {
name: tok.clone(),
accessed_item: None,
line: 0,
});
prev_tok = "id";
i += 1;
i = skip_chain_tokens(&tokens, i);
}
}
result
}
fn skip_chain_collect_args(tokens: &[String], mut i: usize, result: &mut Vec<(String, Option<String>)>) -> usize {
loop {
if i + 1 < tokens.len() && tokens[i] == "." && is_identifier(&tokens[i + 1]) {
i += 2; } else if i + 2 < tokens.len() && tokens[i] == "?" && tokens[i + 1] == "." && is_identifier(&tokens[i + 2]) {
i += 3; } else if i < tokens.len() && tokens[i] == "(" {
i += 1; let arg_start = i;
let mut depth = 1usize;
while i < tokens.len() && depth > 0 {
if tokens[i] == "(" {
depth += 1;
} else if tokens[i] == ")" {
depth -= 1;
}
i += 1;
}
if i > arg_start + 1 {
let arg_tokens = &tokens[arg_start..i - 1];
let arg_expr: String = arg_tokens.join(" ").replace("= >", "=>");
let arrow_params: std::collections::HashSet<String> =
collect_arrow_params(&arg_expr).into_iter().collect();
for n in collect_names_from_expression(&arg_expr) {
if !arrow_params.contains(n.name.as_str()) {
result.push((n.name, n.accessed_item));
}
}
}
} else if i < tokens.len() && tokens[i] == "[" {
let mut depth = 1usize;
i += 1;
while i < tokens.len() && depth > 0 {
if tokens[i] == "[" {
depth += 1;
} else if tokens[i] == "]" {
depth -= 1;
}
i += 1;
}
} else {
break;
}
}
i
}
pub fn collect_base_names_from_expression(expr: &str) -> Vec<String> {
let preprocessed = preprocess_template_literals(expr);
let tokens = tokenize_idents(&preprocessed);
let mut result = Vec::new();
let mut i = 0;
while i < tokens.len() {
let tok = &tokens[i];
if !is_identifier(tok) {
i += 1;
if tok == ")" || tok == "]" {
i = skip_chain_tokens(&tokens, i);
}
continue;
}
if tok == "instanceof" {
i += 1;
if i < tokens.len() && is_identifier(&tokens[i]) {
i += 1;
}
continue;
}
if is_js_keyword(tok) {
i += 1;
continue;
}
if i + 2 < tokens.len() && tokens[i + 1] == "." && is_identifier(&tokens[i + 2]) {
result.push(tok.clone());
i += 3; i = skip_chain_tokens(&tokens, i);
continue;
}
if i + 3 < tokens.len() && tokens[i + 1] == "?" && tokens[i + 2] == "." && is_identifier(&tokens[i + 3]) {
result.push(tok.clone());
i += 4; i = skip_chain_tokens(&tokens, i);
continue;
}
if i + 1 < tokens.len() && tokens[i + 1] == "(" {
i += 2; i = skip_to_matching_close_paren(&tokens, i);
i = skip_chain_tokens(&tokens, i);
continue;
}
result.push(tok.clone());
i += 1;
i = skip_chain_tokens(&tokens, i);
}
result
}
pub fn collect_dotted_accesses_from_expression(expr: &str) -> Vec<(String, Option<String>)> {
let preprocessed = preprocess_template_literals(expr);
let tokens = tokenize_idents(&preprocessed);
let mut result = Vec::new();
let mut i = 0;
let mut prev_tok: &str = "";
while i < tokens.len() {
let tok = &tokens[i];
if !is_identifier(tok) {
let s = tok.as_str();
match s {
"{" | "," => prev_tok = s,
_ => {}
}
i += 1;
if tok == ")" || tok == "]" {
i = skip_chain_tokens(&tokens, i);
}
continue;
}
if tok == "instanceof" {
prev_tok = "id";
i += 1;
if i < tokens.len() && is_identifier(&tokens[i]) {
i += 1;
}
continue;
}
if is_js_keyword(tok) {
prev_tok = tok.as_str();
i += 1;
continue;
}
if (prev_tok == "{" || prev_tok == ",") && i + 1 < tokens.len() && tokens[i + 1] == ":" {
prev_tok = ":";
i += 2; continue;
}
if i + 2 < tokens.len() && tokens[i + 1] == "." && is_identifier(&tokens[i + 2]) {
result.push((tok.clone(), Some(tokens[i + 2].clone())));
prev_tok = "id";
i += 3;
i = skip_chain_collect_args(&tokens, i, &mut result);
continue;
}
if i + 3 < tokens.len() && tokens[i + 1] == "?" && tokens[i + 2] == "." && is_identifier(&tokens[i + 3]) {
result.push((tok.clone(), Some(tokens[i + 3].clone())));
prev_tok = "id";
i += 4;
i = skip_chain_collect_args(&tokens, i, &mut result);
continue;
}
if i + 1 < tokens.len() && tokens[i + 1] == "(" {
prev_tok = "id";
i += 2;
i = skip_to_matching_close_paren(&tokens, i);
i = skip_chain_tokens(&tokens, i);
continue;
}
result.push((tok.clone(), None));
prev_tok = "id";
i += 1;
i = skip_chain_tokens(&tokens, i);
}
result
}
pub fn skip_to_matching_close_paren(tokens: &[String], mut i: usize) -> usize {
let mut depth = 1usize;
while i < tokens.len() && depth > 0 {
match tokens[i].as_str() {
"(" => depth += 1,
")" => depth -= 1,
_ => {}
}
i += 1;
}
i
}
pub fn skip_chain_tokens(tokens: &[String], mut i: usize) -> usize {
loop {
if i + 1 < tokens.len() && tokens[i] == "." && is_identifier(&tokens[i + 1]) {
i += 2; } else if i + 2 < tokens.len() && tokens[i] == "?" && tokens[i + 1] == "." && is_identifier(&tokens[i + 2]) {
i += 3; } else if i < tokens.len() && (tokens[i] == "(" || tokens[i] == "[") {
let (opener, closer) = if tokens[i] == "(" { ("(", ")") } else { ("[", "]") };
let mut depth = 1usize;
i += 1;
while i < tokens.len() && depth > 0 {
if tokens[i] == opener {
depth += 1;
} else if tokens[i] == closer {
depth -= 1;
}
i += 1;
}
} else {
break;
}
}
i
}
pub fn is_identifier(s: &str) -> bool {
s.starts_with(|c: char| c.is_alphabetic() || c == '_')
}
pub fn tokenize_idents(expr: &str) -> Vec<String> {
let preprocessed = preprocess_regex_literals(expr);
let mut tokens = Vec::new();
let mut current = String::new();
let mut in_string = false;
let mut string_char = ' ';
let mut escape_next = false;
for ch in preprocessed.chars() {
if in_string {
if escape_next {
escape_next = false;
continue;
}
if ch == '\\' {
escape_next = true;
continue;
}
if ch == string_char {
in_string = false;
}
continue;
}
match ch {
'"' | '\'' => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
in_string = true;
string_char = ch;
}
'.' => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
tokens.push(".".to_string());
}
'(' | ')' | '{' | '}' | '[' | ']' | ',' | ';' | ':' | '=' | '+' | '-' | '*' | '/' | '%' | '!' | '&'
| '|' | '<' | '>' | '?' => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
tokens.push(ch.to_string());
}
' ' | '\t' => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
pub fn find_matching_open_paren(s: &str, close_pos: usize) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 1i32;
let mut i = close_pos as isize - 1;
while i >= 0 {
match bytes[i as usize] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
return Some(i as usize);
}
}
_ => {}
}
i -= 1;
}
None
}
pub fn collect_function_keyword_params(line: &str) -> Vec<String> {
let mut params = Vec::new();
let mut rest = line;
while let Some(kw_pos) = rest.find("function") {
let before_ok = kw_pos == 0 || {
let c = rest.as_bytes()[kw_pos - 1];
!c.is_ascii_alphanumeric() && c != b'_'
};
let after_kw = &rest[kw_pos + 8..];
let after_kw_ok = after_kw
.chars()
.next()
.map_or(true, |c| !c.is_alphanumeric() && c != '_');
rest = after_kw;
if !before_ok || !after_kw_ok {
continue;
}
let trimmed = after_kw.trim_start();
let trimmed = if trimmed.starts_with(|c: char| c.is_alphabetic() || c == '_') {
let end = trimmed
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(trimmed.len());
trimmed[end..].trim_start()
} else {
trimmed
};
if let Some(after_open) = trimmed.strip_prefix('(') {
if let Some(close) = after_open.find(')') {
for p in after_open[..close].split(',') {
let name = p.trim();
if !name.is_empty() && is_identifier(name) && !is_js_keyword(name) {
params.push(name.to_string());
}
}
}
}
}
params
}
pub fn collect_arrow_params(line: &str) -> Vec<String> {
let mut params = Vec::new();
let mut search_from = 0;
while let Some(rel_pos) = line[search_from..].find("=>") {
let arrow_pos = search_from + rel_pos;
let before = line[..arrow_pos].trim_end();
if before.ends_with(')') {
let close = before.len() - 1;
if let Some(open) = find_matching_open_paren(before, close) {
let params_str = &before[open + 1..close];
for p in params_str.split(',') {
let name = p.trim();
if !name.is_empty() && is_identifier(name) {
params.push(name.to_string());
}
}
}
} else {
let bytes = before.as_bytes();
let mut end = bytes.len();
while end > 0
&& (bytes[end - 1].is_ascii_alphanumeric() || bytes[end - 1] == b'_' || bytes[end - 1] == b'$')
{
end -= 1;
}
let ident = &before[end..];
if !ident.is_empty()
&& ident.chars().next().is_some_and(|c| c.is_alphabetic() || c == '_')
&& !is_js_keyword(ident)
{
params.push(ident.to_string());
}
}
search_from = arrow_pos + 2;
}
params
}
pub fn try_parse_catch_param(line: &str) -> Option<String> {
let pos = line.find("catch")?;
let after = line[pos + 5..].trim_start();
let after = after.strip_prefix('(')?;
let close = after.find(')')?;
let name = after[..close].trim();
if name.is_empty() || is_js_keyword(name) {
return None;
}
if !is_identifier(name) {
return None;
}
Some(name.to_string())
}
pub fn try_parse_for_vars(line: &str) -> Vec<String> {
let Some(rest) = line
.trim()
.strip_prefix("for")
.map(str::trim)
.and_then(|s| s.strip_prefix('('))
else {
return vec![];
};
let Some(rest) = rest
.strip_prefix("let ")
.or_else(|| rest.strip_prefix("const "))
.or_else(|| rest.strip_prefix("var "))
.map(str::trim)
else {
return vec![];
};
if rest.starts_with('[') {
let Some(close) = rest.find(']') else {
return vec![];
};
return rest[1..close]
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty() && is_identifier(s))
.map(str::to_string)
.collect();
}
let name_end = rest
.find(|c: char| c.is_whitespace() || c == ')' || c == '=')
.unwrap_or(rest.len());
let name = &rest[..name_end];
if name.is_empty() || !is_identifier(name) {
return vec![];
}
vec![name.to_string()]
}
pub fn try_parse_object_key(line: &str) -> Option<&str> {
let colon_pos = line.find(':')?;
let key = line[..colon_pos].trim();
if key.is_empty() {
return None;
}
if !is_identifier(key) {
return None;
}
if !key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$') {
return None;
}
if is_js_keyword(key) {
return None;
}
Some(&line[colon_pos + 1..])
}
pub fn try_parse_method_shorthand_params(line: &str) -> Option<Vec<String>> {
let before_brace = line.trim().strip_suffix('{')?.trim();
let before_paren = before_brace.strip_suffix(')')?;
let open_paren = before_paren.rfind('(')?;
let name_part = before_paren[..open_paren].trim();
let params_str = &before_paren[open_paren + 1..];
if name_part.is_empty() || is_js_keyword(name_part) || !is_identifier(name_part) {
return None;
}
if !name_part.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$') {
return None;
}
let params: Vec<String> = params_str
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty() && is_identifier(s))
.map(str::to_string)
.collect();
Some(params)
}
pub fn try_parse_var_decl(line: &str) -> Option<(&str, &str)> {
let rest = line
.strip_prefix("let ")
.or_else(|| line.strip_prefix("const "))
.or_else(|| line.strip_prefix("var "))
.map(str::trim)?;
let name_end = rest
.find(|c: char| c == '=' || c == ';' || c.is_whitespace())
.unwrap_or(rest.len());
let name = &rest[..name_end];
if name.is_empty() || !is_identifier(name) {
return None;
}
let after_name = rest[name_end..].trim_start();
let rhs = after_name.strip_prefix('=').map_or("", str::trim);
Some((name, rhs))
}
pub fn try_parse_member_assignment(line: &str) -> Option<MemberAssignment> {
let tokens = tokenize_idents(line);
if tokens.len() < 4 {
return None;
}
if !is_identifier(&tokens[0])
|| is_js_keyword(&tokens[0])
|| tokens[1] != "."
|| !is_identifier(&tokens[2])
|| tokens[3] != "="
|| tokens.get(4) == Some(&"=".to_string())
{
return None;
}
let object = tokens[0].clone();
let member = tokens[2].clone();
let value = if let Some(rhs) = tokens.get(4) {
let rhs = rhs.as_str();
match rhs {
"true" => PropertyValue::Bool(true),
"false" => PropertyValue::Bool(false),
_ if rhs.parse::<i64>().is_ok() => PropertyValue::Int(rhs.parse().expect("TODO")),
_ if rhs.parse::<f64>().is_ok() => PropertyValue::Double(rhs.parse().expect("TODO")),
_ => PropertyValue::TooComplex, }
} else {
PropertyValue::TooComplex
};
Some(MemberAssignment { object, member, value })
}
pub fn is_js_keyword(s: &str) -> bool {
matches!(
s,
"let"
| "const"
| "var"
| "function"
| "return"
| "if"
| "else"
| "for"
| "while"
| "do"
| "break"
| "continue"
| "new"
| "delete"
| "typeof"
| "instanceof"
| "in"
| "of"
| "null"
| "undefined"
| "true"
| "false"
| "this"
| "super"
| "class"
| "import"
| "export"
| "from"
| "try"
| "catch"
| "finally"
| "throw"
| "switch"
| "case"
| "default"
| "void"
| "async"
| "await"
| "yield"
| "console"
| "JSON"
)
}