@@ -103,13 +103,13 @@ public function process(File $phpcsFile, int $stackPtr)
103103 return ;
104104 }
105105
106- // If the file declares or imports a function named "define",
107- // any unqualified `define()` call may resolve to that function
108- // instead of the global one, so the sniff should bow out.
109- // Fully qualified `\define(...)` calls are unaffected as they
110- // come in as T_NAME_FULLY_QUALIFIED tokens and are handled below.
106+ // If the current namespace declares or imports a function named
107+ // "define", any unqualified `define()` call may resolve to that
108+ // function instead of the global one, so the sniff should bow out.
109+ // Fully qualified `\define(...)` calls are unaffected as they come
110+ // in as T_NAME_FULLY_QUALIFIED tokens and are handled below.
111111 if ($ tokens [$ stackPtr ]['code ' ] === T_STRING
112- && $ this ->fileHasCustomDefine ($ phpcsFile ) === true
112+ && $ this ->namespaceHasCustomDefine ($ phpcsFile, $ stackPtr ) === true
113113 ) {
114114 return ;
115115 }
@@ -160,47 +160,50 @@ public function process(File $phpcsFile, int $stackPtr)
160160
161161
162162 /**
163- * Determine whether a file declares or imports a function named "define".
163+ * Determine whether the namespace containing a token declares or imports
164+ * a function named "define".
164165 *
165- * Checks for top -level `function define(...)` declarations and
166+ * Checks for namespace -level `function define(...)` declarations and
166167 * `use function ...\define;` (or aliased) imports. The result is cached
167- * per file path to avoid rescanning on every visited token.
168+ * per file path and namespace scope to avoid rescanning on every token.
168169 *
169170 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
171+ * @param int $stackPtr The token being checked.
170172 *
171173 * @return bool
172174 */
173- private function fileHasCustomDefine (File $ phpcsFile )
175+ private function namespaceHasCustomDefine (File $ phpcsFile, int $ stackPtr )
174176 {
175177 static $ cache = [];
176178
177179 $ fileKey = $ phpcsFile ->getFilename ();
178180 if (isset ($ cache [$ fileKey ]) === true ) {
179- return $ cache [$ fileKey ];
181+ $ scopeKey = $ this ->getNamespaceScopeKey ($ phpcsFile , $ stackPtr );
182+ return isset ($ cache [$ fileKey ][$ scopeKey ]);
180183 }
181184
182- $ tokens = $ phpcsFile ->getTokens ();
183- $ result = false ;
185+ $ tokens = $ phpcsFile ->getTokens ();
186+ $ cache [ $ fileKey ] = [] ;
184187
185188 for ($ i = 0 ; $ i < $ phpcsFile ->numTokens ; $ i ++) {
186- $ code = $ tokens [$ i ]['code ' ];
189+ $ code = $ tokens [$ i ]['code ' ];
190+ $ scopeKey = $ this ->getNamespaceScopeKey ($ phpcsFile , $ i );
187191
188- // Top -level `function define(...)` declaration.
189- if ($ code === T_FUNCTION && empty ($ tokens [$ i][ ' conditions ' ]) === true ) {
192+ // Namespace -level `function define(...)` declaration.
193+ if ($ code === T_FUNCTION && $ this -> isInGlobalOrNamespaceScope ($ tokens [$ i ]) === true ) {
190194 $ namePtr = $ phpcsFile ->findNext (Tokens::EMPTY_TOKENS , ($ i + 1 ), null , true );
191195 if ($ namePtr !== false
192196 && $ tokens [$ namePtr ]['code ' ] === T_STRING
193197 && strtolower ($ tokens [$ namePtr ]['content ' ]) === 'define '
194198 ) {
195- $ result = true ;
196- break ;
199+ $ cache [$ fileKey ][$ scopeKey ] = true ;
197200 }
198201
199202 continue ;
200203 }
201204
202- // `use function ...define;` import (only top-level use statements) .
203- if ($ code === T_USE && empty ($ tokens [$ i][ ' conditions ' ]) === true ) {
205+ // `use function ...define;` import at file or namespace scope only .
206+ if ($ code === T_USE && $ this -> isInGlobalOrNamespaceScope ($ tokens [$ i ]) === true ) {
204207 $ next = $ phpcsFile ->findNext (Tokens::EMPTY_TOKENS , ($ i + 1 ), null , true );
205208 if ($ next === false || $ tokens [$ next ]['code ' ] !== T_STRING
206209 || strtolower ($ tokens [$ next ]['content ' ]) !== 'function '
@@ -214,8 +217,8 @@ private function fileHasCustomDefine(File $phpcsFile)
214217 }
215218
216219 if ($ this ->useListImportsDefine ($ phpcsFile , $ next , $ end ) === true ) {
217- $ result = true ;
218- break ;
220+ $ cache [ $ fileKey ][ $ scopeKey ] = true ;
221+ continue ;
219222 }
220223
221224 // Group use statement: walk the body looking for a `define` import.
@@ -224,16 +227,57 @@ private function fileHasCustomDefine(File $phpcsFile)
224227 if ($ groupEnd !== false
225228 && $ this ->useListImportsDefine ($ phpcsFile , $ end , $ groupEnd ) === true
226229 ) {
227- $ result = true ;
228- break ;
230+ $ cache [$ fileKey ][$ scopeKey ] = true ;
229231 }
230232 }
231233 }
232234 }
233235
234- $ cache [$ fileKey ] = $ result ;
236+ $ scopeKey = $ this ->getNamespaceScopeKey ($ phpcsFile , $ stackPtr );
237+ return isset ($ cache [$ fileKey ][$ scopeKey ]);
238+ }
239+
240+
241+ /**
242+ * Get a stable cache key for the namespace containing a token.
243+ *
244+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
245+ * @param int $stackPtr The token being checked.
246+ *
247+ * @return string
248+ */
249+ private function getNamespaceScopeKey (File $ phpcsFile , int $ stackPtr )
250+ {
251+ $ namespacePtr = $ phpcsFile ->getCondition ($ stackPtr , T_NAMESPACE );
252+ if ($ namespacePtr === false ) {
253+ return 'global ' ;
254+ }
255+
256+ return 'namespace: ' . $ namespacePtr ;
257+ }
258+
259+
260+ /**
261+ * Determine whether a token is at file scope or directly within a namespace.
262+ *
263+ * @param array<string, mixed> $token Token data.
264+ *
265+ * @return bool
266+ */
267+ private function isInGlobalOrNamespaceScope (array $ token )
268+ {
269+ if (empty ($ token ['conditions ' ]) === true ) {
270+ return true ;
271+ }
272+
273+ if (count ($ token ['conditions ' ]) !== 1 ) {
274+ return false ;
275+ }
276+
277+ $ conditions = $ token ['conditions ' ];
278+ reset ($ conditions );
235279
236- return $ result ;
280+ return current ( $ conditions ) === T_NAMESPACE ;
237281 }
238282
239283
0 commit comments