Skip to content

Commit 8d14adc

Browse files
committed
Fix crash when in variable resolver
1 parent a807327 commit 8d14adc

5 files changed

Lines changed: 267 additions & 73 deletions

File tree

src/completion/variable_completion.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/// position, respecting PHP scoping rules (function, method, closure, and
66
/// top-level scope).
77
use std::collections::HashSet;
8+
use std::panic;
89

910
use bumpalo::Bump;
1011
use mago_span::HasSpan;
@@ -205,13 +206,25 @@ impl Backend {
205206
/// The returned set contains variable names including the `$` prefix
206207
/// (e.g. `"$user"`, `"$this"`).
207208
fn collect_variables_in_scope(content: &str, cursor_offset: u32) -> HashSet<String> {
208-
let arena = Bump::new();
209-
let file_id = mago_database::file::FileId::new("input.php");
210-
let program = parse_file_content(&arena, file_id, content);
211-
212-
let mut vars = HashSet::new();
213-
find_scope_and_collect(program.statements.iter(), cursor_offset, &mut vars);
214-
vars
209+
// Wrap in catch_unwind so a mago-syntax parser panic doesn't
210+
// crash the LSP server (producing a zombie process).
211+
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
212+
let arena = Bump::new();
213+
let file_id = mago_database::file::FileId::new("input.php");
214+
let program = parse_file_content(&arena, file_id, content);
215+
216+
let mut vars = HashSet::new();
217+
find_scope_and_collect(program.statements.iter(), cursor_offset, &mut vars);
218+
vars
219+
}));
220+
221+
match result {
222+
Ok(vars) => vars,
223+
Err(_) => {
224+
log::error!("PHPantomLSP: parser panicked during variable scope collection");
225+
HashSet::new()
226+
}
227+
}
215228
}
216229

217230
/// Walk top-level statements to find the scope enclosing the cursor,

src/completion/variable_resolution.rs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
/// Type narrowing (instanceof, assert, custom type guards) is delegated
3030
/// to the [`super::type_narrowing`] module. Closure/arrow-function scope
3131
/// handling is delegated to [`super::closure_resolution`].
32+
use std::panic;
33+
3234
use bumpalo::Bump;
3335
use mago_span::HasSpan;
3436
use mago_syntax::ast::*;
@@ -61,23 +63,38 @@ impl Backend {
6163
class_loader: &dyn Fn(&str) -> Option<ClassInfo>,
6264
function_loader: FunctionLoaderFn<'_>,
6365
) -> Vec<ClassInfo> {
64-
let arena = Bump::new();
65-
let file_id = mago_database::file::FileId::new("input.php");
66-
let program = parse_file_content(&arena, file_id, content);
67-
68-
let ctx = VarResolutionCtx {
69-
var_name,
70-
current_class,
71-
all_classes,
72-
content,
73-
cursor_offset,
74-
class_loader,
75-
function_loader,
76-
};
66+
// Wrap in catch_unwind so a mago-syntax parser panic doesn't
67+
// crash the LSP server (producing a zombie process).
68+
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
69+
let arena = Bump::new();
70+
let file_id = mago_database::file::FileId::new("input.php");
71+
let program = parse_file_content(&arena, file_id, content);
72+
73+
let ctx = VarResolutionCtx {
74+
var_name,
75+
current_class,
76+
all_classes,
77+
content,
78+
cursor_offset,
79+
class_loader,
80+
function_loader,
81+
};
7782

78-
// Walk top-level (and namespace-nested) statements to find the
79-
// class + method containing the cursor.
80-
Self::resolve_variable_in_statements(program.statements.iter(), &ctx)
83+
// Walk top-level (and namespace-nested) statements to find the
84+
// class + method containing the cursor.
85+
Self::resolve_variable_in_statements(program.statements.iter(), &ctx)
86+
}));
87+
88+
match result {
89+
Ok(classes) => classes,
90+
Err(_) => {
91+
log::error!(
92+
"PHPantomLSP: parser panicked during variable resolution for '{}'",
93+
var_name
94+
);
95+
vec![]
96+
}
97+
}
8198
}
8299

83100
pub(super) fn resolve_variable_in_statements<'b>(
@@ -1398,7 +1415,25 @@ impl Backend {
13981415
}
13991416

