1use std::collections::HashMap;
19use std::ops::ControlFlow;
20
21use php_ast::{
22 Arg, AssignOp, BinaryOp, Block, ClassMember, ClassMemberKind, Expr, ExprKind, Span, Stmt,
23 StmtKind, UnaryPrefixOp,
24 visitor::{Visitor, walk_block, walk_class_member, walk_expr, walk_stmt},
25};
26use tower_lsp_server::ls_types::{
27 CodeAction, CodeActionKind, CodeActionOrCommand, DocumentHighlightKind, Range, TextEdit, Uri,
28 WorkspaceEdit,
29};
30
31use crate::document::ast::ParsedDoc;
32use crate::navigation::walk::collect_var_refs_in_scope;
33use crate::text::word_at_position;
34
35pub fn inline_variable_actions(
36 source: &str,
37 doc: &ParsedDoc,
38 range: Range,
39 uri: &Uri,
40 is_byref_param: &dyn Fn(ArgSlot<'_>) -> bool,
41) -> Vec<CodeActionOrCommand> {
42 let var_name = match word_at_position(source, range.start) {
43 Some(w) if w.starts_with('$') && w.len() > 1 => w,
44 _ => return vec![],
45 };
46 let name = &var_name[1..];
47 let sv = doc.view();
48 let stmts = &doc.program().stmts;
49
50 let mut refs = Vec::new();
51 collect_var_refs_in_scope(
52 stmts,
53 name,
54 sv.byte_of_position(range.start) as usize,
55 &mut refs,
56 );
57 refs.sort_by_key(|(span, _)| span.start);
58 refs.dedup_by_key(|(span, _)| span.start);
59
60 let mut writes = refs
61 .iter()
62 .filter(|(_, kind)| *kind == DocumentHighlightKind::WRITE);
63 let (Some(&(write_span, _)), None) = (writes.next(), writes.next()) else {
64 return vec![];
65 };
66 let reads: Vec<Span> = refs
67 .iter()
68 .filter(|(_, kind)| *kind == DocumentHighlightKind::READ)
69 .map(|(span, _)| *span)
70 .collect();
71
72 let mut finder = AssignmentFinder {
73 target: write_span,
74 containers: Vec::new(),
75 scopes: Vec::new(),
76 found: None,
77 };
78 for stmt in stmts.iter() {
79 let _ = finder.visit_stmt(stmt);
80 }
81 let Some(found) = finder.found else {
82 return vec![];
83 };
84 let Some(region) = found.container else {
85 return vec![];
86 };
87 let region = region.unwrap_or(Span::new(0, source.len() as u32));
88
89 let mut scan = ScopeScan::new(
90 source,
91 found.scope.unwrap_or(Span::new(0, source.len() as u32)),
92 );
93 for stmt in stmts.iter() {
94 let _ = scan.visit_stmt(stmt);
95 }
96 if scan.dynamic_scope
97 || scan
98 .bindings
99 .iter()
100 .any(|(span, bound)| bound == name && *span != write_span)
101 {
102 return vec![];
103 }
104 let usable = |read: &Span| {
105 read.start >= found.stmt.end
106 && contains(region, *read)
107 && !scan.zones.iter().any(|zone| contains(*zone, *read))
108 };
109 if reads.is_empty() || !reads.iter().all(usable) {
110 return vec![];
111 }
112 let after = |spans: &[Span], read: &Span| {
114 spans
115 .iter()
116 .filter(|span| span.start >= found.stmt.end && contains(**span, *read))
117 .map(|span| span.end)
118 .max()
119 };
120 let safe_to_evaluate_at_reads = match found.initializer {
121 Initializer::Constant => true,
122 Initializer::Pure => {
123 let cutoff = reads
124 .iter()
125 .map(|read| after(&scan.loops, read).unwrap_or(read.start))
126 .max()
127 .unwrap_or(found.stmt.end);
128 !reads
129 .iter()
130 .any(|read| after(&scan.arrow_fns, read).is_some())
131 && !found
132 .value_vars
133 .iter()
134 .any(|var| scan.ref_bound.contains(var))
135 && !scan.effects.iter().any(|(span, effect)| {
136 span.start >= found.stmt.end
137 && span.end <= cutoff
138 && match effect {
139 Effect::Output => false,
140 Effect::Write(var) => found.value_vars.contains(var),
141 Effect::Other => true,
142 }
143 })
144 }
145 Initializer::Effectful => {
146 let [read] = reads.as_slice() else {
147 return vec![];
148 };
149 let between = |span: &Span| span.start >= found.stmt.end && span.end <= read.start;
150 [&scan.loops, &scan.arrow_fns, &scan.conditionals]
151 .iter()
152 .all(|spans| after(spans, read).is_none())
153 && !scan.stmts.iter().any(between)
154 && !scan.effects.iter().any(|(span, _)| between(span))
155 }
156 };
157 if !safe_to_evaluate_at_reads {
158 return vec![];
159 }
160
161 let mut sites = ReadSites {
162 source,
163 reads: &reads,
164 args: Vec::new(),
165 callees: Vec::new(),
166 class_refs: Vec::new(),
167 };
168 for stmt in stmts.iter() {
169 let _ = sites.visit_stmt(stmt);
170 }
171 let callees = sites.callees;
172 let class_refs = sites.class_refs;
173 let class_name = found
174 .class_name
175 .map(|span| &source[span.start as usize..span.end as usize]);
176 if !class_refs.is_empty() && class_name.is_none() {
177 return vec![];
178 }
179 if sites.args.into_iter().any(is_byref_param) {
180 return vec![];
181 }
182
183 let rhs = &source[found.value.start as usize..found.value.end as usize];
184 let mut edits: Vec<TextEdit> = reads
185 .iter()
186 .map(|read| {
187 let new_text = if class_refs.contains(read)
188 && let Some(class_name) = class_name
189 {
190 class_name.to_string()
191 } else if callees.contains(read) {
192 if found.value_is_variable {
193 rhs.to_string()
194 } else {
195 format!("({rhs})")
196 }
197 } else if found.value_is_number && touches_dot(source, *read) {
198 format!("({rhs})")
200 } else if found.value_is_atomic || stands_alone(source, *read) {
201 rhs.to_string()
202 } else {
203 format!("({rhs})")
204 };
205 TextEdit {
206 range: Range {
207 start: sv.position_of(read.start),
208 end: sv.position_of(read.end),
209 },
210 new_text,
211 }
212 })
213 .collect();
214
215 let (del_start, del_end) = statement_deletion(source, found.stmt);
216 edits.push(TextEdit {
217 range: Range {
218 start: sv.position_of(del_start),
219 end: sv.position_of(del_end),
220 },
221 new_text: String::new(),
222 });
223
224 let mut changes = HashMap::new();
225 changes.insert(uri.clone(), edits);
226
227 vec![CodeActionOrCommand::CodeAction(CodeAction {
228 title: format!("Inline variable '{var_name}'"),
229 kind: Some(CodeActionKind::REFACTOR_INLINE),
230 edit: Some(WorkspaceEdit {
231 changes: Some(changes),
232 ..Default::default()
233 }),
234 ..Default::default()
235 })]
236}
237
238fn contains(outer: Span, inner: Span) -> bool {
239 outer.start <= inner.start && inner.end <= outer.end
240}
241
242fn touches_dot(source: &str, span: Span) -> bool {
243 source[..span.start as usize].ends_with('.') || source[span.end as usize..].starts_with('.')
244}
245
246enum Initializer {
248 Constant,
250 Pure,
252 Effectful,
254}
255
256struct FoundAssignment {
257 stmt: Span,
258 value: Span,
259 initializer: Initializer,
260 value_is_atomic: bool,
261 value_is_variable: bool,
262 value_is_number: bool,
263 class_name: Option<Span>,
266 value_vars: Vec<String>,
268 scope: Option<Span>,
270 container: Option<Option<Span>>,
274}
275
276struct AssignmentFinder {
279 target: Span,
280 containers: Vec<Option<Span>>,
283 scopes: Vec<Span>,
285 found: Option<FoundAssignment>,
286}
287
288impl AssignmentFinder {
289 fn in_scope(
290 &mut self,
291 span: Span,
292 walk: impl FnOnce(&mut Self) -> ControlFlow<()>,
293 ) -> ControlFlow<()> {
294 let containers = std::mem::take(&mut self.containers);
295 self.scopes.push(span);
296 let flow = walk(self);
297 self.scopes.pop();
298 self.containers = containers;
299 flow
300 }
301}
302
303impl<'arena, 'src> Visitor<'arena, 'src> for AssignmentFinder {
304 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
305 if let StmtKind::Expression(expr) = &stmt.kind
306 && let ExprKind::Assign(assign) = &expr.kind
307 && assign.target.span == self.target
308 {
309 if assign.op == AssignOp::Assign && !assign.by_ref {
310 let value = assign.value;
311 self.found = Some(FoundAssignment {
312 stmt: stmt.span,
313 value: value.span,
314 initializer: if !is_repeatable(value) {
315 Initializer::Effectful
316 } else if reads_state(value) {
317 Initializer::Pure
318 } else {
319 Initializer::Constant
320 },
321 value_is_atomic: is_atomic(value),
322 value_is_variable: matches!(
323 value.kind,
324 ExprKind::Variable(_) | ExprKind::Parenthesized(_)
325 ),
326 value_is_number: matches!(value.kind, ExprKind::Int(_) | ExprKind::Float(_)),
327 class_name: class_name(value),
328 value_vars: variable_names(value),
329 scope: self.scopes.last().copied(),
330 container: match self.containers.last() {
331 None => Some(None),
332 Some(Some(block)) => Some(Some(*block)),
333 Some(None) => None,
334 },
335 });
336 }
337 return ControlFlow::Break(());
338 }
339 if let StmtKind::Function(_) = stmt.kind {
340 return self.in_scope(stmt.span, |this| walk_stmt(this, stmt));
341 }
342 let conditional_body = matches!(
343 stmt.kind,
344 StmtKind::If(_)
345 | StmtKind::While(_)
346 | StmtKind::For(_)
347 | StmtKind::Foreach(_)
348 | StmtKind::DoWhile(_)
349 | StmtKind::Switch(_)
350 | StmtKind::Declare(_)
351 );
352 if conditional_body {
353 self.containers.push(None);
354 }
355 let flow = walk_stmt(self, stmt);
356 if conditional_body {
357 self.containers.pop();
358 }
359 flow
360 }
361
362 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
363 if let ExprKind::Closure(_) = expr.kind {
364 return self.in_scope(expr.span, |this| walk_expr(this, expr));
365 }
366 walk_expr(self, expr)
367 }
368
369 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
370 if let ClassMemberKind::Method(_) = member.kind {
371 return self.in_scope(member.span, |this| walk_class_member(this, member));
372 }
373 walk_class_member(self, member)
374 }
375
376 fn visit_block(&mut self, block: &Block<'arena, 'src>) -> ControlFlow<()> {
377 self.containers.push(Some(block.span));
378 let flow = walk_block(self, block);
379 self.containers.pop();
380 flow
381 }
382}
383
384fn is_repeatable(expr: &Expr<'_, '_>) -> bool {
387 match &expr.kind {
388 ExprKind::Int(_)
389 | ExprKind::Float(_)
390 | ExprKind::String(_)
391 | ExprKind::Nowdoc { .. }
392 | ExprKind::Bool(_)
393 | ExprKind::Null
394 | ExprKind::Variable(_)
395 | ExprKind::Identifier(_)
396 | ExprKind::MagicConst(_)
397 | ExprKind::ClassConstAccess(_) => true,
398 ExprKind::Parenthesized(inner) | ExprKind::Cast(_, inner) => is_repeatable(inner),
399 ExprKind::UnaryPrefix(unary) => {
400 !matches!(
401 unary.op,
402 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
403 ) && is_repeatable(unary.operand)
404 }
405 ExprKind::Binary(binary) => {
406 binary.op != BinaryOp::Pipe && is_repeatable(binary.left) && is_repeatable(binary.right)
407 }
408 ExprKind::PropertyAccess(access) | ExprKind::NullsafePropertyAccess(access) => {
409 is_repeatable(access.object) && is_repeatable(access.property)
410 }
411 ExprKind::StaticPropertyAccess(access) => is_repeatable(access.class),
412 ExprKind::ArrayAccess(access) => {
413 is_repeatable(access.array) && access.index.is_none_or(is_repeatable)
414 }
415 ExprKind::Array(elements) => elements.iter().all(|element| {
416 !element.by_ref
417 && element.key.as_ref().is_none_or(is_repeatable)
418 && is_repeatable(&element.value)
419 }),
420 _ => false,
421 }
422}
423
424fn class_name(expr: &Expr<'_, '_>) -> Option<Span> {
425 match &expr.kind {
426 ExprKind::Variable(_) => Some(expr.span),
427 ExprKind::ClassConstAccess(access)
428 if matches!(access.class.kind, ExprKind::Identifier(_))
429 && access
430 .member
431 .name_str()
432 .is_some_and(|m| m.eq_ignore_ascii_case("class")) =>
433 {
434 Some(access.class.span)
435 }
436 _ => None,
437 }
438}
439
440fn variable_names(expr: &Expr<'_, '_>) -> Vec<String> {
441 struct Names(Vec<String>);
442 impl<'arena, 'src> Visitor<'arena, 'src> for Names {
443 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
444 if let ExprKind::Variable(name) = &expr.kind
445 && name.as_str() != "this"
446 && !self.0.iter().any(|n| n == name.as_str())
447 {
448 self.0.push(name.as_str().to_string());
449 }
450 walk_expr(self, expr)
451 }
452 }
453 let mut names = Names(Vec::new());
454 let _ = names.visit_expr(expr);
455 names.0
456}
457
458fn reads_state(expr: &Expr<'_, '_>) -> bool {
460 struct State(bool);
461 impl<'arena, 'src> Visitor<'arena, 'src> for State {
462 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
463 if matches!(
464 expr.kind,
465 ExprKind::Variable(_)
466 | ExprKind::StaticPropertyAccess(_)
467 | ExprKind::StaticPropertyAccessDynamic { .. }
468 ) {
469 self.0 = true;
470 return ControlFlow::Break(());
471 }
472 walk_expr(self, expr)
473 }
474 }
475 let mut state = State(false);
476 let _ = state.visit_expr(expr);
477 state.0
478}
479
480pub struct ArgSlot<'a> {
483 pub callee_offset: u32,
484 pub position: usize,
485 pub name: Option<&'a str>,
486}
487
488struct ReadSites<'s> {
490 source: &'s str,
491 reads: &'s [Span],
492 args: Vec<ArgSlot<'s>>,
493 callees: Vec<Span>,
495 class_refs: Vec<Span>,
497}
498
499impl ReadSites<'_> {
500 fn note_class_ref(&mut self, span: Span) {
501 if self.reads.contains(&span) {
502 self.class_refs.push(span);
503 }
504 }
505
506 fn note_args(&mut self, callee_offset: u32, args: &[Arg<'_, '_>]) {
507 for (position, arg) in args.iter().enumerate() {
508 if let Some(value) = &arg.value
509 && !arg.unpack
510 && self.reads.contains(&value.span)
511 {
512 let source = self.source;
513 self.args.push(ArgSlot {
514 callee_offset,
515 position,
516 name: arg.name.as_ref().map(|n| {
517 let span = n.span();
518 &source[span.start as usize..span.end as usize]
519 }),
520 });
521 }
522 }
523 }
524}
525
526impl<'arena, 'src> Visitor<'arena, 'src> for ReadSites<'_> {
527 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
528 match &expr.kind {
529 ExprKind::FunctionCall(call) => {
530 if self.reads.contains(&call.name.span) {
531 self.callees.push(call.name.span);
532 }
533 self.note_args(call.name.span.start, &call.args)
534 }
535 ExprKind::MethodCall(call) | ExprKind::NullsafeMethodCall(call) => {
536 self.note_args(call.method.span.start, &call.args)
537 }
538 ExprKind::StaticMethodCall(call) => {
539 self.note_class_ref(call.class.span);
540 self.note_args(call.method.span.start, &call.args)
541 }
542 ExprKind::StaticPropertyAccess(access) | ExprKind::ClassConstAccess(access) => {
543 self.note_class_ref(access.class.span)
544 }
545 ExprKind::New(new) => {
546 self.note_class_ref(new.class.span);
547 self.note_args(new.class.span.start, &new.args)
548 }
549 ExprKind::Binary(binary) if binary.op == BinaryOp::Instanceof => {
550 self.note_class_ref(binary.right.span)
551 }
552 _ => {}
553 }
554 walk_expr(self, expr)
555 }
556}
557
558enum Effect {
560 Write(String),
562 Output,
564 Other,
566}
567
568struct ScopeScan<'s> {
570 source: &'s str,
571 scope: Span,
572 zones: Vec<Span>,
574 bindings: Vec<(Span, String)>,
576 ref_bound: Vec<String>,
578 effects: Vec<(Span, Effect)>,
579 stmts: Vec<Span>,
580 loops: Vec<Span>,
582 arrow_fns: Vec<Span>,
583 conditionals: Vec<Span>,
585 dynamic_scope: bool,
587}
588
589impl<'s> ScopeScan<'s> {
590 fn new(source: &'s str, scope: Span) -> Self {
591 Self {
592 source,
593 scope,
594 zones: Vec::new(),
595 bindings: Vec::new(),
596 ref_bound: Vec::new(),
597 effects: Vec::new(),
598 stmts: Vec::new(),
599 loops: Vec::new(),
600 arrow_fns: Vec::new(),
601 conditionals: Vec::new(),
602 dynamic_scope: false,
603 }
604 }
605
606 fn bind(&mut self, target: &Expr<'_, '_>, by_ref: bool) {
608 match &target.kind {
609 ExprKind::Variable(name) => {
610 self.bindings.push((target.span, name.to_string()));
611 if by_ref {
612 self.ref_bound.push(name.to_string());
613 }
614 }
615 ExprKind::Array(elements) => {
616 for element in elements.iter() {
617 self.bind(&element.value, element.by_ref);
618 }
619 }
620 ExprKind::ArrayAccess(access) => self.bind(access.array, by_ref),
621 ExprKind::PropertyAccess(access) | ExprKind::NullsafePropertyAccess(access) => {
622 self.bind(access.object, false)
623 }
624 ExprKind::Parenthesized(inner) => self.bind(inner, by_ref),
625 _ => {}
626 }
627 }
628
629 fn write(&mut self, span: Span, target: &Expr<'_, '_>) {
630 let effect = match &target.kind {
631 ExprKind::Variable(name) => Effect::Write(name.to_string()),
632 _ => Effect::Other,
633 };
634 self.effects.push((span, effect));
635 }
636}
637
638fn is_dynamic_scope_call(name: &Expr<'_, '_>) -> bool {
639 name.name_str().is_some_and(|name| {
640 let name = name.trim_start_matches('\\');
641 ["compact", "extract", "get_defined_vars", "parse_str"]
642 .iter()
643 .any(|f| name.eq_ignore_ascii_case(f))
644 })
645}
646
647impl<'arena, 'src> Visitor<'arena, 'src> for ScopeScan<'_> {
648 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
649 let scope_like = matches!(
650 stmt.kind,
651 StmtKind::Function(_)
652 | StmtKind::Class(_)
653 | StmtKind::Interface(_)
654 | StmtKind::Trait(_)
655 | StmtKind::Enum(_)
656 );
657 if !contains(self.scope, stmt.span) || (scope_like && stmt.span != self.scope) {
658 return if contains(stmt.span, self.scope) {
659 walk_stmt(self, stmt)
660 } else {
661 ControlFlow::Continue(())
662 };
663 }
664 self.stmts.push(stmt.span);
665 match &stmt.kind {
666 StmtKind::Echo(_) => self.effects.push((stmt.span, Effect::Output)),
667 StmtKind::Unset(targets) => {
668 self.zones.push(stmt.span);
669 self.effects.push((stmt.span, Effect::Other));
670 for target in targets.iter() {
671 self.bind(target, false);
672 }
673 }
674 StmtKind::Global(vars) => {
675 self.zones.push(stmt.span);
676 self.effects.push((stmt.span, Effect::Other));
677 for var in vars.iter() {
678 self.bind(var, true);
679 }
680 }
681 StmtKind::StaticVar(vars) => {
682 self.zones.push(stmt.span);
683 self.effects.push((stmt.span, Effect::Other));
684 for var in vars.iter() {
685 let name = var.name.or_error().to_string();
686 self.bindings.push((var.span, name.clone()));
687 self.ref_bound.push(name);
688 }
689 }
690 StmtKind::Foreach(foreach) => {
691 let by_ref = self.source[..foreach.value.span.start as usize]
692 .trim_end()
693 .ends_with('&');
694 let first = foreach.key.as_ref().unwrap_or(&foreach.value);
695 self.loops.push(Span::new(first.span.start, stmt.span.end));
696 if by_ref {
697 self.zones.push(foreach.expr.span);
698 self.bind(&foreach.expr, true);
699 }
700 for target in foreach.key.iter().chain([&foreach.value]) {
701 self.zones.push(target.span);
702 self.write(target.span, target);
703 }
704 if let Some(key) = &foreach.key {
705 self.bind(key, false);
706 }
707 self.bind(&foreach.value, by_ref);
708 }
709 StmtKind::For(for_stmt) => {
710 let first = for_stmt
711 .condition
712 .first()
713 .or(for_stmt.update.first())
714 .map_or(for_stmt.body.span.start, |expr| expr.span.start);
715 self.loops.push(Span::new(first, stmt.span.end));
716 }
717 StmtKind::While(_) | StmtKind::DoWhile(_) => self.loops.push(stmt.span),
718 StmtKind::If(if_stmt) => {
719 self.conditionals.push(if_stmt.then_branch.span);
720 self.conditionals
721 .extend(if_stmt.elseif_branches.iter().map(|branch| branch.span));
722 self.conditionals
723 .extend(if_stmt.else_branch.map(|branch| branch.span));
724 }
725 StmtKind::Switch(switch) => self.conditionals.push(switch.body.span),
726 StmtKind::TryCatch(try_catch) => {
727 for catch in try_catch.catches.iter() {
728 self.conditionals.push(catch.span);
729 if let Some(var) = catch.var {
730 let span = Span::new(catch.span.start, catch.body.span.start);
731 self.bindings.push((span, var.to_string()));
732 self.effects.push((span, Effect::Write(var.to_string())));
733 }
734 }
735 }
736 _ => {}
737 }
738 walk_stmt(self, stmt)
739 }
740
741 fn visit_class_member(&mut self, member: &ClassMember<'arena, 'src>) -> ControlFlow<()> {
742 if contains(member.span, self.scope) {
743 walk_class_member(self, member)
744 } else {
745 ControlFlow::Continue(())
746 }
747 }
748
749 fn visit_expr(&mut self, expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
750 if !contains(self.scope, expr.span) {
751 return if contains(expr.span, self.scope) {
752 walk_expr(self, expr)
753 } else {
754 ControlFlow::Continue(())
755 };
756 }
757 match &expr.kind {
758 ExprKind::Closure(closure) if expr.span != self.scope => {
759 for use_var in closure.use_vars.iter() {
760 self.zones.push(use_var.span);
761 if use_var.by_ref {
762 let name = use_var.name.trim_start_matches('$').to_string();
763 self.bindings.push((use_var.span, name.clone()));
764 self.ref_bound.push(name);
765 }
766 }
767 return ControlFlow::Continue(());
768 }
769 ExprKind::AnonymousClass(_) => {
770 self.effects.push((expr.span, Effect::Other));
771 return ControlFlow::Continue(());
772 }
773 ExprKind::InterpolatedString(_) | ExprKind::Heredoc { .. } | ExprKind::Isset(_) => {
774 self.zones.push(expr.span)
775 }
776 ExprKind::ShellExec(_) => {
777 self.zones.push(expr.span);
778 self.effects.push((expr.span, Effect::Other));
779 }
780 ExprKind::Assign(assign) => {
781 self.zones.push(assign.target.span);
782 self.bind(assign.target, assign.by_ref);
783 if assign.by_ref {
784 self.zones.push(assign.value.span);
785 self.bind(assign.value, true);
786 self.effects.push((expr.span, Effect::Other));
787 } else {
788 self.write(expr.span, assign.target);
789 }
790 }
791 ExprKind::UnaryPrefix(unary)
792 if matches!(
793 unary.op,
794 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement
795 ) =>
796 {
797 self.zones.push(unary.operand.span);
798 self.bind(unary.operand, false);
799 self.write(expr.span, unary.operand);
800 }
801 ExprKind::UnaryPostfix(unary) => {
802 self.zones.push(unary.operand.span);
803 self.bind(unary.operand, false);
804 self.write(expr.span, unary.operand);
805 }
806 ExprKind::Array(elements) => {
807 for element in elements.iter().filter(|element| element.by_ref) {
808 self.zones.push(element.value.span);
809 self.bind(&element.value, true);
810 }
811 }
812 ExprKind::FunctionCall(call) => {
813 self.dynamic_scope |= is_dynamic_scope_call(call.name);
814 self.effects.push((expr.span, Effect::Other));
815 }
816 ExprKind::NullsafeMethodCall(call) => {
817 self.conditionals
818 .push(Span::new(call.method.span.start, expr.span.end));
819 self.effects.push((expr.span, Effect::Other));
820 }
821 ExprKind::MethodCall(_)
822 | ExprKind::StaticMethodCall(_)
823 | ExprKind::StaticDynMethodCall(_)
824 | ExprKind::New(_)
825 | ExprKind::Clone(_)
826 | ExprKind::CloneWith(..)
827 | ExprKind::Exit(_)
828 | ExprKind::Yield(_)
829 | ExprKind::ThrowExpr(_) => self.effects.push((expr.span, Effect::Other)),
830 ExprKind::Include(..) | ExprKind::Eval(_) | ExprKind::VariableVariable(_) => {
831 self.dynamic_scope = true;
832 }
833 ExprKind::Print(_) => self.effects.push((expr.span, Effect::Output)),
834 ExprKind::Ternary(ternary) => {
835 self.conditionals
836 .extend(ternary.then_expr.map(|then| then.span));
837 self.conditionals.push(ternary.else_expr.span);
838 }
839 ExprKind::NullCoalesce(coalesce) => self.conditionals.push(coalesce.right.span),
840 ExprKind::Binary(binary)
841 if matches!(
842 binary.op,
843 BinaryOp::BooleanAnd
844 | BinaryOp::BooleanOr
845 | BinaryOp::LogicalAnd
846 | BinaryOp::LogicalOr
847 ) =>
848 {
849 self.conditionals.push(binary.right.span)
850 }
851 ExprKind::Match(match_expr) => self
852 .conditionals
853 .extend(match_expr.arms.iter().map(|arm| arm.span)),
854 ExprKind::ArrowFunction(_) => self.arrow_fns.push(expr.span),
855 _ => {}
856 }
857 walk_expr(self, expr)
858 }
859}
860
861fn is_atomic(expr: &Expr<'_, '_>) -> bool {
863 matches!(
864 expr.kind,
865 ExprKind::Int(_)
866 | ExprKind::Float(_)
867 | ExprKind::String(_)
868 | ExprKind::InterpolatedString(_)
869 | ExprKind::Heredoc { .. }
870 | ExprKind::Nowdoc { .. }
871 | ExprKind::Bool(_)
872 | ExprKind::Null
873 | ExprKind::Variable(_)
874 | ExprKind::Identifier(_)
875 | ExprKind::FunctionCall(_)
876 | ExprKind::Array(_)
877 | ExprKind::ArrayAccess(_)
878 | ExprKind::Parenthesized(_)
879 | ExprKind::MagicConst(_)
880 | ExprKind::PropertyAccess(_)
881 | ExprKind::NullsafePropertyAccess(_)
882 | ExprKind::MethodCall(_)
883 | ExprKind::NullsafeMethodCall(_)
884 | ExprKind::StaticPropertyAccess(_)
885 | ExprKind::StaticMethodCall(_)
886 | ExprKind::ClassConstAccess(_)
887 )
888}
889
890fn stands_alone(source: &str, span: Span) -> bool {
894 let before = source[..span.start as usize].trim_end();
895 let after = source[span.end as usize..].trim_start();
896 let opens = before.ends_with(['(', '[', ','])
897 || before.ends_with("=>")
898 || (before.ends_with('=')
899 && !["==", "!=", "<=", ">="]
900 .iter()
901 .any(|op| before.ends_with(op)))
902 || ["return", "echo", "print"].iter().any(|kw| {
903 before.strip_suffix(kw).is_some_and(|rest| {
904 !rest.ends_with(|c: char| c.is_alphanumeric() || c == '_' || c == '$')
905 })
906 });
907 opens && after.starts_with([';', ')', ']', ','])
908}
909
910fn statement_deletion(source: &str, stmt: Span) -> (u32, u32) {
913 let (start, end) = (stmt.start as usize, stmt.end as usize);
914 let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1);
915 let rest_of_line = source[end..].find('\n').map_or(source.len(), |i| end + i);
916 if source[line_start..start].trim().is_empty() && source[end..rest_of_line].trim().is_empty() {
917 let line_end = (rest_of_line + 1).min(source.len());
918 return (line_start as u32, line_end as u32);
919 }
920 let trailing = source[end..rest_of_line].len() - source[end..rest_of_line].trim_start().len();
921 (start as u32, (end + trailing) as u32)
922}