1use super::{auto_import, context::CompletionContext, items::CompletionItem};
6use perl_semantic_analyzer::symbol::{SymbolKind, SymbolTable};
7use std::collections::HashSet;
8
9fn static_receiver_module(prefix: &str) -> Option<&str> {
15 let arrow = prefix.rfind("->")?;
16 let receiver = prefix[..arrow].trim();
17 if !receiver.starts_with('$')
19 && !receiver.starts_with('@')
20 && !receiver.starts_with('%')
21 && receiver.chars().next().is_some_and(|c| c.is_ascii_uppercase())
22 {
23 Some(receiver)
24 } else {
25 None
26 }
27}
28
29pub const DBI_DB_METHODS: &[(&str, &str)] = &[
31 ("do", "Execute a single SQL statement"),
32 ("prepare", "Prepare a SQL statement"),
33 ("prepare_cached", "Prepare and cache a SQL statement"),
34 ("selectrow_array", "Execute and fetch a single row as array"),
35 ("selectrow_arrayref", "Execute and fetch a single row as arrayref"),
36 ("selectrow_hashref", "Execute and fetch a single row as hashref"),
37 ("selectall_arrayref", "Execute and fetch all rows as arrayref"),
38 ("selectall_hashref", "Execute and fetch all rows as hashref"),
39 ("begin_work", "Begin a database transaction"),
40 ("commit", "Commit the current transaction"),
41 ("rollback", "Rollback the current transaction"),
42 ("disconnect", "Disconnect from the database"),
43 ("last_insert_id", "Get the last inserted row ID"),
44 ("quote", "Quote a string for SQL"),
45 ("quote_identifier", "Quote an identifier for SQL"),
46 ("ping", "Check if database connection is alive"),
47];
48
49pub const DBI_ST_METHODS: &[(&str, &str)] = &[
51 ("bind_param", "Bind a parameter to the statement"),
52 ("bind_param_inout", "Bind an in/out parameter"),
53 ("execute", "Execute the prepared statement"),
54 ("fetch", "Fetch the next row as arrayref"),
55 ("fetchrow_array", "Fetch the next row as array"),
56 ("fetchrow_arrayref", "Fetch the next row as arrayref"),
57 ("fetchrow_hashref", "Fetch the next row as hashref"),
58 ("fetchall_arrayref", "Fetch all remaining rows as arrayref"),
59 ("fetchall_hashref", "Fetch all remaining rows as hashref of hashrefs"),
60 ("finish", "Finish the statement handle"),
61 ("rows", "Get the number of rows affected"),
62];
63
64pub const DBI_DB_METHOD_SIGS: &[(&str, &str, &str)] = &[
68 ("do", "do($statement, \\@attr?, @bind_values?)", "Execute a single SQL statement"),
69 ("prepare", "prepare($statement, \\@attr?)", "Prepare a SQL statement for execution"),
70 (
71 "prepare_cached",
72 "prepare_cached($statement, \\@attr?, $if_active?)",
73 "Prepare and cache a SQL statement",
74 ),
75 (
76 "selectrow_array",
77 "selectrow_array($statement, \\@attr?, @bind)",
78 "Execute and return first row as list",
79 ),
80 (
81 "selectrow_arrayref",
82 "selectrow_arrayref($statement, \\@attr?, @bind)",
83 "Execute and return first row as arrayref",
84 ),
85 (
86 "selectrow_hashref",
87 "selectrow_hashref($statement, \\@attr?, @bind)",
88 "Execute and return first row as hashref",
89 ),
90 (
91 "selectall_arrayref",
92 "selectall_arrayref($statement, \\@attr?, @bind)",
93 "Execute and return all rows as arrayref",
94 ),
95 (
96 "selectall_hashref",
97 "selectall_hashref($statement, $key_field, \\@attr?, @bind)",
98 "Execute and return all rows as hashref",
99 ),
100 ("begin_work", "begin_work()", "Begin a database transaction"),
101 ("commit", "commit()", "Commit the current transaction"),
102 ("rollback", "rollback()", "Rollback the current transaction"),
103 ("disconnect", "disconnect()", "Disconnect from the database"),
104 (
105 "last_insert_id",
106 "last_insert_id($catalog, $schema, $table, $field, \\@attr?)",
107 "Get the last inserted row ID",
108 ),
109 ("quote", "quote($value, $data_type?)", "Quote a string value for use in SQL"),
110 ("quote_identifier", "quote_identifier($name)", "Quote an identifier for SQL"),
111 ("ping", "ping()", "Check if the database connection is still alive"),
112];
113
114pub const DBI_ST_METHOD_SIGS: &[(&str, &str, &str)] = &[
118 (
119 "bind_param",
120 "bind_param($param_num, $bind_value, \\@attr?)",
121 "Bind a value to a placeholder",
122 ),
123 (
124 "bind_param_inout",
125 "bind_param_inout($param_num, \\$bind_value, $max_len)",
126 "Bind an in/out parameter",
127 ),
128 ("execute", "execute(@bind_values?)", "Execute the prepared statement"),
129 ("fetch", "fetch()", "Fetch the next row as arrayref (alias for fetchrow_arrayref)"),
130 ("fetchrow_array", "fetchrow_array()", "Fetch the next row as a list"),
131 ("fetchrow_arrayref", "fetchrow_arrayref()", "Fetch the next row as an arrayref"),
132 ("fetchrow_hashref", "fetchrow_hashref($name?)", "Fetch the next row as a hashref"),
133 (
134 "fetchall_arrayref",
135 "fetchall_arrayref($slice?, $max_rows?)",
136 "Fetch all remaining rows as arrayref",
137 ),
138 (
139 "fetchall_hashref",
140 "fetchall_hashref($key_field)",
141 "Fetch all remaining rows as hashref of hashrefs",
142 ),
143 ("finish", "finish()", "Indicate no more rows will be fetched"),
144 ("rows", "rows()", "Return the number of rows affected or returned"),
145];
146
147pub fn get_dbi_method_documentation(
154 receiver_hint: &str,
155 method_name: &str,
156) -> Option<(&'static str, &'static str)> {
157 let is_db = receiver_hint.ends_with("dbh")
158 || receiver_hint.contains("DBI")
159 || receiver_hint.contains("connect");
160 let is_st = receiver_hint.ends_with("sth");
161
162 let table: &[(&str, &str, &str)] = if is_db {
163 DBI_DB_METHOD_SIGS
164 } else if is_st {
165 DBI_ST_METHOD_SIGS
166 } else {
167 if let Some(entry) = DBI_DB_METHOD_SIGS.iter().find(|(n, _, _)| *n == method_name) {
169 return Some((entry.1, entry.2));
170 }
171 DBI_ST_METHOD_SIGS
172 };
173
174 table.iter().find(|(n, _, _)| *n == method_name).map(|(_, sig, desc)| (*sig, *desc))
175}
176
177pub fn infer_receiver_type(context: &CompletionContext, source: &str) -> Option<String> {
179 let prefix = context.prefix.trim_end_matches("->");
181
182 if prefix.ends_with("$dbh") {
184 return Some("DBI::db".to_string());
185 }
186 if prefix.ends_with("$sth") {
187 return Some("DBI::st".to_string());
188 }
189
190 if let Some(var_pos) = source.rfind(prefix) {
192 let before_var = &source[..var_pos];
194 if let Some(assign_pos) = before_var.rfind('=') {
195 let assignment = &source[assign_pos..var_pos + prefix.len()];
196
197 if assignment.contains("DBI") && assignment.contains("connect") {
199 return Some("DBI::db".to_string());
200 }
201
202 if assignment.contains("prepare") {
204 return Some("DBI::st".to_string());
205 }
206 }
207 }
208
209 None
210}
211
212fn moo_accessor_documentation(name: &str, attributes: &[String]) -> String {
218 let mut isa_value: Option<&str> = None;
219 let mut is_value: Option<&str> = None;
220 let mut extra_parts: Vec<&str> = Vec::new();
221
222 for attr in attributes {
223 if let Some((key, value)) = attr.split_once('=') {
224 match key {
225 "isa" => isa_value = Some(value),
226 "is" => is_value = Some(value),
227 _ => extra_parts.push(attr),
228 }
229 }
230 }
231
232 let mut doc = format!("Moo/Moose accessor `{name}`");
233
234 if let Some(isa) = isa_value {
235 doc.push_str(&format!("\n\n**Type**: `{isa}`"));
236 }
237 if let Some(is) = is_value {
238 let mode = match is {
239 "ro" => "read-only",
240 "rw" => "read-write",
241 "rwp" => "read-write private",
242 "lazy" => "lazy",
243 other => other,
244 };
245 doc.push_str(&format!("\n\n**Access**: {mode}"));
246 }
247 if !extra_parts.is_empty() {
248 doc.push_str(&format!("\n\n**Options**: {}", extra_parts.join(", ")));
249 }
250
251 doc
252}
253
254pub fn add_method_completions(
256 completions: &mut Vec<CompletionItem>,
257 context: &CompletionContext,
258 source: &str,
259 symbol_table: &SymbolTable,
260) {
261 let mut seen: HashSet<&str> = HashSet::new();
262
263 let method_prefix = context.prefix.rsplit("->").next().unwrap_or(&context.prefix);
265 for (name, symbols) in &symbol_table.symbols {
266 let is_callable = symbols
267 .iter()
268 .any(|symbol| matches!(symbol.kind, SymbolKind::Subroutine | SymbolKind::Method));
269 if !is_callable {
270 continue;
271 }
272
273 if !method_prefix.is_empty() && !name.starts_with(method_prefix) {
274 continue;
275 }
276
277 let callable_symbol = symbols
279 .iter()
280 .find(|symbol| matches!(symbol.kind, SymbolKind::Subroutine | SymbolKind::Method));
281
282 let is_moo_accessor =
283 callable_symbol.and_then(|s| s.declaration.as_deref()).is_some_and(|d| d == "has");
284
285 let (detail, documentation) = if is_moo_accessor {
286 let attrs = callable_symbol.map(|s| s.attributes.as_slice()).unwrap_or(&[]);
287 ("Moo/Moose accessor".to_string(), Some(moo_accessor_documentation(name, attrs)))
288 } else {
289 let doc = symbols.iter().find_map(|symbol| symbol.documentation.clone());
290 ("method".to_string(), doc)
291 };
292
293 if seen.insert(name.as_str()) {
294 completions.push(CompletionItem {
295 label: name.clone(),
296 kind: crate::completion::items::CompletionItemKind::Function,
297 detail: Some(detail),
298 documentation,
299 insert_text: Some(format!("{}()", name)),
300 sort_text: Some(format!("1_{}", name)),
301 filter_text: Some(name.clone()),
302 additional_edits: vec![],
303 text_edit_range: Some((context.prefix_start, context.position)),
304 commit_characters: None,
305 });
306 }
307 }
308
309 let receiver_type = infer_receiver_type(context, source);
311
312 let import_module: Option<&str> =
316 static_receiver_module(&context.prefix).or(match receiver_type.as_deref() {
317 Some("DBI::db") | Some("DBI::st") => Some("DBI"),
318 _ => None,
319 });
320
321 let auto_import_edit =
323 import_module.and_then(|m| auto_import::build_auto_import_edit(source, m));
324
325 let methods: Vec<(&str, &str)> = match receiver_type.as_deref() {
327 Some("DBI::db") => DBI_DB_METHODS.to_vec(),
328 Some("DBI::st") => DBI_ST_METHODS.to_vec(),
329 _ => {
330 vec![
332 ("new", "Constructor"),
333 ("isa", "Check if object is of given class"),
334 ("can", "Check if object can call method"),
335 ("DOES", "Check if object does role"),
336 ("VERSION", "Get version"),
337 ]
338 }
339 };
340
341 for (method, desc) in methods {
342 if seen.insert(method) {
343 let additional_edits =
344 auto_import_edit.as_ref().map(|e| vec![e.clone()]).unwrap_or_default();
345 completions.push(CompletionItem {
346 label: method.to_string(),
347 kind: crate::completion::items::CompletionItemKind::Function,
348 detail: Some("method".to_string()),
349 documentation: Some(desc.to_string()),
350 insert_text: Some(format!("{}()", method)),
351 sort_text: Some(format!("2_{}", method)),
352 filter_text: Some(method.to_string()),
353 additional_edits,
354 text_edit_range: Some((context.prefix_start, context.position)),
355 commit_characters: None,
356 });
357 }
358 }
359
360 if receiver_type.as_deref() == Some("DBI::db") || receiver_type.as_deref() == Some("DBI::st") {
362 for (method, desc) in [
363 ("isa", "Check if object is of given class"),
364 ("can", "Check if object can call method"),
365 ] {
366 if seen.insert(method) {
367 let additional_edits =
368 auto_import_edit.as_ref().map(|e| vec![e.clone()]).unwrap_or_default();
369 completions.push(CompletionItem {
370 label: method.to_string(),
371 kind: crate::completion::items::CompletionItemKind::Function,
372 detail: Some("method".to_string()),
373 documentation: Some(desc.to_string()),
374 insert_text: Some(format!("{}()", method)),
375 sort_text: Some(format!("9_{}", method)), filter_text: Some(method.to_string()),
377 additional_edits,
378 text_edit_range: Some((context.prefix_start, context.position)),
379 commit_characters: None,
380 });
381 }
382 }
383 }
384}