Skip to content

Commit 12a3d78

Browse files
committed
Refactor
1 parent 69dddfd commit 12a3d78

35 files changed

Lines changed: 1902 additions & 1935 deletions

docs/ARCHITECTURE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ src/
3333
│ └── ast_update.rs # update_ast orchestrator and name resolution helpers
3434
├── docblock/
3535
│ ├── mod.rs # Re-exports from submodules
36-
│ ├── tags.rs # PHPDoc tag extraction (@return, @var, @property, @mixin, …)
36+
│ ├── tags.rs # PHPDoc tag extraction (@return, @var, @mixin, @deprecated, …)
37+
│ ├── templates.rs # Template/generics/type-alias tags (@template, @extends, …)
38+
│ ├── virtual_members.rs # Virtual member tags (@property, @method)
3739
│ ├── conditional.rs # PHPStan conditional return type parsing
3840
│ └── types.rs # Type cleaning utilities (clean_type, strip_nullable, …)
3941
├── completion/
@@ -53,6 +55,7 @@ src/
5355
│ ├── array_shape.rs # Array shape key completion and raw variable type resolution
5456
│ ├── named_args.rs # Named argument completion inside function/method call parens
5557
│ ├── phpdoc.rs # PHPDoc tag completion inside /** … */ blocks
58+
│ ├── phpdoc_context.rs # PHPDoc context detection and symbol info extraction
5659
│ ├── comment_position.rs # Comment and docblock position detection
5760
│ ├── throws_analysis.rs # Shared throw-statement scanning and @throws tag lookup
5861
│ ├── catch_completion.rs # Smart exception type completion inside catch() clauses

src/completion/array_shape.rs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use tower_lsp::lsp_types::*;
3131

3232
use crate::Backend;
3333
use crate::docblock;
34-
use crate::types::ClassInfo;
34+
use crate::types::FileContext;
3535
use crate::util::find_semicolon_balanced;
3636

