Skip to main content

perl_semantic_analyzer/analysis/semantic/
builtins.rs

1//! Perl built-in function documentation and classification.
2
3/// Documentation entry for a Perl built-in function.
4///
5/// Provides signature and description information for display in hover tooltips.
6pub struct BuiltinDoc {
7    /// Function signature showing calling conventions
8    pub signature: &'static str,
9    /// Brief description of what the function does
10    pub description: &'static str,
11}
12
13/// Check if a function name is a Perl control-flow keyword.
14///
15/// Returns `true` if the name is a control-flow keyword like `next`, `last`, etc.
16pub(super) fn is_control_keyword(name: &str) -> bool {
17    matches!(name, "next" | "last" | "redo" | "goto" | "return" | "exit" | "die")
18}
19
20/// Check if a function name is a Perl built-in.
21///
22/// Returns `true` if the name matches a known Perl built-in function.
23pub(super) fn is_builtin_function(name: &str) -> bool {
24    matches!(
25        name,
26        "print"
27            | "say"
28            | "printf"
29            | "sprintf"
30            | "open"
31            | "close"
32            | "read"
33            | "write"
34            | "chomp"
35            | "chop"
36            | "split"
37            | "join"
38            | "push"
39            | "pop"
40            | "shift"
41            | "unshift"
42            | "sort"
43            | "reverse"
44            | "map"
45            | "grep"
46            | "length"
47            | "substr"
48            | "index"
49            | "rindex"
50            | "lc"
51            | "uc"
52            | "lcfirst"
53            | "ucfirst"
54            | "defined"
55            | "undef"
56            | "ref"
57            | "blessed"
58            | "die"
59            | "warn"
60            | "eval"
61            | "require"
62            | "use"
63            | "return"
64            | "next"
65            | "last"
66            | "redo"
67            | "goto" // ... many more
68    )
69}
70
71/// Check if an operator is a file test operator.
72///
73/// File test operators in Perl are unary operators that test file properties:
74/// -e (exists), -d (directory), -f (file), -r (readable), -w (writable), etc.
75pub(super) fn is_file_test_operator(op: &str) -> bool {
76    matches!(
77        op,
78        "-e" | "-d"
79            | "-f"
80            | "-r"
81            | "-w"
82            | "-x"
83            | "-s"
84            | "-z"
85            | "-T"
86            | "-B"
87            | "-M"
88            | "-A"
89            | "-C"
90            | "-l"
91            | "-p"
92            | "-S"
93            | "-u"
94            | "-g"
95            | "-k"
96            | "-t"
97            | "-O"
98            | "-G"
99            | "-R"
100            | "-b"
101            | "-c"
102    )
103}
104
105/// Get documentation for a Perl built-in function.
106///
107/// Returns signature and description for known built-in functions,
108/// or `None` if documentation is not available.
109///
110/// This is also used by the LSP hover handler to show builtin docs when the
111/// semantic analyzer has no symbol-level hit (e.g. bare-word builtins in
112/// fallback path).
113pub fn get_builtin_documentation(name: &str) -> Option<BuiltinDoc> {
114    match name {
115        // I/O
116        "print" => Some(BuiltinDoc {
117            signature: "print FILEHANDLE LIST\nprint LIST\nprint",
118            description: "Prints a string or list of strings. If FILEHANDLE is omitted, prints to the last selected output handle (STDOUT by default).",
119        }),
120        "say" => Some(BuiltinDoc {
121            signature: "say FILEHANDLE LIST\nsay LIST\nsay",
122            description: "Like print, but appends a newline to the output.",
123        }),
124        "printf" => Some(BuiltinDoc {
125            signature: "printf FILEHANDLE FORMAT, LIST\nprintf FORMAT, LIST",
126            description: "Prints a formatted string to FILEHANDLE (default STDOUT).",
127        }),
128        "sprintf" => Some(BuiltinDoc {
129            signature: "sprintf FORMAT, LIST",
130            description: "Returns a formatted string (like C sprintf). Does not print.",
131        }),
132        "open" => Some(BuiltinDoc {
133            signature: "open FILEHANDLE, MODE, EXPR\nopen FILEHANDLE, EXPR\nopen FILEHANDLE",
134            description: "Opens the file whose filename is given by EXPR, and associates it with FILEHANDLE.",
135        }),
136        "close" => Some(BuiltinDoc {
137            signature: "close FILEHANDLE\nclose",
138            description: "Closes the file, socket, or pipe associated with FILEHANDLE.",
139        }),
140        "read" => Some(BuiltinDoc {
141            signature: "read FILEHANDLE, SCALAR, LENGTH, OFFSET\nread FILEHANDLE, SCALAR, LENGTH",
142            description: "Reads LENGTH bytes of data into SCALAR from FILEHANDLE. Returns the number of bytes read, or undef on error.",
143        }),
144        "write" => Some(BuiltinDoc {
145            signature: "write FILEHANDLE\nwrite",
146            description: "Writes a formatted record to FILEHANDLE using the format associated with it.",
147        }),
148        "seek" => Some(BuiltinDoc {
149            signature: "seek FILEHANDLE, POSITION, WHENCE",
150            description: "Sets the position for a filehandle. WHENCE: 0=start, 1=current, 2=end.",
151        }),
152        "tell" => Some(BuiltinDoc {
153            signature: "tell FILEHANDLE\ntell",
154            description: "Returns the current position in bytes for FILEHANDLE.",
155        }),
156        "eof" => Some(BuiltinDoc {
157            signature: "eof FILEHANDLE\neof()\neof",
158            description: "Returns true if the next read on FILEHANDLE would return end of file.",
159        }),
160        "binmode" => Some(BuiltinDoc {
161            signature: "binmode FILEHANDLE, LAYER\nbinmode FILEHANDLE",
162            description: "Sets binary mode on FILEHANDLE, or specifies an I/O layer.",
163        }),
164        "truncate" => Some(BuiltinDoc {
165            signature: "truncate FILEHANDLE, LENGTH",
166            description: "Truncates the file at the given LENGTH.",
167        }),
168
169        // String functions
170        "chomp" => Some(BuiltinDoc {
171            signature: "chomp VARIABLE\nchomp LIST\nchomp",
172            description: "Removes the trailing newline from VARIABLE. Returns the number of characters removed.",
173        }),
174        "chop" => Some(BuiltinDoc {
175            signature: "chop VARIABLE\nchop LIST\nchop",
176            description: "Removes and returns the last character from VARIABLE.",
177        }),
178        "length" => Some(BuiltinDoc {
179            signature: "length EXPR\nlength",
180            description: "Returns the length in characters of the value of EXPR.",
181        }),
182        "substr" => Some(BuiltinDoc {
183            signature: "substr EXPR, OFFSET, LENGTH, REPLACEMENT\nsubstr EXPR, OFFSET, LENGTH\nsubstr EXPR, OFFSET",
184            description: "Extracts a substring out of EXPR and returns it. With REPLACEMENT, replaces the substring in-place.",
185        }),
186        "index" => Some(BuiltinDoc {
187            signature: "index STR, SUBSTR, POSITION\nindex STR, SUBSTR",
188            description: "Returns the position of the first occurrence of SUBSTR in STR at or after POSITION. Returns -1 if not found.",
189        }),
190        "rindex" => Some(BuiltinDoc {
191            signature: "rindex STR, SUBSTR, POSITION\nrindex STR, SUBSTR",
192            description: "Returns the position of the last occurrence of SUBSTR in STR at or before POSITION.",
193        }),
194        "lc" => Some(BuiltinDoc {
195            signature: "lc EXPR\nlc",
196            description: "Returns a lowercased version of EXPR (or $_ if omitted).",
197        }),
198        "uc" => Some(BuiltinDoc {
199            signature: "uc EXPR\nuc",
200            description: "Returns an uppercased version of EXPR (or $_ if omitted).",
201        }),
202        "lcfirst" => Some(BuiltinDoc {
203            signature: "lcfirst EXPR\nlcfirst",
204            description: "Returns EXPR with the first character lowercased.",
205        }),
206        "ucfirst" => Some(BuiltinDoc {
207            signature: "ucfirst EXPR\nucfirst",
208            description: "Returns EXPR with the first character uppercased.",
209        }),
210        "chr" => Some(BuiltinDoc {
211            signature: "chr NUMBER\nchr",
212            description: "Returns the character represented by NUMBER in the character set.",
213        }),
214        "ord" => Some(BuiltinDoc {
215            signature: "ord EXPR\nord",
216            description: "Returns the numeric value of the first character of EXPR.",
217        }),
218        "hex" => Some(BuiltinDoc {
219            signature: "hex EXPR\nhex",
220            description: "Interprets EXPR as a hex string and returns the corresponding numeric value.",
221        }),
222        "oct" => Some(BuiltinDoc {
223            signature: "oct EXPR\noct",
224            description: "Interprets EXPR as an octal string and returns the corresponding value. Handles 0x, 0b, and 0 prefixes.",
225        }),
226        "quotemeta" => Some(BuiltinDoc {
227            signature: "quotemeta EXPR\nquotemeta",
228            description: "Returns EXPR with all non-alphanumeric characters backslashed (escaped for regex).",
229        }),
230        "join" => Some(BuiltinDoc {
231            signature: "join EXPR, LIST",
232            description: "Joins the separate strings of LIST into a single string with fields separated by EXPR, and returns that string.\n\n```perl\nmy $str = join(', ', 'a', 'b', 'c');  # \"a, b, c\"\nmy $csv = join(',', @fields);\n```",
233        }),
234        "split" => Some(BuiltinDoc {
235            signature: "split /PATTERN/, EXPR, LIMIT\nsplit /PATTERN/, EXPR\nsplit /PATTERN/\nsplit",
236            description: "Splits the string EXPR into a list of strings and returns the list. If LIMIT is specified, splits into at most that many fields.\n\n```perl\nmy @words = split /\\s+/, $line;       # split on whitespace\nmy @fields = split /,/, $csv, 10;    # at most 10 fields\n```",
237        }),
238
239        // Array/List
240        "push" => Some(BuiltinDoc {
241            signature: "push ARRAY, LIST",
242            description: "Appends one or more values to the end of ARRAY. Returns the number of elements in the resulting array.\n\n```perl\nmy @list = (1, 2);\npush @list, 3, 4;   # @list is now (1, 2, 3, 4)\n```",
243        }),
244        "pop" => Some(BuiltinDoc {
245            signature: "pop ARRAY\npop",
246            description: "Removes and returns the last element of ARRAY.\n\n```perl\nmy @stack = (1, 2, 3);\nmy $top = pop @stack;   # $top = 3, @stack = (1, 2)\n```",
247        }),
248        "shift" => Some(BuiltinDoc {
249            signature: "shift ARRAY\nshift",
250            description: "Removes and returns the first element of ARRAY, shortening the array by 1.\n\n```perl\nmy @queue = ('first', 'second');\nmy $item = shift @queue;   # $item = 'first'\n```",
251        }),
252        "unshift" => Some(BuiltinDoc {
253            signature: "unshift ARRAY, LIST",
254            description: "Prepends LIST to the front of ARRAY. Returns the number of elements in the new array.\n\n```perl\nmy @list = (3, 4);\nunshift @list, 1, 2;   # @list is now (1, 2, 3, 4)\n```",
255        }),
256        "splice" => Some(BuiltinDoc {
257            signature: "splice ARRAY, OFFSET, LENGTH, LIST\nsplice ARRAY, OFFSET, LENGTH\nsplice ARRAY, OFFSET\nsplice ARRAY",
258            description: "Removes LENGTH elements from ARRAY starting at OFFSET, replacing them with LIST. Returns the removed elements. In scalar context, returns the last removed element.",
259        }),
260        "sort" => Some(BuiltinDoc {
261            signature: "sort SUBNAME LIST\nsort BLOCK LIST\nsort LIST",
262            description: "Sorts LIST and returns the sorted list. BLOCK or SUBNAME provides a custom comparison function using $a and $b. Only valid in list context; using sort in scalar context returns undef (avoid).",
263        }),
264        "reverse" => Some(BuiltinDoc {
265            signature: "reverse LIST",
266            description: "In list context, returns LIST in reverse order. In scalar context, returns a string with characters reversed.",
267        }),
268        "map" => Some(BuiltinDoc {
269            signature: "map BLOCK LIST\nmap EXPR, LIST",
270            description: "Evaluates the BLOCK or EXPR for each element of LIST (locally setting $_ to each element) and composes a list of the results. In scalar context, returns the number of elements the expression would produce.\n\n```perl\nmy @doubled = map { $_ * 2 } @numbers;\nmy @names   = map { $_->{name} } @records;\n```",
271        }),
272        "grep" => Some(BuiltinDoc {
273            signature: "grep BLOCK LIST\ngrep EXPR, LIST",
274            description: "Evaluates BLOCK or EXPR for each element of LIST and returns the list of elements for which the expression is true. In scalar context, returns the number of matching elements rather than the list.\n\n```perl\nmy @evens = grep { $_ % 2 == 0 } @numbers;\nmy $count = grep { /pattern/ } @lines;\n```",
275        }),
276        "scalar" => Some(BuiltinDoc {
277            signature: "scalar EXPR",
278            description: "Forces EXPR to be interpreted in scalar context and returns the value of EXPR.",
279        }),
280        "wantarray" => Some(BuiltinDoc {
281            signature: "wantarray",
282            description: "Returns true if the subroutine is called in list context, false (defined but false) in scalar context, and undef in void context. Use to write context-sensitive subs: `return wantarray ? @list : $count;`",
283        }),
284
285        // Hash
286        "keys" => Some(BuiltinDoc {
287            signature: "keys HASH\nkeys ARRAY",
288            description: "In list context, returns all keys of the named hash or indices of an array. In scalar context, returns the number of keys (an integer count). Note: `scalar keys %h` is the idiomatic way to count hash entries.",
289        }),
290        "values" => Some(BuiltinDoc {
291            signature: "values HASH\nvalues ARRAY",
292            description: "In list context, returns all values of the named hash or values of an array. In scalar context, returns the number of values (same as scalar keys).",
293        }),
294        "each" => Some(BuiltinDoc {
295            signature: "each HASH\neach ARRAY",
296            description: "Returns the next key-value pair from the hash as a two-element list, or an empty list when exhausted. The iterator resets when the list is exhausted, when keys() or values() is called on the hash, or when the hash is modified. Call in a while loop: `while (my ($k, $v) = each %h) { ... }`",
297        }),
298        "exists" => Some(BuiltinDoc {
299            signature: "exists EXPR",
300            description: "Returns true if the specified hash key or array element exists, even if its value is undef.",
301        }),
302        "delete" => Some(BuiltinDoc {
303            signature: "delete EXPR",
304            description: "Deletes the specified keys and their associated values from a hash, or elements from an array.",
305        }),
306        "defined" => Some(BuiltinDoc {
307            signature: "defined EXPR\ndefined",
308            description: "Returns true if EXPR has a value other than undef.",
309        }),
310        "undef" => Some(BuiltinDoc {
311            signature: "undef EXPR\nundef",
312            description: "Undefines the value of EXPR. Can be used on scalars, arrays, hashes, subroutines, and typeglobs.",
313        }),
314
315        // References and OO
316        "ref" => Some(BuiltinDoc {
317            signature: "ref EXPR\nref",
318            description: "Returns a string indicating the type of reference EXPR is, or empty string if not a reference. E.g. HASH, ARRAY, SCALAR, CODE.",
319        }),
320        "bless" => Some(BuiltinDoc {
321            signature: "bless REF, CLASSNAME\nbless REF",
322            description: "Associates the referent of REF with package CLASSNAME (or current package). Returns the reference.",
323        }),
324        "blessed" => Some(BuiltinDoc {
325            signature: "blessed EXPR",
326            description: "Returns the name of the package EXPR is blessed into, or undef if EXPR is not a blessed reference. From Scalar::Util.",
327        }),
328        "tie" => Some(BuiltinDoc {
329            signature: "tie VARIABLE, CLASSNAME, LIST",
330            description: "Binds a variable to a package class that provides the implementation for the variable.",
331        }),
332        "untie" => Some(BuiltinDoc {
333            signature: "untie VARIABLE",
334            description: "Breaks the binding between a variable and its package.",
335        }),
336        "tied" => Some(BuiltinDoc {
337            signature: "tied VARIABLE",
338            description: "Returns a reference to the object underlying VARIABLE if it is tied, or undef if not.",
339        }),
340
341        // Tie magic methods
342        "TIESCALAR" => Some(BuiltinDoc {
343            signature: "TIESCALAR CLASSNAME, LIST",
344            description: "Constructor called when `tie $scalar, CLASSNAME, LIST` is used. Must return a blessed reference.",
345        }),
346        "TIEARRAY" => Some(BuiltinDoc {
347            signature: "TIEARRAY CLASSNAME, LIST",
348            description: "Constructor called when `tie @array, CLASSNAME, LIST` is used. Must return a blessed reference.",
349        }),
350        "TIEHASH" => Some(BuiltinDoc {
351            signature: "TIEHASH CLASSNAME, LIST",
352            description: "Constructor called when `tie %hash, CLASSNAME, LIST` is used. Must return a blessed reference.",
353        }),
354        "TIEHANDLE" => Some(BuiltinDoc {
355            signature: "TIEHANDLE CLASSNAME, LIST",
356            description: "Constructor called when `tie *FH, CLASSNAME, LIST` is used. Must return a blessed reference.",
357        }),
358        "FETCH" => Some(BuiltinDoc {
359            signature: "FETCH this",
360            description: "Called on every access of a tied scalar or array/hash element. Returns the value.",
361        }),
362        "STORE" => Some(BuiltinDoc {
363            signature: "STORE this, value",
364            description: "Called on every assignment to a tied scalar or array/hash element.",
365        }),
366        "FIRSTKEY" => Some(BuiltinDoc {
367            signature: "FIRSTKEY this",
368            description: "Called when `keys` or `each` is first invoked on a tied hash.",
369        }),
370        "NEXTKEY" => Some(BuiltinDoc {
371            signature: "NEXTKEY this, lastkey",
372            description: "Called during iteration of a tied hash with `each` or `keys`.",
373        }),
374        "DESTROY" => Some(BuiltinDoc {
375            signature: "DESTROY this",
376            description: "Called when the tied object goes out of scope or is explicitly untied.",
377        }),
378
379        // Control flow
380        "die" => Some(BuiltinDoc {
381            signature: "die LIST",
382            description: "Raises an exception. If LIST does not end in '\\n', Perl appends the script name and line number. In modules, prefer Carp::croak() to preserve the caller's stack frame. The exception is available in $@ after an eval block.",
383        }),
384        "warn" => Some(BuiltinDoc {
385            signature: "warn LIST",
386            description: "Prints a warning to STDERR. Does not exit. If the message does not end in '\\n', Perl appends the script name and line number. In modules, prefer Carp::carp() to report from the caller's perspective.",
387        }),
388        "eval" => Some(BuiltinDoc {
389            signature: "eval BLOCK\neval EXPR",
390            description: "Evaluates BLOCK or EXPR and traps exceptions. After the eval, check $@ for errors: if ($@) { ... }. BLOCK form is preferred — EXPR form (string eval) is a security risk and triggers the PL600 diagnostic.",
391        }),
392        // Carp module functions
393        "croak" => Some(BuiltinDoc {
394            signature: "croak LIST",
395            description: "Like die but reports the error from the caller's perspective. Part of the Carp module. Use instead of die in library code so the stack trace points to the caller, not the module internals.",
396        }),
397        "carp" => Some(BuiltinDoc {
398            signature: "carp LIST",
399            description: "Like warn but reports the warning from the caller's perspective. Part of the Carp module. Prefer over warn in library code.",
400        }),
401        "confess" => Some(BuiltinDoc {
402            signature: "confess LIST",
403            description: "Like croak but includes a full stack trace. Part of the Carp module. Use when the full call chain is needed for debugging.",
404        }),
405        "cluck" => Some(BuiltinDoc {
406            signature: "cluck LIST",
407            description: "Like carp but includes a full stack trace. Part of the Carp module. Use for warnings that benefit from call chain context.",
408        }),
409        "return" => Some(BuiltinDoc {
410            signature: "return EXPR\nreturn",
411            description: "Returns from a subroutine with the value of EXPR.",
412        }),
413        "next" => Some(BuiltinDoc {
414            signature: "next LABEL\nnext",
415            description: "Starts the next iteration of the loop (like C 'continue').",
416        }),
417        "last" => Some(BuiltinDoc {
418            signature: "last LABEL\nlast",
419            description: "Exits the loop immediately (like C 'break').",
420        }),
421        "redo" => Some(BuiltinDoc {
422            signature: "redo LABEL\nredo",
423            description: "Restarts the loop block without re-evaluating the condition.",
424        }),
425        "goto" => Some(BuiltinDoc {
426            signature: "goto LABEL\ngoto EXPR\ngoto &NAME",
427            description: "Transfers control to the named label, computed label, or substitutes a call to the named subroutine.",
428        }),
429        "caller" => Some(BuiltinDoc {
430            signature: "caller EXPR\ncaller",
431            description: "Without argument, returns (package, filename, line) in list context or the package name in scalar context. With EXPR returns additional call-frame info: (package, filename, line, subroutine, hasargs, wantarray, evaltext, is_require, hints, bitmask, hinthash).",
432        }),
433        "exit" => Some(BuiltinDoc {
434            signature: "exit EXPR\nexit",
435            description: "Exits the program with status EXPR (default 0). Calls END blocks and DESTROY methods before exit.",
436        }),
437
438        // Modules and loading
439        "require" => Some(BuiltinDoc {
440            signature: "require EXPR\nrequire",
441            description: "Loads a library module at runtime. Raises an exception on failure.",
442        }),
443        "use" => Some(BuiltinDoc {
444            signature: "use Module VERSION LIST\nuse Module VERSION\nuse Module LIST\nuse Module",
445            description: "Loads and imports a module at compile time. Equivalent to BEGIN { require Module; Module->import( LIST ); }",
446        }),
447        "do" => Some(BuiltinDoc {
448            signature: "do BLOCK\ndo EXPR",
449            description: "As do BLOCK: executes BLOCK and returns its value. As do EXPR: reads and executes a Perl file.",
450        }),
451
452        // Math
453        "abs" => Some(BuiltinDoc {
454            signature: "abs VALUE\nabs",
455            description: "Returns the absolute value of its argument.",
456        }),
457        "int" => Some(BuiltinDoc {
458            signature: "int EXPR\nint",
459            description: "Returns the integer portion of EXPR (truncates toward zero).",
460        }),
461        "sqrt" => Some(BuiltinDoc {
462            signature: "sqrt EXPR\nsqrt",
463            description: "Returns the positive square root of EXPR.",
464        }),
465        "log" => Some(BuiltinDoc {
466            signature: "log EXPR\nlog",
467            description: "Returns the natural logarithm (base e) of EXPR.",
468        }),
469        "exp" => Some(BuiltinDoc {
470            signature: "exp EXPR\nexp",
471            description: "Returns e (the natural logarithm base) to the power of EXPR.",
472        }),
473        "sin" => Some(BuiltinDoc {
474            signature: "sin EXPR\nsin",
475            description: "Returns the sine of EXPR (expressed in radians).",
476        }),
477        "cos" => Some(BuiltinDoc {
478            signature: "cos EXPR\ncos",
479            description: "Returns the cosine of EXPR (expressed in radians).",
480        }),
481        "atan2" => Some(BuiltinDoc {
482            signature: "atan2 Y, X",
483            description: "Returns the arctangent of Y/X in the range -PI to PI.",
484        }),
485        "rand" => Some(BuiltinDoc {
486            signature: "rand EXPR\nrand",
487            description: "Returns a random fractional number greater than or equal to 0 and less than EXPR (default 1).",
488        }),
489        "srand" => Some(BuiltinDoc {
490            signature: "srand EXPR\nsrand",
491            description: "Sets the random number seed for the rand operator.",
492        }),
493
494        // File tests and operations
495        "stat" => Some(BuiltinDoc {
496            signature: "stat FILEHANDLE\nstat EXPR",
497            description: "Returns a 13-element list (dev, ino, mode, nlink, uid, gid, rdev, size, atime, mtime, ctime, blksize, blocks) or an empty list on failure.",
498        }),
499        "lstat" => Some(BuiltinDoc {
500            signature: "lstat FILEHANDLE\nlstat EXPR",
501            description: "Like stat, but if the last component of the filename is a symbolic link, stats the link itself.",
502        }),
503        "chmod" => Some(BuiltinDoc {
504            signature: "chmod MODE, LIST",
505            description: "Changes the permissions of a list of files. Returns the number of files successfully changed.",
506        }),
507        "chown" => Some(BuiltinDoc {
508            signature: "chown UID, GID, LIST",
509            description: "Changes the owner and group of a list of files.",
510        }),
511        "unlink" => Some(BuiltinDoc {
512            signature: "unlink LIST\nunlink",
513            description: "Deletes a list of files. Returns the number of files successfully deleted.",
514        }),
515        "rename" => Some(BuiltinDoc {
516            signature: "rename OLDNAME, NEWNAME",
517            description: "Renames a file. Returns true on success, false otherwise.",
518        }),
519        "mkdir" => Some(BuiltinDoc {
520            signature: "mkdir FILENAME, MODE\nmkdir FILENAME",
521            description: "Creates the directory specified by FILENAME. Returns true on success.",
522        }),
523        "rmdir" => Some(BuiltinDoc {
524            signature: "rmdir FILENAME\nrmdir",
525            description: "Deletes the directory if it is empty. Returns true on success.",
526        }),
527        "opendir" => Some(BuiltinDoc {
528            signature: "opendir DIRHANDLE, EXPR",
529            description: "Opens a directory for reading by readdir.",
530        }),
531        "readdir" => Some(BuiltinDoc {
532            signature: "readdir DIRHANDLE",
533            description: "Returns the next entry (or entries in list context) from the directory.",
534        }),
535        "closedir" => Some(BuiltinDoc {
536            signature: "closedir DIRHANDLE",
537            description: "Closes a directory opened by opendir.",
538        }),
539        "link" => Some(BuiltinDoc {
540            signature: "link OLDFILE, NEWFILE",
541            description: "Creates a new hard link for an existing file.",
542        }),
543        "symlink" => Some(BuiltinDoc {
544            signature: "symlink OLDFILE, NEWFILE",
545            description: "Creates a new symbolic link for an existing file.",
546        }),
547        "readlink" => Some(BuiltinDoc {
548            signature: "readlink EXPR\nreadlink",
549            description: "Returns the value of a symbolic link.",
550        }),
551        "chdir" => Some(BuiltinDoc {
552            signature: "chdir EXPR\nchdir",
553            description: "Changes the working directory to EXPR (or home directory if omitted).",
554        }),
555        "glob" => Some(BuiltinDoc {
556            signature: "glob EXPR\nglob",
557            description: "Returns the filenames matching the shell-style glob pattern EXPR.",
558        }),
559
560        // System/Process
561        "system" => Some(BuiltinDoc {
562            signature: "system LIST\nsystem PROGRAM LIST",
563            description: "Executes a system command and returns the exit status. The return value is the exit status of the program as returned by the wait call.",
564        }),
565        "exec" => Some(BuiltinDoc {
566            signature: "exec LIST\nexec PROGRAM LIST",
567            description: "Replaces the current process with an external command. Never returns on success.",
568        }),
569        "fork" => Some(BuiltinDoc {
570            signature: "fork",
571            description: "Creates a child process. Returns the child pid to the parent, 0 to the child, or undef on failure.",
572        }),
573        "wait" => Some(BuiltinDoc {
574            signature: "wait",
575            description: "Waits for a child process to terminate and returns the pid of the deceased process.",
576        }),
577        "waitpid" => Some(BuiltinDoc {
578            signature: "waitpid PID, FLAGS",
579            description: "Waits for a particular child process to terminate and returns the pid.",
580        }),
581        "kill" => Some(BuiltinDoc {
582            signature: "kill SIGNAL, LIST",
583            description: "Sends a signal to a list of processes. Returns the number of processes signalled.",
584        }),
585        "sleep" => Some(BuiltinDoc {
586            signature: "sleep EXPR\nsleep",
587            description: "Causes the script to sleep for EXPR seconds (or forever if no argument).",
588        }),
589        "alarm" => Some(BuiltinDoc {
590            signature: "alarm SECONDS\nalarm",
591            description: "Arranges to have a SIGALRM delivered after SECONDS seconds.",
592        }),
593
594        // Encoding/Decoding
595        "pack" => Some(BuiltinDoc {
596            signature: "pack TEMPLATE, LIST",
597            description: "Takes a list of values and packs it into a binary string according to TEMPLATE.",
598        }),
599        "unpack" => Some(BuiltinDoc {
600            signature: "unpack TEMPLATE, EXPR",
601            description: "Takes a binary string and expands it into a list of values according to TEMPLATE.",
602        }),
603        "crypt" => Some(BuiltinDoc {
604            signature: "crypt PLAINTEXT, SALT",
605            description: "Encrypts a string using the system crypt() function.",
606        }),
607
608        // Time
609        "time" => Some(BuiltinDoc {
610            signature: "time",
611            description: "Returns the number of seconds since the epoch (January 1, 1970 UTC).",
612        }),
613        "localtime" => Some(BuiltinDoc {
614            signature: "localtime EXPR\nlocaltime",
615            description: "Converts a time value to a 9-element list with the time analyzed for the local time zone. In scalar context returns a ctime(3) string.",
616        }),
617        "gmtime" => Some(BuiltinDoc {
618            signature: "gmtime EXPR\ngmtime",
619            description: "Like localtime but uses Greenwich Mean Time (UTC). In list context returns a 9-element time list (sec, min, hour, mday, mon, year, wday, yday, isdst). In scalar context returns a ctime(3)-style string.",
620        }),
621
622        // Misc
623        "prototype" => Some(BuiltinDoc {
624            signature: "prototype FUNCTION",
625            description: "Returns the prototype of a function as a string, or undef if the function has no prototype.",
626        }),
627        "local" => Some(BuiltinDoc {
628            signature: "local EXPR",
629            description: "Temporarily localizes the listed global variables to the enclosing block. The original values are restored at the end of the block.",
630        }),
631        "my" => Some(BuiltinDoc {
632            signature: "my VARLIST\nmy TYPE VARLIST",
633            description: "Declares lexically scoped variables. Variables are visible only within the enclosing block.",
634        }),
635        "our" => Some(BuiltinDoc {
636            signature: "our VARLIST",
637            description: "Declares package variables visible in the current lexical scope without qualifying the name.",
638        }),
639        "state" => Some(BuiltinDoc {
640            signature: "state VARLIST",
641            description: "Declares lexically scoped variables that persist across calls to the enclosing subroutine (like C static variables).",
642        }),
643        "BEGIN" => Some(BuiltinDoc {
644            signature: "BEGIN { BLOCK }",
645            description: "Executed at **compile time**, before the rest of the program runs. \
646                          Used to initialize modules, set up the symbol table, or run code \
647                          that must complete before compilation continues. Multiple BEGIN \
648                          blocks run in the order they appear in source.",
649        }),
650        "END" => Some(BuiltinDoc {
651            signature: "END { BLOCK }",
652            description: "Executed at **program exit**, after the main program finishes (including \
653                          `die` and `exit`). Used for cleanup. Multiple END blocks run in \
654                          reverse order of definition. `$?` holds the exit status.",
655        }),
656        "INIT" => Some(BuiltinDoc {
657            signature: "INIT { BLOCK }",
658            description: "Executed after compilation completes but **before** the main program \
659                          runs. Runs in first-seen order. Unlike BEGIN, INIT sees the fully \
660                          compiled symbol table.",
661        }),
662        "CHECK" => Some(BuiltinDoc {
663            signature: "CHECK { BLOCK }",
664            description: "Executed at the **end of compilation**, after all BEGIN blocks. Runs \
665                          in reverse order of definition. Used by modules that need to inspect \
666                          or modify the compiled program before it runs (e.g. B::* modules).",
667        }),
668        "UNITCHECK" => Some(BuiltinDoc {
669            signature: "UNITCHECK { BLOCK }",
670            description: "Executed at the **end of the compilation unit** that defined it \
671                          (file, string eval, or require). Runs in reverse order of definition \
672                          within that unit. More granular than CHECK — each required file's \
673                          UNITCHECK runs before the requiring file's UNITCHECK.",
674        }),
675
676        _ => None,
677    }
678}
679
680/// Get documentation for a Moose/Moo/Mouse built-in type constraint.
681///
682/// Accepts both bare types (`Str`, `ArrayRef`) and parametrized forms
683/// (`ArrayRef[Int]`, `Maybe[Str]`).  For parametrized forms the base
684/// type is extracted and used for the lookup.
685///
686/// Returns signature and description suitable for LSP hover display,
687/// or `None` if the type is not a known Moose built-in.
688pub fn get_moose_type_documentation(type_str: &str) -> Option<BuiltinDoc> {
689    // Strip optional parametrization: "ArrayRef[Int]" -> "ArrayRef"
690    let base = type_str.split('[').next().unwrap_or(type_str).trim();
691
692    match base {
693        // Moose::Util::TypeConstraints — Any / Item
694        "Any" => Some(BuiltinDoc {
695            signature: "Any",
696            description: "The root type. Every value passes this constraint.",
697        }),
698        "Item" => Some(BuiltinDoc {
699            signature: "Item",
700            description: "Synonym for Any. Used as a base for the type hierarchy.",
701        }),
702        // Undef / Defined
703        "Undef" => Some(BuiltinDoc { signature: "Undef", description: "Accepts only undef." }),
704        "Defined" => Some(BuiltinDoc {
705            signature: "Defined",
706            description: "Accepts any defined value (anything that is not undef).",
707        }),
708        // Value / Bool
709        "Value" => Some(BuiltinDoc {
710            signature: "Value",
711            description: "Accepts any defined, non-reference value (scalars and strings).",
712        }),
713        "Bool" => Some(BuiltinDoc {
714            signature: "Bool",
715            description: "Accepts 1, 0, the empty string '', or undef — Perl's boolean-ish values.",
716        }),
717        // Strings
718        "Str" => Some(BuiltinDoc {
719            signature: "Str",
720            description: "Accepts any defined, non-reference scalar value (a string or number).",
721        }),
722        "Num" => Some(BuiltinDoc {
723            signature: "Num",
724            description: "Accepts any value that looks like a number (integer or float).",
725        }),
726        "Int" => Some(BuiltinDoc {
727            signature: "Int",
728            description: "Accepts only integer values (no decimal point).",
729        }),
730        "ClassName" => Some(BuiltinDoc {
731            signature: "ClassName",
732            description: "Accepts a string that is the name of a loaded Perl package/class.",
733        }),
734        "RoleName" => Some(BuiltinDoc {
735            signature: "RoleName",
736            description: "Accepts a string that is the name of a loaded Moose role.",
737        }),
738        // References
739        "Ref" => Some(BuiltinDoc { signature: "Ref", description: "Accepts any reference." }),
740        "ScalarRef" => Some(BuiltinDoc {
741            signature: "ScalarRef[TYPE]",
742            description: "Accepts a scalar reference. Optionally parametrized: ScalarRef[Int] requires the referent to satisfy Int.",
743        }),
744        "ArrayRef" => Some(BuiltinDoc {
745            signature: "ArrayRef[TYPE]",
746            description: "Accepts an array reference. Optionally parametrized: ArrayRef[Int] requires all elements to satisfy Int.",
747        }),
748        "HashRef" => Some(BuiltinDoc {
749            signature: "HashRef[TYPE]",
750            description: "Accepts a hash reference. Optionally parametrized: HashRef[Str] requires all values to satisfy Str.",
751        }),
752        "CodeRef" => Some(BuiltinDoc {
753            signature: "CodeRef",
754            description: "Accepts a code reference (subroutine reference).",
755        }),
756        "RegexpRef" => Some(BuiltinDoc {
757            signature: "RegexpRef",
758            description: "Accepts a compiled regular expression reference (qr//).",
759        }),
760        "GlobRef" => {
761            Some(BuiltinDoc { signature: "GlobRef", description: "Accepts a glob reference." })
762        }
763        "FileHandle" => Some(BuiltinDoc {
764            signature: "FileHandle",
765            description: "Accepts an IO object or a glob reference that can be used as a filehandle.",
766        }),
767        // Object / Role
768        "Object" => Some(BuiltinDoc {
769            signature: "Object",
770            description: "Accepts any blessed reference (an object).",
771        }),
772        // Maybe
773        "Maybe" => Some(BuiltinDoc {
774            signature: "Maybe[TYPE]",
775            description: "Accepts undef or any value satisfying TYPE. Useful for optional attributes: Maybe[Str] accepts either a string or undef.",
776        }),
777        // Type::Tiny extras commonly used with Moo
778        "InstanceOf" => Some(BuiltinDoc {
779            signature: "InstanceOf[CLASSNAME]",
780            description: "Accepts a blessed object that is an instance of CLASSNAME.",
781        }),
782        "ConsumerOf" => Some(BuiltinDoc {
783            signature: "ConsumerOf[ROLENAME]",
784            description: "Accepts a blessed object that consumes ROLENAME.",
785        }),
786        "HasMethods" => Some(BuiltinDoc {
787            signature: "HasMethods[METHOD, ...]",
788            description: "Accepts a blessed object that has all the listed methods.",
789        }),
790        "Dict" => Some(BuiltinDoc {
791            signature: "Dict[KEY => TYPE, ...]",
792            description: "Accepts a hash reference matching a specific key/type schema (Type::Tiny).",
793        }),
794        "Tuple" => Some(BuiltinDoc {
795            signature: "Tuple[TYPE, ...]",
796            description: "Accepts an array reference matching a specific positional type schema (Type::Tiny).",
797        }),
798        "Map" => Some(BuiltinDoc {
799            signature: "Map[KEYTYPE, VALUETYPE]",
800            description: "Accepts a hash reference where keys satisfy KEYTYPE and values satisfy VALUETYPE (Type::Tiny).",
801        }),
802        "Enum" => Some(BuiltinDoc {
803            signature: "Enum[VALUE, ...]",
804            description: "Accepts a string that is one of the listed values (Type::Tiny).",
805        }),
806
807        _ => None,
808    }
809}
810
811/// Get documentation for a Perl subroutine or variable attribute.
812///
813/// Attributes are declared with `:name` syntax, e.g. `sub foo :lvalue { ... }`.
814/// Pass the attribute name without the leading colon.
815///
816/// Returns signature and description suitable for LSP hover display,
817/// or `None` if the attribute is not a known built-in.
818pub fn get_attribute_documentation(attr: &str) -> Option<BuiltinDoc> {
819    // Strip leading colon if present
820    let name = attr.trim_start_matches(':');
821
822    match name {
823        "lvalue" => Some(BuiltinDoc {
824            signature: ":lvalue",
825            description: "Marks a subroutine as an lvalue subroutine. The return value can be assigned to, enabling constructs like `foo() = 42;`.",
826        }),
827        "method" => Some(BuiltinDoc {
828            signature: ":method",
829            description: "Marks a subroutine as a method. Used by some attribute handlers to modify dispatch or prototype checking.",
830        }),
831        "prototype" => Some(BuiltinDoc {
832            signature: ":prototype(PROTO)",
833            description: "Sets the prototype of a subroutine. Controls how Perl parses calls to the sub (e.g. `prototype($$)` for two scalar args).",
834        }),
835        "const" => Some(BuiltinDoc {
836            signature: ":const",
837            description: "Marks a subroutine as a constant. The value is computed once and cached; subsequent calls return the cached value immutably.",
838        }),
839        "shared" => Some(BuiltinDoc {
840            signature: ":shared",
841            description: "Marks a variable or subroutine as shared across threads (requires `threads::shared`). The variable is accessible from all threads.",
842        }),
843        "weak_ref" => Some(BuiltinDoc {
844            signature: ":weak_ref",
845            description: "Marks a Moose/Moo attribute as a weak reference. The stored reference will not prevent the referent from being garbage-collected.",
846        }),
847        "overload" => Some(BuiltinDoc {
848            signature: ":overload(OP)",
849            description: "Declares that a subroutine implements an operator overload for OP.",
850        }),
851        _ => None,
852    }
853}
854
855/// Structured exception context for exception-family functions.
856///
857/// Used by code actions and semantic analysis to understand exception
858/// handling semantics — upgrade paths (die → croak) and associated
859/// error variables.
860#[derive(Debug, Clone)]
861pub struct ExceptionContext {
862    /// Special variable that captures the exception after an eval block (e.g. `$@`).
863    pub error_variable: Option<String>,
864    /// Recommended replacement function, if the current function is not preferred
865    /// (e.g. `die` → `Carp::croak`, `warn` → `Carp::carp`).
866    pub preferred_alternative: Option<String>,
867}
868
869/// Check if a function name is in the Perl exception family.
870///
871/// Returns `true` for: `die`, `warn`, `croak`, `carp`, `confess`, `cluck`.
872///
873/// This is a classification helper for future diagnostic and code-action use.
874/// It is not currently called from any LSP code path — callers may use it to
875/// decide whether to invoke [`get_exception_context`].
876///
877/// # Examples
878/// ```
879/// use perl_semantic_analyzer::analysis::semantic::is_exception_function;
880///
881/// assert!(is_exception_function("die"));
882/// assert!(is_exception_function("croak"));
883/// assert!(!is_exception_function("print"));
884/// ```
885pub fn is_exception_function(name: &str) -> bool {
886    matches!(name, "die" | "warn" | "croak" | "carp" | "confess" | "cluck")
887}
888
889/// Get exception context for upgrade suggestions and error variables.
890///
891/// Returns metadata about exception handling semantics:
892/// - `error_variable`: special variable capturing the exception (`$@`)
893/// - `preferred_alternative`: recommended upgrade path (`die` → `Carp::croak`)
894///
895/// Returns `None` for non-exception functions (e.g. `eval`, `print`).
896///
897/// # Examples
898/// ```
899/// use perl_semantic_analyzer::analysis::semantic::get_exception_context;
900///
901/// let die_ctx = get_exception_context("die").unwrap();
902/// assert_eq!(die_ctx.error_variable, Some("$@".to_string()));
903/// assert_eq!(die_ctx.preferred_alternative, Some("Carp::croak".to_string()));
904///
905/// let croak_ctx = get_exception_context("croak").unwrap();
906/// assert_eq!(croak_ctx.preferred_alternative, None);  // already preferred
907/// ```
908pub fn get_exception_context(name: &str) -> Option<ExceptionContext> {
909    match name {
910        "die" => Some(ExceptionContext {
911            error_variable: Some("$@".to_string()),
912            preferred_alternative: Some("Carp::croak".to_string()),
913        }),
914        "warn" => Some(ExceptionContext {
915            error_variable: None,
916            preferred_alternative: Some("Carp::carp".to_string()),
917        }),
918        "croak" | "confess" => Some(ExceptionContext {
919            error_variable: Some("$@".to_string()),
920            preferred_alternative: None,
921        }),
922        "carp" | "cluck" => {
923            Some(ExceptionContext { error_variable: None, preferred_alternative: None })
924        }
925        _ => None,
926    }
927}
928
929#[cfg(test)]
930mod tests {
931    use super::get_builtin_documentation;
932
933    #[test]
934    fn test_get_builtin_documentation_begin() -> Result<(), Box<dyn std::error::Error>> {
935        let doc = get_builtin_documentation("BEGIN").ok_or("BEGIN should have docs")?;
936        assert!(
937            doc.description.contains("compile time") || doc.description.contains("compile-time"),
938            "BEGIN doc should mention compile time, got: {}",
939            doc.description
940        );
941        Ok(())
942    }
943
944    #[test]
945    fn test_get_builtin_documentation_end() -> Result<(), Box<dyn std::error::Error>> {
946        let doc = get_builtin_documentation("END").ok_or("END should have docs")?;
947        assert!(
948            doc.description.contains("exit") || doc.description.contains("cleanup"),
949            "END doc should mention exit or cleanup, got: {}",
950            doc.description
951        );
952        Ok(())
953    }
954
955    #[test]
956    fn test_get_builtin_documentation_check() -> Result<(), Box<dyn std::error::Error>> {
957        let doc = get_builtin_documentation("CHECK").ok_or("CHECK should have docs")?;
958        assert!(
959            doc.description.contains("compilation") || doc.description.contains("compile"),
960            "CHECK doc should mention compilation, got: {}",
961            doc.description
962        );
963        Ok(())
964    }
965
966    #[test]
967    fn test_get_builtin_documentation_init() -> Result<(), Box<dyn std::error::Error>> {
968        let doc = get_builtin_documentation("INIT").ok_or("INIT should have docs")?;
969        assert!(
970            doc.description.contains("compilation") || doc.description.contains("before"),
971            "INIT doc should mention post-compile execution, got: {}",
972            doc.description
973        );
974        Ok(())
975    }
976
977    #[test]
978    fn test_get_builtin_documentation_unitcheck() -> Result<(), Box<dyn std::error::Error>> {
979        let doc = get_builtin_documentation("UNITCHECK").ok_or("UNITCHECK should have docs")?;
980        assert!(
981            doc.description.contains("compilation unit") || doc.description.contains("unit"),
982            "UNITCHECK doc should mention compilation unit scope, got: {}",
983            doc.description
984        );
985        Ok(())
986    }
987}