1use php_ast::{NamespaceBody, Stmt, StmtKind, UseKind};
2
3use crate::text::fqn_short_name;
4
5pub fn extract_receiver_var_before_cursor(line: &str, cursor_col_utf16: usize) -> Option<String> {
10 let chars: Vec<char> = line.chars().collect();
11
12 let mut utf16 = 0usize;
14 let mut char_idx = 0usize;
15 for ch in &chars {
16 if utf16 >= cursor_col_utf16 {
17 break;
18 }
19 utf16 += ch.len_utf16();
20 char_idx += 1;
21 }
22
23 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
25 let mut word_start = char_idx;
26 while word_start > 0 && is_word_char(chars[word_start - 1]) {
27 word_start -= 1;
28 }
29
30 let (is_arrow, arrow_end) = if word_start >= 3
32 && chars[word_start - 3] == '?'
33 && chars[word_start - 2] == '-'
34 && chars[word_start - 1] == '>'
35 {
36 (true, word_start - 3)
37 } else if word_start >= 2 && chars[word_start - 2] == '-' && chars[word_start - 1] == '>' {
38 (true, word_start - 2)
39 } else {
40 (false, 0)
41 };
42
43 if !is_arrow {
44 return None;
45 }
46
47 extract_name_from_chars_end(&chars[..arrow_end])
48}
49
50pub fn is_arrow_access_on_non_this_receiver(line: &str, cursor_col_utf16: usize) -> bool {
52 let chars: Vec<char> = line.chars().collect();
53 let mut utf16 = 0usize;
54 let mut char_idx = 0usize;
55 for ch in &chars {
56 if utf16 >= cursor_col_utf16 {
57 break;
58 }
59 utf16 += ch.len_utf16();
60 char_idx += 1;
61 }
62 let mut word_start = char_idx;
63 while word_start > 0
64 && (chars[word_start - 1].is_alphanumeric() || chars[word_start - 1] == '_')
65 {
66 word_start -= 1;
67 }
68 let arrow_end = if word_start >= 3 && chars[word_start - 3..word_start] == ['?', '-', '>'] {
69 word_start - 3
70 } else if word_start >= 2 && chars[word_start - 2..word_start] == ['-', '>'] {
71 word_start - 2
72 } else {
73 return false;
74 };
75 let receiver: String = chars[..arrow_end].iter().collect();
76 !receiver.trim_end().ends_with("$this")
77}
78
79pub fn extract_static_class_before_cursor(line: &str, cursor_col_utf16: usize) -> Option<String> {
81 let chars: Vec<char> = line.chars().collect();
82
83 let mut utf16 = 0usize;
84 let mut char_idx = 0usize;
85 for ch in &chars {
86 if utf16 >= cursor_col_utf16 {
87 break;
88 }
89 utf16 += ch.len_utf16();
90 char_idx += 1;
91 }
92
93 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
94 let mut word_start = char_idx;
95 while word_start > 0 && is_word_char(chars[word_start - 1]) {
96 word_start -= 1;
97 }
98
99 if word_start > 0 && chars[word_start - 1] == '$' {
101 word_start -= 1;
102 }
103
104 if word_start < 2 || chars[word_start - 2] != ':' || chars[word_start - 1] != ':' {
105 return None;
106 }
107
108 let before_colons = &chars[..word_start - 2];
109 let is_name_char = |c: char| c.is_alphanumeric() || c == '_' || c == '\\';
111 let end = before_colons.len().saturating_sub(
112 before_colons
113 .iter()
114 .rev()
115 .take_while(|&&c| c == ' ' || c == '\t')
116 .count(),
117 );
118 let mut start = end;
119 while start > 0 && is_name_char(before_colons[start - 1]) {
120 start -= 1;
121 }
122 if start == end {
123 return None;
124 }
125 let full: String = before_colons[start..end].iter().collect();
126 Some(fqn_short_name(&full).to_owned())
128}
129
130pub(crate) fn extract_name_from_chars_end(chars: &[char]) -> Option<String> {
133 let is_var_char = |c: char| c.is_alphanumeric() || c == '_' || c == '$';
134 let end = chars.len()
135 - chars
136 .iter()
137 .rev()
138 .take_while(|&&c| c == ' ' || c == '\t')
139 .count();
140 if end == 0 {
141 return None;
142 }
143 let mut start = end;
144 while start > 0 && is_var_char(chars[start - 1]) {
145 start -= 1;
146 }
147 if start == end {
148 return None;
149 }
150 let name: String = chars[start..end].iter().collect();
151 if name.starts_with('$') && name.len() > 1 {
152 Some(name)
153 } else if !name.is_empty() && !name.starts_with('$') {
154 Some(format!("${}", name))
157 } else {
158 None
159 }
160}
161
162pub fn resolve_use_alias(stmts: &[Stmt<'_, '_>], word: &str) -> Option<String> {
167 resolve_use_alias_fqn(stmts, word).map(|(short, _)| short)
168}
169
170pub fn resolve_use_alias_fqn(stmts: &[Stmt<'_, '_>], word: &str) -> Option<(String, String)> {
175 for stmt in stmts {
176 match &stmt.kind {
177 StmtKind::Use(u) if u.kind == UseKind::Normal => {
178 for item in u.uses.iter() {
179 if let Some(alias) = item.alias
180 && alias == word
181 {
182 let fqn = item.name.to_string_repr().into_owned();
183 let short = fqn_short_name(&fqn).to_owned();
184 return Some((short, fqn));
185 }
186 }
187 }
188 StmtKind::Namespace(ns) => {
189 if let NamespaceBody::Braced(inner) = &ns.body
190 && let Some(s) = resolve_use_alias_fqn(&inner.stmts, word)
191 {
192 return Some(s);
193 }
194 }
195 _ => {}
196 }
197 }
198 None
199}
200
201#[cfg(test)]
202mod tests {
203 use super::is_arrow_access_on_non_this_receiver as foreign;
204
205 #[test]
206 fn arrow_on_this_is_not_foreign() {
207 assert!(!foreign("$this->run()", 8));
208 assert!(!foreign("$this?->run()", 9));
209 }
210
211 #[test]
212 fn arrow_on_property_or_variable_is_foreign() {
213 assert!(foreign("$this->sut->run()", 13));
214 assert!(foreign("$obj->run()", 7));
215 assert!(foreign("$notthis->run()", 11));
216 }
217
218 #[test]
219 fn non_arrow_positions_are_not_foreign() {
220 assert!(!foreign("run()", 2));
221 assert!(!foreign("Foo::run()", 7));
222 }
223}