Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ public function process(File $phpcsFile, int $stackPtr)
return;
}

// If the current namespace declares or imports a function named
// "define", any unqualified `define()` call may resolve to that
// function instead of the global one, so the sniff should bow out.
// Fully qualified `\define(...)` calls are unaffected as they come
// in as T_NAME_FULLY_QUALIFIED tokens and are handled below.
if ($tokens[$stackPtr]['code'] === T_STRING
&& $this->namespaceHasCustomDefine($phpcsFile, $stackPtr) === true
) {
return;
}

// If the next non-whitespace token after this token
// is not an opening parenthesis then it is not a function call.
$openBracket = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($stackPtr + 1), null, true);
Expand Down Expand Up @@ -146,4 +157,186 @@ public function process(File $phpcsFile, int $stackPtr)
$phpcsFile->recordMetric($constPtr, 'Constant name case', 'upper');
}
}


/**
* Determine whether the namespace containing a token declares or imports
* a function named "define".
*
* Checks for namespace-level `function define(...)` declarations and
* `use function ...\define;` (or aliased) imports. The result is cached
* per file path and namespace scope to avoid rescanning on every token.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The token being checked.
*
* @return bool
*/
private function namespaceHasCustomDefine(File $phpcsFile, int $stackPtr)
{
static $cache = [];

$fileKey = $phpcsFile->getFilename();
if (isset($cache[$fileKey]) === true) {
$scopeKey = $this->getNamespaceScopeKey($phpcsFile, $stackPtr);
return isset($cache[$fileKey][$scopeKey]);
}

$tokens = $phpcsFile->getTokens();
$cache[$fileKey] = [];

for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
$code = $tokens[$i]['code'];
$scopeKey = $this->getNamespaceScopeKey($phpcsFile, $i);

// Namespace-level `function define(...)` declaration.
if ($code === T_FUNCTION && $this->isInGlobalOrNamespaceScope($tokens[$i]) === true) {
$namePtr = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
if ($namePtr !== false
&& $tokens[$namePtr]['code'] === T_STRING
&& strtolower($tokens[$namePtr]['content']) === 'define'
) {
$cache[$fileKey][$scopeKey] = true;
}

continue;
}

// `use function ...define;` import at file or namespace scope only.
if ($code === T_USE && $this->isInGlobalOrNamespaceScope($tokens[$i]) === true) {
$next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
if ($next === false || $tokens[$next]['code'] !== T_STRING
|| strtolower($tokens[$next]['content']) !== 'function'
) {
continue;
}

$end = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_USE_GROUP], ($next + 1));
if ($end === false) {
continue;
}

if ($this->useListImportsDefine($phpcsFile, $next, $end) === true) {
$cache[$fileKey][$scopeKey] = true;
continue;
}

// Group use statement: walk the body looking for a `define` import.
if ($tokens[$end]['code'] === T_OPEN_USE_GROUP) {
$groupEnd = $phpcsFile->findNext(T_CLOSE_USE_GROUP, ($end + 1));
if ($groupEnd !== false
&& $this->useListImportsDefine($phpcsFile, $end, $groupEnd) === true
) {
$cache[$fileKey][$scopeKey] = true;
}
}
}
}

$scopeKey = $this->getNamespaceScopeKey($phpcsFile, $stackPtr);
return isset($cache[$fileKey][$scopeKey]);
}


/**
* Get a stable cache key for the namespace containing a token.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The token being checked.
*
* @return string
*/
private function getNamespaceScopeKey(File $phpcsFile, int $stackPtr)
{
$namespacePtr = $phpcsFile->getCondition($stackPtr, T_NAMESPACE);
if ($namespacePtr === false) {
return 'global';
}

return 'namespace:' . $namespacePtr;
}


/**
* Determine whether a token is at file scope or directly within a namespace.
*
* @param array<string, mixed> $token Token data.
*
* @return bool
*/
private function isInGlobalOrNamespaceScope(array $token)
{
if (empty($token['conditions']) === true) {
return true;
}

if (count($token['conditions']) !== 1) {
return false;
}

$conditions = $token['conditions'];
reset($conditions);

return current($conditions) === T_NAMESPACE;
}