14001417
// Delegate all RHS resolution to the shared helper.
1401-
let resolved = Self::resolve_rhs_expression(assignment.rhs, ctx);
1418+
//
1419+
// Use the assignment's own start offset as cursor_offset so
1420+
// that any recursive variable resolution only considers
1421+
// assignments *before* this one. Without this, a
1422+
// self-referential assignment like `$value = $value->value`
1423+
// would infinitely recurse: resolving the RHS `$value`
1424+
// would re-discover the same assignment, resolve its RHS
1425+
// again, and so on until a stack overflow crashes the
1426+
// process.
1427+
let rhs_ctx = VarResolutionCtx {
1428+
var_name: ctx.var_name,
1429+
current_class: ctx.current_class,
1430+
all_classes: ctx.all_classes,
1431+
content: ctx.content,
1432+
cursor_offset: assignment.span().start.offset,
1433+
class_loader: ctx.class_loader,
1434+
function_loader: ctx.function_loader,
1435+
};
1436+
let resolved = Self::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
14021437
push_results(results, resolved, conditional);
14031438
}
14041439
}

src/parser/ast_update.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/// (`resolve_parent_class_names`, `resolve_name`) used to convert short
88
/// class names to fully-qualified names.
99
use std::collections::HashMap;
10+
use std::panic;
1011

1112
use crate::docblock::types::is_scalar;
1213

@@ -24,6 +25,34 @@ impl Backend {
2425
/// Update the ast_map, use_map, and namespace_map for a given file URI
2526
/// by parsing its content.
2627
pub fn update_ast(&self, uri: &str, content: &str) {
28+
// The mago-syntax parser contains `unreachable!()` and `.expect()`
29+
// calls that can panic on malformed PHP (e.g. partially-written
30+
// heredocs/nowdocs, which are common while editing). Wrap the
31+
// entire parse + extraction in `catch_unwind` so a parser panic
32+
// doesn't crash the LSP server and produce a zombie process.
33+
//
34+
// On panic the file is simply skipped — no maps are updated, and
35+
// the user gets stale (but not missing) completions until the
36+
// file is saved in a parseable state.
37+
let content_owned = content.to_string();
38+
let uri_owned = uri.to_string();
39+
40+
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
41+
self.update_ast_inner(&uri_owned, &content_owned);
42+
}));
43+
44+
if result.is_err() {
45+
log::error!(
46+
"PHPantomLSP: parser panicked while parsing {}. Skipping file.",
47+
uri
48+
);
49+
}
50+
}
51+
52+
/// Inner implementation of [`update_ast`] that performs the actual
53+
/// parsing and map updates. Separated so that [`update_ast`] can
54+
/// wrap the call in [`std::panic::catch_unwind`].
55+
fn update_ast_inner(&self, uri: &str, content: &str) {
2756
let arena = Bump::new();
2857
let file_id = mago_database::file::FileId::new("input.php");
2958
let program = parse_file_content(&arena, file_id, content);

src/parser/mod.rs

Lines changed: 104 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -151,46 +151,68 @@ impl Backend {
151151
/// Parse PHP source text and extract class information.
152152
/// Returns a Vec of ClassInfo for all classes found in the file.
153153
pub fn parse_php(&self, content: &str) -> Vec<ClassInfo> {
154-
let arena = bumpalo::Bump::new();
155-
let file_id = mago_database::file::FileId::new("input.php");
156-
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
157-
158-
let doc_ctx = DocblockCtx {
159-
trivias: program.trivia.as_slice(),
160-
content,
161-
};
162-
163-
let mut classes = Vec::new();
164-
Self::extract_classes_from_statements(
165-
program.statements.iter(),
166-
&mut classes,
167-
Some(&doc_ctx),
168-
);
169-
classes
154+
let content_owned = content.to_string();
155+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
156+
let arena = bumpalo::Bump::new();
157+
let file_id = mago_database::file::FileId::new("input.php");
158+
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &content_owned);
159+
160+
let doc_ctx = DocblockCtx {
161+
trivias: program.trivia.as_slice(),
162+
content: &content_owned,
163+
};
164+
165+
let mut classes = Vec::new();
166+
Self::extract_classes_from_statements(
167+
program.statements.iter(),
168+
&mut classes,
169+
Some(&doc_ctx),
170+
);
171+
classes
172+
}));
173+
174+
match result {
175+
Ok(classes) => classes,
176+
Err(_) => {
177+
log::error!("PHPantomLSP: parser panicked in parse_php");
178+
Vec::new()
179+
}
180+
}
170181
}
171182

