php_lsp/analysis/
inline_value.rs1use tower_lsp::lsp_types::*;
2
3pub fn inline_values_in_range(source: &str, range: Range) -> Vec<InlineValue> {
11 let mut result = Vec::new();
12
13 let is_ident_start = |c: char| c.is_alphabetic() || c == '_';
17 let is_ident_cont = |c: char| c.is_alphanumeric() || c == '_';
18
19 for (line_idx, line) in source.lines().enumerate() {
20 let line_num = line_idx as u32;
21 if line_num < range.start.line || line_num > range.end.line {
22 continue;
23 }
24 let line_min_col: Option<u32> =
28 (line_num == range.start.line).then_some(range.start.character);
29 let line_max_col: Option<u32> = (line_num == range.end.line).then_some(range.end.character);
30
31 let chars: Vec<(u32, char)> = {
34 let mut out = Vec::with_capacity(line.len());
35 let mut col: u32 = 0;
36 for ch in line.chars() {
37 out.push((col, ch));
38 col += ch.len_utf16() as u32;
39 }
40 out
41 };
42
43 let mut i = 0usize;
44 while i < chars.len() {
45 if chars[i].1 != '$' {
46 i += 1;
47 continue;
48 }
49 if chars.get(i + 1).map(|(_, c)| *c) == Some('$') {
51 i += 2;
52 continue;
53 }
54 let dollar_col = chars[i].0;
55 i += 1;
56 let Some(&(_, first)) = chars.get(i) else {
58 continue;
59 };
60 if !is_ident_start(first) {
61 continue;
62 }
63 let name_start_idx = i;
64 while i < chars.len() && is_ident_cont(chars[i].1) {
65 i += 1;
66 }
67 let name_end_idx = i;
68 let var_name: String = chars[name_start_idx..name_end_idx]
69 .iter()
70 .map(|(_, c)| *c)
71 .collect();
72
73 if var_name == "this" {
75 continue;
76 }
77
78 let end_col = chars.get(name_end_idx).map(|(c, _)| *c).unwrap_or_else(|| {
79 chars
80 .last()
81 .map(|(c, ch)| c + ch.len_utf16() as u32)
82 .unwrap_or(0)
83 });
84
85 if let Some(min) = line_min_col
88 && dollar_col < min
89 {
90 continue;
91 }
92 if let Some(max) = line_max_col
93 && end_col > max
94 {
95 continue;
96 }
97 result.push(InlineValue::VariableLookup(InlineValueVariableLookup {
98 range: Range {
99 start: Position {
100 line: line_num,
101 character: dollar_col,
102 },
103 end: Position {
104 line: line_num,
105 character: end_col,
106 },
107 },
108 variable_name: Some(var_name),
111 case_sensitive_lookup: true,
112 }));
113 }
114 }
115
116 result
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn range(sl: u32, sc: u32, el: u32, ec: u32) -> Range {
124 Range {
125 start: Position {
126 line: sl,
127 character: sc,
128 },
129 end: Position {
130 line: el,
131 character: ec,
132 },
133 }
134 }
135
136 #[test]
137 fn finds_variables_in_range() {
138 let src = "<?php\n$foo = 1;\n$bar = 2;\n";
139 let vals = inline_values_in_range(src, range(1, 0, 2, 99));
140 assert_eq!(vals.len(), 2);
141 if let InlineValue::VariableLookup(v) = &vals[0] {
142 assert_eq!(v.variable_name.as_deref(), Some("foo"));
143 assert_eq!(v.range.start.line, 1);
144 } else {
145 panic!("expected VariableLookup");
146 }
147 }
148
149 #[test]
150 fn skips_this() {
151 let src = "<?php\n$this->foo = $bar;";
152 let vals = inline_values_in_range(src, range(1, 0, 1, 99));
153 assert_eq!(vals.len(), 1);
154 if let InlineValue::VariableLookup(v) = &vals[0] {
155 assert_eq!(v.variable_name.as_deref(), Some("bar"));
156 }
157 }
158
159 #[test]
160 fn excludes_lines_outside_range() {
161 let src = "<?php\n$x = 1;\n$y = 2;\n$z = 3;\n";
162 let vals = inline_values_in_range(src, range(2, 0, 2, 99));
163 assert_eq!(vals.len(), 1);
164 if let InlineValue::VariableLookup(v) = &vals[0] {
165 assert_eq!(v.variable_name.as_deref(), Some("y"));
166 }
167 }
168
169 #[test]
170 fn skips_variable_variables() {
171 let src = "<?php\n$$dynamic = 1;";
172 let vals = inline_values_in_range(src, range(1, 0, 1, 99));
173 assert!(vals.is_empty(), "variable-variables should be skipped");
174 }
175}