@@ -188,6 +188,21 @@ impl Backend {
188188 functions
189189 }
190190
191+ /// Parse PHP source text and extract constant names from `define()` calls.
192+ ///
193+ /// Returns a list of constant name strings for every `define('NAME', …)`
194+ /// call found at the top level, inside namespace blocks, block
195+ /// statements, or `if` guards.
196+ pub fn parse_defines ( & self , content : & str ) -> Vec < String > {
197+ let arena = Bump :: new ( ) ;
198+ let file_id = mago_database:: file:: FileId :: new ( "input.php" ) ;
199+ let program = parse_file_content ( & arena, file_id, content) ;
200+
201+ let mut defines = Vec :: new ( ) ;
202+ Self :: extract_defines_from_statements ( program. statements . iter ( ) , & mut defines) ;
203+ defines
204+ }
205+
191206 /// Parse PHP source text and extract `use` statement mappings.
192207 ///
193208 /// Returns a `HashMap` mapping short (imported) names to their
@@ -510,6 +525,102 @@ impl Backend {
510525 }
511526 }
512527
528+ // ─── define() constant extraction ───────────────────────────────
529+
530+ /// Walk statements and extract constant names from `define()` calls.
531+ ///
532+ /// Handles top-level `define('NAME', value)` calls, as well as those
533+ /// nested inside namespace blocks, block statements, and `if` guards
534+ /// (the common `if (!defined('X')) { define('X', …); }` pattern).
535+ ///
536+ /// Uses the parsed AST rather than regex, so it piggybacks on the
537+ /// parse pass that `update_ast` already performs.
538+ pub ( crate ) fn extract_defines_from_statements < ' a > (
539+ statements : impl Iterator < Item = & ' a Statement < ' a > > ,
540+ defines : & mut Vec < String > ,
541+ ) {
542+ for statement in statements {
543+ match statement {
544+ Statement :: Expression ( expr_stmt) => {
545+ if let Some ( name) = Self :: try_extract_define_name ( expr_stmt. expression ) {
546+ defines. push ( name) ;
547+ }
548+ }
549+ Statement :: Namespace ( namespace) => {
550+ Self :: extract_defines_from_statements ( namespace. statements ( ) . iter ( ) , defines) ;
551+ }
552+ Statement :: Block ( block) => {
553+ Self :: extract_defines_from_statements ( block. statements . iter ( ) , defines) ;
554+ }
555+ Statement :: If ( if_stmt) => {
556+ Self :: extract_defines_from_if_body ( & if_stmt. body , defines) ;
557+ }
558+ _ => { }
559+ }
560+ }
561+ }
562+
563+ /// Helper: recurse into an `if` statement body to extract `define()`
564+ /// calls. Mirrors `extract_functions_from_if_body`.
565+ fn extract_defines_from_if_body < ' a > ( body : & ' a IfBody < ' a > , defines : & mut Vec < String > ) {
566+ match body {
567+ IfBody :: Statement ( body) => {
568+ Self :: extract_defines_from_statements ( std:: iter:: once ( body. statement ) , defines) ;
569+ for else_if in body. else_if_clauses . iter ( ) {
570+ Self :: extract_defines_from_statements (
571+ std:: iter:: once ( else_if. statement ) ,
572+ defines,
573+ ) ;
574+ }
575+ if let Some ( else_clause) = & body. else_clause {
576+ Self :: extract_defines_from_statements (
577+ std:: iter:: once ( else_clause. statement ) ,
578+ defines,
579+ ) ;
580+ }
581+ }
582+ IfBody :: ColonDelimited ( body) => {
583+ Self :: extract_defines_from_statements ( body. statements . iter ( ) , defines) ;
584+ for else_if in body. else_if_clauses . iter ( ) {
585+ Self :: extract_defines_from_statements ( else_if. statements . iter ( ) , defines) ;
586+ }
587+ if let Some ( else_clause) = & body. else_clause {
588+ Self :: extract_defines_from_statements ( else_clause. statements . iter ( ) , defines) ;
589+ }
590+ }
591+ }
592+ }
593+
594+ /// Try to extract the constant name from a `define('NAME', …)` call
595+ /// expression. Returns `Some(name)` if the expression is a function
596+ /// call to `define` whose first argument is a string literal.
597+ fn try_extract_define_name ( expr : & Expression < ' _ > ) -> Option < String > {
598+ if let Expression :: Call ( Call :: Function ( func_call) ) = expr {
599+ let func_name = match func_call. function {
600+ Expression :: Identifier ( ident) => ident. value ( ) ,
601+ _ => return None ,
602+ } ;
603+ if !func_name. eq_ignore_ascii_case ( "define" ) {
604+ return None ;
605+ }
606+ let args: Vec < _ > = func_call. argument_list . arguments . iter ( ) . collect ( ) ;
607+ if args. is_empty ( ) {
608+ return None ;
609+ }
610+ let first_expr = match & args[ 0 ] {
611+ Argument :: Positional ( pos) => pos. value ,
612+ Argument :: Named ( named) => named. value ,
613+ } ;
614+ if let Expression :: Literal ( Literal :: String ( lit_str) ) = first_expr
615+ && let Some ( value) = lit_str. value
616+ && !value. is_empty ( )
617+ {
618+ return Some ( value. to_string ( ) ) ;
619+ }
620+ }
621+ None
622+ }
623+
513624 pub ( crate ) fn extract_classes_from_statements < ' a > (
514625 statements : impl Iterator < Item = & ' a Statement < ' a > > ,
515626 classes : & mut Vec < ClassInfo > ,
@@ -1021,6 +1132,20 @@ impl Backend {
10211132 }
10221133 }
10231134
1135+ // Extract define() constants from the already-parsed AST and
1136+ // store them in the global_defines map so they appear in
1137+ // completions. This reuses the parse pass above rather than
1138+ // doing a separate regex scan over the raw content.
1139+ let mut define_names = Vec :: new ( ) ;
1140+ Self :: extract_defines_from_statements ( program. statements . iter ( ) , & mut define_names) ;
1141+ if !define_names. is_empty ( )
1142+ && let Ok ( mut dmap) = self . global_defines . lock ( )
1143+ {
1144+ for name in define_names {
1145+ dmap. entry ( name) . or_insert_with ( || uri. to_string ( ) ) ;
1146+ }
1147+ }
1148+
10241149 // Post-process: resolve parent_class short names to fully-qualified
10251150 // names using the file's use_map and namespace so that cross-file
10261151 // inheritance resolution can find parent classes via PSR-4.
0 commit comments