Skip to content

Commit 436a670

Browse files
committed
Generic/UpperCaseConstantName: make custom define cache namespace-aware
1 parent cf3c5e6 commit 436a670

3 files changed

Lines changed: 91 additions & 26 deletions

File tree

src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Foo {
4+
function define($name, $value) {
5+
// Do something.
6+
}
7+
8+
define('name', 'value');
9+
}
10+
11+
namespace Bar {
12+
// This must still be flagged: Foo's custom define() is not in scope here.
13+
define('lowercase_global', 'value');
14+
}

src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ public function getErrorList($testFile = '')
7676
8 => 1,
7777
];
7878

79+
case 'UpperCaseConstantNameUnitTest.9.inc':
80+
return [
81+
// A custom define() in one namespace must not suppress
82+
// checks for bare define() calls in a different namespace.
83+
13 => 1,
84+
];
85+
7986
default:
8087
return [];
8188
}

0 commit comments

Comments
 (0)