Skip to content

Commit 2783641

Browse files
committed
Refactor throws analysis to use PhpType for exception types
1 parent 85277cd commit 2783641

9 files changed

Lines changed: 241 additions & 136 deletions

File tree

src/code_actions/update_docblock.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,8 @@ fn check_needs_update(
675675
.map(|t| short_name(t).to_lowercase())
676676
.collect();
677677
for exc in &uncaught {
678-
let short = short_name(exc);
678+
let exc_str = exc.to_string();
679+
let short = short_name(&exc_str);
679680
if !existing_lower.contains(&short.to_lowercase()) {
680681
return true;
681682
}
@@ -883,7 +884,8 @@ fn build_updated_docblock(
883884

884885
let mut new_throws: Vec<String> = Vec::new();
885886
for exc in &uncaught {
886-
let short = short_name(exc);
887+
let exc_str = exc.to_string();
888+
let short = short_name(&exc_str);
887889
if !existing_throws_lower.contains(&short.to_lowercase()) {
888890
new_throws.push(short.to_string());
889891
}

src/completion/context/catch_completion.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@ pub(crate) fn detect_catch_context(content: &str, position: Position) -> Option<
9898
// 1. Direct `throw new ExceptionType(…)` statements
9999
let throws = throws_analysis::find_throw_statements(&try_body);
100100
for throw in &throws {
101-
let short_name = throw
102-
.type_name
101+
let raw = throw.type_name.to_string();
102+
let short_name = raw
103103
.trim_start_matches('\\')
104104
.rsplit('\\')
105105
.next()
106-
.unwrap_or(&throw.type_name);
106+
.unwrap_or(&raw);
107107
if !short_name.is_empty() && seen.insert(short_name.to_lowercase()) {
108108
suggested_types.push(short_name.to_string());
109109
}
@@ -112,20 +112,20 @@ pub(crate) fn detect_catch_context(content: &str, position: Position) -> Option<
112112
// 2. Inline `/** @throws ExceptionType */` annotations
113113
let inline_throws = throws_analysis::find_inline_throws_annotations(&try_body);
114114
for info in &inline_throws {
115-
let short_name = info
116-
.type_name
115+
let raw = info.type_name.to_string();
116+
let short_name = raw
117117
.trim_start_matches('\\')
118118
.rsplit('\\')
119119
.next()
120-
.unwrap_or(&info.type_name);
120+
.unwrap_or(&raw);
121121
if !short_name.is_empty() && seen.insert(short_name.to_lowercase()) {
122122
suggested_types.push(short_name.to_string());
123123
}
124124
}
125125

126126
// 3. Propagated @throws from called methods
127127
let propagated = throws_analysis::find_propagated_throws(&try_body, content);
128-
let propagated: Vec<String> = propagated.iter().map(|t| t.type_name.clone()).collect();
128+
let propagated: Vec<String> = propagated.iter().map(|t| t.type_name.to_string()).collect();
129129
for exc_type in &propagated {
130130
let short_name = exc_type
131131
.trim_start_matches('\\')
@@ -141,7 +141,7 @@ pub(crate) fn detect_catch_context(content: &str, position: Position) -> Option<
141141
let throw_expr_types = throws_analysis::find_throw_expression_types(&try_body, content);
142142
let throw_expr_types: Vec<String> = throw_expr_types
143143
.iter()
144-
.map(|t| t.type_name.clone())
144+
.map(|t| t.type_name.to_string())
145145
.collect();
146146
for exc_type in &throw_expr_types {
147147
let short_name = exc_type

src/completion/context/catch_completion_tests.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ fn test_find_method_throws_tags_with_private() {
1111
"}\n",
1212
);
1313
let result = throws_analysis::find_method_throws_tags(content, "riskyOperation");
14+
let names: Vec<String> = result.iter().map(|t| t.to_string()).collect();
1415
assert_eq!(
15-
result,
16+
names,
1617
vec!["ValidationException"],
1718
"Should find @throws through 'private' modifier"
1819
);
@@ -28,8 +29,9 @@ fn test_find_method_throws_tags_with_protected_static() {
2829
"}\n",
2930
);
3031
let result = throws_analysis::find_method_throws_tags(content, "dangerousCall");
32+
let names: Vec<String> = result.iter().map(|t| t.to_string()).collect();
3133
assert_eq!(
32-
result,
34+
names,
3335
vec!["RuntimeException"],
3436
"Should find @throws through 'protected static' modifiers"
3537
);
@@ -43,8 +45,9 @@ fn test_find_method_throws_tags_without_modifier() {
4345
"function standalone(): void {}\n",
4446
);
4547
let result = throws_analysis::find_method_throws_tags(content, "standalone");
48+
let names: Vec<String> = result.iter().map(|t| t.to_string()).collect();
4649
assert_eq!(
47-
result,
50+
names,
4851
vec!["LogicException"],
4952
"Should find @throws on a standalone function (no modifier)"
5053
);
@@ -136,7 +139,7 @@ fn test_find_inline_throws_annotations_in_catch() {
136139
"#;
137140
let result = throws_analysis::find_inline_throws_annotations(body);
138141
// Raw names are returned; short-name extraction happens in detect_catch_context
139-
let names: Vec<&str> = result.iter().map(|t| t.type_name.as_str()).collect();
142+
let names: Vec<String> = result.iter().map(|t| t.type_name.to_string()).collect();
140143
assert_eq!(
141144
names,
142145
vec!["ModelNotFoundException", "App\\Exceptions\\AuthException"]
@@ -152,7 +155,7 @@ fn test_find_inline_throws_multiline_docblock_in_catch() {
152155
doStuff();
153156
"#;
154157
let result = throws_analysis::find_inline_throws_annotations(body);
155-
let names: Vec<&str> = result.iter().map(|t| t.type_name.as_str()).collect();
158+
let names: Vec<String> = result.iter().map(|t| t.type_name.to_string()).collect();
156159
assert_eq!(names, vec!["RuntimeException"]);
157160
}
158161

src/completion/handler.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,7 @@ impl Backend {
327327
// Named arg items are always valid alongside normal
328328
// completions, so collect them here and merge them into
329329
// whatever strategy wins below.
330-
let named_arg_items =
331-
self.collect_named_arg_items(&uri, &content, position, &ctx);
330+
let named_arg_items = self.collect_named_arg_items(&uri, &content, position, &ctx);
332331

333332
// ── String context detection ────────────────────────────
334333
// Classify once and use throughout the remaining pipeline.
@@ -397,7 +396,10 @@ impl Backend {
397396

398397
// ── Smart catch clause completion ───────────────────────
399398
if let Some(response) = self.try_catch_completion(&content, position, &ctx, &uri) {
400-
return Ok(Some(merge_named_args_into_response(response, named_arg_items)));
399+
return Ok(Some(merge_named_args_into_response(
400+
response,
401+
named_arg_items,
402+
)));
401403
}
402404

403405
// ── `throw new` completion ──────────────────────────────
@@ -417,7 +419,10 @@ impl Backend {
417419
if let Some(response) =
418420
self.try_class_constant_function_completion(&content, position, &ctx, &uri)
419421
{
420-
return Ok(Some(merge_named_args_into_response(response, named_arg_items)));
422+
return Ok(Some(merge_named_args_into_response(
423+
response,
424+
named_arg_items,
425+
)));
421426
}
422427

423428
// No strategy matched, but we may still have named arg items.
@@ -726,9 +731,8 @@ impl Backend {
726731
// from incomplete code).
727732
let na_ctx = match self
728733
.detect_named_arg_from_symbol_map(uri, content, position)
729-
.or_else(|| {
730-
crate::completion::named_args::detect_named_arg_context(content, position)
731-
}) {
734+
.or_else(|| crate::completion::named_args::detect_named_arg_context(content, position))
735+
{
732736
Some(ctx) => ctx,
733737
None => return Vec::new(),
734738
};
@@ -784,8 +788,11 @@ impl Backend {
784788
// expression that is itself an argument, named-arg completion
785789
// for the outer call must not fire — the user wants normal
786790
// value completion, not parameter names.
787-
if cursor_inside_nested_bracket(content, cs.args_start as usize, cursor_byte_offset as usize)
788-
{
791+
if cursor_inside_nested_bracket(
792+
content,
793+
cs.args_start as usize,
794+
cursor_byte_offset as usize,
795+
) {
789796
return None;
790797
}
791798

src/completion/phpdoc/generation.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1820,7 +1820,8 @@ fn build_throws_import_edits(
18201820
let mut edits = Vec::new();
18211821

18221822
for exc in &uncaught {
1823-
if let Some(fqn) = throws_analysis::resolve_exception_fqn(exc, use_map, file_namespace)
1823+
let exc_str = exc.to_string();
1824+
if let Some(fqn) = throws_analysis::resolve_exception_fqn(&exc_str, use_map, file_namespace)
18241825
&& !throws_analysis::has_use_import(content, &fqn)
18251826
&& let Some(edit) = build_use_edit(&fqn, &use_block, file_namespace)
18261827
{

src/completion/phpdoc/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,8 +577,9 @@ pub fn build_phpdoc_completions(
577577
let existing_throws = find_existing_throws_tags(content, position);
578578

579579
// Filter out already-documented throws
580-
let missing: Vec<&String> = uncaught
580+
let missing: Vec<String> = uncaught
581581
.iter()
582+
.map(|t| t.to_string())
582583
.filter(|t| {
583584
!existing_throws
584585
.iter()
@@ -596,7 +597,7 @@ pub fn build_phpdoc_completions(
596597
// Build an auto-import edit if the exception type
597598
// isn't already imported.
598599
let additional_edits = throws_analysis::resolve_exception_fqn(
599-
exc_type,
600+
exc_type.as_str(),
600601
use_map,
601602
file_namespace,
602603
)

src/completion/resolver.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ pub(crate) enum SubjectOutcome {
10471047
/// (e.g. `int`, `string`, `bool|int`) with null stripped.
10481048
Scalar(PhpType),
10491049
/// Subject resolved to a class name that couldn't be loaded.
1050-
UnresolvableClass(String),
1050+
UnresolvableClass(PhpType),
10511051
/// Subject type could not be resolved — no class information
10521052
/// available.
10531053
Untyped,
@@ -1272,7 +1272,7 @@ fn resolve_call_scalar_return(
12721272
fn check_unresolvable_class_name(
12731273
raw_type: &PhpType,
12741274
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
1275-
) -> Option<String> {
1275+
) -> Option<PhpType> {
12761276
if raw_type.all_members_scalar() {
12771277
return None;
12781278
}
@@ -1281,7 +1281,7 @@ fn check_unresolvable_class_name(
12811281
let base = effective.base_name()?;
12821282

12831283
if class_loader(base).is_none() {
1284-
Some(base.to_string())
1284+
Some(PhpType::Named(base.to_string()))
12851285
} else {
12861286
None
12871287
}

0 commit comments

Comments
 (0)