Returns `true` when the selected text parses as a valid, self-contained PHP expression. We wrap it in `<?php $__x = ;` and check that the parser produces no errors. This rejects fragments like `save` (bare method name), `$this` when it's part of `$this->foo()`, partial tokens, and other nonsensical selections.
(selected_text: &str)
| 54 | /// `save` (bare method name), `$this` when it's part of `$this->foo()`, |
| 55 | /// partial tokens, and other nonsensical selections. |
| 56 | fn is_valid_expression(selected_text: &str) -> bool { |
| 57 | let trimmed = selected_text.trim(); |
| 58 | if trimmed.is_empty() { |
| 59 | return false; |
| 60 | } |
| 61 | |
| 62 | // Quick rejects for obvious non-expressions: |
| 63 | // - Bare identifiers that aren't `$var`, `self`, `static`, `parent`, |
| 64 | // `true`, `false`, `null`, or a numeric/string literal. |
| 65 | // e.g. `save`, `getName` — these are method/function names, not |
| 66 | // standalone expressions. |
| 67 | if !trimmed.starts_with('$') |
| 68 | && !trimmed.starts_with('\'') |
| 69 | && !trimmed.starts_with('"') |
| 70 | && !trimmed.starts_with('[') |
| 71 | && !trimmed.starts_with('(') |
| 72 | && !trimmed.starts_with("new ") |
| 73 | && !trimmed.starts_with("clone ") |
| 74 | && !trimmed.starts_with("fn(") |
| 75 | && !trimmed.starts_with("fn (") |
| 76 | && !trimmed.starts_with("function") |
| 77 | && !trimmed.starts_with("match") |
| 78 | && !trimmed.starts_with("yield") |
| 79 | && !trimmed.starts_with("throw") |
| 80 | && !trimmed.starts_with('!') |
| 81 | && !trimmed.starts_with('-') |
| 82 | && !trimmed.starts_with('~') |
| 83 | && !trimmed.starts_with('\\') |
| 84 | && !trimmed.starts_with("self::") |
| 85 | && !trimmed.starts_with("static::") |
| 86 | && !trimmed.starts_with("parent::") |
| 87 | { |
| 88 | // Could be a numeric literal (0, 1.5, 0x1F, etc.), a constant |
| 89 | // (true/false/null/CONST), or a function/static-method call. |
| 90 | // Allow those through if they look like a call or known keyword. |
| 91 | let first_char = trimmed.as_bytes()[0]; |
| 92 | let is_numeric = first_char.is_ascii_digit(); |
| 93 | let is_keyword = matches!( |
| 94 | trimmed, |
| 95 | "true" | "false" | "null" | "self" | "static" | "parent" |
| 96 | ); |
| 97 | // Allow `ClassName::method(...)`, `func(...)`, `CONST_NAME`. |
| 98 | let has_call_parens = trimmed.contains('('); |
| 99 | let has_double_colon = trimmed.contains("::"); |
| 100 | let is_all_upper_const = trimmed.chars().all(|c| c.is_ascii_uppercase() || c == '_'); |
| 101 | |
| 102 | if !is_numeric |
| 103 | && !is_keyword |
| 104 | && !has_call_parens |
| 105 | && !has_double_colon |
| 106 | && !is_all_upper_const |
| 107 | { |
| 108 | return false; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // Reject selections that contain a semicolon in a non-trailing |
| 113 | // position — this indicates multiple statements (e.g. |
no test coverage detected