3737
/// Well-known keys for the `$_SERVER` superglobal.
@@ -258,16 +258,14 @@ impl Backend {
258258
ctx: &ArrayKeyContext,
259259
content: &str,
260260
position: Position,
261-
classes: &[ClassInfo],
262-
file_use_map: &std::collections::HashMap<String, String>,
263-
file_namespace: &Option<String>,
261+
file_ctx: &FileContext,
264262
) -> Vec<CompletionItem> {
265263
// ── $_SERVER superglobal — hardcoded keys ────────────────────
266264
if ctx.var_name == "$_SERVER" && ctx.prefix_keys.is_empty() {
267265
return self.build_server_key_completions(ctx, content, position);
268266
}
269267

270-
let cursor_offset = Self::position_to_offset(content, position).unwrap_or(0);
268+
let cursor_offset = Self::position_to_offset(content, position);
271269

272270
// Try to find the raw type annotation for this variable.
273271
// We also track which set of classes was used for resolution so
@@ -277,9 +275,7 @@ impl Backend {
277275
&ctx.var_name,
278276
content,
279277
cursor_offset as usize,
280-
classes,
281-
file_use_map,
282-
file_namespace,
278+
file_ctx,
283279
);
284280

285281
// If initial resolution failed, the content likely has a syntax
@@ -288,21 +284,24 @@ impl Backend {
288284
// the array access, re-parse, and retry.
289285
let patched_classes_storage;
290286
let (raw_type, effective_classes) = match raw_type {
291-
Some(t) => (t, classes),
287+
Some(t) => (t, file_ctx.classes.as_slice()),
292288
None => {
293289
let patched = patch_array_access_at_cursor(content, position);
294290
if patched == content {
295291
return vec![];
296292
}
297293
patched_classes_storage = self.parse_php(&patched);
298-
let patched_offset = Self::position_to_offset(&patched, position).unwrap_or(0);
294+
let patched_offset = Self::position_to_offset(&patched, position);
295+
let patched_ctx = FileContext {
296+
classes: patched_classes_storage.clone(),
297+
use_map: file_ctx.use_map.clone(),
298+
namespace: file_ctx.namespace.clone(),
299+
};
299300
match self.resolve_variable_raw_type(
300301
&ctx.var_name,
301302
&patched,
302303
patched_offset as usize,
303-
&patched_classes_storage,
304-
file_use_map,
305-
file_namespace,
304+
&patched_ctx,
306305
) {
307306
Some(t) => (t, patched_classes_storage.as_slice()),
308307
None => return vec![],
@@ -323,7 +322,8 @@ impl Backend {
323322
// resolves to `array{name: string, email: string}`.
324323
// Uses `effective_classes` which may be the patched classes when
325324
// the original parse failed due to syntax errors.
326-
let class_loader = self.class_loader_with(effective_classes, file_use_map, file_namespace);
325+
let class_loader =
326+
self.class_loader_with(effective_classes, &file_ctx.use_map, &file_ctx.namespace);
327327
let effective_type =
328328
Self::resolve_type_alias(&effective_type, "", effective_classes, &class_loader)
329329
.unwrap_or(effective_type);
@@ -494,9 +494,7 @@ impl Backend {
494494
var_name: &str,
495495
content: &str,
496496
cursor_offset: usize,
497-
classes: &[ClassInfo],
498-
file_use_map: &std::collections::HashMap<String, String>,
499-
file_namespace: &Option<String>,
497+
file_ctx: &FileContext,
500498
) -> Option<String> {
501499
// 1. Direct @var / @param annotation on the variable.
502500
if let Some(raw) =
@@ -508,14 +506,14 @@ impl Backend {
508506
// 2. Delegate to the shared text-based assignment resolver which
509507
// handles array literals, method/function calls, chained calls,
510508
// `new` expressions, array functions, and property access.
511-
let current_class = Self::find_class_at_offset(classes, cursor_offset as u32);
512-
let class_loader = self.class_loader_with(classes, file_use_map, file_namespace);
509+
let current_class = Self::find_class_at_offset(&file_ctx.classes, cursor_offset as u32);
510+
let class_loader = self.class_loader(file_ctx);
513511
Self::extract_raw_type_from_assignment_text(
514512
var_name,
515513
content,
516514
cursor_offset,
517515
current_class,
518-
classes,
516+
&file_ctx.classes,
519517
&class_loader,
520518
)
521519
}

src/completion/catch_completion.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ pub struct CatchContext {
2626
pub partial: String,
2727
/// Exception type names found in the corresponding try block.
2828
pub suggested_types: Vec<String>,
29-
/// Types already listed in this catch clause (for multi-catch `|`).
30-
#[allow(dead_code)]
31-
pub already_listed: Vec<String>,
3229
/// Whether specific thrown types were discovered in the try block.
3330
/// When `false`, the caller should fall back to generic class
3431
/// completion instead of showing only `Throwable`.
@@ -156,7 +153,6 @@ pub fn detect_catch_context(content: &str, position: Position) -> Option<CatchCo
156153
Some(CatchContext {
157154
partial,
158155
suggested_types,
159-
already_listed,
160156
has_specific_types,
161157
})
162158
}
@@ -722,7 +718,6 @@ mod tests {
722718
"RuntimeException".to_string(),
723719
"InvalidArgumentException".to_string(),
724720
],
725-
already_listed: vec![],
726721
has_specific_types: true,
727722
};
728723
let items = build_catch_completions(&ctx);
@@ -738,7 +733,6 @@ mod tests {
738733
"RuntimeException".to_string(),
739734
"InvalidArgumentException".to_string(),
740735
],
741-
already_listed: vec![],
742736
has_specific_types: true,
743737
};
744738
let items = build_catch_completions(&ctx);
@@ -762,7 +756,6 @@ mod tests {
762756
let ctx = detect_catch_context(content, pos);
763757
assert!(ctx.is_some());
764758
let ctx = ctx.unwrap();
765-
assert_eq!(ctx.already_listed, vec!["IOException"]);
766759
// IOException should be filtered out since it's already listed
767760
assert!(
768761
!ctx.suggested_types.contains(&"IOException".to_string()),

src/completion/class_completion.rs

Lines changed: 11 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ impl Backend {
114114
/// identifier (at the cursor) is `throw new` (with optional
115115
/// whitespace). This tells the handler to restrict completion to
116116
/// Throwable descendants only and skip constants / functions.
117-
pub fn is_throw_new_context(content: &str, position: Position) -> bool {
117+
pub(crate) fn is_throw_new_context(content: &str, position: Position) -> bool {
118118
let lines: Vec<&str> = content.lines().collect();
119119
if position.line as usize >= lines.len() {
120120
return false;
@@ -243,6 +243,8 @@ impl Backend {
243243
/// re-request as the user types more characters.
244244
const MAX_CLASS_COMPLETIONS: usize = 100;
245245

246+
/// Build completion items for class, interface, trait, and enum names
247+
/// matching `prefix`.
246248
pub(crate) fn build_class_name_completions(
247249
&self,
248250
file_use_map: &HashMap<String, String>,
@@ -506,38 +508,7 @@ impl Backend {
506508
}
507509

508510
// Look up ClassInfo from ast_map (no disk I/O).
509-
let last_segment = short_name(normalized);
510-
let expected_ns: Option<&str> = if normalized.contains('\\') {
511-
Some(&normalized[..normalized.len() - last_segment.len() - 1])
512-
} else {
513-
None
514-
};
515-
516-
let cls = {
517-
let Some(map) = self.ast_map.lock().ok() else {
518-
return false;
519-
};
520-
let nmap = self.namespace_map.lock().ok();
521-
let mut found: Option<ClassInfo> = None;
522-
for (uri, classes) in map.iter() {
523-
if let Some(c) = classes.iter().find(|c| c.name == last_segment) {
524-
if let Some(exp_ns) = expected_ns {
525-
let file_ns = nmap
526-
.as_ref()
527-
.and_then(|nm| nm.get(uri))
528-
.and_then(|opt| opt.as_deref());
529-
if file_ns != Some(exp_ns) {
530-
continue;
531-
}
532-
}
533-
found = Some(c.clone());
534-
break;
535-
}
536-
}
537-
found
538-
};
539-
540-
match cls {
511+
match self.find_class_in_ast_map(class_name) {
541512
Some(ci) => match &ci.parent_class {
542513
Some(parent) => self.is_throwable_descendant(parent, depth + 1),
543514
None => false, // no parent, not a Throwable type
@@ -557,34 +528,10 @@ impl Backend {
557528
/// interface, trait, enum, or abstract class. This never triggers
558529
/// disk I/O.
559530
fn is_instantiable_or_unloaded(&self, class_name: &str) -> bool {
560-
let normalized = class_name.strip_prefix('\\').unwrap_or(class_name);
561-
let last_segment = short_name(normalized);
562-
let expected_ns: Option<&str> = if normalized.contains('\\') {
563-
Some(&normalized[..normalized.len() - last_segment.len() - 1])
564-
} else {
565-
None
566-
};
567-
568-
let Some(map) = self.ast_map.lock().ok() else {
569-
return true;
570-
};
571-
let nmap = self.namespace_map.lock().ok();
572-
for (uri, classes) in map.iter() {
573-
if let Some(c) = classes.iter().find(|c| c.name == last_segment) {
574-
if let Some(exp_ns) = expected_ns {
575-
let file_ns = nmap
576-
.as_ref()
577-
.and_then(|nm| nm.get(uri))
578-
.and_then(|opt| opt.as_deref());
579-
if file_ns != Some(exp_ns) {
580-
continue;
581-
}
582-
}
583-
return c.kind == ClassLikeKind::Class && !c.is_abstract;
584-
}
531+
match self.find_class_in_ast_map(class_name) {
532+
Some(c) => c.kind == ClassLikeKind::Class && !c.is_abstract,
533+
None => true, // not found in ast_map — unloaded, so allow it
585534
}
586-
// Not found in ast_map — unloaded, so allow it.
587-
true
588535
}
589536

590537
/// Check whether the class identified by `class_name` is a concrete,
@@ -595,33 +542,8 @@ impl Backend {
595542
/// and classes that are not currently loaded. This never triggers
596543
/// disk I/O.
597544
fn is_concrete_class_in_ast_map(&self, class_name: &str) -> bool {
598-
let normalized = class_name.strip_prefix('\\').unwrap_or(class_name);
599-
let last_segment = short_name(normalized);
600-
let expected_ns: Option<&str> = if normalized.contains('\\') {
601-
Some(&normalized[..normalized.len() - last_segment.len() - 1])
602-
} else {
603-
None
604-
};
605-
606-
let Some(map) = self.ast_map.lock().ok() else {
607-
return false;
608-
};
609-
let nmap = self.namespace_map.lock().ok();
610-
for (uri, classes) in map.iter() {
611-
if let Some(c) = classes.iter().find(|c| c.name == last_segment) {
612-
if let Some(exp_ns) = expected_ns {
613-
let file_ns = nmap
614-
.as_ref()
615-
.and_then(|nm| nm.get(uri))
616-
.and_then(|opt| opt.as_deref());
617-
if file_ns != Some(exp_ns) {
618-
continue;
619-
}
620-
}
621-
return c.kind == ClassLikeKind::Class && !c.is_abstract;
622-
}
623-
}
624-
false
545+
self.find_class_in_ast_map(class_name)
546+
.is_some_and(|c| c.kind == ClassLikeKind::Class && !c.is_abstract)
625547
}
626548

627549
/// Collect the FQN of every class that is currently loaded in the
@@ -923,6 +845,7 @@ impl Backend {
923845
/// is truncated and `is_incomplete` is `true`.
924846
const MAX_CONSTANT_COMPLETIONS: usize = 100;
925847

848+
/// Build completion items for global constants matching `prefix`.
926849
pub(crate) fn build_constant_completions(&self, prefix: &str) -> (Vec<CompletionItem>, bool) {
927850
let prefix_lower = prefix.strip_prefix('\\').unwrap_or(prefix).to_lowercase();
928851
let mut seen: HashSet<String> = HashSet::new();
@@ -1032,6 +955,7 @@ impl Backend {
1032955
/// is truncated and `is_incomplete` is `true`.
1033956
const MAX_FUNCTION_COMPLETIONS: usize = 100;
1034957

958+
/// Build completion items for global functions matching `prefix`.
1035959
pub(crate) fn build_function_completions(&self, prefix: &str) -> (Vec<CompletionItem>, bool) {
1036960
let prefix_lower = prefix.strip_prefix('\\').unwrap_or(prefix).to_lowercase();
1037961
let mut seen: HashSet<String> = HashSet::new();

src/completion/comment_position.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@ pub fn is_inside_docblock(content: &str, position: Position) -> bool {
2727
let before_cursor = &content[..byte_offset.min(content.len())];
2828

2929
// Find the last `/**` before the cursor
30-
let last_open = before_cursor.rfind("/**");
31-
if last_open.is_none() {
30+
let Some(open_pos) = before_cursor.rfind("/**") else {
3231
return false;
33-
}
34-
let open_pos = last_open.unwrap();
32+
};
3533

3634
// Check if there is a `*/` between the `/**` and the cursor
3735
// (which would mean the docblock is closed)

0 commit comments

Comments
 (0)