172183
/// Parse PHP source text and extract standalone function definitions.
173184
///
174185
/// Returns a list of `FunctionInfo` for every `function` declaration
175186
/// found at the top level (or inside a namespace block).
176187
pub fn parse_functions(&self, content: &str) -> Vec<FunctionInfo> {
177-
let arena = bumpalo::Bump::new();
178-
let file_id = mago_database::file::FileId::new("input.php");
179-
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
180-
181-
let doc_ctx = DocblockCtx {
182-
trivias: program.trivia.as_slice(),
183-
content,
184-
};
185-
186-
let mut functions = Vec::new();
187-
Self::extract_functions_from_statements(
188-
program.statements.iter(),
189-
&mut functions,
190-
&None,
191-
Some(&doc_ctx),
192-
);
193-
functions
188+
let content_owned = content.to_string();
189+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
190+
let arena = bumpalo::Bump::new();
191+
let file_id = mago_database::file::FileId::new("input.php");
192+
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &content_owned);
193+
194+
let doc_ctx = DocblockCtx {
195+
trivias: program.trivia.as_slice(),
196+
content: &content_owned,
197+
};
198+
199+
let mut functions = Vec::new();
200+
Self::extract_functions_from_statements(
201+
program.statements.iter(),
202+
&mut functions,
203+
&None,
204+
Some(&doc_ctx),
205+
);
206+
functions
207+
}));
208+
209+
match result {
210+
Ok(functions) => functions,
211+
Err(_) => {
212+
log::error!("PHPantomLSP: parser panicked in parse_functions");
213+
Vec::new()
214+
}
215+
}
194216
}
195217

196218
/// Parse PHP source text and extract constant names from `define()` calls.
@@ -199,13 +221,24 @@ impl Backend {
199221
/// call found at the top level, inside namespace blocks, block
200222
/// statements, or `if` guards.
201223
pub fn parse_defines(&self, content: &str) -> Vec<String> {
202-
let arena = bumpalo::Bump::new();
203-
let file_id = mago_database::file::FileId::new("input.php");
204-
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
224+
let content_owned = content.to_string();
225+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
226+
let arena = bumpalo::Bump::new();
227+
let file_id = mago_database::file::FileId::new("input.php");
228+
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &content_owned);
205229

206-
let mut defines = Vec::new();
207-
Self::extract_defines_from_statements(program.statements.iter(), &mut defines);
208-
defines
230+
let mut defines = Vec::new();
231+
Self::extract_defines_from_statements(program.statements.iter(), &mut defines);
232+
defines
233+
}));
234+
235+
match result {
236+
Ok(defines) => defines,
237+
Err(_) => {
238+
log::error!("PHPantomLSP: parser panicked in parse_defines");
239+
Vec::new()
240+
}
241+
}
209242
}
210243

211244
/// Parse PHP source text and extract `use` statement mappings.
@@ -224,24 +257,46 @@ impl Backend {
224257
/// (function / const imports are skipped — we only track classes)
225258
/// - Use statements inside namespace bodies
226259
pub fn parse_use_statements(&self, content: &str) -> std::collections::HashMap<String, String> {
227-
let arena = bumpalo::Bump::new();
228-
let file_id = mago_database::file::FileId::new("input.php");
229-
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
260+
let content_owned = content.to_string();
261+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
262+
let arena = bumpalo::Bump::new();
263+
let file_id = mago_database::file::FileId::new("input.php");
264+
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &content_owned);
230265

231-
let mut use_map = std::collections::HashMap::new();
232-
Self::extract_use_statements_from_statements(program.statements.iter(), &mut use_map);
233-
use_map
266+
let mut use_map = std::collections::HashMap::new();
267+
Self::extract_use_statements_from_statements(program.statements.iter(), &mut use_map);
268+
use_map
269+
}));
270+
271+
match result {
272+
Ok(use_map) => use_map,
273+
Err(_) => {
274+
log::error!("PHPantomLSP: parser panicked in parse_use_statements");
275+
std::collections::HashMap::new()
276+
}
277+
}
234278
}
235279

236280
/// Parse PHP source text and extract the declared namespace (if any).
237281
///
238282
/// Returns the namespace string (e.g. `"Klarna\Rest\Checkout"`) or
239283
/// `None` if the file has no namespace declaration.
240284
pub fn parse_namespace(&self, content: &str) -> Option<String> {
241-
let arena = bumpalo::Bump::new();
242-
let file_id = mago_database::file::FileId::new("input.php");
243-
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
285+
let content_owned = content.to_string();
286+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
287+
let arena = bumpalo::Bump::new();
288+
let file_id = mago_database::file::FileId::new("input.php");
289+
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &content_owned);
290+
291+
Self::extract_namespace_from_statements(program.statements.iter())
292+
}));
244293

245-
Self::extract_namespace_from_statements(program.statements.iter())
294+
match result {
295+
Ok(ns) => ns,
296+
Err(_) => {
297+
log::error!("PHPantomLSP: parser panicked in parse_namespace");
298+
None
299+
}
300+
}
246301
}
247302
}

0 commit comments

Comments
 (0)