use crate::types::AccessKind;
use crate::util::strip_fqn_prefix;
fn skip_balanced_parens_back(chars: &[char], pos: usize) -> Option<usize> {
if pos == 0 || chars[pos - 1] != ')' {
return None;
}
let mut depth: u32 = 0;
let mut j = pos;
while j > 0 {
j -= 1;
match chars[j] {
')' => depth += 1,
'(' => {
depth -= 1;
if depth == 0 {
return Some(j);
}
}
_ => {}
}
}
None
}
fn skip_balanced_brackets_back(chars: &[char], pos: usize) -> Option<usize> {
if pos == 0 || chars[pos - 1] != ']' {
return None;
}
let mut depth: u32 = 0;
let mut j = pos;
while j > 0 {
j -= 1;
match chars[j] {
']' => depth += 1,
'[' => {
depth -= 1;
if depth == 0 {
return Some(j);
}
}
_ => {}
}
}
None
}
fn check_new_keyword_before(
chars: &[char],
ident_start: usize,
class_name: &str,
) -> Option<String> {
let mut j = ident_start;
while j > 0 && chars[j - 1] == ' ' {
j -= 1;
}
if j >= 3 && chars[j - 3] == 'n' && chars[j - 2] == 'e' && chars[j - 1] == 'w' {
let before_ok = j == 3 || {
let prev = chars[j - 4];
!prev.is_alphanumeric() && prev != '_'
};
if before_ok {
let name = strip_fqn_prefix(class_name);
return Some(name.to_string());
}
}
None
}
fn extract_clone_expression_inside_parens(
chars: &[char],
open: usize,
close: usize,
) -> Option<String> {
let inner_start = open + 1;
let inner_end = close - 1;
if inner_start >= inner_end {
return None;
}
let mut k = inner_start;
while k < inner_end && chars[k] == ' ' {
k += 1;
}
if k + 5 > inner_end {
return None;
}
if chars[k] != 'c'
|| chars[k + 1] != 'l'
|| chars[k + 2] != 'o'
|| chars[k + 3] != 'n'
|| chars[k + 4] != 'e'
{
return None;
}
k += 5;
if k >= inner_end || chars[k] != ' ' {
return None;
}
while k < inner_end && chars[k] == ' ' {
k += 1;
}
let rest: String = chars[k..inner_end].iter().collect();
let rest = rest.trim();
if rest.is_empty() {
return None;
}
Some(rest.to_string())
}
fn extract_new_expression_inside_parens(
chars: &[char],
open: usize,
close: usize,
) -> Option<String> {
let inner_start = open + 1;
let inner_end = close - 1;
if inner_start >= inner_end {
return None;
}
let mut k = inner_start;
while k < inner_end && chars[k] == ' ' {
k += 1;
}
if k + 3 >= inner_end {
return None;
}
if chars[k] != 'n' || chars[k + 1] != 'e' || chars[k + 2] != 'w' {
return None;
}
k += 3;
if k >= inner_end || chars[k] != ' ' {
return None;
}
while k < inner_end && chars[k] == ' ' {
k += 1;
}
let name_start = k;
while k < inner_end && (chars[k].is_alphanumeric() || chars[k] == '_' || chars[k] == '\\') {
k += 1;
}
if k == name_start {
return None;
}
let class_name: String = chars[name_start..k].iter().collect();
let name = strip_fqn_prefix(&class_name);
Some(name.to_string())
}
fn extract_arrow_subject(chars: &[char], arrow_pos: usize) -> String {
let mut end = arrow_pos;
let mut i = end;
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
if i > 0 && chars[i - 1] == '?' {
i -= 1;
}
end = i;
if i > 0 && chars[i - 1] == ']' {
let mut segments: Vec<String> = Vec::new();
let mut bracket_ranges: Vec<(usize, usize)> = Vec::new();
let mut pos = i;
while pos > 0
&& chars[pos - 1] == ']'
&& let Some(bracket_open) = skip_balanced_brackets_back(chars, pos)
{
let inner: String = chars[bracket_open + 1..pos - 1].iter().collect();
let inner_trimmed = inner.trim();
if (inner_trimmed.starts_with('\'') && inner_trimmed.ends_with('\''))
|| (inner_trimmed.starts_with('"') && inner_trimmed.ends_with('"'))
{
segments.push(format!("[{}]", inner_trimmed));
} else {
segments.push("[]".to_string());
}
bracket_ranges.push((bracket_open, pos));
pos = bracket_open;
}
if !segments.is_empty() {
let before = extract_simple_variable(chars, pos);
if !before.is_empty() {
let base = if !before.starts_with('$') {
let prop_start = pos - before.len();
if prop_start >= 2
&& chars[prop_start - 2] == '-'
&& chars[prop_start - 1] == '>'
{
let chain = extract_arrow_subject(chars, prop_start - 2);
if !chain.is_empty() {
format!("{}->{}", chain, before)
} else {
before
}
} else if prop_start >= 3
&& chars[prop_start - 3] == '?'
&& chars[prop_start - 2] == '-'
&& chars[prop_start - 1] == '>'
{
let chain = extract_arrow_subject(chars, prop_start - 3);
if !chain.is_empty() {
format!("{}?->{}", chain, before)
} else {
before
}
} else {
before
}
} else {
before
};
segments.reverse();
return format!("{}{}", base, segments.join(""));
}
if pos > 0
&& chars[pos - 1] == ')'
&& let Some(call_base) = extract_call_subject(chars, pos)
{
segments.reverse();
return format!("{}{}", call_base, segments.join(""));
}
if segments.len() >= 2 {
let (lit_open, lit_close) = bracket_ranges[bracket_ranges.len() - 1];
let literal: String = chars[lit_open..lit_close].iter().collect();
let mut index_segs: Vec<String> = segments[..segments.len() - 1].to_vec();
index_segs.reverse();
return format!("{}{}", literal, index_segs.join(""));
}
}
}
if i > 0
&& chars[i - 1] == ')'
&& let Some(call_subject) = extract_call_subject(chars, i)
{
return call_subject;
}
let ident_end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
if i > 0 && chars[i - 1] == '$' {
i -= 1;
}
let ident_start = i;
if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' {
let inner_arrow = i - 2;
let inner_subject = extract_arrow_subject(chars, inner_arrow);
if !inner_subject.is_empty() {
let prop: String = chars[ident_start..ident_end].iter().collect();
return format!("{}->{}", inner_subject, prop);
}
}
if i >= 3 && chars[i - 3] == '?' && chars[i - 2] == '-' && chars[i - 1] == '>' {
let inner_arrow = i - 3;
let inner_subject = extract_arrow_subject(chars, inner_arrow);
if !inner_subject.is_empty() {
let prop: String = chars[ident_start..ident_end].iter().collect();
return format!("{}?->{}", inner_subject, prop);
}
}
if i >= 2 && chars[i - 2] == ':' && chars[i - 1] == ':' {
let class_subject = extract_double_colon_subject(chars, i - 2);
if !class_subject.is_empty() {
let ident: String = chars[ident_start..ident_end].iter().collect();
return format!("{}::{}", class_subject, ident);
}
}
extract_simple_variable(chars, end)
}
fn extract_call_subject(chars: &[char], paren_end: usize) -> Option<String> {
let open = skip_balanced_parens_back(chars, paren_end)?;
let args_text: String = chars[open + 1..paren_end - 1].iter().collect();
let args_text = args_text.trim();
let mut i = open;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\') {
i -= 1;
}
if i > 0 && chars[i - 1] == '$' {
i -= 1;
}
if i == open {
if let Some(new_expr) = extract_new_expression_inside_parens(chars, open, paren_end) {
return Some(new_expr);
}
if let Some(clone_inner) = extract_clone_expression_inside_parens(chars, open, paren_end) {
return Some(clone_inner);
}
if open > 0
&& chars[open - 1] == ')'
&& let Some(inner_open) = skip_balanced_parens_back(chars, open)
{
let inner: String = chars[inner_open + 1..open - 1].iter().collect();
let inner = inner.trim();
if !inner.is_empty() {
return Some(format!("({})({})", inner, args_text));
}
}
return None;
}
let func_name: String = chars[i..open].iter().collect();
if let Some(class_name) = check_new_keyword_before(chars, i, &func_name) {
return Some(class_name);
}
let rhs = if args_text.is_empty() {
format!("{}()", func_name)
} else {
format!("{}({})", func_name, args_text)
};
if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' {
let arrow_pos = i - 2;
let mut j = arrow_pos;
while j > 0 && chars[j - 1] == ' ' {
j -= 1;
}
if j > 0
&& chars[j - 1] == ')'
&& let Some(inner_call) = extract_call_subject(chars, j)
{
return Some(format!("{}->{}", inner_call, rhs));
}
let inner_subject = extract_arrow_subject(chars, arrow_pos);
if !inner_subject.is_empty() {
return Some(format!("{}->{}", inner_subject, rhs));
}
}
if i >= 3 && chars[i - 3] == '?' && chars[i - 2] == '-' && chars[i - 1] == '>' {
let inner_subject = extract_simple_variable(chars, i - 3);
if !inner_subject.is_empty() {
return Some(format!("{}?->{}", inner_subject, rhs));
}
}
if i >= 2 && chars[i - 2] == ':' && chars[i - 1] == ':' {
let class_subject = extract_double_colon_subject(chars, i - 2);
if !class_subject.is_empty() {
return Some(format!("{}::{}", class_subject, rhs));
}
}
Some(rhs)
}
fn extract_simple_variable(chars: &[char], end: usize) -> String {
let mut i = end;
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
let var_end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
if i > 0 && chars[i - 1] == '$' {
i -= 1;
chars[i..var_end].iter().collect()
} else {
chars[i..var_end].iter().collect()
}
}
fn extract_double_colon_subject(chars: &[char], colon_pos: usize) -> String {
let mut i = colon_pos;
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
let end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\') {
i -= 1;
}
if i > 0 && chars[i - 1] == '$' {
i -= 1;
}
if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' {
let prop: String = chars[i..end].iter().collect();
let inner = extract_arrow_subject(chars, i - 2);
if !inner.is_empty() {
return format!("{}->{}", inner, prop);
}
}
if i >= 3 && chars[i - 3] == '?' && chars[i - 2] == '-' && chars[i - 1] == '>' {
let prop: String = chars[i..end].iter().collect();
let inner = extract_arrow_subject(chars, i - 3);
if !inner.is_empty() {
return format!("{}?->{}", inner, prop);
}
}
chars[i..end].iter().collect()
}
pub(crate) fn detect_access_operator(chars: &[char], col: usize) -> Option<(String, AccessKind)> {
let col = col.min(chars.len());
if chars.is_empty() {
return None;
}
let operator_end = {
let mut i = col;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
if i > 0 && chars[i - 1] == '$' {
i -= 1;
}
while i > 0 && chars[i - 1] == ' ' {
i -= 1;
}
i
};
if operator_end >= 2 && chars[operator_end - 2] == ':' && chars[operator_end - 1] == ':' {
let subject = extract_double_colon_subject(chars, operator_end - 2);
if !subject.is_empty() {
return Some((subject, AccessKind::DoubleColon));
}
}
if operator_end >= 2 && chars[operator_end - 2] == '-' && chars[operator_end - 1] == '>' {
let subject = extract_arrow_subject(chars, operator_end - 2);
if !subject.is_empty() {
return Some((subject, AccessKind::Arrow));
}
}
if operator_end >= 3
&& chars[operator_end - 3] == '?'
&& chars[operator_end - 2] == '-'
&& chars[operator_end - 1] == '>'
{
let subject = extract_arrow_subject(chars, operator_end - 3);
if !subject.is_empty() {
return Some((subject, AccessKind::Arrow));
}
}
None
}
#[cfg(test)]
#[path = "subject_extraction_tests.rs"]
mod tests;