/**
* Inspect a `use function` import list for a `define` import.
*
* Handles plain imports (`use function Foo\define;`), aliases away from
* `define` (`use function Foo\define as something;` — does NOT count) and
* aliases to `define` (`use function Foo\bar as define;` — counts).
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $start Position to start scanning after.
* @param int $end Position to stop scanning at.
*
* @return bool
*/
private function useListImportsDefine(File $phpcsFile, int $start, int $end)
{
$tokens = $phpcsFile->getTokens();

for ($j = ($start + 1); $j < $end; $j++) {
$code = $tokens[$j]['code'];

// Explicit alias: `... as define`.
if ($code === T_AS) {
$aliasPtr = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($j + 1), $end, true);
if ($aliasPtr !== false
&& $tokens[$aliasPtr]['code'] === T_STRING
&& strtolower($tokens[$aliasPtr]['content']) === 'define'
) {
return true;
}

continue;
}

if ($code !== T_STRING
&& $code !== T_NAME_QUALIFIED
&& $code !== T_NAME_FULLY_QUALIFIED
) {
continue;
}

$segments = explode('\\', $tokens[$j]['content']);
$last = strtolower(end($segments));
if ($last !== 'define') {
continue;
}

// Skip if the next non-empty token is `as` — the import is renamed
// and is therefore not in scope as `define`.
$after = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($j + 1), $end, true);
if ($after !== false && $tokens[$after]['code'] === T_AS) {
continue;
}

return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ define(condition() ? 'name1' : 'name2', 'sniff should bow out');

$callable = define(...);

// Valid if outside the global namespace. Sniff should bow out.
function define($param) {}

class MyClass {
public function define($param) {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Foo;

// These are calls to the namespaced function `Foo\define()`,
// which is declared further down in the file.
define('name', 'value');
namespace\define('name', 'value');

// These are calls to other namespaced functions, never the global one.
Sub\define('name', 'value');
\My\Other\NS\define('name', 'value');

// Calls to the global function via the leading backslash MUST still be checked.
\define('VALID_NAME', true);

// Lowercase here SHOULD be flagged: the leading backslash forces global resolution.
\define('lowercase_global', 'value');

function define($name, $value) {
// Do something.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Foo;

use function Bar\define;
use function Bar\{otherFunc, define as renamedDefine};

// This is a call to the imported namespaced function, not the global one.
define('name', 'value');

// Lowercase here SHOULD be flagged: the leading backslash forces global resolution.
\define('lowercase_global', 'value');
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Foo;

// `define` is aliased AWAY: the local symbol is `something`, not `define`.
use function Bar\define as something;

define('name', 'value');
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Foo {
function define($name, $value) {
// Do something.
}

define('name', 'value');
}

namespace Bar {
// This must still be flagged: Foo's custom define() is not in scope here.
define('lowercase_global', 'value');
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,38 @@ public function getErrorList($testFile = '')
51 => 1,
71 => 1,
73 => 1,
94 => 1,
91 => 1,
];

case 'UpperCaseConstantNameUnitTest.6.inc':
return [
// Only the fully qualified `\define()` call should be flagged;
// the unqualified `define()` calls may resolve to the local
// `Foo\define` function declared further down in the file.
18 => 1,
];

case 'UpperCaseConstantNameUnitTest.7.inc':
return [
// `use function Bar\define;` brings a `define` symbol into scope,
// so unqualified `define()` calls must not be flagged. Only the
// fully qualified `\define()` call is.
12 => 1,
];

case 'UpperCaseConstantNameUnitTest.8.inc':
return [
// `use function Bar\define as something;` aliases AWAY from
// `define`, so the local `define` symbol is unaffected and
// unqualified `define()` calls should still be flagged.
8 => 1,
];

case 'UpperCaseConstantNameUnitTest.9.inc':
return [
// A custom define() in one namespace must not suppress
// checks for bare define() calls in a different namespace.
13 => 1,
];

default:
Expand Down
Loading