Update dependencies

This commit is contained in:
grandeljay 2022-06-21 18:04:22 +02:00
parent 03788f71ff
commit 8ea13a5173
132 changed files with 24789 additions and 805 deletions

View file

@ -1,6 +1,7 @@
{
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1"
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
"wp-coding-standards/wpcs": "^2.3"
},
"require": {
"embed/embed": "^4.3",

109
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a1fb82d56813359d39e2e661463e8dd0",
"content-hash": "d34836b165b17de57def80469fb24ee5",
"packages": [
{
"name": "composer/ca-bundle",
@ -435,22 +435,22 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.3",
"version": "7.4.5",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab"
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/74a8602c6faec9ef74b7a9391ac82c5e65b1cdab",
"reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5",
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
"guzzlehttp/psr7": "^1.9 || ^2.4",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@ -539,7 +539,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.3"
"source": "https://github.com/guzzle/guzzle/tree/7.4.5"
},
"funding": [
{
@ -555,7 +555,7 @@
"type": "tidelift"
}
],
"time": "2022-05-25T13:24:33+00:00"
"time": "2022-06-20T22:16:13+00:00"
},
{
"name": "guzzlehttp/promises",
@ -643,16 +643,16 @@
},
{
"name": "guzzlehttp/psr7",
"version": "2.2.1",
"version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2"
"reference": "13388f00956b1503577598873fffb5ae994b5737"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/13388f00956b1503577598873fffb5ae994b5737",
"reference": "13388f00956b1503577598873fffb5ae994b5737",
"shasum": ""
},
"require": {
@ -676,7 +676,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
"dev-master": "2.4-dev"
}
},
"autoload": {
@ -738,7 +738,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.2.1"
"source": "https://github.com/guzzle/psr7/tree/2.4.0"
},
"funding": [
{
@ -754,7 +754,7 @@
"type": "tidelift"
}
],
"time": "2022-03-20T21:55:58+00:00"
"time": "2022-06-20T21:43:11+00:00"
},
{
"name": "ml/iri",
@ -1160,25 +1160,25 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.0.1",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c"
"reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918",
"reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918",
"shasum": ""
},
"require": {
"php": ">=8.0.2"
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
"dev-main": "3.1-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -1207,7 +1207,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.1"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0"
},
"funding": [
{
@ -1223,7 +1223,7 @@
"type": "tidelift"
}
],
"time": "2022-01-02T09:55:41+00:00"
"time": "2022-02-25T11:15:52+00:00"
},
{
"name": "symfony/polyfill-php80",
@ -1449,16 +1449,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.0",
"version": "3.7.1",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563"
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563",
"reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"shasum": ""
},
"require": {
@ -1501,7 +1501,58 @@
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2022-06-13T06:31:38+00:00"
"time": "2022-06-18T07:21:10+00:00"
},
{
"name": "wp-coding-standards/wpcs",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
"reference": "7da1894633f168fe244afc6de00d141f27517b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62",
"reference": "7da1894633f168fe244afc6de00d141f27517b62",
"shasum": ""
},
"require": {
"php": ">=5.4",
"squizlabs/php_codesniffer": "^3.3.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6",
"phpcompatibility/php-compatibility": "^9.0",
"phpcsstandards/phpcsdevtools": "^1.0",
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
},
"type": "phpcodesniffer-standard",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Contributors",
"homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors"
}
],
"description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
"keywords": [
"phpcs",
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
},
"time": "2020-05-13T23:57:56+00:00"
}
],
"aliases": [],
@ -1511,5 +1562,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.0.0"
}

View file

@ -8,7 +8,7 @@
namespace wishthis;
$api = true;
$api = true;
$response = array(
'success' => false,
);

View file

@ -42,75 +42,30 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
@ -120,47 +75,28 @@ class ClassLoader
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
@ -175,11 +111,9 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
@ -222,13 +156,11 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
@ -272,10 +204,8 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
@ -290,12 +220,10 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
@ -315,8 +243,6 @@ class ClassLoader
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
@ -339,8 +265,6 @@ class ClassLoader
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
@ -361,8 +285,6 @@ class ClassLoader
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
@ -383,8 +305,6 @@ class ClassLoader
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
@ -404,8 +324,6 @@ class ClassLoader
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
@ -420,7 +338,7 @@ class ClassLoader
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
@ -429,8 +347,6 @@ class ClassLoader
return true;
}
return null;
}
/**
@ -485,11 +401,6 @@ class ClassLoader
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
@ -561,10 +472,6 @@ class ClassLoader
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{

View file

@ -1,350 +1,511 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
*/
private static $installed;
private static $installed = array (
'root' =>
array (
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'aliases' =>
array (
),
'reference' => '03788f71ffb580b6f1b6469fdc74463f381928d8',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'aliases' =>
array (
),
'reference' => '03788f71ffb580b6f1b6469fdc74463f381928d8',
),
'composer/ca-bundle' =>
array (
'pretty_version' => '1.3.2',
'version' => '1.3.2.0',
'aliases' =>
array (
),
'reference' => 'fd5dd441932a7e10ca6e5b490e272d34c8430640',
),
'dealerdirect/phpcodesniffer-composer-installer' =>
array (
'pretty_version' => 'v0.7.2',
'version' => '0.7.2.0',
'aliases' =>
array (
),
'reference' => '1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db',
),
'embed/embed' =>
array (
'pretty_version' => 'v4.4.4',
'version' => '4.4.4.0',
'aliases' =>
array (
),
'reference' => '52c2d77f58672868346ae09b0fa1eecc818bdf42',
),
'gettext/gettext' =>
array (
'pretty_version' => 'v5.6.1',
'version' => '5.6.1.0',
'aliases' =>
array (
),
'reference' => '017e249601d32b9a88c2eb4c10eac89bf582a7d3',
),
'gettext/languages' =>
array (
'pretty_version' => '2.9.0',
'version' => '2.9.0.0',
'aliases' =>
array (
),
'reference' => 'ed56dd2c7f4024cc953ed180d25f02f2640e3ffa',
),
'gettext/translator' =>
array (
'pretty_version' => 'v1.1.1',
'version' => '1.1.1.0',
'aliases' =>
array (
),
'reference' => 'b18ff33e8203de623854561f5e47e992fc5c50bb',
),
'grandel/include-directory' =>
array (
'pretty_version' => 'v0.2.2',
'version' => '0.2.2.0',
'aliases' =>
array (
),
'reference' => 'a5c830e8f1527c818b521ab18f2accecb02f9919',
),
'guzzlehttp/guzzle' =>
array (
'pretty_version' => '7.4.5',
'version' => '7.4.5.0',
'aliases' =>
array (
),
'reference' => '1dd98b0564cb3f6bd16ce683cb755f94c10fbd82',
),
'guzzlehttp/promises' =>
array (
'pretty_version' => '1.5.1',
'version' => '1.5.1.0',
'aliases' =>
array (
),
'reference' => 'fe752aedc9fd8fcca3fe7ad05d419d32998a06da',
),
'guzzlehttp/psr7' =>
array (
'pretty_version' => '2.4.0',
'version' => '2.4.0.0',
'aliases' =>
array (
),
'reference' => '13388f00956b1503577598873fffb5ae994b5737',
),
'ml/iri' =>
array (
'pretty_version' => '1.1.4',
'version' => '1.1.4.0',
'aliases' =>
array (
),
'reference' => 'cbd44fa913e00ea624241b38cefaa99da8d71341',
),
'ml/json-ld' =>
array (
'pretty_version' => '1.2.0',
'version' => '1.2.0.0',
'aliases' =>
array (
),
'reference' => 'c74a1aed5979ed1cfb1be35a55a305fd30e30b93',
),
'oscarotero/html-parser' =>
array (
'pretty_version' => 'v0.1.6',
'version' => '0.1.6.0',
'aliases' =>
array (
),
'reference' => 'b61e92f634d0dc184339d24630a6968d3ac64ded',
),
'psr/http-client' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
),
'psr/http-client-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-factory' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
),
'psr/http-factory-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-message' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
),
'psr/http-message-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'qferr/mjml-php' =>
array (
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'aliases' =>
array (
),
'reference' => 'c6ea36c190e304e399a957f7e03b5a378faf41b9',
),
'ralouphie/getallheaders' =>
array (
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'aliases' =>
array (
),
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
),
'squizlabs/php_codesniffer' =>
array (
'pretty_version' => '3.7.1',
'version' => '3.7.1.0',
'aliases' =>
array (
),
'reference' => '1359e176e9307e906dc3d890bcc9603ff6d90619',
),
'symfony/deprecation-contracts' =>
array (
'pretty_version' => 'v3.1.0',
'version' => '3.1.0.0',
'aliases' =>
array (
),
'reference' => '07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918',
),
'symfony/polyfill-php80' =>
array (
'pretty_version' => 'v1.26.0',
'version' => '1.26.0.0',
'aliases' =>
array (
),
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
),
'symfony/process' =>
array (
'pretty_version' => 'v5.4.8',
'version' => '5.4.8.0',
'aliases' =>
array (
),
'reference' => '597f3fff8e3e91836bb0bd38f5718b56ddbde2f3',
),
'wp-coding-standards/wpcs' =>
array (
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'aliases' =>
array (
),
'reference' => '7da1894633f168fe244afc6de00d141f27517b62',
),
),
);
private static $canGetVendors;
private static $installedByVendor = array();
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
$installed[] = self::$installed;
return $installed;
}
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
public static function isInstalled($packageName)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return true;
}
}
return false;
}
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
public static function getRawData()
{
return self::$installed;
}
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
}
}
}
$installed[] = self::$installed;
return $installed;
}
}

View file

@ -65,16 +65,11 @@ class ComposerAutoloaderInit5f3db9fc1d0cf1dd6a77a1d84501b4b1
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire5f3db9fc1d0cf1dd6a77a1d84501b4b1($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

View file

@ -525,23 +525,23 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.3",
"version_normalized": "7.4.3.0",
"version": "7.4.5",
"version_normalized": "7.4.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab"
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/74a8602c6faec9ef74b7a9391ac82c5e65b1cdab",
"reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5",
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
"guzzlehttp/psr7": "^1.9 || ^2.4",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@ -561,7 +561,7 @@
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"time": "2022-05-25T13:24:33+00:00",
"time": "2022-06-20T22:16:13+00:00",
"type": "library",
"extra": {
"branch-alias": {
@ -632,7 +632,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.3"
"source": "https://github.com/guzzle/guzzle/tree/7.4.5"
},
"funding": [
{
@ -739,17 +739,17 @@
},
{
"name": "guzzlehttp/psr7",
"version": "2.2.1",
"version_normalized": "2.2.1.0",
"version": "2.4.0",
"version_normalized": "2.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2"
"reference": "13388f00956b1503577598873fffb5ae994b5737"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/13388f00956b1503577598873fffb5ae994b5737",
"reference": "13388f00956b1503577598873fffb5ae994b5737",
"shasum": ""
},
"require": {
@ -770,11 +770,11 @@
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"time": "2022-03-20T21:55:58+00:00",
"time": "2022-06-20T21:43:11+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
"dev-master": "2.4-dev"
}
},
"installation-source": "dist",
@ -837,7 +837,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.2.1"
"source": "https://github.com/guzzle/psr7/tree/2.4.0"
},
"funding": [
{
@ -1283,17 +1283,17 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.0",
"version_normalized": "3.7.0.0",
"version": "3.7.1",
"version_normalized": "3.7.1.0",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563"
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563",
"reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"shasum": ""
},
"require": {
@ -1305,7 +1305,7 @@
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"time": "2022-06-13T06:31:38+00:00",
"time": "2022-06-18T07:21:10+00:00",
"bin": [
"bin/phpcs",
"bin/phpcbf"
@ -1342,27 +1342,27 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.0.1",
"version_normalized": "3.0.1.0",
"version": "v3.1.0",
"version_normalized": "3.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c"
"reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918",
"reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918",
"shasum": ""
},
"require": {
"php": ">=8.0.2"
"php": ">=8.1"
},
"time": "2022-01-02T09:55:41+00:00",
"time": "2022-02-25T11:15:52+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
"dev-main": "3.1-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -1392,7 +1392,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.1"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0"
},
"funding": [
{
@ -1560,11 +1560,66 @@
}
],
"install-path": "../symfony/process"
},
{
"name": "wp-coding-standards/wpcs",
"version": "2.3.0",
"version_normalized": "2.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
"reference": "7da1894633f168fe244afc6de00d141f27517b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62",
"reference": "7da1894633f168fe244afc6de00d141f27517b62",
"shasum": ""
},
"require": {
"php": ">=5.4",
"squizlabs/php_codesniffer": "^3.3.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6",
"phpcompatibility/php-compatibility": "^9.0",
"phpcsstandards/phpcsdevtools": "^1.0",
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
},
"time": "2020-05-13T23:57:56+00:00",
"type": "phpcodesniffer-standard",
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Contributors",
"homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors"
}
],
"description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
"keywords": [
"phpcs",
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
},
"install-path": "../wp-coding-standards/wpcs"
}
],
"dev": true,
"dev-package-names": [
"dealerdirect/phpcodesniffer-composer-installer",
"squizlabs/php_codesniffer"
"squizlabs/php_codesniffer",
"wp-coding-standards/wpcs"
]
}

View file

@ -1,239 +1,252 @@
<?php return array(
'root' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'e38691fe5efc940251b1b92d3fe3438a3088f3a7',
'name' => '__root__',
'dev' => true,
<?php return array (
'root' =>
array (
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'aliases' =>
array (
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => 'e38691fe5efc940251b1b92d3fe3438a3088f3a7',
'dev_requirement' => false,
),
'composer/ca-bundle' => array(
'pretty_version' => '1.3.2',
'version' => '1.3.2.0',
'type' => 'library',
'install_path' => __DIR__ . '/./ca-bundle',
'aliases' => array(),
'reference' => 'fd5dd441932a7e10ca6e5b490e272d34c8430640',
'dev_requirement' => false,
),
'dealerdirect/phpcodesniffer-composer-installer' => array(
'pretty_version' => 'v0.7.2',
'version' => '0.7.2.0',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../dealerdirect/phpcodesniffer-composer-installer',
'aliases' => array(),
'reference' => '1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db',
'dev_requirement' => true,
),
'embed/embed' => array(
'pretty_version' => 'v4.4.4',
'version' => '4.4.4.0',
'type' => 'library',
'install_path' => __DIR__ . '/../embed/embed',
'aliases' => array(),
'reference' => '52c2d77f58672868346ae09b0fa1eecc818bdf42',
'dev_requirement' => false,
),
'gettext/gettext' => array(
'pretty_version' => 'v5.6.1',
'version' => '5.6.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../gettext/gettext',
'aliases' => array(),
'reference' => '017e249601d32b9a88c2eb4c10eac89bf582a7d3',
'dev_requirement' => false,
),
'gettext/languages' => array(
'pretty_version' => '2.9.0',
'version' => '2.9.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../gettext/languages',
'aliases' => array(),
'reference' => 'ed56dd2c7f4024cc953ed180d25f02f2640e3ffa',
'dev_requirement' => false,
),
'gettext/translator' => array(
'pretty_version' => 'v1.1.1',
'version' => '1.1.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../gettext/translator',
'aliases' => array(),
'reference' => 'b18ff33e8203de623854561f5e47e992fc5c50bb',
'dev_requirement' => false,
),
'grandel/include-directory' => array(
'pretty_version' => 'v0.2.2',
'version' => '0.2.2.0',
'type' => 'library',
'install_path' => __DIR__ . '/../grandel/include-directory',
'aliases' => array(),
'reference' => 'a5c830e8f1527c818b521ab18f2accecb02f9919',
'dev_requirement' => false,
),
'guzzlehttp/guzzle' => array(
'pretty_version' => '7.4.3',
'version' => '7.4.3.0',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
'aliases' => array(),
'reference' => '74a8602c6faec9ef74b7a9391ac82c5e65b1cdab',
'dev_requirement' => false,
),
'guzzlehttp/promises' => array(
'pretty_version' => '1.5.1',
'version' => '1.5.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/promises',
'aliases' => array(),
'reference' => 'fe752aedc9fd8fcca3fe7ad05d419d32998a06da',
'dev_requirement' => false,
),
'guzzlehttp/psr7' => array(
'pretty_version' => '2.2.1',
'version' => '2.2.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
'aliases' => array(),
'reference' => 'c94a94f120803a18554c1805ef2e539f8285f9a2',
'dev_requirement' => false,
),
'ml/iri' => array(
'pretty_version' => '1.1.4',
'version' => '1.1.4.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ml/iri/ML/IRI',
'aliases' => array(),
'reference' => 'cbd44fa913e00ea624241b38cefaa99da8d71341',
'dev_requirement' => false,
),
'ml/json-ld' => array(
'pretty_version' => '1.2.0',
'version' => '1.2.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ml/json-ld',
'aliases' => array(),
'reference' => 'c74a1aed5979ed1cfb1be35a55a305fd30e30b93',
'dev_requirement' => false,
),
'oscarotero/html-parser' => array(
'pretty_version' => 'v0.1.6',
'version' => '0.1.6.0',
'type' => 'library',
'install_path' => __DIR__ . '/../oscarotero/html-parser',
'aliases' => array(),
'reference' => 'b61e92f634d0dc184339d24630a6968d3ac64ded',
'dev_requirement' => false,
),
'psr/http-client' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-client',
'aliases' => array(),
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
'dev_requirement' => false,
),
'psr/http-client-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/http-factory' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(),
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
'dev_requirement' => false,
),
'psr/http-factory-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/http-message' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
'dev_requirement' => false,
),
'psr/http-message-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'qferr/mjml-php' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../qferr/mjml-php',
'aliases' => array(),
'reference' => 'c6ea36c190e304e399a957f7e03b5a378faf41b9',
'dev_requirement' => false,
),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'type' => 'library',
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
'aliases' => array(),
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
'dev_requirement' => false,
),
'squizlabs/php_codesniffer' => array(
'pretty_version' => '3.7.0',
'version' => '3.7.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../squizlabs/php_codesniffer',
'aliases' => array(),
'reference' => 'a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563',
'dev_requirement' => true,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.0.1',
'version' => '3.0.1.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'reference' => '26954b3d62a6c5fd0ea8a2a00c0353a14978d05c',
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.26.0',
'version' => '1.26.0.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
'dev_requirement' => false,
),
'symfony/process' => array(
'pretty_version' => 'v5.4.8',
'version' => '5.4.8.0',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/process',
'aliases' => array(),
'reference' => '597f3fff8e3e91836bb0bd38f5718b56ddbde2f3',
'dev_requirement' => false,
),
'reference' => '03788f71ffb580b6f1b6469fdc74463f381928d8',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'aliases' =>
array (
),
'reference' => '03788f71ffb580b6f1b6469fdc74463f381928d8',
),
'composer/ca-bundle' =>
array (
'pretty_version' => '1.3.2',
'version' => '1.3.2.0',
'aliases' =>
array (
),
'reference' => 'fd5dd441932a7e10ca6e5b490e272d34c8430640',
),
'dealerdirect/phpcodesniffer-composer-installer' =>
array (
'pretty_version' => 'v0.7.2',
'version' => '0.7.2.0',
'aliases' =>
array (
),
'reference' => '1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db',
),
'embed/embed' =>
array (
'pretty_version' => 'v4.4.4',
'version' => '4.4.4.0',
'aliases' =>
array (
),
'reference' => '52c2d77f58672868346ae09b0fa1eecc818bdf42',
),
'gettext/gettext' =>
array (
'pretty_version' => 'v5.6.1',
'version' => '5.6.1.0',
'aliases' =>
array (
),
'reference' => '017e249601d32b9a88c2eb4c10eac89bf582a7d3',
),
'gettext/languages' =>
array (
'pretty_version' => '2.9.0',
'version' => '2.9.0.0',
'aliases' =>
array (
),
'reference' => 'ed56dd2c7f4024cc953ed180d25f02f2640e3ffa',
),
'gettext/translator' =>
array (
'pretty_version' => 'v1.1.1',
'version' => '1.1.1.0',
'aliases' =>
array (
),
'reference' => 'b18ff33e8203de623854561f5e47e992fc5c50bb',
),
'grandel/include-directory' =>
array (
'pretty_version' => 'v0.2.2',
'version' => '0.2.2.0',
'aliases' =>
array (
),
'reference' => 'a5c830e8f1527c818b521ab18f2accecb02f9919',
),
'guzzlehttp/guzzle' =>
array (
'pretty_version' => '7.4.5',
'version' => '7.4.5.0',
'aliases' =>
array (
),
'reference' => '1dd98b0564cb3f6bd16ce683cb755f94c10fbd82',
),
'guzzlehttp/promises' =>
array (
'pretty_version' => '1.5.1',
'version' => '1.5.1.0',
'aliases' =>
array (
),
'reference' => 'fe752aedc9fd8fcca3fe7ad05d419d32998a06da',
),
'guzzlehttp/psr7' =>
array (
'pretty_version' => '2.4.0',
'version' => '2.4.0.0',
'aliases' =>
array (
),
'reference' => '13388f00956b1503577598873fffb5ae994b5737',
),
'ml/iri' =>
array (
'pretty_version' => '1.1.4',
'version' => '1.1.4.0',
'aliases' =>
array (
),
'reference' => 'cbd44fa913e00ea624241b38cefaa99da8d71341',
),
'ml/json-ld' =>
array (
'pretty_version' => '1.2.0',
'version' => '1.2.0.0',
'aliases' =>
array (
),
'reference' => 'c74a1aed5979ed1cfb1be35a55a305fd30e30b93',
),
'oscarotero/html-parser' =>
array (
'pretty_version' => 'v0.1.6',
'version' => '0.1.6.0',
'aliases' =>
array (
),
'reference' => 'b61e92f634d0dc184339d24630a6968d3ac64ded',
),
'psr/http-client' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
),
'psr/http-client-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-factory' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
),
'psr/http-factory-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-message' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
),
'psr/http-message-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'qferr/mjml-php' =>
array (
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'aliases' =>
array (
),
'reference' => 'c6ea36c190e304e399a957f7e03b5a378faf41b9',
),
'ralouphie/getallheaders' =>
array (
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'aliases' =>
array (
),
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
),
'squizlabs/php_codesniffer' =>
array (
'pretty_version' => '3.7.1',
'version' => '3.7.1.0',
'aliases' =>
array (
),
'reference' => '1359e176e9307e906dc3d890bcc9603ff6d90619',
),
'symfony/deprecation-contracts' =>
array (
'pretty_version' => 'v3.1.0',
'version' => '3.1.0.0',
'aliases' =>
array (
),
'reference' => '07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918',
),
'symfony/polyfill-php80' =>
array (
'pretty_version' => 'v1.26.0',
'version' => '1.26.0.0',
'aliases' =>
array (
),
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
),
'symfony/process' =>
array (
'pretty_version' => 'v5.4.8',
'version' => '5.4.8.0',
'aliases' =>
array (
),
'reference' => '597f3fff8e3e91836bb0bd38f5718b56ddbde2f3',
),
'wp-coding-standards/wpcs' =>
array (
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'aliases' =>
array (
),
'reference' => '7da1894633f168fe244afc6de00d141f27517b62',
),
),
);

View file

@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80002)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.2". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {

View file

@ -2,6 +2,16 @@
Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
## 7.4.5 - 2022-06-20
* Fix change in port should be considered a change in origin
* Fix `CURLOPT_HTTPAUTH` option not cleared on change of origin
## 7.4.4 - 2022-06-09
* Fix failure to strip Authorization header on HTTP downgrade
* Fix failure to strip the Cookie header on change in host or HTTP downgrade
## 7.4.3 - 2022-05-25
* Fix cross-domain cookie leakage

View file

@ -44,7 +44,7 @@ We use GitHub issues only to discuss bugs and new features. For support please r
- [Documentation](https://docs.guzzlephp.org)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle)
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/)
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
- [Gitter](https://gitter.im/guzzle/guzzle)
@ -73,10 +73,10 @@ composer require guzzlehttp/guzzle
[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
[guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
[guzzle-7-repo]: https://github.com/guzzle/guzzle
[guzzle-3-docs]: http://guzzle3.readthedocs.org
[guzzle-5-docs]: http://docs.guzzlephp.org/en/5.3/
[guzzle-6-docs]: http://docs.guzzlephp.org/en/6.5/
[guzzle-7-docs]: http://docs.guzzlephp.org/en/latest/
[guzzle-3-docs]: https://guzzle3.readthedocs.io/
[guzzle-5-docs]: https://docs.guzzlephp.org/en/5.3/
[guzzle-6-docs]: https://docs.guzzlephp.org/en/6.5/
[guzzle-7-docs]: https://docs.guzzlephp.org/en/latest/
## Security

View file

@ -54,7 +54,7 @@
"php": "^7.2.5 || ^8.0",
"ext-json": "*",
"guzzlehttp/promises": "^1.5",
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
"guzzlehttp/psr7": "^1.9 || ^2.4",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},

View file

@ -88,10 +88,8 @@ class RedirectMiddleware
$this->guardMax($request, $response, $options);
$nextRequest = $this->modifyRequest($request, $options, $response);
// If authorization is handled by curl, unset it if host is different.
if ($request->getUri()->getHost() !== $nextRequest->getUri()->getHost()
&& defined('\CURLOPT_HTTPAUTH')
) {
// If authorization is handled by curl, unset it if URI is cross-origin.
if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) {
unset(
$options['curl'][\CURLOPT_HTTPAUTH],
$options['curl'][\CURLOPT_USERPWD]
@ -142,7 +140,7 @@ class RedirectMiddleware
}
/**
* Check for too many redirects
* Check for too many redirects.
*
* @throws TooManyRedirectsException Too many redirects.
*/
@ -178,7 +176,7 @@ class RedirectMiddleware
$modify['body'] = '';
}
$uri = $this->redirectUri($request, $response, $protocols);
$uri = self::redirectUri($request, $response, $protocols);
if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) {
$idnOptions = ($options['idn_conversion'] === true) ? \IDNA_DEFAULT : $options['idn_conversion'];
$uri = Utils::idnUriConvert($uri, $idnOptions);
@ -198,19 +196,23 @@ class RedirectMiddleware
$modify['remove_headers'][] = 'Referer';
}
// Remove Authorization header if host is different.
if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
// Remove Authorization and Cookie headers if URI is cross-origin.
if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) {
$modify['remove_headers'][] = 'Authorization';
$modify['remove_headers'][] = 'Cookie';
}
return Psr7\Utils::modifyRequest($request, $modify);
}
/**
* Set the appropriate URL on the request based on the location header
* Set the appropriate URL on the request based on the location header.
*/
private function redirectUri(RequestInterface $request, ResponseInterface $response, array $protocols): UriInterface
{
private static function redirectUri(
RequestInterface $request,
ResponseInterface $response,
array $protocols
): UriInterface {
$location = Psr7\UriResolver::resolve(
$request->getUri(),
new Psr7\Uri($response->getHeaderLine('Location'))

View file

@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## 2.4.0 - 2022-06-20
### Added
- Added provisional PHP 8.2 support
- Added `UriComparator::isCrossOrigin` method
## 2.3.0 - 2022-06-09
### Fixed
- Added `Header::splitList` method
- Added `Utils::tryGetContents` method
- Improved `Stream::getContents` method
- Updated mimetype mappings
## 2.2.2 - 2022-06-08
### Fixed
- Fix `Message::parseRequestUri` for numeric headers
- Re-wrap exceptions thrown in `fread` into runtime exceptions
- Throw an exception when multipart options is misformatted
## 2.2.1 - 2022-03-20
### Fixed

View file

@ -1,6 +1,6 @@
# PSR-7 Message Implementation
This repository contains a full [PSR-7](http://www.php-fig.org/psr/psr-7/)
This repository contains a full [PSR-7](https://www.php-fig.org/psr/psr-7/)
message implementation, several stream decorators, and some helpful
functionality like query string parsing.
@ -527,6 +527,17 @@ When fopen fails, PHP normally raises a warning. This function adds an
error handler that checks for errors and throws an exception instead.
## `GuzzleHttp\Psr7\Utils::tryGetContents`
`public static function tryGetContents(resource $stream): string`
Safely gets the contents of a given stream.
When stream_get_contents fails, PHP normally raises a warning. This
function adds an error handler that checks for errors and throws an
exception instead.
## `GuzzleHttp\Psr7\Utils::uriFor`
`public static function uriFor(string|UriInterface $uri): UriInterface`
@ -658,7 +669,7 @@ manually but instead is used indirectly via `Psr\Http\Message\UriInterface::__to
`public static function fromParts(array $parts): UriInterface`
Creates a URI from a hash of [`parse_url`](http://php.net/manual/en/function.parse-url.php) components.
Creates a URI from a hash of [`parse_url`](https://www.php.net/manual/en/function.parse-url.php) components.
### `GuzzleHttp\Psr7\Uri::withQueryValue`
@ -683,6 +694,16 @@ associative array of key => value.
Creates a new URI with a specific query string value removed. Any existing query string values that exactly match the
provided key are removed.
## Cross-Origin Detection
`GuzzleHttp\Psr7\UriComparator` provides methods to determine if a modified URL should be considered cross-origin.
### `GuzzleHttp\Psr7\UriComparator::isCrossOrigin`
`public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool`
Determines if a modified URL should be considered cross-origin with respect to an original URL.
## Reference Resolution
`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according
@ -808,14 +829,24 @@ This of course assumes they will be resolved against the same base URI. If this
equivalence or difference of relative references does not mean anything.
## Version Guidance
| Version | Status | PHP Version |
|---------|----------------|------------------|
| 1.x | Security fixes | >=5.4,<8.1 |
| 2.x | Latest | ^7.2.5 \|\| ^8.0 |
## Security
If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/psr7/security/policy) for more information.
## License
Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information.
## For Enterprise
Available as part of the Tidelift Subscription

View file

@ -79,7 +79,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
"dev-master": "2.4-dev"
}
},
"config": {
@ -87,9 +87,6 @@
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
"sort-packages": true
}
}

View file

@ -20,6 +20,11 @@ final class CachingStream implements StreamInterface
/** @var int Number of bytes to skip reading due to a write on the buffer */
private $skipReadBytes = 0;
/**
* @var StreamInterface
*/
private $stream;
/**
* We will treat the buffer object as the body of the stream
*

View file

@ -17,6 +17,9 @@ final class DroppingStream implements StreamInterface
/** @var int */
private $maxLength;
/** @var StreamInterface */
private $stream;
/**
* @param StreamInterface $stream Underlying stream to decorate.
* @param int $maxLength Maximum size before dropping data.

View file

@ -12,6 +12,7 @@ use Psr\Http\Message\StreamInterface;
* Allows for easy testing and extension of a provided stream without needing
* to create a concrete class for a simple extension point.
*/
#[\AllowDynamicProperties]
final class FnStream implements StreamInterface
{
private const SLOTS = [

View file

@ -19,20 +19,22 @@ final class Header
static $trimmed = "\"' \n\t\r";
$params = $matches = [];
foreach (self::normalize($header) as $val) {
$part = [];
foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) {
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
$m = $matches[0];
if (isset($m[1])) {
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
} else {
$part[] = trim($m[0], $trimmed);
foreach ((array) $header as $value) {
foreach (self::splitList($value) as $val) {
$part = [];
foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) {
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
$m = $matches[0];
if (isset($m[1])) {
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
} else {
$part[] = trim($m[0], $trimmed);
}
}
}
}
if ($part) {
$params[] = $part;
if ($part) {
$params[] = $part;
}
}
}
@ -44,25 +46,86 @@ final class Header
* headers into an array of headers with no comma separated values.
*
* @param string|array $header Header to normalize.
*
* @deprecated Use self::splitList() instead.
*/
public static function normalize($header): array
{
$result = [];
foreach ((array) $header as $value) {
foreach ((array) $value as $v) {
if (strpos($v, ',') === false) {
$trimmed = trim($v);
if ($trimmed !== '') {
$result[] = $trimmed;
}
foreach (self::splitList($value) as $parsed) {
$result[] = $parsed;
}
}
return $result;
}
/**
* Splits a HTTP header defined to contain comma-separated list into
* each individual value. Empty values will be removed.
*
* Example headers include 'accept', 'cache-control' and 'if-none-match'.
*
* This method must not be used to parse headers that are not defined as
* a list, such as 'user-agent' or 'set-cookie'.
*
* @param string|string[] $values Header value as returned by MessageInterface::getHeader()
*
* @return string[]
*/
public static function splitList($values): array
{
if (!\is_array($values)) {
$values = [$values];
}
$result = [];
foreach ($values as $value) {
if (!\is_string($value)) {
throw new \TypeError('$header must either be a string or an array containing strings.');
}
$v = '';
$isQuoted = false;
$isEscaped = false;
for ($i = 0, $max = \strlen($value); $i < $max; $i++) {
if ($isEscaped) {
$v .= $value[$i];
$isEscaped = false;
continue;
}
foreach (preg_split('/,(?=([^"]*"([^"]|\\\\.)*")*[^"]*$)/', $v) as $vv) {
$trimmed = trim($vv);
if ($trimmed !== '') {
$result[] = $trimmed;
if (!$isQuoted && $value[$i] === ',') {
$v = \trim($v);
if ($v !== '') {
$result[] = $v;
}
$v = '';
continue;
}
if ($isQuoted && $value[$i] === '\\') {
$isEscaped = true;
$v .= $value[$i];
continue;
}
if ($value[$i] === '"') {
$isQuoted = !$isQuoted;
$v .= $value[$i];
continue;
}
$v .= $value[$i];
}
$v = \trim($v);
if ($v !== '') {
$result[] = $v;
}
}

View file

@ -21,6 +21,9 @@ final class InflateStream implements StreamInterface
{
use StreamDecoratorTrait;
/** @var StreamInterface */
private $stream;
public function __construct(StreamInterface $stream)
{
$resource = StreamWrapper::getResource($stream);

View file

@ -10,6 +10,7 @@ use Psr\Http\Message\StreamInterface;
* Lazily reads or writes to a file that is opened only after an IO operation
* take place on the stream.
*/
#[\AllowDynamicProperties]
final class LazyOpenStream implements StreamInterface
{
use StreamDecoratorTrait;

View file

@ -19,6 +19,9 @@ final class LimitStream implements StreamInterface
/** @var int Limit the number of bytes that can be read */
private $limit;
/** @var StreamInterface */
private $stream;
/**
* @param StreamInterface $stream Stream to wrap
* @param int $limit Total number of bytes to allow to be read

View file

@ -175,6 +175,9 @@ final class Message
public static function parseRequestUri(string $path, array $headers): string
{
$hostKey = array_filter(array_keys($headers), function ($k) {
// Numeric array keys are converted to int by PHP.
$k = (string) $k;
return strtolower($k) === 'host';
});

View file

@ -145,11 +145,9 @@ trait MessageTrait
{
$this->headerNames = $this->headers = [];
foreach ($headers as $header => $value) {
if (is_int($header)) {
// Numeric array keys are converted to int by PHP but having a header name '123' is not forbidden by the spec
// and also allowed in withHeader(). So we need to cast it to string again for the following assertion to pass.
$header = (string) $header;
}
// Numeric array keys are converted to int by PHP.
$header = (string) $header;
$this->assertHeader($header);
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);

View file

@ -32,6 +32,7 @@ final class MimeType
'aep' => 'application/vnd.audiograph',
'afm' => 'application/x-font-type1',
'afp' => 'application/vnd.ibm.modcap',
'age' => 'application/vnd.age',
'ahead' => 'application/vnd.ahead.space',
'ai' => 'application/pdf',
'aif' => 'audio/x-aiff',
@ -60,6 +61,8 @@ final class MimeType
'atomsvc' => 'application/atomsvc+xml',
'atx' => 'application/vnd.antix.game-component',
'au' => 'audio/x-au',
'avci' => 'image/avci',
'avcs' => 'image/avcs',
'avi' => 'video/x-msvideo',
'avif' => 'image/avif',
'aw' => 'application/applixware',
@ -154,6 +157,7 @@ final class MimeType
'com' => 'application/x-msdownload',
'conf' => 'text/plain',
'cpio' => 'application/x-cpio',
'cpl' => 'application/cpl+xml',
'cpp' => 'text/x-c',
'cpt' => 'application/mac-compactpro',
'crd' => 'application/x-mscardfile',
@ -316,6 +320,7 @@ final class MimeType
'gca' => 'application/x-gca-compressed',
'gdl' => 'model/vnd.gdl',
'gdoc' => 'application/vnd.google-apps.document',
'ged' => 'text/vnd.familysearch.gedcom',
'geo' => 'application/vnd.dynageo',
'geojson' => 'application/geo+json',
'gex' => 'application/vnd.geometry-explorer',
@ -576,6 +581,7 @@ final class MimeType
'mpd' => 'application/dash+xml',
'mpe' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'mpf' => 'application/media-policy-dataset+xml',
'mpg' => 'video/mpeg',
'mpg4' => 'video/mp4',
'mpga' => 'audio/mpeg',
@ -719,6 +725,7 @@ final class MimeType
'pgm' => 'image/x-portable-graymap',
'pgn' => 'application/x-chess-pgn',
'pgp' => 'application/pgp',
'phar' => 'application/octet-stream',
'php' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'php4' => 'application/x-httpd-php',
@ -753,7 +760,7 @@ final class MimeType
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'pqa' => 'application/vnd.palm',
'prc' => 'application/x-pilot',
'prc' => 'model/prc',
'pre' => 'application/vnd.lotus-freelance',
'prf' => 'application/pics-rules',
'provx' => 'application/provenance+xml',
@ -993,6 +1000,7 @@ final class MimeType
'txd' => 'application/vnd.genomatix.tuxedo',
'txf' => 'application/vnd.mobius.txf',
'txt' => 'text/plain',
'u3d' => 'model/u3d',
'u8dsn' => 'message/global-delivery-status',
'u8hdr' => 'message/global-headers',
'u8mdn' => 'message/global-disposition-notification',
@ -1089,6 +1097,7 @@ final class MimeType
'webp' => 'image/webp',
'wg' => 'application/vnd.pmi.widget',
'wgt' => 'application/widget',
'wif' => 'application/watcherinfo+xml',
'wks' => 'application/vnd.ms-works',
'wm' => 'video/x-ms-wm',
'wma' => 'audio/x-ms-wma',

View file

@ -17,6 +17,9 @@ final class MultipartStream implements StreamInterface
/** @var string */
private $boundary;
/** @var StreamInterface */
private $stream;
/**
* @param array $elements Array of associative arrays, each containing a
* required "name" key mapping to the form field,
@ -68,6 +71,9 @@ final class MultipartStream implements StreamInterface
$stream = new AppendStream();
foreach ($elements as $element) {
if (!is_array($element)) {
throw new \UnexpectedValueException("An array is expected");
}
$this->addElement($stream, $element);
}

View file

@ -13,6 +13,9 @@ final class NoSeekStream implements StreamInterface
{
use StreamDecoratorTrait;
/** @var StreamInterface */
private $stream;
public function seek($offset, $whence = SEEK_SET): void
{
throw new \RuntimeException('Cannot seek a NoSeekStream');

View file

@ -96,13 +96,11 @@ class Stream implements StreamInterface
throw new \RuntimeException('Stream is detached');
}
$contents = stream_get_contents($this->stream);
if ($contents === false) {
throw new \RuntimeException('Unable to read stream contents');
if (!$this->readable) {
throw new \RuntimeException('Cannot read from non-readable stream');
}
return $contents;
return Utils::tryGetContents($this->stream);
}
public function close(): void
@ -229,7 +227,12 @@ class Stream implements StreamInterface
return '';
}
$string = fread($this->stream, $length);
try {
$string = fread($this->stream, $length);
} catch (\Exception $e) {
throw new \RuntimeException('Unable to read from stream', 0, $e);
}
if (false === $string) {
throw new \RuntimeException('Unable to read from stream');
}

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\UriInterface;
/**
* Provides methods to determine if a modified URL should be considered cross-origin.
*
* @author Graham Campbell
*/
final class UriComparator
{
/**
* Determines if a modified URL should be considered cross-origin with
* respect to an original URL.
*/
public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
{
if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
return true;
}
if ($original->getScheme() !== $modified->getScheme()) {
return true;
}
if (self::computePort($original) !== self::computePort($modified)) {
return true;
}
return false;
}
private static function computePort(UriInterface $uri): int
{
$port = $uri->getPort();
if (null !== $port) {
return $port;
}
return 'https' === $uri->getScheme() ? 443 : 80;
}
private function __construct()
{
// cannot be instantiated
}
}

View file

@ -386,6 +386,53 @@ final class Utils
return $handle;
}
/**
* Safely gets the contents of a given stream.
*
* When stream_get_contents fails, PHP normally raises a warning. This
* function adds an error handler that checks for errors and throws an
* exception instead.
*
* @param resource $stream
*
* @throws \RuntimeException if the stream cannot be read
*/
public static function tryGetContents($stream): string
{
$ex = null;
set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool {
$ex = new \RuntimeException(sprintf(
'Unable to read stream contents: %s',
$errstr
));
return true;
});
try {
/** @var string|false $contents */
$contents = stream_get_contents($stream);
if ($contents === false) {
$ex = new \RuntimeException('Unable to read stream contents');
}
} catch (\Throwable $e) {
$ex = new \RuntimeException(sprintf(
'Unable to read stream contents: %s',
$e->getMessage()
), 0, $e);
}
restore_error_handler();
if ($ex) {
/** @var $ex \RuntimeException */
throw $ex;
}
return $contents;
}
/**
* Returns a UriInterface for the given value.
*

View file

@ -15,7 +15,7 @@
}
],
"require": {
"php": ">=8.0.2"
"php": ">=8.1"
},
"autoload": {
"files": [
@ -25,7 +25,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
"dev-main": "3.1-dev"
},
"thanks": {
"name": "symfony/contracts",

View file

@ -0,0 +1,25 @@
#
# Exclude these files from release archives.
# This will also make them unavailable when using Composer with `--prefer-dist`.
# If you develop for WPCS using Composer, use `--prefer-source`.
# https://blog.madewithlove.be/post/gitattributes/
#
/.travis.yml export-ignore
/.phpcs.xml.dist export-ignore
/phpunit.xml.dist export-ignore
/.github export-ignore
/bin export-ignore
/WordPress/Tests export-ignore
#
# Auto detect text files and perform LF normalization
# http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/
#
* text=auto
#
# The above will handle all files NOT found below
#
*.md text
*.php text
*.inc text

View file

@ -0,0 +1,5 @@
vendor
composer.lock
phpunit.xml
phpcs.xml
.phpcs.xml

File diff suppressed because it is too large Load diff

21
vendor/wp-coding-standards/wpcs/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2009 John Godley and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,296 @@
<div aria-hidden="true">
[![Latest Stable Version](https://poser.pugx.org/wp-coding-standards/wpcs/v/stable)](https://packagist.org/packages/wp-coding-standards/wpcs)
[![Travis Build Status](https://travis-ci.com/WordPress/WordPress-Coding-Standards.svg?branch=master)](https://travis-ci.com/WordPress/WordPress-Coding-Standards)
[![Release Date of the Latest Version](https://img.shields.io/github/release-date/WordPress/WordPress-Coding-Standards.svg?maxAge=1800)](https://github.com/WordPress/WordPress-Coding-Standards/releases)
:construction:
[![Latest Unstable Version](https://img.shields.io/badge/unstable-dev--develop-e68718.svg?maxAge=2419200)](https://packagist.org/packages/wp-coding-standards/wpcs#dev-develop)
[![Travis Build Status](https://travis-ci.com/WordPress/WordPress-Coding-Standards.svg?branch=develop)](https://travis-ci.com/WordPress/WordPress-Coding-Standards)
[![Last Commit to Unstable](https://img.shields.io/github/last-commit/WordPress/WordPress-Coding-Standards/develop.svg)](https://github.com/WordPress/WordPress-Coding-Standards/commits/develop)
[![Minimum PHP Version](https://img.shields.io/packagist/php-v/wp-coding-standards/wpcs.svg?maxAge=3600)](https://packagist.org/packages/wp-coding-standards/wpcs)
[![Tested on PHP 5.4 to 7.4 snapshot](https://img.shields.io/badge/tested%20on-PHP%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%207.3%20|%207.4snapshot-green.svg?maxAge=2419200)](https://travis-ci.com/WordPress/WordPress-Coding-Standards)
[![License: MIT](https://poser.pugx.org/wp-coding-standards/wpcs/license)](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/LICENSE)
[![Total Downloads](https://poser.pugx.org/wp-coding-standards/wpcs/downloads)](https://packagist.org/packages/wp-coding-standards/wpcs/stats)
[![Number of Contributors](https://img.shields.io/github/contributors/WordPress/WordPress-Coding-Standards.svg?maxAge=3600)](https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors)
</div>
# WordPress Coding Standards for PHP_CodeSniffer
* [Introduction](#introduction)
* [Project history](#project-history)
* [Installation](#installation)
+ [Requirements](#requirements)
+ [Composer](#composer)
+ [Standalone](#standalone)
* [Rulesets](#rulesets)
+ [Standards subsets](#standards-subsets)
+ [Using a custom ruleset](#using-a-custom-ruleset)
+ [Customizing sniff behaviour](#customizing-sniff-behaviour)
+ [Recommended additional rulesets](#recommended-additional-rulesets)
* [How to use](#how-to-use)
+ [Command line](#command-line)
+ [Using PHPCS and WPCS from within your IDE](#using-phpcs-and-wpcs-from-within-your-ide)
* [Running your code through WPCS automatically using CI tools](#running-your-code-through-wpcs-automatically-using-ci-tools)
+ [Travis CI](#travis-ci)
* [Fixing errors or whitelisting them](#fixing-errors-or-whitelisting-them)
+ [Tools shipped with WPCS](#tools-shipped-with-wpcs)
* [Contributing](#contributing)
* [License](#license)
## Introduction
This project is a collection of [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) rules (sniffs) to validate code developed for WordPress. It ensures code quality and adherence to coding conventions, especially the official [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/).
## Project history
- On 22nd April 2009, the original project from [Urban Giraffe](https://urbangiraffe.com/articles/wordpress-codesniffer-standard/) was packaged and published.
- In May 2011 the project was forked and [added](https://github.com/WordPress/WordPress-Coding-Standards/commit/04fd547c691ca2baae3fa8e195a46b0c9dd671c5) to GitHub by [Chris Adams](https://chrisadams.me.uk/).
- In April 2012 [XWP](https://xwp.co/) started to dedicate resources to develop and lead the creation of the sniffs and rulesets for `WordPress-Core`, `WordPress-VIP` (WordPress.com VIP), and `WordPress-Extra`.
- In May 2015, an initial documentation ruleset was [added](https://github.com/WordPress/WordPress-Coding-Standards/commit/b1a4bf8232a22563ef66f8a529357275a49f47dc#diff-a17c358c3262a26e9228268eb0a7b8c8) as `WordPress-Docs`.
- In 2015, [J.D. Grimes](https://github.com/JDGrimes) began significant contributions, along with maintenance from [Gary Jones](https://github.com/GaryJones).
- In 2016, [Juliette Reinders Folmer](https://github.com/jrfnl) began contributing heavily, adding more commits in a year than anyone else in the five years since the project was added to GitHub.
- In July 2018, version [`1.0.0`](https://github.com/WordPress/WordPress-Coding-Standards/releases/tag/1.0.0) of the project was released.
## Installation
### Requirements
The WordPress Coding Standards require PHP 5.4 or higher and [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version **3.3.1** or higher.
### Composer
Standards can be installed with the [Composer](https://getcomposer.org/) dependency manager:
composer create-project wp-coding-standards/wpcs --no-dev
Running this command will:
1. Install WordPress standards into `wpcs` directory.
2. Install PHP_CodeSniffer.
3. Register WordPress standards in PHP_CodeSniffer configuration.
4. Make `phpcs` command available from `wpcs/vendor/bin`.
For the convenience of using `phpcs` as a global command, you may want to add the path to the `wpcs/vendor/bin` directory to a `PATH` environment variable for your operating system.
#### Installing WPCS as a dependency
When installing the WordPress Coding Standards as a dependency in a larger project, the above mentioned step 3 will not be executed automatically.
There are two actively maintained Composer plugins which can handle the registration of standards with PHP_CodeSniffer for you:
* [composer-phpcodesniffer-standards-plugin](https://github.com/higidi/composer-phpcodesniffer-standards-plugin)
* [phpcodesniffer-composer-installer](https://github.com/DealerDirect/phpcodesniffer-composer-installer):"^0.6"
It is strongly suggested to `require` one of these plugins in your project to handle the registration of external standards with PHPCS for you.
### Standalone
1. Install PHP_CodeSniffer by following its [installation instructions](https://github.com/squizlabs/PHP_CodeSniffer#installation) (via Composer, Phar file, PEAR, or Git checkout).
Do ensure that PHP_CodeSniffer's version matches our [requirements](#requirements), if, for example, you're using [VVV](https://github.com/Varying-Vagrant-Vagrants/VVV).
2. Clone the WordPress standards repository:
git clone -b master https://github.com/WordPress/WordPress-Coding-Standards.git wpcs
3. Add its path to the PHP_CodeSniffer configuration:
phpcs --config-set installed_paths /path/to/wpcs
**Pro-tip:** Alternatively, you can tell PHP_CodeSniffer the path to the WordPress standards by adding the following snippet to your custom ruleset:
```xml
<config name="installed_paths" value="/path/to/wpcs" />
```
To summarize:
```bash
cd ~/projects
git clone https://github.com/squizlabs/PHP_CodeSniffer.git phpcs
git clone -b master https://github.com/WordPress/WordPress-Coding-Standards.git wpcs
cd phpcs
./bin/phpcs --config-set installed_paths ../wpcs
```
And then add the `~/projects/phpcs/bin` directory to your `PATH` environment variable via your `.bashrc`.
You should then see `WordPress-Core` et al listed when you run `phpcs -i`.
## Rulesets
### Standards subsets
The project encompasses a super-set of the sniffs that the WordPress community may need. If you use the `WordPress` standard you will get all the checks.
You can use the following as standard names when invoking `phpcs` to select sniffs, fitting your needs:
* `WordPress` - complete set with all of the sniffs in the project
- `WordPress-Core` - main ruleset for [WordPress core coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)
- `WordPress-Docs` - additional ruleset for [WordPress inline documentation standards](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/)
- `WordPress-Extra` - extended ruleset for recommended best practices, not sufficiently covered in the WordPress core coding standards
- includes `WordPress-Core`
**Note:** The WPCS package used to include a `WordPress-VIP` ruleset and associated sniffs, prior to WPCS 2.0.0.
The `WordPress-VIP` ruleset was originally intended to aid with the [WordPress.com VIP coding requirements](https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/), but has been superseded. It is recommended to use the [official VIP coding standards](https://github.com/Automattic/VIP-Coding-Standards) ruleset instead for checking code against the VIP platform requirements.
### Using a custom ruleset
If you need to further customize the selection of sniffs for your project - you can create a custom ruleset file. When you name this file either `.phpcs.xml`, `phpcs.xml`, `.phpcs.xml.dist` or `phpcs.xml.dist`, PHP_CodeSniffer will automatically locate it as long as it is placed in the directory from which you run the CodeSniffer or in a directory above it. If you follow these naming conventions you don't have to supply a `--standard` arg. For more info, read about [using a default configuration file](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file). See also provided [`phpcs.xml.dist.sample`](phpcs.xml.dist.sample) file and [fully annotated example](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml) in the PHP_CodeSniffer documentation.
### Customizing sniff behaviour
The WordPress Coding Standard contains a number of sniffs which are configurable. This means that you can turn parts of the sniff on or off, or change the behaviour by setting a property for the sniff in your custom `.phpcs.xml.dist` file.
You can find a complete list of all the properties you can change in the [wiki](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties).
### Recommended additional rulesets
The [PHPCompatibility](https://github.com/PHPCompatibility/PHPCompatibility) ruleset and its subset [PHPCompatibilityWP](https://github.com/PHPCompatibility/PHPCompatibilityWP) come highly recommended.
The [PHPCompatibility](https://github.com/PHPCompatibility/PHPCompatibility) sniffs are designed to analyse your code for cross-PHP version compatibility.
The [PHPCompatibilityWP](https://github.com/PHPCompatibility/PHPCompatibilityWP) ruleset is based on PHPCompatibility, but specifically crafted to prevent false positives for projects which expect to run within the context of WordPress, i.e. core, plugins and themes.
Install either as a separate ruleset and run it separately against your code or add it to your custom ruleset, like so:
```xml
<config name="testVersion" value="5.2-"/>
<rule ref="PHPCompatibilityWP">
<include-pattern>*\.php$</include-pattern>
</rule>
```
Whichever way you run it, do make sure you set the `testVersion` to run the sniffs against. The `testVersion` determines for which PHP versions you will receive compatibility information. The recommended setting for this at this moment is `5.2-` to support the same PHP versions as WordPress Core supports.
For more information about setting the `testVersion`, see:
* [PHPCompatibility: Sniffing your code for compatibility with specific PHP version(s)](https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions)
* [PHPCompatibility: Using a custom ruleset](https://github.com/PHPCompatibility/PHPCompatibility#using-a-custom-ruleset)
## How to use
### Command line
Run the `phpcs` command line tool on a given file or directory, for example:
phpcs --standard=WordPress wp-load.php
Will result in following output:
------------------------------------------------------------------------------------------
FOUND 8 ERRORS AND 10 WARNINGS AFFECTING 11 LINES
------------------------------------------------------------------------------------------
24 | WARNING | [ ] error_reporting() can lead to full path disclosure.
24 | WARNING | [ ] error_reporting() found. Changing configuration at runtime is rarely
| | necessary.
37 | WARNING | [x] "require_once" is a statement not a function; no parentheses are
| | required
39 | WARNING | [ ] Silencing errors is discouraged
39 | WARNING | [ ] Silencing errors is discouraged
42 | WARNING | [x] "require_once" is a statement not a function; no parentheses are
| | required
46 | ERROR | [ ] Inline comments must end in full-stops, exclamation marks, or
| | question marks
46 | ERROR | [x] There must be no blank line following an inline comment
49 | WARNING | [x] "require_once" is a statement not a function; no parentheses are
| | required
54 | WARNING | [x] "require_once" is a statement not a function; no parentheses are
| | required
63 | WARNING | [ ] Detected access of super global var $_SERVER, probably needs manual
| | inspection.
63 | ERROR | [ ] Detected usage of a non-validated input variable: $_SERVER
63 | ERROR | [ ] Missing wp_unslash() before sanitization.
63 | ERROR | [ ] Detected usage of a non-sanitized input variable: $_SERVER
69 | WARNING | [x] "require_once" is a statement not a function; no parentheses are
| | required
74 | ERROR | [ ] Inline comments must end in full-stops, exclamation marks, or
| | question marks
92 | ERROR | [ ] All output should be run through an escaping function (see the
| | Security sections in the WordPress Developer Handbooks), found
| | '$die'.
92 | ERROR | [ ] All output should be run through an escaping function (see the
| | Security sections in the WordPress Developer Handbooks), found '__'.
------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 6 MARKED SNIFF VIOLATIONS AUTOMATICALLY
------------------------------------------------------------------------------------------
### Using PHPCS and WPCS from within your IDE
* **PhpStorm** : Please see "[PHP Code Sniffer with WordPress Coding Standards Integration](https://confluence.jetbrains.com/display/PhpStorm/WordPress+Development+using+PhpStorm#WordPressDevelopmentusingPhpStorm-PHPCodeSnifferwithWordPressCodingStandardsIntegrationinPhpStorm)" in the PhpStorm documentation.
* **Sublime Text** : Please see "[Setting up WPCS to work in Sublime Text](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Setting-up-WPCS-to-work-in-Sublime-Text)" in the wiki.
* **Atom**: Please see "[Setting up WPCS to work in Atom](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Setting-up-WPCS-to-work-in-Atom)" in the wiki.
* **Visual Studio**: Please see "[Setting up PHP CodeSniffer in Visual Studio Code](https://tommcfarlin.com/php-codesniffer-in-visual-studio-code/)", a tutorial by Tom McFarlin.
* **Eclipse with XAMPP**: Please see "[Setting up WPCS when using Eclipse with XAMPP](https://github.com/WordPress/WordPress-Coding-Standards/wiki/How-to-use-WPCS-with-Eclipse-and-XAMPP)" in the wiki.
## Running your code through WPCS automatically using CI tools
### [Travis CI](https://travis-ci.com/)
To integrate PHPCS with WPCS with Travis CI, you'll need to install both `before_install` and add the run command to the `script`.
If your project uses Composer, the typical instructions might be different.
If you use a matrix setup in Travis to test your code against different PHP and/or WordPress versions, you don't need to run PHPCS on each variant of the matrix as the results will be same.
You can set an environment variable in the Travis matrix to only run the sniffs against one setup in the matrix.
#### Travis CI example
```yaml
language: php
matrix:
include:
# Arbitrary PHP version to run the sniffs against.
- php: '7.0'
env: SNIFF=1
before_install:
- if [[ "$SNIFF" == "1" ]]; then export PHPCS_DIR=/tmp/phpcs; fi
- if [[ "$SNIFF" == "1" ]]; then export SNIFFS_DIR=/tmp/sniffs; fi
# Install PHP_CodeSniffer.
- if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/squizlabs/PHP_CodeSniffer.git $PHPCS_DIR; fi
# Install WordPress Coding Standards.
- if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/WordPress/WordPress-Coding-Standards.git $SNIFFS_DIR; fi
# Set install path for WordPress Coding Standards.
- if [[ "$SNIFF" == "1" ]]; then $PHPCS_DIR/bin/phpcs --config-set installed_paths $SNIFFS_DIR; fi
# After CodeSniffer install you should refresh your path.
- if [[ "$SNIFF" == "1" ]]; then phpenv rehash; fi
script:
# Run against WordPress Coding Standards.
# If you use a custom ruleset, change `--standard=WordPress` to point to your ruleset file,
# for example: `--standard=wpcs.xml`.
# You can use any of the normal PHPCS command line arguments in the command:
# https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage
- if [[ "$SNIFF" == "1" ]]; then $PHPCS_DIR/bin/phpcs -p . --standard=WordPress; fi
```
More examples and advice about integrating PHPCS in your Travis build tests can be found here: https://github.com/jrfnl/make-phpcs-work-for-you/tree/master/travis-examples
## Fixing errors or whitelisting them
You can find information on how to deal with some of the more frequent issues in the [wiki](https://github.com/WordPress/WordPress-Coding-Standards/wiki).
### Tools shipped with WPCS
Since version 1.2.0, WPCS has a special sniff category `Utils`.
This sniff category contains some tools which, generally speaking, will only be needed to be run once over a codebase and for which the fixers can be considered _risky_, i.e. very careful review by a developer is needed before accepting the fixes made by these sniffs.
The sniffs in this category are disabled by default and can only be activated by adding some properties for each sniff via a custom ruleset.
At this moment, WPCS offer the following tools:
* `WordPress.Utils.I18nTextDomainFixer` - This sniff can replace the text domain used in a code-base.
The sniff will fix the text domains in both I18n function calls as well as in a plugin/theme header.
Passing the following properties will activate the sniff:
- `old_text_domain`: an array with one or more (old) text domain names which need to be replaced;
- `new_text_domain`: the correct (new) text domain as a string.
## Contributing
See [CONTRIBUTING](.github/CONTRIBUTING.md), including information about [unit testing](.github/CONTRIBUTING.md#unit-testing) the standard.
## License
See [LICENSE](LICENSE) (MIT).

View file

@ -0,0 +1,529 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="WordPress Core" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd">
<description>Non-controversial generally-agreed upon WordPress Coding Standards</description>
<!-- Default tab width for indentation fixes and such. -->
<arg name="tab-width" value="4"/>
<!--
#############################################################################
Handbook: PHP - Single and Double Quotes.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#single-and-double-quotes
#############################################################################
-->
<!-- Covers rule: Use single and double quotes when appropriate.
If you're not evaluating anything in the string, use single quotes. -->
<rule ref="Squiz.Strings.DoubleQuoteUsage.NotRequired"/>
<!-- Rule: Text that goes into attributes should be run through esc_attr().
https://github.com/WordPress/WordPress-Coding-Standards/issues/527 -->
<!--
#############################################################################
Handbook: PHP - Indentation.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
#############################################################################
-->
<!-- Covers rule: Your indentation should always reflect logical structure. -->
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="exact" value="false"/>
<property name="indent" value="4"/>
<property name="tabIndent" value="true"/>
<property name="ignoreIndentationTokens" type="array">
<element value="T_HEREDOC"/>
<element value="T_NOWDOC"/>
<element value="T_INLINE_HTML"/>
</property>
</properties>
</rule>
<rule ref="WordPress.Arrays.ArrayIndentation"/>
<!-- Covers rule: Use real tabs and not spaces. -->
<rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
<rule ref="WordPress.WhiteSpace.PrecisionAlignment"/>
<!-- Generic array layout check. -->
<!-- Covers rule: For associative arrays, each item should start on a new line
when the array contains more than one item.
Also covers various single-line array whitespace issues. -->
<rule ref="WordPress.Arrays.ArrayDeclarationSpacing"/>
<!-- Covers rule: Note the comma after the last array item: this is recommended. -->
<rule ref="WordPress.Arrays.CommaAfterArrayItem"/>
<!-- Covers rule: For switch structures case should indent one tab from the
switch statement and break one tab from the case statement. -->
<rule ref="PSR2.ControlStructures.SwitchDeclaration"/>
<!-- Prevent duplicate messages for the same issue. Covered by other sniffs. -->
<rule ref="PSR2.ControlStructures.SwitchDeclaration.NotLower">
<severity>0</severity>
</rule>
<rule ref="PSR2.ControlStructures.SwitchDeclaration.BreakNotNewLine">
<severity>0</severity>
</rule>
<rule ref="PSR2.ControlStructures.SwitchDeclaration.BodyOnNextLine">
<severity>0</severity>
</rule>
<!-- Covers rule: ... while spaces can be used mid-line for alignment. -->
<rule ref="WordPress.WhiteSpace.DisallowInlineTabs"/>
<!-- Implied through the examples: align the assignment operator in a block of assignments. -->
<rule ref="Generic.Formatting.MultipleStatementAlignment">
<properties>
<property name="maxPadding" value="40"/>
</properties>
</rule>
<!-- Implied through the examples: align the double arrows. -->
<rule ref="WordPress.Arrays.MultipleStatementAlignment">
<properties>
<property name="maxColumn" value="60"/>
</properties>
</rule>
<!--
#############################################################################
Handbook: PHP - Brace Style.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#brace-style
#############################################################################
-->
<!-- Covers rule: Braces shall be used for all blocks. -->
<rule ref="Squiz.ControlStructures.ControlSignature"/>
<!-- Covers rule: Braces should always be used, even when they are not required. -->
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<!--
#############################################################################
Handbook: PHP - Declaring Arrays.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#declaring-arrays
#############################################################################
-->
<!-- Covers rule: Arrays must be declared using long array syntax. -->
<rule ref="Generic.Arrays.DisallowShortArraySyntax"/>
<!--
#############################################################################
Handbook: PHP - Use elseif, not else if.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#use-elseif-not-else-if
#############################################################################
-->
<!-- Covers rule: ... use elseif for conditionals. -->
<rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
<!--
#############################################################################
Handbook: PHP - Multiline Function Calls.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#multiline-function-calls
#############################################################################
-->
<!-- Rule: When splitting a function call over multiple lines, each parameter must be on a separate line.
Covered via PEAR.Functions.FunctionCallSignature in Space Usage section. -->
<!-- Rule: Single line inline comments can take up their own line. -->
<!-- Rule: Each parameter must take up no more than a single line. Multi-line parameter
values must be assigned to a variable and then that variable should be passed to the function call.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1330 -->
<!--
#############################################################################
Handbook: PHP - Regular Expressions.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#regular-expressions
#############################################################################
-->
<!-- Covers rule: Perl compatible regular expressions should be used in preference
to their POSIX counterparts. -->
<rule ref="WordPress.PHP.POSIXFunctions"/>
<!-- Rule: Never use the /e switch, use preg_replace_callback instead.
https://github.com/WordPress/WordPress-Coding-Standards/issues/632 -->
<!-- Rule: It's most convenient to use single-quoted strings for regular expressions.
Already covered by Squiz.Strings.DoubleQuoteUsage -->
<!--
#############################################################################
Handbook: PHP - Opening and Closing PHP Tags.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#opening-and-closing-php-tags
#############################################################################
-->
<!-- Covers rule: When embedding multi-line PHP snippets within a HTML block, the
PHP open and close tags must be on a line by themselves. -->
<rule ref="Squiz.PHP.EmbeddedPhp"/>
<rule ref="Squiz.PHP.EmbeddedPhp.SpacingBefore">
<severity>0</severity>
</rule>
<rule ref="Squiz.PHP.EmbeddedPhp.Indent">
<severity>0</severity>
</rule>
<rule ref="Squiz.PHP.EmbeddedPhp.OpenTagIndent">
<severity>0</severity>
</rule>
<rule ref="Squiz.PHP.EmbeddedPhp.SpacingAfter">
<severity>0</severity>
</rule>
<!--
#############################################################################
Handbook: PHP - No Shorthand PHP Tags.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#no-shorthand-php-tags
#############################################################################
-->
<!-- Covers rule: Never use shorthand PHP start tags. Always use full PHP tags. -->
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.DisallowAlternativePHPTags"/>
<!--
#############################################################################
Handbook: PHP - Remove Trailing Spaces.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#remove-trailing-spaces
#############################################################################
-->
<!-- Covers rule: Remove trailing whitespace at the end of each line of code. -->
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
<!-- Covers rule: Omitting the closing PHP tag at the end of a file is preferred. -->
<rule ref="PSR2.Files.ClosingTag"/>
<!--
#############################################################################
Handbook: PHP - Space Usage.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage
#############################################################################
-->
<!-- Covers rule: Always put spaces after commas, and on both sides of logical,
comparison, string and assignment operators. -->
<rule ref="WordPress.WhiteSpace.OperatorSpacing"/>
<rule ref="Squiz.Strings.ConcatenationSpacing">
<properties>
<property name="spacing" value="1"/>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<!-- Covers rule: Put spaces on both sides of the opening and closing parenthesis of
if, elseif, foreach, for, and switch blocks. -->
<rule ref="WordPress.WhiteSpace.ControlStructureSpacing"/>
<!-- Covers rule: Define a function like so: function my_function( $param1 = 'foo', $param2 = 'bar' ) { -->
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie">
<properties>
<property name="checkClosures" value="true"/>
</properties>
</rule>
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
<properties>
<property name="equalsSpacing" value="1"/>
<property name="requiredSpacesAfterOpen" value="1"/>
<property name="requiredSpacesBeforeClose" value="1"/>
</properties>
</rule>
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingBeforeClose">
<severity>0</severity>
</rule>
<!-- Covers rule: Call a function, like so: my_function( $param1, func_param( $param2 ) ); -->
<rule ref="PEAR.Functions.FunctionCallSignature">
<properties>
<property name="requiredSpacesAfterOpen" value="1"/>
<property name="requiredSpacesBeforeClose" value="1"/>
<!-- ... and for multi-line function calls, there should only be one parameter per line. -->
<property name="allowMultipleArguments" value="false"/>
</properties>
</rule>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<!-- Rule: Perform logical comparisons, like so: if ( ! $foo ) { -->
<!-- Covers rule: Type casts must be lowercase. Always prefer the short form
of type casts, (int) instead of (integer) and (bool) rather than (boolean).
For float casts use (float). -->
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Squiz.WhiteSpace.CastSpacing"/>
<rule ref="WordPress.WhiteSpace.CastStructureSpacing"/>
<rule ref="WordPress.PHP.TypeCasts"/>
<rule ref="PSR12.Keywords.ShortFormTypeKeywords"/>
<!-- N.B.: This sniff also checks the case of (parameter/return) type declarations, not just type casts. -->
<rule ref="Generic.PHP.LowerCaseType"/>
<!-- Covers rule: ... array items, only include a space around the index if it is a variable. -->
<rule ref="WordPress.Arrays.ArrayKeySpacingRestrictions"/>
<!-- Rule: In a switch block, there must be no space before the colon for a case statement. -->
<!-- Covered by the PSR2.ControlStructures.SwitchDeclaration sniff. -->
<!-- Rule: Similarly, there should be no space before the colon on return type declarations. -->
<!-- Covers rule: Unless otherwise specified, parentheses should have spaces inside of them. -->
<rule ref="Generic.WhiteSpace.ArbitraryParenthesesSpacing">
<properties>
<property name="spacing" value="1"/>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<!--
#############################################################################
Handbook: PHP - Formatting SQL statements.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#formatting-sql-statements
#############################################################################
-->
<!-- Rule: Always capitalize the SQL parts of the statement like UPDATE or WHERE.
https://github.com/WordPress/WordPress-Coding-Standards/issues/639 -->
<!-- Rule: Functions that update the database should expect their parameters to lack
SQL slash escaping when passed.
https://github.com/WordPress/WordPress-Coding-Standards/issues/640 -->
<!-- Rule: in $wpdb->prepare - %s is used for string placeholders and %d is used for integer
placeholders. Note that they are not 'quoted'! -->
<rule ref="WordPress.DB.PreparedSQLPlaceholders"/>
<!-- Covers rule: $wpdb->prepare()... The benefit of this is that we don't have to remember
to manually use esc_sql(), and also that it is easy to see at a glance whether something
has been escaped or not, because it happens right when the query happens. -->
<rule ref="WordPress.DB.PreparedSQL"/>
<!--
#############################################################################
Handbook: PHP - Database Queries.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries
#############################################################################
-->
<!-- Covers rule: Avoid touching the database directly. -->
<rule ref="WordPress.DB.RestrictedFunctions"/>
<rule ref="WordPress.DB.RestrictedClasses"/>
<!--
#############################################################################
Handbook: PHP - Naming Conventions.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
#############################################################################
-->
<!-- Covers rule: Use lowercase letters in variable, action/filter, and function names.
Separate words via underscores. -->
<rule ref="WordPress.NamingConventions.ValidFunctionName"/>
<rule ref="WordPress.NamingConventions.ValidHookName"/>
<rule ref="WordPress.NamingConventions.ValidVariableName"/>
<!-- Covers rule: Class names should use capitalized words separated by underscores. -->
<rule ref="PEAR.NamingConventions.ValidClassName"/>
<!-- Covers rule: Constants should be in all upper-case with underscores separating words. -->
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
<!-- Covers rule: Files should be named descriptively using lowercase letters.
Hyphens should separate words. -->
<!-- Covers rule: Class file names should be based on the class name with "class-"
prepended and the underscores in the class name replaced with hyphens.
https://github.com/WordPress/WordPress-Coding-Standards/issues/642 -->
<!-- Covers rule: Files containing template tags in wp-includes should have "-template"
appended to the end of the name.
https://github.com/WordPress/WordPress-Coding-Standards/issues/642 -->
<rule ref="WordPress.Files.FileName"/>
<!--
#############################################################################
Handbook: PHP - Self-Explanatory Flag Values for Function Arguments.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#self-explanatory-flag-values-for-function-arguments
#############################################################################
-->
<!--
#############################################################################
Handbook: PHP - Interpolation for Naming Dynamic Hooks.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#interpolation-for-naming-dynamic-hooks
https://github.com/WordPress/WordPress-Coding-Standards/issues/751
#############################################################################
-->
<!-- Rule: Dynamic hooks should be named using interpolation rather than concatenation. -->
<!-- Rule: Variables used in hook tags should be wrapped in curly braces { and },
with the complete outer tag name wrapped in double quotes. -->
<!-- Rule: Where possible, dynamic values in tag names should also be as succinct
and to the point as possible. -->
<!--
#############################################################################
Handbook: PHP - Ternary Operator.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#ternary-operator
#############################################################################
-->
<!-- Rule: Always have Ternaries test if the statement is true, not false.
An exception would be using ! empty(), as testing for false here is generally more intuitive.
https://github.com/WordPress/WordPress-Coding-Standards/issues/643 -->
<!-- Rule: The short ternary operator must not be used. -->
<rule ref="WordPress.PHP.DisallowShortTernary"/>
<!--
#############################################################################
Handbook: PHP - Yoda Conditions.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#yoda-conditions
#############################################################################
-->
<!-- Covers rule: When doing logical comparisons, always put the variable on the right side,
constants or literals on the left. -->
<rule ref="WordPress.PHP.YodaConditions"/>
<!-- Rule: Yoda conditions for <, >, <= or >= are significantly more difficult to read
and are best avoided. -->
<!--
#############################################################################
Handbook: PHP - Clever Code.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#clever-code
#############################################################################
-->
<!-- Rule: In general, readability is more important than cleverness or brevity.
https://github.com/WordPress/WordPress-Coding-Standards/issues/607 -->
<rule ref="Squiz.PHP.DisallowMultipleAssignments"/>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<!-- Rule: Unless absolutely necessary, loose comparisons should not be used,
as their behaviour can be misleading. -->
<rule ref="WordPress.PHP.StrictComparisons"/>
<rule ref="WordPress.PHP.StrictInArray"/>
<!-- Rule: Assignments must not be placed in placed in conditionals.
Note: sniff is a duplicate of upstream. Can be removed once minimum PHPCS requirement has gone up.
https://github.com/squizlabs/PHP_CodeSniffer/pull/1594
Update: the "assignment in ternary" part of the sniff is currently not yet covered in
the upstream version. This needs to be pulled first before we can defer to upstream. -->
<rule ref="WordPress.CodeAnalysis.AssignmentInCondition"/>
<!-- Rule: In a switch statement... If a case contains a block, then falls through
to the next block, this must be explicitly commented. -->
<!-- Covered by the PSR2.ControlStructures.SwitchDeclaration sniff. -->
<!-- Rule: The goto statement must never be used. -->
<rule ref="Generic.PHP.DiscourageGoto">
<type>error</type>
<message>The "goto" language construct should not be used.</message>
</rule>
<!-- Rule: The eval() construct is very dangerous, and is impossible to secure. ... these must not be used. -->
<rule ref="Squiz.PHP.Eval.Discouraged">
<type>error</type>
<message>eval() is a security risk so not allowed.</message>
</rule>
<!-- Rule: create_function() function, which internally performs an eval(),
is deprecated in PHP 7.2. ... these must not be used. -->
<rule ref="WordPress.PHP.RestrictedPHPFunctions"/>
<!--
#############################################################################
Handbook: PHP - (No) Error Control Operator @.
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#error-control-operator
#############################################################################
-->
<!-- Covers rule: This operator is often used lazily instead of doing proper error checking.
Its use is highly discouraged. -->
<rule ref="WordPress.PHP.NoSilencedErrors"/>
<!--
#############################################################################
Handbook: PHP - Don't extract().
Ref: https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#dont-extract
#############################################################################
-->
<rule ref="WordPress.PHP.DontExtract"/>
<!--
#############################################################################
Not in the handbook: Generic sniffs.
#############################################################################
-->
<!-- Important to prevent issues with content being sent before headers. -->
<rule ref="Generic.Files.ByteOrderMark"/>
<!-- All line endings should be \n. -->
<rule ref="Generic.Files.LineEndings">
<properties>
<property name="eolChar" value="\n"/>
</properties>
</rule>
<!-- All files should end with a new line. -->
<rule ref="Generic.Files.EndFileNewline"/>
<!-- No whitespace should come before semicolons. -->
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
<!-- There should be no empty statements, i.e. lone semi-colons or open/close tags without content. -->
<rule ref="WordPress.CodeAnalysis.EmptyStatement"/>
<!-- Lowercase PHP constants, like true, false and null. -->
<!-- https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions -->
<rule ref="Generic.PHP.LowerCaseConstant"/>
<!-- Lowercase PHP keywords, like class, function and case. -->
<rule ref="Generic.PHP.LowerCaseKeyword"/>
<!-- Class opening braces should be on the same line as the statement. -->
<rule ref="Generic.Classes.OpeningBraceSameLine"/>
<!-- Object operators should not have whitespace around them unless they are multi-line. -->
<rule ref="Squiz.WhiteSpace.ObjectOperatorSpacing">
<properties>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<!-- References to self in a class should be lower-case and not have extraneous spaces,
per implicit conventions in the core codebase; the NotUsed code refers to using the
fully-qualified class name instead of self, for which there are instances in core. -->
<rule ref="Squiz.Classes.SelfMemberReference"/>
<rule ref="Squiz.Classes.SelfMemberReference.NotUsed">
<severity>0</severity>
</rule>
<!--
#############################################################################
Not in the coding standard handbook: WP specific sniffs.
Ref: https://make.wordpress.org/core/handbook/best-practices/internationalization/ (limited info)
Ref: https://developer.wordpress.org/plugins/internationalization/ (more extensive)
#############################################################################
-->
<!-- Check for correct usage of the WP i18n functions. -->
<rule ref="WordPress.WP.I18n"/>
<!-- Check for correct spelling of WordPress. -->
<rule ref="WordPress.WP.CapitalPDangit"/>
<!-- Use the appropriate DateTime functions.
See: https://github.com/WordPress/WordPress-Coding-Standards/issues/1713 -->
<rule ref="WordPress.DateTime.RestrictedFunctions"/>
<!-- Don't use current_time() to retrieve a timestamp. -->
<rule ref="WordPress.DateTime.CurrentTimeTimestamp"/>
</ruleset>

View file

@ -0,0 +1,109 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="WordPress Docs" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd">
<description>WordPress Coding Standards for Inline Documentation and Comments</description>
<!--
Handbook: PHP Documentation Standards
Ref: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/php/
-->
<rule ref="Squiz.Commenting">
<!-- Excluded to allow /* translators: ... */ comments -->
<exclude name="Squiz.Commenting.BlockComment.SingleLine"/>
<!-- Sniff seems to require indenting with spaces -->
<exclude name="Squiz.Commenting.BlockComment.FirstLineIndent"/>
<!-- Sniff seems to require indenting with spaces -->
<exclude name="Squiz.Commenting.BlockComment.LineIndent"/>
<!-- Sniff seems to require indenting with spaces -->
<exclude name="Squiz.Commenting.BlockComment.LastLineIndent"/>
<!-- WP requires /** for require() et al. See https://github.com/squizlabs/PHP_CodeSniffer/pull/581 -->
<exclude name="Squiz.Commenting.BlockComment.WrongStart"/>
<!-- WP handbook doesn't clarify one way or another, so ignore -->
<exclude name="Squiz.Commenting.BlockComment.NoEmptyLineAfter"/>
<!-- WP prefers indicating @since, @package, @subpackage etc in class comments -->
<exclude name="Squiz.Commenting.ClassComment.TagNotAllowed"/>
<!-- WP doesn't require //end ... for classes and functions -->
<exclude name="Squiz.Commenting.ClosingDeclarationComment.Missing"/>
<!-- Excluded to allow param documentation for arrays -->
<exclude name="Squiz.Commenting.DocCommentAlignment.SpaceAfterStar"/>
<!-- WP doesn't require a @author value for Squiz -->
<exclude name="Squiz.Commenting.FileComment.IncorrectAuthor"/>
<!-- WP doesn't require a @copyright value for Squiz -->
<exclude name="Squiz.Commenting.FileComment.IncorrectCopyright"/>
<!-- WP doesn't require @author tags -->
<exclude name="Squiz.Commenting.FileComment.MissingAuthorTag"/>
<!-- WP doesn't require @subpackage tags -->
<exclude name="Squiz.Commenting.FileComment.MissingSubpackageTag"/>
<!-- WP doesn't require @copyright tags -->
<exclude name="Squiz.Commenting.FileComment.MissingCopyrightTag"/>
<!-- WP has a different prefered order of tags -->
<exclude name="Squiz.Commenting.FileComment.PackageTagOrder"/>
<!-- WP has a different prefered order of tags -->
<exclude name="Squiz.Commenting.FileComment.SubpackageTagOrder"/>
<!-- WP has a different prefered order of tags -->
<exclude name="Squiz.Commenting.FileComment.AuthorTagOrder"/>
<!-- WP has a different prefered order of tags -->
<exclude name="Squiz.Commenting.FileComment.CopyrightTagOrder"/>
<!-- WP prefers int and bool instead of integer and boolean -->
<exclude name="Squiz.Commenting.FunctionComment.IncorrectParamVarName"/>
<!-- WP prefers int and bool instead of integer and boolean -->
<exclude name="Squiz.Commenting.FunctionComment.InvalidReturn"/>
<!-- WP prefers indicating a @return null for early returns -->
<exclude name="Squiz.Commenting.FunctionComment.InvalidReturnNotVoid"/>
<!-- WP states not all functions require @return -->
<exclude name="Squiz.Commenting.FunctionComment.MissingReturn"/>
<!-- Excluded to allow param documentation for arrays -->
<exclude name="Squiz.Commenting.FunctionComment.ParamCommentNotCapital"/>
<!-- Excluded to allow param documentation for arrays -->
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamName"/>
<!-- It is too early for PHP7 features to be required -->
<exclude name="Squiz.Commenting.FunctionComment.ScalarTypeHintMissing"/>
<!-- WP doesn't require type hints -->
<exclude name="Squiz.Commenting.FunctionComment.TypeHintMissing"/>
<!-- Exclude to allow duplicate hooks to be documented -->
<exclude name="Squiz.Commenting.InlineComment.DocBlock"/>
<!-- Excluded to allow /* translators: ... */ comments -->
<exclude name="Squiz.Commenting.InlineComment.NotCapital"/>
<!-- WP handbook doesn't clarify one way or another, so ignore -->
<exclude name="Squiz.Commenting.InlineComment.SpacingAfter"/>
<!-- Not in Inline Docs standard, and a code smell -->
<exclude name="Squiz.Commenting.LongConditionClosingComment"/>
<!-- Not in Inline Docs standard, and needed to bypass WPCS checks -->
<exclude name="Squiz.Commenting.PostStatementComment"/>
<!-- WP prefers int and bool instead of integer and boolean -->
<exclude name="Squiz.Commenting.VariableComment.IncorrectVarType"/>
<!-- WP demands a @since tag for class variables -->
<exclude name="Squiz.Commenting.VariableComment.TagNotAllowed"/>
<!-- WP prefers @since first -->
<exclude name="Squiz.Commenting.VariableComment.VarOrder"/>
</rule>
<rule ref="Generic.Commenting.DocComment">
<!-- WP has different alignment of tag values -->
<exclude name="Generic.Commenting.DocComment.TagValueIndent"/>
<!-- WP has a different prefered order of tags -->
<exclude name="Generic.Commenting.DocComment.ParamNotFirst"/>
<!-- Excluded to allow param documentation for arrays -->
<exclude name="Generic.Commenting.DocComment.ParamGroup"/>
<!-- WP prefers no empty line between @param tags and @return -->
<exclude name="Generic.Commenting.DocComment.NonParamGroup"/>
<!-- Excluded to allow param documentation for arrays -->
<exclude name="Generic.Commenting.DocComment.TagsNotGrouped"/>
<!-- Exclude to allow duplicate hooks to be documented -->
<exclude name="Generic.Commenting.DocComment.ContentAfterOpen"/>
<!-- Exclude to allow duplicate hooks to be documented -->
<exclude name="Generic.Commenting.DocComment.SpacingBeforeShort"/>
<!-- Exclude to allow duplicate hooks to be documented -->
<exclude name="Generic.Commenting.DocComment.ContentBeforeClose"/>
</rule>
</ruleset>

View file

@ -0,0 +1,187 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="WordPress Extra" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd">
<description>Best practices beyond core WordPress Coding Standards</description>
<rule ref="WordPress-Core"/>
<!-- Generic PHP best practices.
https://github.com/WordPress/WordPress-Coding-Standards/pull/382 -->
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.Strings.UnnecessaryStringConcat">
<properties>
<property name="allowMultiline" value="true"/>
</properties>
</rule>
<!-- More generic PHP best practices.
https://github.com/WordPress/WordPress-Coding-Standards/issues/607 -->
<rule ref="Squiz.PHP.NonExecutableCode"/>
<rule ref="Squiz.Operators.IncrementDecrementUsage"/>
<rule ref="Squiz.Operators.ValidLogicalOperators"/>
<rule ref="Squiz.Functions.FunctionDuplicateArgument"/>
<!-- And even more generic PHP best practices.
https://github.com/WordPress/WordPress-Coding-Standards/pull/809 -->
<rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
<!-- And yet more best practices.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1143 -->
<rule ref="PEAR.Files.IncludingFile.BracketsNotRequired">
<type>warning</type>
</rule>
<rule ref="PEAR.Files.IncludingFile.UseRequire">
<type>warning</type>
</rule>
<rule ref="PEAR.Files.IncludingFile.UseRequireOnce">
<type>warning</type>
</rule>
<!-- Check correct spacing of language constructs. This also ensures that the
above rule for not using brackets with require is fixed correctly.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1153 -->
<rule ref="Squiz.WhiteSpace.LanguageConstructSpacing"/>
<!-- Hook callbacks may not use all params -->
<!-- https://github.com/WordPress/WordPress-Coding-Standards/pull/382#discussion_r29981655 -->
<!--<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>-->
<!-- Encourage having only one class/interface/trait per file. -->
<rule ref="Generic.Files.OneObjectStructurePerFile">
<type>warning</type>
<message>Best practice suggestion: Declare only one class/interface/trait in a file.</message>
</rule>
<!-- Verify modifier keywords for declared methods and properties in classes.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1101 -->
<rule ref="Squiz.Scope.MethodScope"/>
<rule ref="PSR2.Classes.PropertyDeclaration"/>
<rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/>
<rule ref="PSR2.Methods.MethodDeclaration"/>
<!-- Warn against using fully-qualified class names instead of the self keyword. -->
<rule ref="Squiz.Classes.SelfMemberReference.NotUsed">
<!-- Restore default severity of 5 which WordPress-Core sets to 0. -->
<severity>5</severity>
</rule>
<rule ref="WordPress.Security.EscapeOutput"/>
<!-- Encourage use of wp_safe_redirect() to avoid open redirect vulnerabilities.
https://github.com/WordPress/WordPress-Coding-Standards/pull/1264 -->
<rule ref="WordPress.Security.SafeRedirect"/>
<!-- Verify that a nonce check is done before using values in superglobals.
https://github.com/WordPress/WordPress-Coding-Standards/issues/73 -->
<rule ref="WordPress.Security.NonceVerification"/>
<rule ref="WordPress.PHP.DevelopmentFunctions"/>
<rule ref="WordPress.PHP.DiscouragedPHPFunctions"/>
<rule ref="WordPress.WP.DeprecatedFunctions"/>
<rule ref="WordPress.WP.DeprecatedClasses"/>
<rule ref="WordPress.WP.DeprecatedParameters"/>
<rule ref="WordPress.WP.DeprecatedParameterValues"/>
<rule ref="WordPress.WP.AlternativeFunctions"/>
<rule ref="WordPress.WP.DiscouragedConstants"/>
<rule ref="WordPress.WP.DiscouragedFunctions"/>
<!-- Scripts & style should be enqueued.
https://github.com/WordPress/WordPress-Coding-Standards/issues/35 -->
<rule ref="WordPress.WP.EnqueuedResources"/>
<!-- Warn against overriding WP global variables.
https://github.com/WordPress/WordPress-Coding-Standards/issues/26 -->
<rule ref="WordPress.WP.GlobalVariablesOverride"/>
<!-- Detect incorrect or risky use of the `ini_set()` function.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1447 -->
<rule ref="WordPress.PHP.IniSet"/>
<!-- Check enqueue and register styles and scripts to have version and in_footer parameters explicitly set.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1146 -->
<rule ref="WordPress.WP.EnqueuedResourceParameters"/>
<!-- Discourage use of the backtick operator (execution of shell commands).
https://github.com/WordPress/WordPress-Coding-Standards/pull/646 -->
<rule ref="Generic.PHP.BacktickOperator"/>
<!-- Check for PHP Parse errors.
https://github.com/WordPress/WordPress-Coding-Standards/issues/522 -->
<rule ref="Generic.PHP.Syntax"/>
<!-- Make the translators comment check which is included in core stricter. -->
<rule ref="WordPress.WP.I18n.MissingTranslatorsComment">
<type>error</type>
</rule>
<rule ref="WordPress.WP.I18n.TranslatorsCommentWrongStyle">
<type>error</type>
</rule>
<!-- Verify that everything in the global namespace is prefixed. -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals"/>
<!-- Validates post type slugs for valid characters, length and reserved keywords. -->
<rule ref="WordPress.NamingConventions.ValidPostTypeSlug"/>
<!-- Check that object instantiations always have braces & are not assigned by reference.
https://github.com/WordPress/WordPress-Coding-Standards/issues/919
Note: there is a similar upstream sniff `PSR12.Classes.ClassInstantiation`, however
that sniff:
- does not cover JS files;
- does not demand parentheses for PHP anonymous classes;
- does not check the whitespace between the class name and the parentheses;
- does not check for PHP new by reference.
For those reasons, the WPCS version should remain. -->
<rule ref="WordPress.Classes.ClassInstantiation"/>
<!-- https://github.com/WordPress/WordPress-Coding-Standards/issues/1157 -->
<rule ref="WordPress.Security.PluginMenuSlug"/>
<rule ref="WordPress.WP.CronInterval"/>
<rule ref="WordPress.WP.PostsPerPage"/>
<!-- Verify some regex best practices.
https://github.com/WordPress/WordPress-Coding-Standards/issues/1371 -->
<rule ref="WordPress.PHP.PregQuoteDelimiter"/>
<!-- The Core ruleset respects the whitelist. For `Extra` the sniff is stricter.
https://github.com/WordPress/WordPress-Coding-Standards/pull/1450 -->
<rule ref="WordPress.PHP.NoSilencedErrors">
<properties>
<property name="use_default_whitelist" value="false"/>
</properties>
</rule>
<!-- Commented out code should not be committed.
https://github.com/WordPress/WordPress-Coding-Standards/pull/1463 -->
<rule ref="Squiz.PHP.CommentedOutCode">
<properties>
<property name="maxPercentage" value="40"/>
</properties>
</rule>
<!-- Prevent some typical mistakes people make accidentally.
https://github.com/WordPress/WordPress-Coding-Standards/pull/1777 -->
<rule ref="WordPress.CodeAnalysis.EscapedNotTranslated"/>
<!--
#############################################################################
Code style sniffs for more recent PHP features and syntaxes.
#############################################################################
-->
<!-- Check for single blank line after namespace declaration. -->
<rule ref="PSR2.Namespaces.NamespaceDeclaration"/>
</ruleset>

View file

@ -0,0 +1,240 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress;
use WordPressCS\WordPress\Sniff;
/**
* Restricts array assignment of certain keys.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.10.0 Class became a proper abstract class. This was already the behaviour.
* Moved the file and renamed the class from
* `\WordPressCS\WordPress\Sniffs\Arrays\ArrayAssignmentRestrictionsSniff` to
* `\WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff`.
*/
abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff {
/**
* Exclude groups.
*
* Example: 'foo,bar'
*
* @since 0.3.0
* @since 1.0.0 This property now expects to be passed an array.
* Previously a comma-delimited string was expected.
*
* @var array
*/
public $exclude = array();
/**
* Groups of variable data to check against.
* Don't use this in extended classes, override getGroups() instead.
* This is only used for Unit tests.
*
* @var array
*/
public static $groups = array();
/**
* Cache for the excluded groups information.
*
* @since 0.11.0
*
* @var array
*/
protected $excluded_groups = array();
/**
* Cache for the group information.
*
* @since 0.13.0
*
* @var array
*/
protected $groups_cache = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
// Retrieve the groups only once and don't set up a listener if there are no groups.
if ( false === $this->setup_groups() ) {
return array();
}
return array(
\T_DOUBLE_ARROW,
\T_CLOSE_SQUARE_BRACKET,
\T_CONSTANT_ENCAPSED_STRING,
\T_DOUBLE_QUOTED_STRING,
);
}
/**
* Groups of variables to restrict.
*
* This method should be overridden in extending classes.
*
* Example: groups => array(
* 'groupname' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Dont use this one please!',
* 'keys' => array( 'key1', 'another_key' ),
* 'callback' => array( 'class', 'method' ), // Optional.
* )
* )
*
* @return array
*/
abstract public function getGroups();
/**
* Cache the groups.
*
* @since 0.13.0
*
* @return bool True if the groups were setup. False if not.
*/
protected function setup_groups() {
$this->groups_cache = $this->getGroups();
if ( empty( $this->groups_cache ) && empty( self::$groups ) ) {
return false;
}
// Allow for adding extra unit tests.
if ( ! empty( self::$groups ) ) {
$this->groups_cache = array_merge( $this->groups_cache, self::$groups );
}
return true;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$this->excluded_groups = $this->merge_custom_array( $this->exclude );
if ( array_diff_key( $this->groups_cache, $this->excluded_groups ) === array() ) {
// All groups have been excluded.
// Don't remove the listener as the exclude property can be changed inline.
return;
}
$token = $this->tokens[ $stackPtr ];
if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] ) {
$equal = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true );
if ( \T_EQUAL !== $this->tokens[ $equal ]['code'] ) {
return; // This is not an assignment!
}
}
// Instances: Multi-dimensional array, keyed by line.
$inst = array();
/*
* Covers:
* $foo = array( 'bar' => 'taz' );
* $foo['bar'] = $taz;
*/
if ( \in_array( $token['code'], array( \T_CLOSE_SQUARE_BRACKET, \T_DOUBLE_ARROW ), true ) ) {
$operator = $stackPtr; // T_DOUBLE_ARROW.
if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] ) {
$operator = $this->phpcsFile->findNext( \T_EQUAL, ( $stackPtr + 1 ) );
}
$keyIdx = $this->phpcsFile->findPrevious( array( \T_WHITESPACE, \T_CLOSE_SQUARE_BRACKET ), ( $operator - 1 ), null, true );
if ( ! is_numeric( $this->tokens[ $keyIdx ]['content'] ) ) {
$key = $this->strip_quotes( $this->tokens[ $keyIdx ]['content'] );
$valStart = $this->phpcsFile->findNext( array( \T_WHITESPACE ), ( $operator + 1 ), null, true );
$valEnd = $this->phpcsFile->findNext( array( \T_COMMA, \T_SEMICOLON ), ( $valStart + 1 ), null, false, null, true );
$val = $this->phpcsFile->getTokensAsString( $valStart, ( $valEnd - $valStart ) );
$val = $this->strip_quotes( $val );
$inst[ $key ][] = array( $val, $token['line'] );
}
} elseif ( \in_array( $token['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) {
// $foo = 'bar=taz&other=thing';
if ( preg_match_all( '#(?:^|&)([a-z_]+)=([^&]*)#i', $this->strip_quotes( $token['content'] ), $matches ) <= 0 ) {
return; // No assignments here, nothing to check.
}
foreach ( $matches[1] as $i => $_k ) {
$inst[ $_k ][] = array( $matches[2][ $i ], $token['line'] );
}
}
if ( empty( $inst ) ) {
return;
}
foreach ( $this->groups_cache as $groupName => $group ) {
if ( isset( $this->excluded_groups[ $groupName ] ) ) {
continue;
}
$callback = ( isset( $group['callback'] ) && is_callable( $group['callback'] ) ) ? $group['callback'] : array( $this, 'callback' );
foreach ( $inst as $key => $assignments ) {
foreach ( $assignments as $occurance ) {
list( $val, $line ) = $occurance;
if ( ! \in_array( $key, $group['keys'], true ) ) {
continue;
}
$output = \call_user_func( $callback, $key, $val, $line, $group );
if ( ! isset( $output ) || false === $output ) {
continue;
} elseif ( true === $output ) {
$message = $group['message'];
} else {
$message = $output;
}
$this->addMessage(
$message,
$stackPtr,
( 'error' === $group['type'] ),
$this->string_to_errorcode( $groupName . '_' . $key ),
array( $key, $val )
);
}
}
}
}
/**
* Callback to process each confirmed key, to check value.
*
* This method must be extended to add the logic to check assignment value.
*
* @param string $key Array index / key.
* @param mixed $val Assigned value.
* @param int $line Token line.
* @param array $group Group definition.
* @return mixed FALSE if no match, TRUE if matches, STRING if matches
* with custom error message passed to ->process().
*/
abstract public function callback( $key, $val, $line, $group );
}

View file

@ -0,0 +1,245 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Restricts usage of some classes.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
*/
abstract class AbstractClassRestrictionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Regex pattern with placeholder for the class names.
*
* @var string
*/
protected $regex_pattern = '`^\\\\(?:%s)$`i';
/**
* Temporary storage for retrieved class name.
*
* @var string
*/
protected $classname;
/**
* Groups of classes to restrict.
*
* This method should be overridden in extending classes.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Avoid direct calls to the database.',
* 'classes' => array( 'PDO', '\Namespace\Classname' ),
* )
* )
*
* You can use * wildcards to target a group of (namespaced) classes.
* Aliased namespaces (use ..) are currently not supported.
*
* Documented here for clarity. Not (re)defined as it is already defined in the parent class.
*
* @return array
*
abstract public function getGroups();
*/
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
// Prepare the function group regular expressions only once.
if ( false === $this->setup_groups( 'classes' ) ) {
return array();
}
return array(
\T_DOUBLE_COLON,
\T_NEW,
\T_EXTENDS,
\T_IMPLEMENTS,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* {@internal Unlike in the `AbstractFunctionRestrictionsSniff`,
* we can't do a preliminary check on classes as at this point
* we don't know the class name yet.}}
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Reset the temporary storage before processing the token.
unset( $this->classname );
$this->excluded_groups = $this->merge_custom_array( $this->exclude );
if ( array_diff_key( $this->groups, $this->excluded_groups ) === array() ) {
// All groups have been excluded.
// Don't remove the listener as the exclude property can be changed inline.
return;
}
if ( true === $this->is_targetted_token( $stackPtr ) ) {
return $this->check_for_matches( $stackPtr );
}
}
/**
* Determine if we have a valid classname for the target token.
*
* @since 0.11.0 This logic was originally contained in the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return bool
*/
public function is_targetted_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
$classname = '';
if ( \in_array( $token['code'], array( \T_NEW, \T_EXTENDS, \T_IMPLEMENTS ), true ) ) {
if ( \T_NEW === $token['code'] ) {
$nameEnd = ( $this->phpcsFile->findNext( array( \T_OPEN_PARENTHESIS, \T_WHITESPACE, \T_SEMICOLON, \T_OBJECT_OPERATOR ), ( $stackPtr + 2 ) ) - 1 );
} else {
$nameEnd = ( $this->phpcsFile->findNext( array( \T_CLOSE_CURLY_BRACKET, \T_WHITESPACE ), ( $stackPtr + 2 ) ) - 1 );
}
$length = ( $nameEnd - ( $stackPtr + 1 ) );
$classname = $this->phpcsFile->getTokensAsString( ( $stackPtr + 2 ), $length );
if ( \T_NS_SEPARATOR !== $this->tokens[ ( $stackPtr + 2 ) ]['code'] ) {
$classname = $this->get_namespaced_classname( $classname, ( $stackPtr - 1 ) );
}
}
if ( \T_DOUBLE_COLON === $token['code'] ) {
$nameEnd = $this->phpcsFile->findPrevious( \T_STRING, ( $stackPtr - 1 ) );
$nameStart = ( $this->phpcsFile->findPrevious( array( \T_STRING, \T_NS_SEPARATOR, \T_NAMESPACE ), ( $nameEnd - 1 ), null, true, null, true ) + 1 );
$length = ( $nameEnd - ( $nameStart - 1 ) );
$classname = $this->phpcsFile->getTokensAsString( $nameStart, $length );
if ( \T_NS_SEPARATOR !== $this->tokens[ $nameStart ]['code'] ) {
$classname = $this->get_namespaced_classname( $classname, ( $nameStart - 1 ) );
}
}
// Stop if we couldn't determine a classname.
if ( empty( $classname ) ) {
return false;
}
// Nothing to do if 'parent', 'self' or 'static'.
if ( \in_array( $classname, array( 'parent', 'self', 'static' ), true ) ) {
return false;
}
$this->classname = $classname;
return true;
}
/**
* Verify if the current token is one of the targetted classes.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function check_for_matches( $stackPtr ) {
$skip_to = array();
foreach ( $this->groups as $groupName => $group ) {
if ( isset( $this->excluded_groups[ $groupName ] ) ) {
continue;
}
if ( preg_match( $group['regex'], $this->classname ) === 1 ) {
$skip_to[] = $this->process_matched_token( $stackPtr, $groupName, $this->classname );
}
}
if ( empty( $skip_to ) || min( $skip_to ) === 0 ) {
return;
}
return min( $skip_to );
}
/**
* Prepare the class name for use in a regular expression.
*
* The getGroups() method allows for providing class names with a wildcard * to target
* a group of classes within a namespace. It also allows for providing class names as
* 'ordinary' names or prefixed with one or more namespaces.
* This prepare routine takes that into account while still safely escaping the
* class name for use in a regular expression.
*
* @param string $classname Class name, potentially prefixed with namespaces.
* @return string Regex escaped class name.
*/
protected function prepare_name_for_regex( $classname ) {
$classname = trim( $classname, '\\' ); // Make sure all classnames have a \ prefix, but only one.
return parent::prepare_name_for_regex( $classname );
}
/**
* See if the classname was found in a namespaced file and if so, add the namespace to the classname.
*
* @param string $classname The full classname as found.
* @param int $search_from The token position to search up from.
* @return string Classname, potentially prefixed with the namespace.
*/
protected function get_namespaced_classname( $classname, $search_from ) {
// Don't do anything if this is already a fully qualified classname.
if ( empty( $classname ) || '\\' === $classname[0] ) {
return $classname;
}
// Remove the namespace keyword if used.
if ( 0 === strpos( $classname, 'namespace\\' ) ) {
$classname = substr( $classname, 10 );
}
$namespace_keyword = $this->phpcsFile->findPrevious( \T_NAMESPACE, $search_from );
if ( false === $namespace_keyword ) {
// No namespace keyword found at all, so global namespace.
$classname = '\\' . $classname;
} else {
$namespace = $this->determine_namespace( $search_from );
if ( ! empty( $namespace ) ) {
$classname = '\\' . $namespace . '\\' . $classname;
} else {
// No actual namespace found, so global namespace.
$classname = '\\' . $classname;
}
}
return $classname;
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Advises about parameters used in function calls.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
*/
abstract class AbstractFunctionParameterSniff extends AbstractFunctionRestrictionsSniff {
/**
* The group name for this group of functions.
*
* Intended to be overruled in the child class.
*
* @var string
*/
protected $group_name = 'restricted_parameters';
/**
* Functions this sniff is looking for. Should be defined in the child class.
*
* @var array The only requirement for this array is that the top level
* array keys are the names of the functions you're looking for.
* Other than that, the array can have arbitrary content
* depending on your needs.
*/
protected $target_functions = array();
/**
* Groups of functions to restrict.
*
* @return array
*/
public function getGroups() {
if ( empty( $this->target_functions ) ) {
return array();
}
return array(
$this->group_name => array(
'functions' => array_keys( $this->target_functions ),
),
);
}
/**
* Process a matched token.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
$parameters = $this->get_function_call_parameters( $stackPtr );
if ( empty( $parameters ) ) {
return $this->process_no_parameters( $stackPtr, $group_name, $matched_content );
} else {
return $this->process_parameters( $stackPtr, $group_name, $matched_content, $parameters );
}
}
/**
* Process the parameters of a matched function.
*
* This method has to be made concrete in child classes.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
abstract public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters );
/**
* Process the function if no parameters were found.
*
* Defaults to doing nothing. Can be overloaded in child classes to handle functions
* were parameters are expected, but none found.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_no_parameters( $stackPtr, $group_name, $matched_content ) {
return;
}
}

View file

@ -0,0 +1,342 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Restricts usage of some functions.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.10.0 Class became a proper abstract class. This was already the behaviour.
* Moved the file and renamed the class from
* `\WordPressCS\WordPress\Sniffs\Functions\FunctionRestrictionsSniff` to
* `\WordPressCS\WordPress\AbstractFunctionRestrictionsSniff`.
* @since 0.11.0 Extends the WordPressCS native `Sniff` class.
*/
abstract class AbstractFunctionRestrictionsSniff extends Sniff {
/**
* Exclude groups.
*
* Example: 'switch_to_blog,user_meta'
*
* @since 0.3.0
* @since 1.0.0 This property now expects to be passed an array.
* Previously a comma-delimited string was expected.
*
* @var array
*/
public $exclude = array();
/**
* Groups of function data to check against.
* Don't use this in extended classes, override getGroups() instead.
* This is only used for Unit tests.
*
* @since 0.10.0
*
* @var array
*/
public static $unittest_groups = array();
/**
* Regex pattern with placeholder for the function names.
*
* @since 0.10.0
*
* @var string
*/
protected $regex_pattern = '`^(?:%s)$`i';
/**
* Cache for the group information.
*
* @since 0.10.0
*
* @var array
*/
protected $groups = array();
/**
* Cache for the excluded groups information.
*
* @since 0.11.0
*
* @var array
*/
protected $excluded_groups = array();
/**
* Regex containing the name of all functions handled by a sniff.
*
* Set in `register()` and used to do an initial check.
*
* @var string
*/
private $prelim_check_regex;
/**
* Groups of functions to restrict.
*
* This method should be overridden in extending classes.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function', 'mysql_*' ),
* // Only useful when using wildcards:
* 'whitelist' => array( 'mysql_to_rfc3339' => true, ),
* )
* )
*
* You can use * wildcards to target a group of functions.
* When you use * wildcards, you may inadvertently restrict too many
* functions. In that case you can add the `whitelist` key to
* whitelist individual functions to prevent false positives.
*
* @return array
*/
abstract public function getGroups();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
// Prepare the function group regular expressions only once.
if ( false === $this->setup_groups( 'functions' ) ) {
return array();
}
return array(
\T_STRING,
);
}
/**
* Set up the regular expressions for each group.
*
* @since 0.10.0
*
* @param string $key The group array index key where the input for the regular expression can be found.
* @return bool True if the groups were setup. False if not.
*/
protected function setup_groups( $key ) {
// Prepare the function group regular expressions only once.
$this->groups = $this->getGroups();
if ( empty( $this->groups ) && empty( self::$unittest_groups ) ) {
return false;
}
// Allow for adding extra unit tests.
if ( ! empty( self::$unittest_groups ) ) {
$this->groups = array_merge( $this->groups, self::$unittest_groups );
}
$all_items = array();
foreach ( $this->groups as $groupName => $group ) {
if ( empty( $group[ $key ] ) ) {
unset( $this->groups[ $groupName ] );
} else {
$items = array_map( array( $this, 'prepare_name_for_regex' ), $group[ $key ] );
$all_items[] = $items;
$items = implode( '|', $items );
$this->groups[ $groupName ]['regex'] = sprintf( $this->regex_pattern, $items );
}
}
if ( empty( $this->groups ) ) {
return false;
}
// Create one "super-regex" to allow for initial filtering.
$all_items = \call_user_func_array( 'array_merge', $all_items );
$all_items = implode( '|', array_unique( $all_items ) );
$this->prelim_check_regex = sprintf( $this->regex_pattern, $all_items );
return true;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
$this->excluded_groups = $this->merge_custom_array( $this->exclude );
if ( array_diff_key( $this->groups, $this->excluded_groups ) === array() ) {
// All groups have been excluded.
// Don't remove the listener as the exclude property can be changed inline.
return;
}
// Preliminary check. If the content of the T_STRING is not one of the functions we're
// looking for, we can bow out before doing the heavy lifting of checking whether
// this is a function call.
if ( preg_match( $this->prelim_check_regex, $this->tokens[ $stackPtr ]['content'] ) !== 1 ) {
return;
}
if ( true === $this->is_targetted_token( $stackPtr ) ) {
return $this->check_for_matches( $stackPtr );
}
}
/**
* Verify is the current token is a function call.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return bool
*/
public function is_targetted_token( $stackPtr ) {
if ( \T_STRING !== $this->tokens[ $stackPtr ]['code'] ) {
return false;
}
// Exclude function definitions, class methods, and namespaced calls.
if ( $this->is_class_object_call( $stackPtr ) === true ) {
return false;
}
if ( $this->is_token_namespaced( $stackPtr ) === true ) {
return false;
}
$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
if ( false !== $prev ) {
// Skip sniffing on function, class definitions or for function aliases in use statements.
$skipped = array(
\T_FUNCTION => \T_FUNCTION,
\T_CLASS => \T_CLASS,
\T_AS => \T_AS, // Use declaration alias.
);
if ( isset( $skipped[ $this->tokens[ $prev ]['code'] ] ) ) {
return false;
}
}
// Check if this could even be a function call.
$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false === $next ) {
return false;
}
// Check for `use function ... (as|;)`.
if ( ( \T_STRING === $this->tokens[ $prev ]['code'] && 'function' === $this->tokens[ $prev ]['content'] )
&& ( \T_AS === $this->tokens[ $next ]['code'] || \T_SEMICOLON === $this->tokens[ $next ]['code'] )
) {
return true;
}
// If it's not a `use` statement, there should be parenthesis.
if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next ]['code'] ) {
return false;
}
return true;
}
/**
* Verify if the current token is one of the targetted functions.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function check_for_matches( $stackPtr ) {
$token_content = strtolower( $this->tokens[ $stackPtr ]['content'] );
$skip_to = array();
foreach ( $this->groups as $groupName => $group ) {
if ( isset( $this->excluded_groups[ $groupName ] ) ) {
continue;
}
if ( isset( $group['whitelist'][ $token_content ] ) ) {
continue;
}
if ( preg_match( $group['regex'], $token_content ) === 1 ) {
$skip_to[] = $this->process_matched_token( $stackPtr, $groupName, $token_content );
}
}
if ( empty( $skip_to ) || min( $skip_to ) === 0 ) {
return;
}
return min( $skip_to );
}
/**
* Process a matched token.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
$this->addMessage(
$this->groups[ $group_name ]['message'],
$stackPtr,
( 'error' === $this->groups[ $group_name ]['type'] ),
$this->string_to_errorcode( $group_name . '_' . $matched_content ),
array( $matched_content )
);
}
/**
* Prepare the function name for use in a regular expression.
*
* The getGroups() method allows for providing function names with a wildcard * to target
* a group of functions. This prepare routine takes that into account while still safely
* escaping the function name for use in a regular expression.
*
* @since 0.10.0
*
* @param string $function Function name.
* @return string Regex escaped function name.
*/
protected function prepare_name_for_regex( $function ) {
$function = str_replace( array( '.*', '*' ), '@@', $function ); // Replace wildcards with placeholder.
$function = preg_quote( $function, '`' );
$function = str_replace( '@@', '.*', $function ); // Replace placeholder with regex wildcard.
return $function;
}
}

View file

@ -0,0 +1,112 @@
<documentation title="Array Indentation">
<standard>
<![CDATA[
The array closing bracket indentation should line up with the start of the content on the line containing the array opener.
]]>
</standard>
<code_comparison>
<code title="Valid: Closing bracket lined up correctly">
<![CDATA[
$args = array(
'post_id' => 22,
<em>);</em>
]]>
</code>
<code title="Invalid: Closing bracket lined up incorrectly">
<![CDATA[
$args = array(
'post_id' => 22,
<em>);</em>
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
In multi-line arrays, array items should be indented by a 4-space tab for each level of nested array, so that the array visually matches its structure.
]]>
</standard>
<code_comparison>
<code title="Valid: Correctly indented array">
<![CDATA[
$args = array(
'post_id' => 22,
'comment_count' => array(
<em>'value' => 25,
'compare' => '>=',</em>
),
'post_type' => array(
'post',
'page',
),
);
]]>
</code>
<code title="Invalid: Indented incorrectly; harder to read.">
<![CDATA[
$args = array(
'post_id' => 22,
'comment_count' => array(
<em>'value' => 25,
'compare' => '>=',</em>
),
'post_type' => array(
<em>'post',
'page',</em>
),
);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Subsequent lines in multiline array items should be indented at least as much as the first line of the array item.
For heredocs/nowdocs, this does not apply to the content of the heredoc/nowdoc or the closer, but it does apply to the comma separating the item from the next.
]]>
</standard>
<code_comparison>
<code title="Valid: Subsequent lines are indented correctly.">
<![CDATA[
$args = array(
'phrase' => 'start of phrase'
. 'concatented additional phrase'
. 'more text',
);
]]>
</code>
<code title="Invalid: Subsequent items are indented before the first line item.">
<![CDATA[
$args = array(
'phrase' => 'start of phrase'
. 'concatented additional phrase'
. 'more text',
);
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: Opener and comma after closer are indented correctly">
<![CDATA[
$text = array(
<<<EOD
start of phrase
concatented additional phrase
more text
EOD
,
);
]]>
</code>
<code title="Invalid: Opener is aligned incorrectly to match the closer. The comma does not align correctly with the array indentation.">
<![CDATA[
$text = array(
<em><<<EOD</em>
start of phrase
concatented additional phrase
more text
EOD
<em>,</em>
);
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,27 @@
<documentation title="Array Key Spacing Restrictions">
<standard>
<![CDATA[
When referring to array items, only include a space around the index if it is a variable or the key is concatenated.
]]>
</standard>
<code_comparison>
<code title="Valid: Correct spacing around the index keys">
<![CDATA[
$post = $posts<em>[ </em>$post_id<em> ]</em>;
$post_title = $post<em>[ </em>'concatenated' . $title<em> ]</em>;
$post = $posts<em>[ </em>HOME_PAGE<em> ]</em>;
$post = $posts<em>[</em>123<em>]</em>;
$post_title = $post<em>[</em>'post_title'<em>]</em>;
]]>
</code>
<code title="Invalid: Incorrect spacing around the index keys">
<![CDATA[
$post = $posts<em>[</em>$post_id<em>]</em>;
$post_title = $post<em>[</em>'concatenated' . $title<em> ]</em>;
$post = $posts<em>[</em>HOME_PAGE<em>]</em>;
$post = $posts<em>[ </em>123<em> ]</em>;
$post_title = $post<em>[ </em>'post_title'<em> ]</em>;
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,46 @@
<documentation title="Multiple Statement Alignment">
<standard>
<![CDATA[
When declaring arrays, there should be one space on either side of a double arrow operator used to assign a value to a key.
]]>
</standard>
<code_comparison>
<code title="Valid: correct spacing between the key and value.">
<![CDATA[
$foo = array( 'cat'<em> => </em>22 );
$bar = array( 'year'<em> => </em>$current_year );
]]>
</code>
<code title="Invalid: No or incorrect spacing between the key and value.">
<![CDATA[
$foo = array( 'cat'<em>=></em>22 );
$bar = array( 'year'<em>=> </em>$current_year );
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
In the case of a block of related assignments, it is recommended to align the arrows to promote readability.
]]>
</standard>
<code_comparison>
<code title="Valid: Double arrow operators aligned">
<![CDATA[
$args = array(
'cat'<em> => </em>22,
'year'<em> => </em>$current_year,
'monthnum'<em> => </em>$current_month,
);
]]>
</code>
<code title="Invalid: Not aligned; harder to read">
<![CDATA[
$args = array(
'cat' <em>=></em> 22,
'year' <em>=></em> $current_year,
'monthnum' <em>=></em> $current_month,
);
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,53 @@
<documentation title="Class Instantiation">
<standard>
<![CDATA[
Instantiation of an object should be done with parenthesis.
]]>
</standard>
<code_comparison>
<code title="Valid: with parenthesis.">
<![CDATA[
$a = new Foobar<em>()</em>;
]]>
</code>
<code title="Invalid: without parenthesis.">
<![CDATA[
$a = new Foobar;
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Don't use spaces between the object name and the open parenthesis when instantiating new object.
]]>
</standard>
<code_comparison>
<code title="Valid: no whitespace between the object name and the parenthesis.">
<![CDATA[
$a = new Foobar();
]]>
</code>
<code title="Invalid: a space between the object name and the parenthesis.">
<![CDATA[
$a = new Foobar<em> </em>();
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Assigning the return value of "new" by reference was deprecated in PHP 5.3 and removed in PHP 7.0. New by reference should no longer be used.
]]>
</standard>
<code_comparison>
<code title="Valid: object instantiation without reference.">
<![CDATA[
$a = <em>new</em> Foobar();
]]>
</code>
<code title="Invalid: object instantiation by reference.">
<![CDATA[
$a = <em>& new</em> Foobar();
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,20 @@
<documentation title="Escaped Not Translated Text">
<standard>
<![CDATA[
Text intended for translation needs to be wrapped in a localization function call.
This sniff will help you find instances where text is escaped for output, but no localization function is called, even though an (unexpected) text domain argument is passed to the escape function.
]]>
</standard>
<code_comparison>
<code title="Valid: esc_html__() used to translate and escape.">
<![CDATA[
echo <em>esc_html__</em>( 'text', 'domain' );
]]>
</code>
<code title="Invalid: esc_html() used to only escape a string intended to be translated as well.">
<![CDATA[
echo <em>esc_html</em>( 'text', 'domain' );
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,31 @@
<documentation title="Current Time Timestamp">
<standard>
<![CDATA[
Don't use current_time() to get a timestamp as it doesn't produce a Unix (UTC) timestamp, but a "WordPress timestamp", i.e. a Unix timestamp with current timezone offset.
]]>
</standard>
<code_comparison>
<code title="Valid: using time() to get a Unix (UTC) timestamp.">
<![CDATA[
$timestamp = <em>time()</em>;
]]>
</code>
<code title="Invalid: using current_time() to get a Unix (UTC) timestamp.">
<![CDATA[
$timestamp = <em>current_time( 'timestamp', true )</em>;
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: using current_time() with a non-timestamp format.">
<![CDATA[
$timestamp = current_time( <em>'Y-m-d'</em> );
]]>
</code>
<code title="Invalid: using current_time() to get a timezone corrected timestamp.">
<![CDATA[
$timestamp = <em>current_time( 'U', false )</em>;
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<documentation title="Valid Hook Name">
<standard>
<![CDATA[
Use lowercase letters in action and filter names. Separate words using underscores.
]]>
</standard>
<code_comparison>
<code title="Valid: lowercase hook name.">
<![CDATA[
do_action( <em>'prefix_hook_name'</em>, $var );
]]>
</code>
<code title="Invalid: mixed case hook name.">
<![CDATA[
do_action( <em>'Prefix_Hook_NAME'</em>, $var );
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: words separated by underscores.">
<![CDATA[
apply_filters( <em>'prefix_hook_name'</em>, $var );
]]>
</code>
<code title="Invalid: using non-underscore characters to separate words.">
<![CDATA[
apply_filters( <em>'prefix\hook-name'</em>, $var );
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,117 @@
<documentation title="Validate post type slugs">
<standard>
<![CDATA[
The post type slug used in register_post_type() must be between 1 and 20 characters.
]]>
</standard>
<code_comparison>
<code title="Valid: short post type slug.">
<![CDATA[
register_post_type(
<em>'my_short_slug'</em>,
array()
);
]]>
</code>
<code title="Invalid: too long post type slug.">
<![CDATA[
register_post_type(
<em>'my_own_post_type_too_long'</em>,
array()
);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
The post type slug used in register_post_type() can only contain lowercase alphanumeric characters, dashes and underscores.
]]>
</standard>
<code_comparison>
<code title="Valid: no special characters in post type slug.">
<![CDATA[
register_post_type(
<em>'my_post_type_slug'</em>,
array()
);
]]>
</code>
<code title="Invalid: invalid characters in post type slug.">
<![CDATA[
register_post_type(
<em>'my/post/type/slug'</em>,
array()
);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
One should be careful with passing dynamic slug names to "register_post_type()", as the slug may become too long and could contain invalid characters.
]]>
</standard>
<code_comparison>
<code title="Valid: static post type slug.">
<![CDATA[
register_post_type(
<em>'my_post_active'</em>,
array()
);
]]>
</code>
<code title="Invalid: dynamic post type slug.">
<![CDATA[
register_post_type(
<em>"my_post_{$status}"</em>,
array()
);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
The post type slug used in register_post_type() can not use reserved keywords, such as the ones used by WordPress itself.
]]>
</standard>
<code_comparison>
<code title="Valid: prefixed post slug.">
<![CDATA[
register_post_type(
<em>'prefixed_author'</em>,
array()
);
]]>
</code>
<code title="Invalid: using a reserved keyword as slug.">
<![CDATA[
register_post_type(
<em>'author'</em>,
array()
);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
The post type slug used in register_post_type() can not use reserved prefixes, such as 'wp_', which is used by WordPress itself.
]]>
</standard>
<code_comparison>
<code title="Valid: custom prefix post slug.">
<![CDATA[
register_post_type(
<em>'prefixed_author'</em>,
array()
);
]]>
</code>
<code title="Invalid: using a reserved prefix.">
<![CDATA[
register_post_type(
<em>'wp_author'</em>,
array()
);
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,20 @@
<documentation title="Disallow Short Ternaries">
<standard>
<![CDATA[
The short ternary operator must not be used.
]]>
</standard>
<code_comparison>
<code title="Valid: long ternary.">
<![CDATA[
$height = ! empty( $data['height'] ) <em>?</em>
$data['height'] <em>:</em> 0;
]]>
</code>
<code title="Invalid: short ternary.">
<![CDATA[
$height = $data['height'] <em>? :</em> 0;
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,36 @@
<documentation title="Detect use of `ini_set()`">
<standard>
<![CDATA[
Using ini_set() and similar functions for altering PHP settings at runtime is discouraged. Changing runtime configuration might break other plugins and themes, and even WordPress itself.
]]>
</standard>
<code_comparison>
<code title="Valid: ini_set() for a possibly breaking setting.">
<![CDATA[
// ini_set() should not be used.
]]>
</code>
<code title="Invalid: ini_set() for a possibly breaking setting.">
<![CDATA[
ini_set( <em>'short_open_tag'</em>, 'off' );
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
For some configuration values there are alternative ways available - either via WordPress native functionality of via standard PHP - to achieve the same without the risk of breaking interoperability. These alternatives are preferred.
]]>
</standard>
<code_comparison>
<code title="Valid: WordPress functional alternative.">
<![CDATA[
<em>wp_raise_memory_limit();</em>
]]>
</code>
<code title="Invalid: ini_set() to alter memory limits.">
<![CDATA[
ini_set( <em>'memory_limit'</em>, '256M' );
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,19 @@
<documentation title="Safe Redirect">
<standard>
<![CDATA[
wp_safe_redirect() should be used whenever possible to prevent open redirect vulnerabilities. One of the main uses of an open redirect vulnerability is to make phishing attacks more credible. In this case the user sees your (trusted) domain and might get redirected to an attacker controlled website aimed at stealing private information.
]]>
</standard>
<code_comparison>
<code title="Valid: Redirect can only go to allowed domains.">
<![CDATA[
<em>wp_safe_redirect</em>( $location );
]]>
</code>
<code title="Invalid: Unsafe redirect, can be abused.">
<![CDATA[
<em>wp_redirect</em>( $location );
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,41 @@
<documentation title="Cron interval">
<standard>
<![CDATA[
Cron schedules running more often than once every 15 minutes are discouraged. Crons running that frequently can negatively impact the performance of a site.
]]>
</standard>
<code_comparison>
<code title="Valid: Cron schedule is created to run once every hour.">
<![CDATA[
function adjust_schedules( $schedules ) {
$schedules['every_hour'] = array(
'interval' => <em>HOUR_IN_SECONDS</em>,
'display' => __( 'Every hour' )
);
return $schedules;
}
add_filter(
'cron_schedules',
'adjust_schedules'
);
]]>
</code>
<code title="Invalid: Cron schedule is added to run more than once per 15 minutes.">
<![CDATA[
function adjust_schedules( $schedules ) {
$schedules['every_9_mins'] = array(
'interval' => <em>9 * 60</em>,
'display' => __( 'Every 9 minutes' )
);
return $schedules;
}
add_filter(
'cron_schedules',
'adjust_schedules'
);
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,19 @@
<documentation title="Deprecated Classes">
<standard>
<![CDATA[
Please refrain from using deprecated WordPress classes.
]]>
</standard>
<code_comparison>
<code title="Valid: use of a current (non-deprecated) class.">
<![CDATA[
$a = new <em>WP_User_Query</em>();
]]>
</code>
<code title="Invalid: use of a deprecated class.">
<![CDATA[
$a = new <em>WP_User_Search</em>(); // Deprecated WP 3.1.
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,19 @@
<documentation title="Deprecated Functions">
<standard>
<![CDATA[
Please refrain from using deprecated WordPress functions.
]]>
</standard>
<code_comparison>
<code title="Valid: use of a current (non-deprecated) function.">
<![CDATA[
$sites = <em>get_sites</em>();
]]>
</code>
<code title="Invalid: use of a deprecated function.">
<![CDATA[
$sites = <em>wp_get_sites</em>(); // Deprecated WP 4.6.
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,19 @@
<documentation title="Deprecated Function Parameter Values">
<standard>
<![CDATA[
Please refrain from using deprecated WordPress function parameter values.
]]>
</standard>
<code_comparison>
<code title="Valid: passing a valid function parameter value.">
<![CDATA[
bloginfo( <em>'url'</em> );
]]>
</code>
<code title="Invalid: passing a deprecated function parameter value.">
<![CDATA[
bloginfo ( <em>'home'</em> ); // Deprecated WP 2.2.0.
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,36 @@
<documentation title="Deprecated Function Parameters">
<standard>
<![CDATA[
Please refrain from passing deprecated WordPress function parameters.
In case, you need to pass an optional parameter positioned <em>after</em> the deprecated parameter, only ever pass the default value.
]]>
</standard>
<code_comparison>
<code title="Valid: not passing a deprecated parameter.">
<![CDATA[
// First - and only - parameter deprecated.
get_the_author();
]]>
</code>
<code title="Invalid: passing a deprecated parameter.">
<![CDATA[
// First - and only - parameter deprecated.
get_the_author( <em>$string</em> );
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: passing default value for a deprecated parameter.">
<![CDATA[
// Third parameter deprecated in WP 2.3.0.
add_option( 'option_name', 123, <em>''</em>, 'yes' );
]]>
</code>
<code title="Invalid: not passing the default value for a deprecated parameter.">
<![CDATA[
// Third parameter deprecated in WP 2.3.0.
add_option( 'my_name', 123, <em>'oops'</em>, 'yes' );
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,53 @@
<documentation title="Enqueued Resources">
<standard>
<![CDATA[
Scripts must be registered/enqueued via wp_enqueue_script().
]]>
</standard>
<code_comparison>
<code title="Valid: Script registered and enqueued correctly.">
<![CDATA[
<em>wp_enqueue_script</em>(
'someScript-js',
$path_to_file,
array( 'jquery' ),
'1.0.0',
true
);
]]>
</code>
<code title="Invalid: Script is directly embedded in HTML.">
<![CDATA[
printf(
'<em><script src="%s"></script></em>',
esc_url( $path_to_file )
);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Stylesheets must be registered/enqueued via wp_enqueue_style().
]]>
</standard>
<code_comparison>
<code title="Valid: Stylesheet registered and enqueued correctly.">
<![CDATA[
<em>wp_enqueue_style</em>(
'style-name',
$path_to_file,
array(),
'1.0.0'
);
]]>
</code>
<code title="Invalid: Stylesheet is directly embedded in HTML.">
<![CDATA[
printf(
'<em><link rel="stylesheet" href="%s" /></em>',
esc_url( $path_to_file )
);
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,69 @@
<documentation title="High Posts Per Page Limit">
<standard>
<![CDATA[
Using "posts_per_page" or "numberposts" with the value set to an high number opens up the potential for making requests slow if the query ends up querying thousands of posts.
You should always fetch the lowest number possible that still gives you the number of results you find acceptable.
]]>
</standard>
<code_comparison>
<code title="Valid: posts_per_page is not over limit (default 100).">
<![CDATA[
$args = array(
'posts_per_page' => <em>-1</em>,
);
$args = array(
'posts_per_page' => <em>100</em>,
);
$args = array(
'posts_per_page' => <em>'10'</em>,
);
$query_args['posts_per_page'] = <em>100</em>;
_query_posts( 'nopaging=1&posts_per_page=<em>50</em>' );
]]>
</code>
<code title="Invalid: posts_per_page is over limit (default 100).">
<![CDATA[
$args = array(
'posts_per_page' => <em>101</em>,
);
$query_args['posts_per_page'] = <em>200</em>;
_query_posts( 'nopaging=1&posts_per_page=<em>999</em>' );
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: numberposts is not over limit (default 100).">
<![CDATA[
$args = array(
'numberposts' => <em>-1</em>,
);
$args = array(
'numberposts' => <em>100</em>,
);
$args = array(
'numberposts' => <em>'10'</em>,
);
$query_args['numberposts'] = <em>'-1'<em>;
_query_posts( 'numberposts=<em>50</em>' );
]]>
</code>
<code title="Invalid: numberposts is over limit (default 100).">
<![CDATA[
$args = array(
'numberposts' => <em>101</em>,
);
$query_args['numberposts'] = <em>'200'</em>;
_query_posts( 'numberposts=<em>999</em>' );
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,23 @@
<documentation title="Cast Structure Spacing">
<standard>
<![CDATA[
A type cast should be preceded by whitespace.
There is only one exception to this rule: when the cast is preceded by the spread operator there should be no space between the spread operator and the cast.
]]>
</standard>
<code_comparison>
<code title="Valid: space before typecast.">
<![CDATA[
$a =<em> </em>(int) '420';
// No space between spread operator and cast.
$a = function_call( <em>...(array)</em> $mixed );
]]>
</code>
<code title="Invalid: no space before typecast.">
<![CDATA[
$a <em>=(</em>int) '420';
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,25 @@
<documentation title="Disallow Inline Tabs">
<standard>
<![CDATA[
Use spaces for inline alignment.
]]>
</standard>
<code_comparison>
<code title="Valid: spaces used for inline alignment.">
<![CDATA[
$a = array(
'abc'<em>[space]</em>=> 'lor',
'b'<em>[space][space][space]</em>=> 'em',
);
]]>
</code>
<code title="Invalid: tabs used for inline alignment.">
<![CDATA[
$a = array(
'abc'<em>[tab]</em>=> 'lor',
'b'<em>[tab]</em>=> 'em',
);
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,31 @@
<documentation title="Precision Alignment">
<standard>
<![CDATA[
Line indentation should only use tabs. Precision alignment with spaces after the tabs is strongly discouraged.
]]>
</standard>
<code_comparison>
<code title="Valid: only tabstops used for indentation.">
<![CDATA[
<em>[tab]</em>$var = true;
]]>
</code>
<code title="Invalid: precision alignment of 2 spaces.">
<![CDATA[
<em>[space][space]</em>$var = true;
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: four spaces counts as a tab, the replacement of these will be handled by another sniff.">
<![CDATA[
<em>[tab][space][space][space][space]</em>$var = true;
]]>
</code>
<code title="Invalid: precision alignment of 3 spaces.">
<![CDATA[
<em>[tab][space][space][space]</em>$var = true;
]]>
</code>
</code_comparison>
</documentation>

View file

@ -0,0 +1,109 @@
<?php
/**
* PHPCS cross-version compatibility helper class.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
/**
* PHPCSHelper
*
* PHPCS cross-version compatibility helper class.
*
* Deals with files which cannot be aliased 1-on-1 as the original
* class was split up into several classes.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.13.0
*/
class PHPCSHelper {
/**
* Get the PHPCS version number.
*
* @since 0.13.0
*
* @return string
*/
public static function get_version() {
return Config::VERSION;
}
/**
* Pass config data to PHPCS.
*
* PHPCS cross-version compatibility helper.
*
* @since 0.13.0
*
* @param string $key The name of the config value.
* @param string|null $value The value to set. If null, the config entry
* is deleted, reverting it to the default value.
* @param boolean $temp Set this config data temporarily for this script run.
* This will not write the config data to the config file.
*/
public static function set_config_data( $key, $value, $temp = false ) {
Config::setConfigData( $key, $value, $temp );
}
/**
* Get the value of a single PHPCS config key.
*
* @since 0.13.0
*
* @param string $key The name of the config value.
*
* @return string|null
*/
public static function get_config_data( $key ) {
return Config::getConfigData( $key );
}
/**
* Get the tab width as passed to PHPCS from the command-line or the ruleset.
*
* @since 0.13.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
*
* @return int Tab width. Defaults to 4.
*/
public static function get_tab_width( File $phpcsFile ) {
$tab_width = 4;
if ( isset( $phpcsFile->config->tabWidth ) && $phpcsFile->config->tabWidth > 0 ) {
$tab_width = $phpcsFile->config->tabWidth;
}
return $tab_width;
}
/**
* Check whether the `--ignore-annotations` option has been used.
*
* @since 0.13.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile Optional. The current file being processed.
*
* @return bool True if annotations should be ignored, false otherwise.
*/
public static function ignore_annotations( File $phpcsFile = null ) {
if ( isset( $phpcsFile, $phpcsFile->config->annotations ) ) {
return ! $phpcsFile->config->annotations;
} else {
$annotations = Config::getConfigData( 'annotations' );
if ( isset( $annotations ) ) {
return ! $annotations;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,468 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces WordPress array spacing format.
*
* - Check for no space between array keyword and array opener.
* - Check for no space between the parentheses of an empty array.
* - Checks for one space after the array opener / before the array closer in single-line arrays.
* - Checks that associative arrays are multi-line.
* - Checks that each array item in a multi-line array starts on a new line.
* - Checks that the array closer in a multi-line array is on a new line.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0 - The WordPress specific additional checks have now been split off
* from the `WordPress.Arrays.ArrayDeclaration` sniff into this sniff.
* - Added sniffing & fixing for associative arrays.
* @since 0.12.0 Decoupled this sniff from the upstream sniff completely.
* This sniff now extends the WordPressCS native `Sniff` class instead.
* @since 0.13.0 Added the last remaining checks from the `WordPress.Arrays.ArrayDeclaration`
* sniff which were not covered elsewhere.
* The `WordPress.Arrays.ArrayDeclaration` sniff has now been deprecated.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 Single item associative arrays are now by default exempt from the
* "must be multi-line" rule. This behaviour can be changed using the
* `allow_single_item_single_line_associative_arrays` property.
*/
class ArrayDeclarationSpacingSniff extends Sniff {
/**
* Whether or not to allow single item associative arrays to be single line.
*
* @since 0.14.0
*
* @var bool Defaults to true.
*/
public $allow_single_item_single_line_associative_arrays = true;
/**
* Token this sniff targets.
*
* Also used for distinguishing between the array and an array value
* which is also an array.
*
* @since 0.12.0
*
* @var array
*/
private $targets = array(
\T_ARRAY => \T_ARRAY,
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.12.0
*
* @return array
*/
public function register() {
return $this->targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.12.0 The actual checks contained in this method used to
* be in the `processSingleLineArray()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
unset( $array_open_close );
/*
* Long arrays only: Check for space between the array keyword and the open parenthesis.
*/
if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) {
if ( ( $stackPtr + 1 ) !== $opener ) {
$error = 'There must be no space between the "array" keyword and the opening parenthesis';
$error_code = 'SpaceAfterKeyword';
$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), ( $opener + 1 ), true );
if ( $nextNonWhitespace !== $opener ) {
// Don't auto-fix: Something other than whitespace found between keyword and open parenthesis.
$this->phpcsFile->addError( $error, $stackPtr, $error_code );
} else {
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $stackPtr + 1 ); $i < $opener; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
unset( $i );
}
}
unset( $error, $error_code, $nextNonWhitespace, $fix );
}
}
/*
* Check for empty arrays.
*/
$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $opener + 1 ), ( $closer + 1 ), true );
if ( $nextNonWhitespace === $closer ) {
if ( ( $opener + 1 ) !== $closer ) {
$fix = $this->phpcsFile->addFixableError(
'Empty array declaration must have no space between the parentheses',
$stackPtr,
'SpaceInEmptyArray'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $opener + 1 ); $i < $closer; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
unset( $i );
}
}
// This array is empty, so the below checks aren't necessary.
return;
}
unset( $nextNonWhitespace );
// Pass off to either the single line or multi-line array analysis.
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
$this->process_single_line_array( $stackPtr, $opener, $closer );
} else {
$this->process_multi_line_array( $stackPtr, $opener, $closer );
}
}
/**
* Process a single-line array.
*
* @since 0.13.0 The actual checks contained in this method used to
* be in the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_single_line_array( $stackPtr, $opener, $closer ) {
/*
* Check that associative arrays are always multi-line.
*/
$array_has_keys = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $opener, $closer );
if ( false !== $array_has_keys ) {
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( ( false === $this->allow_single_item_single_line_associative_arrays
&& ! empty( $array_items ) )
|| ( true === $this->allow_single_item_single_line_associative_arrays
&& \count( $array_items ) > 1 )
) {
/*
* Make sure the double arrow is for *this* array, not for a nested one.
*/
$array_has_keys = false; // Reset before doing more detailed check.
foreach ( $array_items as $item ) {
for ( $ptr = $item['start']; $ptr <= $item['end']; $ptr++ ) {
if ( \T_DOUBLE_ARROW === $this->tokens[ $ptr ]['code'] ) {
$array_has_keys = true;
break 2;
}
// Skip passed any nested arrays.
if ( isset( $this->targets[ $this->tokens[ $ptr ]['code'] ] ) ) {
$nested_array_open_close = $this->find_array_open_close( $ptr );
if ( false === $nested_array_open_close ) {
// Nested array open/close could not be determined.
continue;
}
$ptr = $nested_array_open_close['closer'];
}
}
}
if ( true === $array_has_keys ) {
$phrase = 'an';
if ( true === $this->allow_single_item_single_line_associative_arrays ) {
$phrase = 'a multi-item';
}
$fix = $this->phpcsFile->addFixableError(
'When %s array uses associative keys, each value should start on a new line.',
$closer,
'AssociativeArrayFound',
array( $phrase )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
foreach ( $array_items as $item ) {
/*
* Add a line break before the first non-empty token in the array item.
* Prevents extraneous whitespace at the start of the line which could be
* interpreted as alignment whitespace.
*/
$first_non_empty = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$item['start'],
( $item['end'] + 1 ),
true
);
if ( false === $first_non_empty ) {
continue;
}
if ( $item['start'] <= ( $first_non_empty - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $first_non_empty - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $first_non_empty - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $first_non_empty );
}
$this->phpcsFile->fixer->endChangeset();
}
// No need to check for spacing around opener/closer as this array should be multi-line.
return;
}
}
}
/*
* Check that there is a single space after the array opener and before the array closer.
*/
if ( \T_WHITESPACE !== $this->tokens[ ( $opener + 1 ) ]['code'] ) {
$fix = $this->phpcsFile->addFixableError(
'Missing space after array opener.',
$opener,
'NoSpaceAfterArrayOpener'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $opener, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $opener + 1 ) ]['content'] ) {
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space after array opener, found %s.',
$opener,
'SpaceAfterArrayOpener',
array( \strlen( $this->tokens[ ( $opener + 1 ) ]['content'] ) )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $opener + 1 ), ' ' );
}
}
if ( \T_WHITESPACE !== $this->tokens[ ( $closer - 1 ) ]['code'] ) {
$fix = $this->phpcsFile->addFixableError(
'Missing space before array closer.',
$closer,
'NoSpaceBeforeArrayCloser'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $closer, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $closer - 1 ) ]['content'] ) {
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space before array closer, found %s.',
$closer,
'SpaceBeforeArrayCloser',
array( \strlen( $this->tokens[ ( $closer - 1 ) ]['content'] ) )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), ' ' );
}
}
}
/**
* Process a multi-line array.
*
* @since 0.13.0 The actual checks contained in this method used to
* be in the `ArrayDeclaration` sniff.
*
* @param int $stackPtr The position of the current token in the stack.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_multi_line_array( $stackPtr, $opener, $closer ) {
/*
* Check that the closing bracket is on a new line.
*/
$last_content = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $closer - 1 ), $opener, true );
if ( false !== $last_content
&& $this->tokens[ $last_content ]['line'] === $this->tokens[ $closer ]['line']
) {
$fix = $this->phpcsFile->addFixableError(
'Closing parenthesis of array declaration must be on a new line',
$closer,
'CloseBraceNewLine'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( $last_content < ( $closer - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $closer );
$this->phpcsFile->fixer->endChangeset();
}
}
/*
* Check that each array item starts on a new line.
*/
$array_items = $this->get_function_call_parameters( $stackPtr );
$end_of_last_item = $opener;
foreach ( $array_items as $item ) {
$end_of_this_item = ( $item['end'] + 1 );
// Find the line on which the item starts.
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$item['start'],
$end_of_this_item,
true
);
// Ignore comments after array items if the next real content starts on a new line.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_last_item ]['line']
&& ( \T_COMMENT === $this->tokens[ $first_content ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $first_content ]['code'] ] ) )
) {
$end_of_comment = $first_content;
// Find the end of (multi-line) /* */- style trailing comments.
if ( substr( ltrim( $this->tokens[ $end_of_comment ]['content'] ), 0, 2 ) === '/*' ) {
while ( ( \T_COMMENT === $this->tokens[ $end_of_comment ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $end_of_comment ]['code'] ] ) )
&& substr( rtrim( $this->tokens[ $end_of_comment ]['content'] ), -2 ) !== '*/'
&& ( $end_of_comment + 1 ) < $end_of_this_item
) {
$end_of_comment++;
}
if ( $this->tokens[ $end_of_comment ]['line'] !== $this->tokens[ $end_of_last_item ]['line'] ) {
// Multi-line trailing comment.
$end_of_last_item = $end_of_comment;
}
}
$next = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
( $end_of_comment + 1 ),
$end_of_this_item,
true
);
if ( false === $next ) {
// Shouldn't happen, but just in case.
$end_of_last_item = $end_of_this_item;
continue;
}
if ( $this->tokens[ $next ]['line'] !== $this->tokens[ $first_content ]['line'] ) {
$first_content = $next;
}
}
if ( false === $first_content ) {
// Shouldn't happen, but just in case.
$end_of_last_item = $end_of_this_item;
continue;
}
if ( $this->tokens[ $end_of_last_item ]['line'] === $this->tokens[ $first_content ]['line'] ) {
$fix = $this->phpcsFile->addFixableError(
'Each item in a multi-line array must be on a new line',
$first_content,
'ArrayItemNoNewLine'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( ( $end_of_last_item + 1 ) <= ( $first_content - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $first_content - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $first_content - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $first_content );
$this->phpcsFile->fixer->endChangeset();
}
}
$end_of_last_item = $end_of_this_item;
}
}
}

View file

@ -0,0 +1,542 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use WordPressCS\WordPress\PHPCSHelper;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces WordPress array indentation for multi-line arrays.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* {@internal This sniff should eventually be pulled upstream as part of a solution
* for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }}
*/
class ArrayIndentationSniff extends Sniff {
/**
* Should tabs be used for indenting?
*
* If TRUE, fixes will be made using tabs instead of spaces.
* The size of each tab is important, so it should be specified
* using the --tab-width CLI argument.
*
* {@internal While for WPCS this should always be `true`, this property
* was added in anticipation of upstreaming the sniff.
* This property is the same as used in `Generic.WhiteSpace.ScopeIndent`.}}
*
* @var bool
*/
public $tabIndent = true;
/**
* The --tab-width CLI value that is being used.
*
* @var int
*/
private $tab_width;
/**
* Tokens to ignore for subsequent lines in a multi-line array item.
*
* Property is set in the register() method.
*
* @var array
*/
private $ignore_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
/*
* Set the $ignore_tokens property.
*
* Existing heredoc, nowdoc and inline HTML indentation should be respected at all times.
*/
$this->ignore_tokens = Tokens::$heredocTokens;
unset( $this->ignore_tokens[ \T_START_HEREDOC ], $this->ignore_tokens[ \T_START_NOWDOC ] );
$this->ignore_tokens[ \T_INLINE_HTML ] = \T_INLINE_HTML;
return array(
\T_ARRAY,
\T_OPEN_SHORT_ARRAY,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( ! isset( $this->tab_width ) ) {
$this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile );
}
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
// Not interested in single line arrays.
return;
}
/*
* Check the closing bracket is lined up with the start of the content on the line
* containing the array opener.
*/
$opener_line_spaces = $this->get_indentation_size( $opener );
$closer_line_spaces = ( $this->tokens[ $closer ]['column'] - 1 );
if ( $closer_line_spaces !== $opener_line_spaces ) {
$error = 'Array closer not aligned correctly; expected %s space(s) but found %s';
$error_code = 'CloseBraceNotAligned';
/*
* Report & fix the issue if the close brace is on its own line with
* nothing or only indentation whitespace before it.
*/
if ( 0 === $closer_line_spaces
|| ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
&& 1 === $this->tokens[ ( $closer - 1 ) ]['column'] )
) {
$this->add_array_alignment_error(
$closer,
$error,
$error_code,
$opener_line_spaces,
$closer_line_spaces,
$this->get_indentation_string( $opener_line_spaces )
);
} else {
/*
* Otherwise, only report the error, don't try and fix it (yet).
*
* It will get corrected in a future loop of the fixer once the closer
* has been moved to its own line by the `ArrayDeclarationSpacing` sniff.
*/
$this->phpcsFile->addError(
$error,
$closer,
$error_code,
array( $opener_line_spaces, $closer_line_spaces )
);
}
unset( $error, $error_code );
}
/*
* Verify & correct the array item indentation.
*/
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
// Strange, no array items found.
return;
}
$expected_spaces = ( $opener_line_spaces + $this->tab_width );
$expected_indent = $this->get_indentation_string( $expected_spaces );
$end_of_previous_item = $opener;
foreach ( $array_items as $item ) {
$end_of_this_item = ( $item['end'] + 1 );
// Find the line on which the item starts.
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$item['start'],
$end_of_this_item,
true
);
// Deal with trailing comments.
if ( false !== $first_content
&& \T_COMMENT === $this->tokens[ $first_content ]['code']
&& $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line']
) {
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE, \T_COMMENT ),
( $first_content + 1 ),
$end_of_this_item,
true
);
}
if ( false === $first_content ) {
$end_of_previous_item = $end_of_this_item;
continue;
}
// Bow out from reporting and fixing mixed multi-line/single-line arrays.
// That is handled by the ArrayDeclarationSpacingSniff.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line']
|| ( 1 !== $this->tokens[ $first_content ]['column']
&& \T_WHITESPACE !== $this->tokens[ ( $first_content - 1 ) ]['code'] )
) {
return $closer;
}
$found_spaces = ( $this->tokens[ $first_content ]['column'] - 1 );
if ( $found_spaces !== $expected_spaces ) {
$this->add_array_alignment_error(
$first_content,
'Array item not aligned correctly; expected %s spaces but found %s',
'ItemNotAligned',
$expected_spaces,
$found_spaces,
$expected_indent
);
}
// No need for further checking if this is a one-line array item.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
$end_of_previous_item = $end_of_this_item;
continue;
}
/*
* Multi-line array items.
*
* Verify & if needed, correct the indentation of subsequent lines.
* Subsequent lines may be indented more or less than the mimimum expected indent,
* but the "first line after" should be indented - at least - as much as the very first line
* of the array item.
* Indentation correction for subsequent lines will be based on that diff.
*/
// Find first token on second line of the array item.
// If the second line is a heredoc/nowdoc, continue on until we find a line with a different token.
// Same for the second line of a multi-line text string.
for ( $ptr = ( $first_content + 1 ); $ptr <= $item['end']; $ptr++ ) {
if ( $this->tokens[ $first_content ]['line'] !== $this->tokens[ $ptr ]['line']
&& 1 === $this->tokens[ $ptr ]['column']
&& false === $this->ignore_token( $ptr )
) {
break;
}
}
$first_content_on_line2 = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$ptr,
$end_of_this_item,
true
);
if ( false === $first_content_on_line2 ) {
/*
* Apparently there were only tokens in the ignore list on subsequent lines.
*
* In that case, the comma after the array item might be on a line by itself,
* so check its placement.
*/
if ( $this->tokens[ $item['end'] ]['line'] !== $this->tokens[ $end_of_this_item ]['line']
&& \T_COMMA === $this->tokens[ $end_of_this_item ]['code']
&& ( $this->tokens[ $end_of_this_item ]['column'] - 1 ) !== $expected_spaces
) {
$this->add_array_alignment_error(
$end_of_this_item,
'Comma after multi-line array item not aligned correctly; expected %s spaces, but found %s',
'MultiLineArrayItemCommaNotAligned',
$expected_spaces,
( $this->tokens[ $end_of_this_item ]['column'] - 1 ),
$expected_indent
);
}
$end_of_previous_item = $end_of_this_item;
continue;
}
$found_spaces_on_line2 = $this->get_indentation_size( $first_content_on_line2 );
$expected_spaces_on_line2 = $expected_spaces;
if ( $found_spaces < $found_spaces_on_line2 ) {
$expected_spaces_on_line2 += ( $found_spaces_on_line2 - $found_spaces );
}
if ( $found_spaces_on_line2 !== $expected_spaces_on_line2 ) {
$fix = $this->phpcsFile->addFixableError(
'Multi-line array item not aligned correctly; expected %s spaces, but found %s',
$first_content_on_line2,
'MultiLineArrayItemNotAligned',
array(
$expected_spaces_on_line2,
$found_spaces_on_line2,
)
);
if ( true === $fix ) {
$expected_indent_on_line2 = $this->get_indentation_string( $expected_spaces_on_line2 );
$this->phpcsFile->fixer->beginChangeset();
// Fix second line for the array item.
if ( 1 === $this->tokens[ $first_content_on_line2 ]['column']
&& \T_COMMENT === $this->tokens[ $first_content_on_line2 ]['code']
) {
$actual_comment = ltrim( $this->tokens[ $first_content_on_line2 ]['content'] );
$replacement = $expected_indent_on_line2 . $actual_comment;
$this->phpcsFile->fixer->replaceToken( $first_content_on_line2, $replacement );
} else {
$this->fix_alignment_error( $first_content_on_line2, $expected_indent_on_line2 );
}
// Fix subsequent lines.
for ( $i = ( $first_content_on_line2 + 1 ); $i <= $item['end']; $i++ ) {
// We're only interested in the first token on each line.
if ( 1 !== $this->tokens[ $i ]['column'] ) {
if ( $this->tokens[ $i ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
// We might as well quit if we're past the first token on the last line.
break;
}
continue;
}
$first_content_on_line = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$i,
$end_of_this_item,
true
);
if ( false === $first_content_on_line ) {
break;
}
// Ignore lines with heredoc and nowdoc tokens and subsequent lines in multi-line strings.
if ( true === $this->ignore_token( $first_content_on_line ) ) {
$i = $first_content_on_line;
continue;
}
$found_spaces_on_line = $this->get_indentation_size( $first_content_on_line );
$expected_spaces_on_line = ( $expected_spaces_on_line2 + ( $found_spaces_on_line - $found_spaces_on_line2 ) );
$expected_spaces_on_line = max( $expected_spaces_on_line, 0 ); // Can't be below 0.
$expected_indent_on_line = $this->get_indentation_string( $expected_spaces_on_line );
if ( $found_spaces_on_line !== $expected_spaces_on_line ) {
if ( 1 === $this->tokens[ $first_content_on_line ]['column']
&& \T_COMMENT === $this->tokens[ $first_content_on_line ]['code']
) {
$actual_comment = ltrim( $this->tokens[ $first_content_on_line ]['content'] );
$replacement = $expected_indent_on_line . $actual_comment;
$this->phpcsFile->fixer->replaceToken( $first_content_on_line, $replacement );
} else {
$this->fix_alignment_error( $first_content_on_line, $expected_indent_on_line );
}
}
// Move past any potential empty lines between the previous non-empty line and this one.
// No need to do the fixes twice.
$i = $first_content_on_line;
}
/*
* Check the placement of the comma after the array item as it might be on a line by itself.
*/
if ( $this->tokens[ $item['end'] ]['line'] !== $this->tokens[ $end_of_this_item ]['line']
&& \T_COMMA === $this->tokens[ $end_of_this_item ]['code']
&& ( $this->tokens[ $end_of_this_item ]['column'] - 1 ) !== $expected_spaces
) {
$this->add_array_alignment_error(
$end_of_this_item,
'Comma after array item not aligned correctly; expected %s spaces, but found %s',
'MultiLineArrayItemCommaNotAligned',
$expected_spaces,
( $this->tokens[ $end_of_this_item ]['column'] - 1 ),
$expected_indent
);
}
$this->phpcsFile->fixer->endChangeset();
}
}
$end_of_previous_item = $end_of_this_item;
}
}
/**
* Should the token be ignored ?
*
* This method is only intended to be used with the first token on a line
* for subsequent lines in an multi-line array item.
*
* @param int $ptr Stack pointer to the first token on a line.
*
* @return bool
*/
protected function ignore_token( $ptr ) {
$token_code = $this->tokens[ $ptr ]['code'];
if ( isset( $this->ignore_tokens[ $token_code ] ) ) {
return true;
}
/*
* If it's a subsequent line of a multi-line sting, it will not start with a quote
* character, nor just *be* a quote character.
*/
if ( \T_CONSTANT_ENCAPSED_STRING === $token_code
|| \T_DOUBLE_QUOTED_STRING === $token_code
) {
// Deal with closing quote of a multi-line string being on its own line.
if ( "'" === $this->tokens[ $ptr ]['content']
|| '"' === $this->tokens[ $ptr ]['content']
) {
return true;
}
// Deal with subsequent lines of a multi-line string where the token is broken up per line.
if ( "'" !== $this->tokens[ $ptr ]['content'][0]
&& '"' !== $this->tokens[ $ptr ]['content'][0]
) {
return true;
}
}
return false;
}
/**
* Determine the line indentation whitespace.
*
* @param int $ptr Stack pointer to an arbitrary token on a line.
*
* @return int Nr of spaces found. Where necessary, tabs are translated to spaces.
*/
protected function get_indentation_size( $ptr ) {
// Find the first token on the line.
for ( ; $ptr >= 0; $ptr-- ) {
if ( 1 === $this->tokens[ $ptr ]['column'] ) {
break;
}
}
$whitespace = '';
if ( \T_WHITESPACE === $this->tokens[ $ptr ]['code']
|| \T_DOC_COMMENT_WHITESPACE === $this->tokens[ $ptr ]['code']
) {
return $this->tokens[ $ptr ]['length'];
}
/*
* Special case for multi-line, non-docblock comments.
* Only applicable for subsequent lines in an array item.
*
* First/Single line is tokenized as T_WHITESPACE + T_COMMENT
* Subsequent lines are tokenized as T_COMMENT including the indentation whitespace.
*/
if ( \T_COMMENT === $this->tokens[ $ptr ]['code'] ) {
$content = $this->tokens[ $ptr ]['content'];
$actual_comment = ltrim( $content );
$whitespace = str_replace( $actual_comment, '', $content );
}
return \strlen( $whitespace );
}
/**
* Create an indentation string.
*
* @param int $nr Number of spaces the indentation should be.
*
* @return string
*/
protected function get_indentation_string( $nr ) {
if ( 0 >= $nr ) {
return '';
}
// Space-based indentation.
if ( false === $this->tabIndent ) {
return str_repeat( ' ', $nr );
}
// Tab-based indentation.
$num_tabs = (int) floor( $nr / $this->tab_width );
$remaining = ( $nr % $this->tab_width );
$tab_indent = str_repeat( "\t", $num_tabs );
$tab_indent .= str_repeat( ' ', $remaining );
return $tab_indent;
}
/**
* Throw an error and fix incorrect array alignment.
*
* @param int $ptr Stack pointer to the first content on the line.
* @param string $error Error message.
* @param string $error_code Error code.
* @param int $expected Expected nr of spaces (tabs translated to space value).
* @param int $found Found nr of spaces (tabs translated to space value).
* @param string $new_indent Whitespace indent replacement content.
*/
protected function add_array_alignment_error( $ptr, $error, $error_code, $expected, $found, $new_indent ) {
$fix = $this->phpcsFile->addFixableError( $error, $ptr, $error_code, array( $expected, $found ) );
if ( true === $fix ) {
$this->fix_alignment_error( $ptr, $new_indent );
}
}
/**
* Fix incorrect array alignment.
*
* @param int $ptr Stack pointer to the first content on the line.
* @param string $new_indent Whitespace indent replacement content.
*/
protected function fix_alignment_error( $ptr, $new_indent ) {
if ( 1 === $this->tokens[ $ptr ]['column'] ) {
$this->phpcsFile->fixer->addContentBefore( $ptr, $new_indent );
} else {
$this->phpcsFile->fixer->replaceToken( ( $ptr - 1 ), $new_indent );
}
}
}

View file

@ -0,0 +1,192 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check for proper spacing in array key references.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.7.0 This sniff now has the ability to fix a number of the issues it flags.
* @since 0.12.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 2.2.0 The sniff now also checks the size of the spacing, if applicable.
*/
class ArrayKeySpacingRestrictionsSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_OPEN_SQUARE_BRACKET,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
if ( ! isset( $token['bracket_closer'] ) ) {
$this->phpcsFile->addWarning( 'Missing bracket closer.', $stackPtr, 'MissingBracketCloser' );
return;
}
$need_spaces = $this->phpcsFile->findNext(
array( \T_CONSTANT_ENCAPSED_STRING, \T_LNUMBER, \T_WHITESPACE, \T_MINUS ),
( $stackPtr + 1 ),
$token['bracket_closer'],
true
);
$spaced1 = ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] );
$spaced2 = ( \T_WHITESPACE === $this->tokens[ ( $token['bracket_closer'] - 1 ) ]['code'] );
// It should have spaces unless if it only has strings or numbers as the key.
if ( false !== $need_spaces
&& ( false === $spaced1 || false === $spaced2 )
) {
$error = 'Array keys must be surrounded by spaces unless they contain a string or an integer.';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpacesAroundArrayKeys' );
if ( true === $fix ) {
if ( ! $spaced1 ) {
$this->phpcsFile->fixer->addContentBefore( ( $stackPtr + 1 ), ' ' );
}
if ( ! $spaced2 ) {
$this->phpcsFile->fixer->addContentBefore( $token['bracket_closer'], ' ' );
}
}
} elseif ( false === $need_spaces && ( $spaced1 || $spaced2 ) ) {
$error = 'Array keys must NOT be surrounded by spaces if they only contain a string or an integer.';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpacesAroundArrayKeys' );
if ( true === $fix ) {
if ( $spaced1 ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' );
for ( $i = ( $stackPtr + 2 ); $i < $token['bracket_closer']; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
if ( $spaced2 ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( ( $token['bracket_closer'] - 1 ), '' );
for ( $i = ( $token['bracket_closer'] - 2 ); $i > $stackPtr; $i-- ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
// If spaces are needed, check that there is only one space.
if ( false !== $need_spaces && ( $spaced1 || $spaced2 ) ) {
if ( $spaced1 ) {
$ptr = ( $stackPtr + 1 );
$length = 0;
if ( $this->tokens[ $ptr ]['line'] !== $this->tokens[ ( $ptr + 1 ) ]['line'] ) {
$length = 'newline';
} else {
$length = $this->tokens[ $ptr ]['length'];
}
if ( 1 !== $length ) {
$error = 'There should be exactly one space before the array key. Found: %s';
$data = array( $length );
$fix = $this->phpcsFile->addFixableError(
$error,
$ptr,
'TooMuchSpaceBeforeKey',
$data
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( $ptr, ' ' );
for ( $i = ( $ptr + 1 ); $i < $token['bracket_closer']; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
if ( $spaced2 ) {
$prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $token['bracket_closer'] - 1 ), null, true );
$ptr = ( $prev_non_empty + 1 );
$length = 0;
if ( $this->tokens[ $ptr ]['line'] !== $this->tokens[ $token['bracket_closer'] ]['line'] ) {
$length = 'newline';
} else {
$length = $this->tokens[ $ptr ]['length'];
}
if ( 1 !== $length ) {
$error = 'There should be exactly one space after the array key. Found: %s';
$data = array( $length );
$fix = $this->phpcsFile->addFixableError(
$error,
$ptr,
'TooMuchSpaceAfterKey',
$data
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( $ptr, ' ' );
for ( $i = ( $ptr + 1 ); $i < $token['bracket_closer']; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
}
}
}

View file

@ -0,0 +1,313 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces a comma after each array item and the spacing around it.
*
* Rules:
* - For multi-line arrays, a comma is needed after each array item.
* - Same for single-line arrays, but no comma is allowed after the last array item.
* - There should be no space between the comma and the end of the array item.
* - There should be exactly one space between the comma and the start of the
* next array item for single-line items.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class CommaAfterArrayItemSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_ARRAY,
\T_OPEN_SHORT_ARRAY,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
unset( $array_open_close );
// This array is empty, so the below checks aren't necessary.
if ( ( $opener + 1 ) === $closer ) {
return;
}
$single_line = true;
if ( $this->tokens[ $opener ]['line'] !== $this->tokens[ $closer ]['line'] ) {
$single_line = false;
}
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
// Strange, no array items found.
return;
}
$array_item_count = \count( $array_items );
// Note: $item_index is 1-based and the array items are split on the commas!
foreach ( $array_items as $item_index => $item ) {
$maybe_comma = ( $item['end'] + 1 );
$is_comma = false;
if ( isset( $this->tokens[ $maybe_comma ] ) && \T_COMMA === $this->tokens[ $maybe_comma ]['code'] ) {
$is_comma = true;
}
/*
* Check if this is a comma at the end of the last item in a single line array.
*/
if ( true === $single_line && $item_index === $array_item_count ) {
$this->phpcsFile->recordMetric(
$stackPtr,
'Single line array - comma after last item',
( true === $is_comma ? 'yes' : 'no' )
);
if ( true === $is_comma ) {
$fix = $this->phpcsFile->addFixableError(
'Comma not allowed after last value in single-line array declaration',
$maybe_comma,
'CommaAfterLast'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( $maybe_comma, '' );
}
}
/*
* No need to do the spacing checks for the last item in a single line array.
* This is handled by another sniff checking the spacing before the array closer.
*/
continue;
}
$last_content = $this->phpcsFile->findPrevious(
Tokens::$emptyTokens,
$item['end'],
$item['start'],
true
);
if ( false === $last_content ) {
// Shouldn't be able to happen, but just in case, ignore this array item.
continue;
}
/**
* Make sure every item in a multi-line array has a comma at the end.
*
* Should in reality only be triggered by the last item in a multi-line array
* as otherwise we'd have a parse error already.
*/
if ( false === $is_comma && false === $single_line ) {
$fix = $this->phpcsFile->addFixableError(
'Each array item in a multi-line array declaration must end in a comma',
$last_content,
'NoComma'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $last_content, ',' );
}
}
if ( false === $single_line && $item_index === $array_item_count ) {
$this->phpcsFile->recordMetric(
$stackPtr,
'Multi-line array - comma after last item',
( true === $is_comma ? 'yes' : 'no' )
);
}
if ( false === $is_comma ) {
// Can't check spacing around the comma if there is no comma.
continue;
}
/*
* Check for whitespace at the end of the array item.
*/
if ( $last_content !== $item['end']
// Ignore whitespace at the end of a multi-line item if it is the end of a heredoc/nowdoc.
&& ( true === $single_line
|| ! isset( Tokens::$heredocTokens[ $this->tokens[ $last_content ]['code'] ] ) )
) {
$newlines = 0;
$spaces = 0;
for ( $i = $item['end']; $i > $last_content; $i-- ) {
if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) {
if ( $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar ) {
$newlines++;
} else {
$spaces += $this->tokens[ $i ]['length'];
}
} elseif ( \T_COMMENT === $this->tokens[ $i ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $i ]['code'] ] )
) {
break;
}
}
$space_phrases = array();
if ( $spaces > 0 ) {
$space_phrases[] = $spaces . ' spaces';
}
if ( $newlines > 0 ) {
$space_phrases[] = $newlines . ' newlines';
}
unset( $newlines, $spaces );
$fix = $this->phpcsFile->addFixableError(
'Expected 0 spaces between "%s" and comma; %s found',
$maybe_comma,
'SpaceBeforeComma',
array(
$this->tokens[ $last_content ]['content'],
implode( ' and ', $space_phrases ),
)
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = $item['end']; $i > $last_content; $i-- ) {
if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
} elseif ( \T_COMMENT === $this->tokens[ $i ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $i ]['code'] ] )
) {
// We need to move the comma to before the comment.
$this->phpcsFile->fixer->addContent( $last_content, ',' );
$this->phpcsFile->fixer->replaceToken( $maybe_comma, '' );
/*
* No need to worry about removing too much whitespace in
* combination with a `//` comment as in that case, the newline
* is part of the comment, so we're good.
*/
break;
}
}
$this->phpcsFile->fixer->endChangeset();
}
}
if ( ! isset( $this->tokens[ ( $maybe_comma + 1 ) ] ) ) {
// Shouldn't be able to happen, but just in case.
continue;
}
/*
* Check whitespace after the comma.
*/
$next_token = $this->tokens[ ( $maybe_comma + 1 ) ];
if ( \T_WHITESPACE === $next_token['code'] ) {
if ( false === $single_line && $this->phpcsFile->eolChar === $next_token['content'] ) {
continue;
}
$next_non_whitespace = $this->phpcsFile->findNext(
\T_WHITESPACE,
( $maybe_comma + 1 ),
$closer,
true
);
if ( false === $next_non_whitespace
|| ( false === $single_line
&& $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $maybe_comma ]['line']
&& ( \T_COMMENT === $this->tokens[ $next_non_whitespace ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $next_non_whitespace ]['code'] ] ) ) )
) {
continue;
}
$space_length = $next_token['length'];
if ( 1 === $space_length ) {
continue;
}
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space between comma and "%s"; %s found',
$maybe_comma,
'SpaceAfterComma',
array(
$this->tokens[ $next_non_whitespace ]['content'],
$space_length,
)
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $maybe_comma + 1 ), ' ' );
}
} else {
// This is either a comment or a mixed single/multi-line array.
// Just add a space and let other sniffs sort out the array layout.
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space between comma and "%s"; 0 found',
$maybe_comma,
'NoSpaceAfterComma',
array( $next_token['content'] )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $maybe_comma, ' ' );
}
}
}
}
}

View file

@ -0,0 +1,617 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
/**
* Enforces alignment of the double arrow assignment operator for multi-item, multi-line arrays.
*
* - Align the double arrow operator to the same column for each item in a multi-item array.
* - Allows for setting a maxColumn property to aid in managing line-length.
* - Allows for new line(s) before a double arrow (configurable).
* - Allows for handling multi-line array items differently if so desired (configurable).
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*
* {@internal This sniff should eventually be pulled upstream as part of a solution
* for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }}
*/
class MultipleStatementAlignmentSniff extends Sniff {
/**
* Whether or not to ignore an array item for the purpose of alignment
* when a new line is found between the array key and the double arrow.
*
* @since 0.14.0
*
* @var bool
*/
public $ignoreNewlines = true;
/**
* Whether the alignment should be exact.
*
* Exact in this context means "largest index key + 1 space".
* When `false`, that is seen as the minimum alignment.
*
* @since 0.14.0
*
* @var bool
*/
public $exact = true;
/**
* The maximum column on which the double arrow alignment should be set.
*
* This property allows for limiting the whitespace padding to prevent
* overly long lines.
*
* If this value is set to, for instance, 60, it will:
* - if the expected column < 60, align at the expected column.
* - if the expected column >= 60, align at column 60.
* - for the outliers, i.e. the array indexes where the end position
* goes past column 60, it will not align the arrow, the sniff will
* just make sure there is only one space between the end of the
* array index and the double arrow.
*
* The column value is regarded as a hard value, i.e. includes indentation,
* so setting it very low is not a good idea.
*
* @since 0.14.0
*
* @var int
*/
public $maxColumn = 1000;
/**
* Whether or not to align the arrow operator for multi-line array items.
*
* Whether or not an item is regarded as multi-line is based on the **value**
* of the item, not the key.
*
* Valid values are:
* - 'always': Default. Align all arrays items regardless of single/multi-line.
* - 'never': Never align array items which span multiple lines.
* This will enforce one space between the array index and the
* double arrow operator for multi-line array items, independently
* of the alignment of the rest of the array items.
* Multi-line items where the arrow is already aligned with the
* "expected" alignment, however, will be left alone.
* - operator : Only align the operator for multi-line arrays items if the
* + number percentage of multi-line items passes the comparison.
* - As it is a percentage, the number has to be between 0 and 100.
* - Supported operators: <, <=, >, >=, ==, =, !=, <>
* - The percentage is calculated against all array items
* (with and without assignment operator).
* - The (new) expected alignment will be calculated based only
* on the items being aligned.
* - Multi-line items where the arrow is already aligned with the
* (new) "expected" alignment, however, will be left alone.
* Examples:
* * Setting this to `!=100` or `<100` means that alignment will
* be enforced, unless *all* array items are multi-line.
* This is probably the most commonly desired situation.
* * Setting this to `=100` means that alignment will only
* be enforced, if *all* array items are multi-line.
* * Setting this to `<50` means that the majority of array items
* need to be single line before alignment is enforced for
* multi-line items in the array.
* * Setting this to `=0` is useless as in that case there are
* no multi-line items in the array anyway.
*
* This setting will respect the `ignoreNewlines` and `maxColumnn` settings.
*
* @since 0.14.0
*
* @var string|int
*/
public $alignMultilineItems = 'always';
/**
* Storage for parsed $alignMultilineItems operator part.
*
* @since 0.14.0
*
* @var string
*/
private $operator;
/**
* Storage for parsed $alignMultilineItems numeric part.
*
* Stored as a string as the comparison will be done string based.
*
* @since 0.14.0
*
* @var string
*/
private $number;
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.14.0
*
* @return array
*/
public function register() {
return array(
\T_ARRAY,
\T_OPEN_SHORT_ARRAY,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
return;
}
// Pass off to either the single line or multi-line array analysis.
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
return $this->process_single_line_array( $stackPtr, $array_items, $opener, $closer );
} else {
return $this->process_multi_line_array( $stackPtr, $array_items, $opener, $closer );
}
}
/**
* Process a single-line array.
*
* While the WP standard does not allow single line multi-item associative arrays,
* this sniff should function independently of that.
*
* The `WordPress.WhiteSpace.OperatorSpacing` sniff already covers checking that
* there is a space between the array key and the double arrow, but doesn't
* enforce it to be exactly one space for single line arrays.
* That is what this method covers.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $items Info array containing information on each array item.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
protected function process_single_line_array( $stackPtr, $items, $opener, $closer ) {
/*
* For single line arrays, we don't care about what level the arrow is from.
* Just find and fix them all.
*/
$next_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
( $opener + 1 ),
$closer
);
while ( false !== $next_arrow ) {
if ( \T_WHITESPACE === $this->tokens[ ( $next_arrow - 1 ) ]['code'] ) {
$space_length = $this->tokens[ ( $next_arrow - 1 ) ]['length'];
if ( 1 !== $space_length ) {
$error = 'Expected 1 space between "%s" and double arrow; %s found';
$data = array(
$this->tokens[ ( $next_arrow - 2 ) ]['content'],
$space_length,
);
$fix = $this->phpcsFile->addFixableWarning( $error, $next_arrow, 'SpaceBeforeDoubleArrow', $data );
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $next_arrow - 1 ), ' ' );
}
}
}
// Find the position of the next double arrow.
$next_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
( $next_arrow + 1 ),
$closer
);
}
// Ignore any child-arrays as the double arrows in these will already have been handled.
return ( $closer + 1 );
}
/**
* Process a multi-line array.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $items Info array containing information on each array item.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_multi_line_array( $stackPtr, $items, $opener, $closer ) {
$this->maxColumn = (int) $this->maxColumn;
$this->validate_align_multiline_items();
/*
* Determine what the spacing before the arrow should be.
*
* Will unset any array items without double arrow and with new line whitespace
* if newlines are to be ignored, so the second foreach loop only has to deal
* with items which need attention.
*
* This sniff does not take incorrect indentation of array keys into account.
* That's for the `WordPress.Arrays.ArrayIndentation` sniff to fix.
* If that would affect the alignment, a second (or third) loop of the fixer
* will correct it (again) after the indentation has been fixed.
*/
$index_end_cols = array(); // Keep track of the end column position of index keys.
$double_arrow_cols = array(); // Keep track of arrow column position and count.
$multi_line_count = 0;
$total_items = \count( $items );
foreach ( $items as $key => $item ) {
if ( strpos( $item['raw'], '=>' ) === false ) {
// Ignore items without assignment operators.
unset( $items[ $key ] );
continue;
}
// Find the position of the first double arrow.
$double_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
$item['start'],
( $item['end'] + 1 )
);
if ( false === $double_arrow ) {
// Shouldn't happen, just in case.
unset( $items[ $key ] );
continue;
}
// Make sure the arrow is for this item and not for a nested array value assignment.
$has_array_opener = $this->phpcsFile->findNext(
$this->register(),
$item['start'],
$double_arrow
);
if ( false !== $has_array_opener ) {
// Double arrow is for a nested array.
unset( $items[ $key ] );
continue;
}
// Find the end of the array key.
$last_index_token = $this->phpcsFile->findPrevious(
\T_WHITESPACE,
( $double_arrow - 1 ),
$item['start'],
true
);
if ( false === $last_index_token ) {
// Shouldn't happen, but just in case.
unset( $items[ $key ] );
continue;
}
if ( true === $this->ignoreNewlines
&& $this->tokens[ $last_index_token ]['line'] !== $this->tokens[ $double_arrow ]['line']
) {
// Ignore this item as it has a new line between the item key and the double arrow.
unset( $items[ $key ] );
continue;
}
$index_end_position = ( $this->tokens[ $last_index_token ]['column'] + ( $this->tokens[ $last_index_token ]['length'] - 1 ) );
$items[ $key ]['operatorPtr'] = $double_arrow;
$items[ $key ]['last_index_token'] = $last_index_token;
$items[ $key ]['last_index_col'] = $index_end_position;
if ( $this->tokens[ $last_index_token ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
$items[ $key ]['single_line'] = true;
} else {
$items[ $key ]['single_line'] = false;
$multi_line_count++;
}
if ( ( $index_end_position + 2 ) <= $this->maxColumn ) {
$index_end_cols[] = $index_end_position;
}
if ( ! isset( $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] ) ) {
$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] = 1;
} else {
$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ]++;
}
}
unset( $key, $item, $double_arrow, $has_array_opener, $last_index_token );
if ( empty( $items ) || empty( $index_end_cols ) ) {
// No actionable array items found.
return;
}
/*
* Determine whether the operators for multi-line items should be aligned.
*/
if ( 'always' === $this->alignMultilineItems ) {
$alignMultilineItems = true;
} elseif ( 'never' === $this->alignMultilineItems ) {
$alignMultilineItems = false;
} else {
$percentage = (string) round( ( $multi_line_count / $total_items ) * 100, 0 );
// Bit hacky, but this is the only comparison function in PHP which allows to
// pass the comparison operator. And hey, it works ;-).
$alignMultilineItems = version_compare( $percentage, $this->number, $this->operator );
}
/*
* If necessary, rebuild the $index_end_cols and $double_arrow_cols arrays
* excluding multi-line items.
*/
if ( false === $alignMultilineItems ) {
$select_index_end_cols = array();
$double_arrow_cols = array();
foreach ( $items as $item ) {
if ( false === $item['single_line'] ) {
continue;
}
if ( ( $item['last_index_col'] + 2 ) <= $this->maxColumn ) {
$select_index_end_cols[] = $item['last_index_col'];
}
if ( ! isset( $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] ) ) {
$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] = 1;
} else {
$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ]++;
}
}
}
/*
* Determine the expected position of the double arrows.
*/
if ( ! empty( $select_index_end_cols ) ) {
$max_index_width = max( $select_index_end_cols );
} else {
$max_index_width = max( $index_end_cols );
}
$expected_col = ( $max_index_width + 2 );
if ( false === $this->exact && ! empty( $double_arrow_cols ) ) {
/*
* If the alignment does not have to be exact, see if a majority
* group of the arrows is already at an acceptable position.
*/
arsort( $double_arrow_cols, \SORT_NUMERIC );
reset( $double_arrow_cols );
$count = current( $double_arrow_cols );
if ( $count > 1 || ( 1 === $count && \count( $items ) === 1 ) ) {
// Allow for several groups of arrows having the same $count.
$filtered_double_arrow_cols = array_keys( $double_arrow_cols, $count, true );
foreach ( $filtered_double_arrow_cols as $col ) {
if ( $col > $expected_col && $col <= $this->maxColumn ) {
$expected_col = $col;
break;
}
}
}
}
unset( $max_index_width, $count, $filtered_double_arrow_cols, $col );
/*
* Verify and correct the spacing around the double arrows.
*/
foreach ( $items as $item ) {
if ( $this->tokens[ $item['operatorPtr'] ]['column'] === $expected_col
&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
) {
// Already correctly aligned.
continue;
}
if ( \T_WHITESPACE !== $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['code'] ) {
$before = 0;
} else {
if ( $this->tokens[ $item['last_index_token'] ]['line'] !== $this->tokens[ $item['operatorPtr'] ]['line'] ) {
$before = 'newline';
} else {
$before = $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['length'];
}
}
/*
* Deal with index sizes larger than maxColumn and with multi-line
* array items which should not be aligned.
*/
if ( ( $item['last_index_col'] + 2 ) > $this->maxColumn
|| ( false === $alignMultilineItems && false === $item['single_line'] )
) {
if ( ( $item['last_index_col'] + 2 ) === $this->tokens[ $item['operatorPtr'] ]['column']
&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
) {
// MaxColumn/Multi-line item exception, already correctly aligned.
continue;
}
$prefix = 'LongIndex';
if ( false === $alignMultilineItems && false === $item['single_line'] ) {
$prefix = 'MultilineItem';
}
$error_code = $prefix . 'SpaceBeforeDoubleArrow';
if ( 0 === $before ) {
$error_code = $prefix . 'NoSpaceBeforeDoubleArrow';
}
$fix = $this->phpcsFile->addFixableWarning(
'Expected 1 space between "%s" and double arrow; %s found.',
$item['operatorPtr'],
$error_code,
array(
$this->tokens[ $item['last_index_token'] ]['content'],
$before,
)
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
// Remove whitespace tokens between the end of the index and the arrow, if any.
for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
// Add the correct whitespace.
$this->phpcsFile->fixer->addContent( $item['last_index_token'], ' ' );
$this->phpcsFile->fixer->endChangeset();
}
continue;
}
/*
* Deal with the space before double arrows in all other cases.
*/
$expected_whitespace = $expected_col - ( $this->tokens[ $item['last_index_token'] ]['column'] + $this->tokens[ $item['last_index_token'] ]['length'] );
$fix = $this->phpcsFile->addFixableWarning(
'Array double arrow not aligned correctly; expected %s space(s) between "%s" and double arrow, but found %s.',
$item['operatorPtr'],
'DoubleArrowNotAligned',
array(
$expected_whitespace,
$this->tokens[ $item['last_index_token'] ]['content'],
$before,
)
);
if ( true === $fix ) {
if ( 0 === $before || 'newline' === $before ) {
$this->phpcsFile->fixer->beginChangeset();
// Remove whitespace tokens between the end of the index and the arrow, if any.
for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
// Add the correct whitespace.
$this->phpcsFile->fixer->addContent(
$item['last_index_token'],
str_repeat( ' ', $expected_whitespace )
);
$this->phpcsFile->fixer->endChangeset();
} elseif ( $expected_whitespace > $before ) {
// Add to the existing whitespace to prevent replacing tabs with spaces.
// That's the concern of another sniff.
$this->phpcsFile->fixer->addContent(
( $item['operatorPtr'] - 1 ),
str_repeat( ' ', ( $expected_whitespace - $before ) )
);
} else {
// Too much whitespace found.
$this->phpcsFile->fixer->replaceToken(
( $item['operatorPtr'] - 1 ),
str_repeat( ' ', $expected_whitespace )
);
}
}
}
}
/**
* Validate that a valid value has been received for the alignMultilineItems property.
*
* This message may be thrown more than once if the property is being changed inline in a file.
*
* @since 0.14.0
*/
protected function validate_align_multiline_items() {
$alignMultilineItems = $this->alignMultilineItems;
if ( 'always' === $alignMultilineItems || 'never' === $alignMultilineItems ) {
return;
} else {
// Correct for a potentially added % sign.
$alignMultilineItems = rtrim( $alignMultilineItems, '%' );
if ( preg_match( '`^([=<>!]{1,2})(100|[0-9]{1,2})$`', $alignMultilineItems, $matches ) > 0 ) {
$operator = $matches[1];
$number = (int) $matches[2];
if ( \in_array( $operator, array( '<', '<=', '>', '>=', '==', '=', '!=', '<>' ), true ) === true
&& ( $number >= 0 && $number <= 100 )
) {
$this->alignMultilineItems = $alignMultilineItems;
$this->number = (string) $number;
$this->operator = $operator;
return;
}
}
}
$this->phpcsFile->addError(
'Invalid property value passed: "%s". The value for the "alignMultilineItems" property for the "WordPress.Arrays.MultipleStatementAlignment" sniff should be either "always", "never" or an comparison operator + a number between 0 and 100.',
0,
'InvalidPropertyPassed',
array( $this->alignMultilineItems )
);
// Reset to the default if an invalid value was received.
$this->alignMultilineItems = 'always';
}
}

View file

@ -0,0 +1,204 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Classes;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Verifies object instantiation statements.
*
* - Demand the use of parenthesis.
* - Demand no space between the class name and the parenthesis.
* - Forbid assigning new by reference.
*
* {@internal Note: This sniff currently does not examine the parenthesis of new object
* instantiations where the class name is held in a variable variable.}}
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class ClassInstantiationSniff extends Sniff {
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = array(
'PHP',
'JS',
);
/**
* Tokens which can be part of a "classname".
*
* Set from within the register() method.
*
* @var array
*/
protected $classname_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
/*
* Set the $classname_tokens property.
*
* Currently does not account for classnames passed as a variable variable.
*/
$this->classname_tokens = Tokens::$emptyTokens;
$this->classname_tokens[ \T_NS_SEPARATOR ] = \T_NS_SEPARATOR;
$this->classname_tokens[ \T_STRING ] = \T_STRING;
$this->classname_tokens[ \T_SELF ] = \T_SELF;
$this->classname_tokens[ \T_STATIC ] = \T_STATIC;
$this->classname_tokens[ \T_PARENT ] = \T_PARENT;
$this->classname_tokens[ \T_ANON_CLASS ] = \T_ANON_CLASS;
// Classname in a variable.
$this->classname_tokens[ \T_VARIABLE ] = \T_VARIABLE;
$this->classname_tokens[ \T_DOUBLE_COLON ] = \T_DOUBLE_COLON;
$this->classname_tokens[ \T_OBJECT_OPERATOR ] = \T_OBJECT_OPERATOR;
$this->classname_tokens[ \T_OPEN_SQUARE_BRACKET ] = \T_OPEN_SQUARE_BRACKET;
$this->classname_tokens[ \T_CLOSE_SQUARE_BRACKET ] = \T_CLOSE_SQUARE_BRACKET;
$this->classname_tokens[ \T_CONSTANT_ENCAPSED_STRING ] = \T_CONSTANT_ENCAPSED_STRING;
$this->classname_tokens[ \T_LNUMBER ] = \T_LNUMBER;
return array(
\T_NEW,
\T_STRING, // JS.
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
// Make sure we have the right token, JS vs PHP.
if ( ( 'PHP' === $this->phpcsFile->tokenizerType && \T_NEW !== $this->tokens[ $stackPtr ]['code'] )
|| ( 'JS' === $this->phpcsFile->tokenizerType
&& ( \T_STRING !== $this->tokens[ $stackPtr ]['code']
|| 'new' !== strtolower( $this->tokens[ $stackPtr ]['content'] ) ) )
) {
return;
}
/*
* Check for new by reference used in PHP files.
*/
if ( 'PHP' === $this->phpcsFile->tokenizerType ) {
$prev_non_empty = $this->phpcsFile->findPrevious(
Tokens::$emptyTokens,
( $stackPtr - 1 ),
null,
true
);
if ( false !== $prev_non_empty && 'T_BITWISE_AND' === $this->tokens[ $prev_non_empty ]['type'] ) {
$this->phpcsFile->recordMetric( $stackPtr, 'Assigning new by reference', 'yes' );
$this->phpcsFile->addError(
'Assigning the return value of new by reference is no longer supported by PHP.',
$stackPtr,
'NewByReferenceFound'
);
} else {
$this->phpcsFile->recordMetric( $stackPtr, 'Assigning new by reference', 'no' );
}
}
/*
* Check for parenthesis & correct placement thereof.
*/
$next_non_empty_after_class_name = $this->phpcsFile->findNext(
$this->classname_tokens,
( $stackPtr + 1 ),
null,
true,
null,
true
);
if ( false === $next_non_empty_after_class_name ) {
// Live coding.
return;
}
// Walk back to the last part of the class name.
$has_comment = false;
for ( $classname_ptr = ( $next_non_empty_after_class_name - 1 ); $classname_ptr >= $stackPtr; $classname_ptr-- ) {
if ( ! isset( Tokens::$emptyTokens[ $this->tokens[ $classname_ptr ]['code'] ] ) ) {
// Prevent a false positive on variable variables, disregard them for now.
if ( $stackPtr === $classname_ptr ) {
return;
}
break;
}
if ( \T_WHITESPACE !== $this->tokens[ $classname_ptr ]['code'] ) {
$has_comment = true;
}
}
if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty_after_class_name ]['code'] ) {
$this->phpcsFile->recordMetric( $stackPtr, 'Object instantiation with parenthesis', 'no' );
$fix = $this->phpcsFile->addFixableError(
'Parenthesis should always be used when instantiating a new object.',
$classname_ptr,
'MissingParenthesis'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $classname_ptr, '()' );
}
} else {
$this->phpcsFile->recordMetric( $stackPtr, 'Object instantiation with parenthesis', 'yes' );
if ( ( $next_non_empty_after_class_name - 1 ) !== $classname_ptr ) {
$this->phpcsFile->recordMetric(
$stackPtr,
'Space between classname and parenthesis',
( $next_non_empty_after_class_name - $classname_ptr )
);
$error = 'There must be no spaces between the class name and the open parenthesis when instantiating a new object.';
$error_code = 'SpaceBeforeParenthesis';
if ( false === $has_comment ) {
$fix = $this->phpcsFile->addFixableError( $error, $next_non_empty_after_class_name, $error_code );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $next_non_empty_after_class_name - 1 ); $i > $classname_ptr; $i-- ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
} else {
$this->phpcsFile->addError( $error, $next_non_empty_after_class_name, $error_code );
}
} else {
$this->phpcsFile->recordMetric( $stackPtr, 'Space between classname and parenthesis', 0 );
}
}
}
}

View file

@ -0,0 +1,235 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Detects variable assignments being made within conditions.
*
* This is a typical code smell and more often than not a comparison was intended.
*
* Note: this sniff does not detect variable assignments in ternaries without parentheses!
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*
* {@internal This sniff is a duplicate of the same sniff as pulled upstream.
* Once the upstream sniff has been merged and the minimum WPCS PHPCS requirement has gone up to
* the version in which the sniff was merged, this version can be safely removed.
* {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/1594} }}
*/
class AssignmentInConditionSniff extends Sniff {
/**
* Assignment tokens to trigger on.
*
* Set in the register() method.
*
* @since 0.14.0
*
* @var array
*/
protected $assignment_tokens = array();
/**
* The tokens that indicate the start of a condition.
*
* @since 0.14.0
*
* @var array
*/
protected $condition_start_tokens = array();
/**
* Registers the tokens that this sniff wants to listen for.
*
* @since 0.14.0
*
* @return array
*/
public function register() {
$this->assignment_tokens = Tokens::$assignmentTokens;
unset( $this->assignment_tokens[ \T_DOUBLE_ARROW ] );
$starters = Tokens::$booleanOperators;
$starters[ \T_SEMICOLON ] = \T_SEMICOLON;
$starters[ \T_OPEN_PARENTHESIS ] = \T_OPEN_PARENTHESIS;
$starters[ \T_INLINE_ELSE ] = \T_INLINE_ELSE;
$this->condition_start_tokens = $starters;
return array(
\T_IF,
\T_ELSEIF,
\T_FOR,
\T_SWITCH,
\T_CASE,
\T_WHILE,
\T_INLINE_THEN,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
// Find the condition opener/closer.
if ( \T_FOR === $token['code'] ) {
if ( isset( $token['parenthesis_opener'], $token['parenthesis_closer'] ) === false ) {
return;
}
$semicolon = $this->phpcsFile->findNext( \T_SEMICOLON, ( $token['parenthesis_opener'] + 1 ), $token['parenthesis_closer'] );
if ( false === $semicolon ) {
return;
}
$opener = $semicolon;
$semicolon = $this->phpcsFile->findNext( \T_SEMICOLON, ( $opener + 1 ), $token['parenthesis_closer'] );
if ( false === $semicolon ) {
return;
}
$closer = $semicolon;
unset( $semicolon );
} elseif ( \T_CASE === $token['code'] ) {
if ( isset( $token['scope_opener'] ) === false ) {
return;
}
$opener = $stackPtr;
$closer = $token['scope_opener'];
} elseif ( \T_INLINE_THEN === $token['code'] ) {
// Check if the condition for the ternary is bracketed.
$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
if ( false === $prev ) {
// Shouldn't happen, but in that case we don't have anything to examine anyway.
return;
}
if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $prev ]['code'] ) {
if ( ! isset( $this->tokens[ $prev ]['parenthesis_opener'] ) ) {
return;
}
$opener = $this->tokens[ $prev ]['parenthesis_opener'];
$closer = $prev;
} elseif ( isset( $token['nested_parenthesis'] ) ) {
$closer = end( $token['nested_parenthesis'] );
$opener = key( $token['nested_parenthesis'] );
$next_statement_closer = $this->phpcsFile->findEndOfStatement( $stackPtr, array( \T_COLON, \T_CLOSE_PARENTHESIS, \T_CLOSE_SQUARE_BRACKET ) );
if ( false !== $next_statement_closer && $next_statement_closer < $closer ) {
// Parentheses are unrelated to the ternary.
return;
}
$prev_statement_closer = $this->phpcsFile->findStartOfStatement( $stackPtr, array( \T_COLON, \T_OPEN_PARENTHESIS, \T_OPEN_SQUARE_BRACKET ) );
if ( false !== $prev_statement_closer && $opener < $prev_statement_closer ) {
// Parentheses are unrelated to the ternary.
return;
}
if ( $closer > $stackPtr ) {
$closer = $stackPtr;
}
} else {
// No parenthesis found, can't determine where the conditional part of the ternary starts.
return;
}
} else {
if ( isset( $token['parenthesis_opener'], $token['parenthesis_closer'] ) === false ) {
return;
}
$opener = $token['parenthesis_opener'];
$closer = $token['parenthesis_closer'];
}
$startPos = $opener;
do {
$hasAssignment = $this->phpcsFile->findNext( $this->assignment_tokens, ( $startPos + 1 ), $closer );
if ( false === $hasAssignment ) {
return;
}
// Examine whether the left side is a variable.
$hasVariable = false;
$conditionStart = $startPos;
$altConditionStart = $this->phpcsFile->findPrevious(
$this->condition_start_tokens,
( $hasAssignment - 1 ),
$startPos
);
if ( false !== $altConditionStart ) {
$conditionStart = $altConditionStart;
}
for ( $i = $hasAssignment; $i > $conditionStart; $i-- ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
// If this is a variable or array, we've seen all we need to see.
if ( \T_VARIABLE === $this->tokens[ $i ]['code']
|| \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $i ]['code']
) {
$hasVariable = true;
break;
}
// If this is a function call or something, we are OK.
if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] ) {
break;
}
}
if ( true === $hasVariable ) {
$errorCode = 'Found';
if ( \T_WHILE === $token['code'] ) {
$errorCode = 'FoundInWhileCondition';
} elseif ( \T_INLINE_THEN === $token['code'] ) {
$errorCode = 'FoundInTernaryCondition';
}
$this->phpcsFile->addWarning(
'Variable assignment found within a condition. Did you mean to do a comparison?',
$hasAssignment,
$errorCode
);
} else {
$this->phpcsFile->addWarning(
'Assignment found within a condition. Did you mean to do a comparison?',
$hasAssignment,
'NonVariableAssignmentFound'
);
}
$startPos = $hasAssignment;
} while ( $startPos < $closer );
}
}

View file

@ -0,0 +1,161 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Checks against empty statements.
*
* - Check against two semi-colons with no executable code in between.
* - Check against an empty PHP open - close tag combination.
*
* {@internal This check should at some point in the future be pulled upstream and probably
* merged into the upstream `Generic.CodeAnalysis.EmptyStatement` sniff.
* This will need to wait until the WPCS minimum requirements have gone up
* beyond PHPCS 3.x though as it is not likely that new features will be accepted
* still for the PHPCS 2.x branch.}}
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class EmptyStatementSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_SEMICOLON,
\T_CLOSE_TAG,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
switch ( $this->tokens[ $stackPtr ]['type'] ) {
/*
* Detect `something();;`.
*/
case 'T_SEMICOLON':
$prevNonEmpty = $this->phpcsFile->findPrevious(
Tokens::$emptyTokens,
( $stackPtr - 1 ),
null,
true
);
if ( false === $prevNonEmpty
|| ( \T_SEMICOLON !== $this->tokens[ $prevNonEmpty ]['code']
&& \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code']
&& \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] )
) {
return;
}
if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
$nested = $this->tokens[ $stackPtr ]['nested_parenthesis'];
$last_closer = array_pop( $nested );
if ( isset( $this->tokens[ $last_closer ]['parenthesis_owner'] )
&& \T_FOR === $this->tokens[ $this->tokens[ $last_closer ]['parenthesis_owner'] ]['code']
) {
// Empty for() condition.
return;
}
}
$fix = $this->phpcsFile->addFixableWarning(
'Empty PHP statement detected: superfluous semi-colon.',
$stackPtr,
'SemicolonWithoutCodeDetected'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( \T_OPEN_TAG === $this->tokens[ $prevNonEmpty ]['code']
|| \T_OPEN_TAG_WITH_ECHO === $this->tokens[ $prevNonEmpty ]['code']
) {
/*
* Check for superfluous whitespace after the semi-colon which will be
* removed as the `<?php ` open tag token already contains whitespace,
* either a space or a new line and in case of a new line, the indentation
* should be done via tabs, so spaces can be safely removed.
*/
if ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] ) {
$replacement = str_replace( ' ', '', $this->tokens[ ( $stackPtr + 1 ) ]['content'] );
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), $replacement );
}
}
for ( $i = $stackPtr; $i > $prevNonEmpty; $i-- ) {
if ( \T_SEMICOLON !== $this->tokens[ $i ]['code']
&& \T_WHITESPACE !== $this->tokens[ $i ]['code']
) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
break;
/*
* Detect `<?php ?>`.
*/
case 'T_CLOSE_TAG':
$prevNonEmpty = $this->phpcsFile->findPrevious(
\T_WHITESPACE,
( $stackPtr - 1 ),
null,
true
);
if ( false === $prevNonEmpty
|| ( \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code']
&& \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] )
) {
return;
}
$fix = $this->phpcsFile->addFixableWarning(
'Empty PHP open/close tag combination detected.',
$prevNonEmpty,
'EmptyPHPOpenCloseTagsDetected'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = $prevNonEmpty; $i <= $stackPtr; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
break;
default:
/* Deliberately left empty. */
break;
}
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag calls to escaping functions which look like they may have been intended
* as calls to the "translate + escape" sister-function due to the presence of
* more than one parameter.
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class EscapedNotTranslatedSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 2.2.0
*
* @var string
*/
protected $group_name = 'escapednottranslated';
/**
* List of functions to examine.
*
* @link https://developer.wordpress.org/reference/functions/esc_html/
* @link https://developer.wordpress.org/reference/functions/esc_html__/
* @link https://developer.wordpress.org/reference/functions/esc_attr/
* @link https://developer.wordpress.org/reference/functions/esc_attr__/
*
* @since 2.2.0
*
* @var array <string function_name> => <string alternative function>
*/
protected $target_functions = array(
'esc_html' => 'esc_html__',
'esc_attr' => 'esc_attr__',
);
/**
* Process the parameters of a matched function.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
if ( \count( $parameters ) === 1 ) {
return;
}
/*
* We already know that there will be a valid open+close parenthesis, otherwise the sniff
* would have bowed out long before.
*/
$opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
$closer = $this->tokens[ $opener ]['parenthesis_closer'];
$data = array(
$matched_content,
$this->target_functions[ $matched_content ],
$this->phpcsFile->getTokensAsString( $stackPtr, ( $closer - $stackPtr + 1 ) ),
);
$this->phpcsFile->addWarning(
'%s() expects only one parameter. Did you mean to use %s() ? Found: %s',
$stackPtr,
'Found',
$data
);
}
}

View file

@ -0,0 +1,265 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag Database direct queries.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#direct-database-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.6.0 Removed the add_unique_message() function as it is no longer needed.
* @since 0.11.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `VIP` category to the `DB` category.
*/
class DirectDatabaseQuerySniff extends Sniff {
/**
* List of custom cache get functions.
*
* @since 0.6.0
*
* @var string|string[]
*/
public $customCacheGetFunctions = array();
/**
* List of custom cache set functions.
*
* @since 0.6.0
*
* @var string|string[]
*/
public $customCacheSetFunctions = array();
/**
* List of custom cache delete functions.
*
* @since 0.6.0
*
* @var string|string[]
*/
public $customCacheDeleteFunctions = array();
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.11.0
*
* @var array
*/
protected $addedCustomFunctions = array(
'cacheget' => array(),
'cacheset' => array(),
'cachedelete' => array(),
);
/**
* The lists of $wpdb methods.
*
* @since 0.6.0
* @since 0.11.0 Changed from static to non-static.
*
* @var array[]
*/
protected $methods = array(
'cachable' => array(
'delete' => true,
'get_var' => true,
'get_col' => true,
'get_row' => true,
'get_results' => true,
'query' => true,
'replace' => true,
'update' => true,
),
'noncachable' => array(
'insert' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_VARIABLE,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Check for $wpdb variable.
if ( '$wpdb' !== $this->tokens[ $stackPtr ]['content'] ) {
return;
}
$is_object_call = $this->phpcsFile->findNext( \T_OBJECT_OPERATOR, ( $stackPtr + 1 ), null, false, null, true );
if ( false === $is_object_call ) {
return; // This is not a call to the wpdb object.
}
$methodPtr = $this->phpcsFile->findNext( array( \T_WHITESPACE ), ( $is_object_call + 1 ), null, true, null, true );
$method = $this->tokens[ $methodPtr ]['content'];
$this->mergeFunctionLists();
if ( ! isset( $this->methods['all'][ $method ] ) ) {
return;
}
$endOfStatement = $this->phpcsFile->findNext( \T_SEMICOLON, ( $stackPtr + 1 ), null, false, null, true );
$endOfLineComment = '';
for ( $i = ( $endOfStatement + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) {
if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $endOfStatement ]['line'] ) {
break;
}
if ( \T_COMMENT === $this->tokens[ $i ]['code'] ) {
$endOfLineComment .= $this->tokens[ $i ]['content'];
}
}
$whitelisted_db_call = false;
if ( preg_match( '/db call\W*(?:ok|pass|clear|whitelist)/i', $endOfLineComment ) ) {
$whitelisted_db_call = true;
}
// Check for Database Schema Changes.
for ( $_pos = ( $stackPtr + 1 ); $_pos < $endOfStatement; $_pos++ ) {
$_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $_pos, $endOfStatement, false, null, true );
if ( false === $_pos ) {
break;
}
if ( preg_match( '#\b(?:ALTER|CREATE|DROP)\b#i', $this->tokens[ $_pos ]['content'] ) > 0 ) {
$this->phpcsFile->addWarning( 'Attempting a database schema change is discouraged.', $_pos, 'SchemaChange' );
}
}
// Flag instance if not whitelisted.
if ( ! $whitelisted_db_call ) {
$this->phpcsFile->addWarning( 'Usage of a direct database call is discouraged.', $stackPtr, 'DirectQuery' );
}
if ( ! isset( $this->methods['cachable'][ $method ] ) ) {
return $endOfStatement;
}
$whitelisted_cache = false;
$cached = false;
$wp_cache_get = false;
if ( preg_match( '/cache\s+(?:ok|pass|clear|whitelist)/i', $endOfLineComment ) ) {
$whitelisted_cache = true;
}
if ( ! $whitelisted_cache && ! empty( $this->tokens[ $stackPtr ]['conditions'] ) ) {
$scope_function = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
if ( false === $scope_function ) {
$scope_function = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
}
if ( false !== $scope_function ) {
$scopeStart = $this->tokens[ $scope_function ]['scope_opener'];
$scopeEnd = $this->tokens[ $scope_function ]['scope_closer'];
for ( $i = ( $scopeStart + 1 ); $i < $scopeEnd; $i++ ) {
if ( \T_STRING === $this->tokens[ $i ]['code'] ) {
if ( isset( $this->cacheDeleteFunctions[ $this->tokens[ $i ]['content'] ] ) ) {
if ( \in_array( $method, array( 'query', 'update', 'replace', 'delete' ), true ) ) {
$cached = true;
break;
}
} elseif ( isset( $this->cacheGetFunctions[ $this->tokens[ $i ]['content'] ] ) ) {
$wp_cache_get = true;
} elseif ( isset( $this->cacheSetFunctions[ $this->tokens[ $i ]['content'] ] ) ) {
if ( $wp_cache_get ) {
$cached = true;
break;
}
}
}
}
}
}
if ( ! $cached && ! $whitelisted_cache ) {
$message = 'Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().';
$this->phpcsFile->addWarning( $message, $stackPtr, 'NoCaching' );
}
return $endOfStatement;
}
/**
* Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @return void
*/
protected function mergeFunctionLists() {
if ( ! isset( $this->methods['all'] ) ) {
$this->methods['all'] = array_merge( $this->methods['cachable'], $this->methods['noncachable'] );
}
if ( $this->customCacheGetFunctions !== $this->addedCustomFunctions['cacheget'] ) {
$this->cacheGetFunctions = $this->merge_custom_array(
$this->customCacheGetFunctions,
$this->cacheGetFunctions
);
$this->addedCustomFunctions['cacheget'] = $this->customCacheGetFunctions;
}
if ( $this->customCacheSetFunctions !== $this->addedCustomFunctions['cacheset'] ) {
$this->cacheSetFunctions = $this->merge_custom_array(
$this->customCacheSetFunctions,
$this->cacheSetFunctions
);
$this->addedCustomFunctions['cacheset'] = $this->customCacheSetFunctions;
}
if ( $this->customCacheDeleteFunctions !== $this->addedCustomFunctions['cachedelete'] ) {
$this->cacheDeleteFunctions = $this->merge_custom_array(
$this->customCacheDeleteFunctions,
$this->cacheDeleteFunctions
);
$this->addedCustomFunctions['cachedelete'] = $this->customCacheDeleteFunctions;
}
}
}

View file

@ -0,0 +1,661 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Check for incorrect use of the $wpdb->prepare method.
*
* Check the following issues:
* - The only placeholders supported are: %d, %f (%F) and %s and their variations.
* - Literal % signs need to be properly escaped as `%%`.
* - Simple placeholders (%d, %f, %F, %s) should be left unquoted in the query string.
* - Complex placeholders - numbered and formatted variants - will not be quoted
* automagically by $wpdb->prepare(), so if used for values, should be quoted in
* the query string.
* - Either an array of replacements should be passed matching the number of
* placeholders found or individual parameters for each placeholder should
* be passed.
* - Wildcards for LIKE compare values should be passed in via a replacement parameter.
*
* The sniff allows for a specific pattern with a variable number of placeholders
* created using code along the lines of:
* `sprintf( 'query .... IN (%s) ...', implode( ',', array_fill( 0, count( $something ), '%s' ) ) )`.
*
* A "PreparedSQLPlaceholders replacement count" whitelist comment is supported
* specifically to silence the `ReplacementsWrongNumber` and `UnfinishedPrepare`
* error codes. The other error codes are not affected by it.
*
* @link https://developer.wordpress.org/reference/classes/wpdb/prepare/
* @link https://core.trac.wordpress.org/changeset/41496
* @link https://core.trac.wordpress.org/changeset/41471
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*/
class PreparedSQLPlaceholdersSniff extends Sniff {
/**
* These regexes copied from http://php.net/manual/en/function.sprintf.php#93552
* and adjusted for limitations in `$wpdb->prepare()`.
*
* Near duplicate of the one used in the WP.I18n sniff, but with fewer types allowed.
*
* Note: The regex delimiters and modifiers are not included to allow this regex to be
* concatenated together with other regex partials.
*
* @since 0.14.0
*
* @var string
*/
const PREPARE_PLACEHOLDER_REGEX = '(?:
(?<![^%]%) # Don\'t match a literal % (%%), including when it could overlap with a placeholder.
(?:
% # Start of placeholder.
(?:[0-9]+\\\\?\$)? # Optional ordering of the placeholders.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[dfFs] # Type specifier.
)
)';
/**
* Similar to above, but for the placeholder types *not* supported.
*
* Note: all optional parts are forced to be greedy to allow for the negative look ahead
* at the end to work.
*
* @since 0.14.0
*
* @var string
*/
const UNSUPPORTED_PLACEHOLDER_REGEX = '`(?:
(?<!%) # Don\'t match a literal % (%%).
(
% # Start of placeholder.
(?! # Negative look ahead.
%[^%] # Not a correct literal % (%%).
|
%%[dfFs] # Nor a correct literal % (%%), followed by a simple placeholder.
)
(?:[0-9]+\\\\??\$)?+ # Optional ordering of the placeholders.
[+-]?+ # Optional sign specifier.
(?:
(?:0|\'.)?+ # Optional padding specifier - excluding the space.
-?+ # Optional alignment specifier.
[0-9]*+ # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)?+ # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])?+ # Optional space padding specifier.
-?+ # Optional alignment specifier.
[0-9]++ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)?+ # Optional precision specifier with optional padding character.
)
(?![dfFs]) # Negative look ahead: not one of the supported placeholders.
(?:[^ \'"]*|$) # but something else instead.
)
)`x';
/**
* List of $wpdb methods we are interested in.
*
* @since 0.14.0
*
* @var array
*/
protected $target_methods = array(
'prepare' => true,
);
/**
* Storage for the stack pointer to the method call token.
*
* @since 0.14.0
*
* @var int
*/
protected $methodPtr;
/**
* Simple regex snippet to recognize and remember quotes.
*
* @since 0.14.0
*
* @var string
*/
private $regex_quote = '["\']';
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.14.0
*
* @return array
*/
public function register() {
return array(
\T_VARIABLE,
\T_STRING,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
if ( ! $this->is_wpdb_method_call( $stackPtr, $this->target_methods ) ) {
return;
}
$parameters = $this->get_function_call_parameters( $this->methodPtr );
if ( empty( $parameters ) ) {
return;
}
$query = $parameters[1];
$text_string_tokens_found = false;
$variable_found = false;
$sql_wildcard_found = false;
$total_placeholders = 0;
$total_parameters = \count( $parameters );
$valid_in_clauses = array(
'uses_in' => 0,
'implode_fill' => 0,
'adjustment_count' => 0,
);
for ( $i = $query['start']; $i <= $query['end']; $i++ ) {
// Skip over groups of tokens if they are part of an inline function call.
if ( isset( $skip_from, $skip_to ) && $i >= $skip_from && $i < $skip_to ) {
$i = $skip_to;
continue;
}
if ( ! isset( Tokens::$textStringTokens[ $this->tokens[ $i ]['code'] ] ) ) {
if ( \T_VARIABLE === $this->tokens[ $i ]['code'] ) {
if ( '$wpdb' !== $this->tokens[ $i ]['content'] ) {
$variable_found = true;
}
continue;
}
// Detect a specific pattern for variable replacements in combination with `IN`.
if ( \T_STRING === $this->tokens[ $i ]['code'] ) {
if ( 'sprintf' === strtolower( $this->tokens[ $i ]['content'] ) ) {
$sprintf_parameters = $this->get_function_call_parameters( $i );
if ( ! empty( $sprintf_parameters ) ) {
$skip_from = ( $sprintf_parameters[1]['end'] + 1 );
$last_param = end( $sprintf_parameters );
$skip_to = ( $last_param['end'] + 1 );
$valid_in_clauses['implode_fill'] += $this->analyse_sprintf( $sprintf_parameters );
$valid_in_clauses['adjustment_count'] += ( \count( $sprintf_parameters ) - 1 );
}
unset( $sprintf_parameters, $last_param );
} elseif ( 'implode' === strtolower( $this->tokens[ $i ]['content'] ) ) {
$prev = $this->phpcsFile->findPrevious(
Tokens::$textStringTokens,
( $i - 1 ),
$query['start']
);
$prev_content = $this->strip_quotes( $this->tokens[ $prev ]['content'] );
$regex_quote = $this->get_regex_quote_snippet( $prev_content, $this->tokens[ $prev ]['content'] );
// Only examine the implode if preceded by an ` IN (`.
if ( preg_match( '`\s+IN\s*\(\s*(' . $regex_quote . ')?$`i', $prev_content, $match ) > 0 ) {
if ( isset( $match[1] ) && $regex_quote !== $this->regex_quote ) {
$this->phpcsFile->addError(
'Dynamic placeholder generation should not have surrounding quotes.',
$i,
'QuotedDynamicPlaceholderGeneration'
);
}
if ( $this->analyse_implode( $i ) === true ) {
++$valid_in_clauses['uses_in'];
++$valid_in_clauses['implode_fill'];
$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code']
&& isset( $this->tokens[ $next ]['parenthesis_closer'] )
) {
$skip_from = ( $i + 1 );
$skip_to = ( $this->tokens[ $next ]['parenthesis_closer'] + 1 );
}
}
}
unset( $prev, $next, $prev_content, $regex_quote, $match );
}
}
continue;
}
$text_string_tokens_found = true;
$content = $this->tokens[ $i ]['content'];
$regex_quote = $this->regex_quote;
if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) ) {
$content = $this->strip_quotes( $content );
$regex_quote = $this->get_regex_quote_snippet( $content, $this->tokens[ $i ]['content'] );
}
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code']
|| \T_HEREDOC === $this->tokens[ $i ]['code']
) {
// Only interested in actual query text, so strip out variables.
$stripped_content = $this->strip_interpolated_variables( $content );
if ( $stripped_content !== $content ) {
$interpolated_vars = $this->get_interpolated_variables( $content );
$vars_without_wpdb = array_diff( $interpolated_vars, array( 'wpdb' ) );
$content = $stripped_content;
if ( ! empty( $vars_without_wpdb ) ) {
$variable_found = true;
}
}
unset( $stripped_content, $interpolated_vars, $vars_without_wpdb );
}
$placeholders = preg_match_all( '`' . self::PREPARE_PLACEHOLDER_REGEX . '`x', $content, $matches );
if ( $placeholders > 0 ) {
$total_placeholders += $placeholders;
}
/*
* Analyse the query for incorrect LIKE queries.
*
* - `LIKE %s` is the only correct one.
* - `LIKE '%s'` or `LIKE "%s"` will not be reported here, but in the quote check.
* - Any other `LIKE` statement should be reported, either for using `LIKE` without
* SQL wildcards or for not passing the SQL wildcards via the replacement.
*/
$regex = '`\s+LIKE\s*(?:(' . $regex_quote . ')(?!%s(?:\1|$))(?P<content>.*?)(?:\1|$)|(?:concat\((?![^\)]*%s[^\)]*\))(?P<concat>[^\)]*))\))`i';
if ( preg_match_all( $regex, $content, $matches ) > 0 ) {
$walk = array();
if ( ! empty( $matches['content'] ) ) {
$matches['content'] = array_filter( $matches['content'] );
if ( ! empty( $matches['content'] ) ) {
$walk[] = 'content';
}
}
if ( ! empty( $matches['concat'] ) ) {
$matches['concat'] = array_filter( $matches['concat'] );
if ( ! empty( $matches['concat'] ) ) {
$walk[] = 'concat';
}
}
if ( ! empty( $walk ) ) {
foreach ( $walk as $match_key ) {
foreach ( $matches[ $match_key ] as $index => $match ) {
$data = array( $matches[0][ $index ] );
// Both a `%` as well as a `_` are wildcards in SQL.
if ( strpos( $match, '%' ) === false && strpos( $match, '_' ) === false ) {
$this->phpcsFile->addWarning(
'Unless you are using SQL wildcards, using LIKE is inefficient. Use a straight compare instead. Found: %s.',
$i,
'LikeWithoutWildcards',
$data
);
} else {
$sql_wildcard_found = true;
if ( strpos( $match, '%s' ) === false ) {
$this->phpcsFile->addError(
'SQL wildcards for a LIKE query should be passed in through a replacement parameter. Found: %s.',
$i,
'LikeWildcardsInQuery',
$data
);
} else {
$this->phpcsFile->addError(
'SQL wildcards for a LIKE query should be passed in through a replacement parameter and the variable part of the replacement should be escaped using "esc_like()". Found: %s.',
$i,
'LikeWildcardsInQueryWithPlaceholder',
$data
);
}
}
/*
* Don't throw `UnescapedLiteral`, `UnsupportedPlaceholder` or `QuotedPlaceholder`
* for this part of the SQL query.
*/
$content = preg_replace( '`' . preg_quote( $match, '`' ) . '`', '', $content, 1 );
}
}
}
unset( $matches, $index, $match, $data );
}
if ( strpos( $content, '%' ) === false ) {
continue;
}
/*
* Analyse the query for unsupported placeholders.
*/
if ( preg_match_all( self::UNSUPPORTED_PLACEHOLDER_REGEX, $content, $matches ) > 0 ) {
if ( ! empty( $matches[0] ) ) {
foreach ( $matches[0] as $match ) {
if ( '%' === $match ) {
$this->phpcsFile->addError(
'Found unescaped literal "%%" character.',
$i,
'UnescapedLiteral',
array( $match )
);
} else {
$this->phpcsFile->addError(
'Unsupported placeholder used in $wpdb->prepare(). Found: "%s".',
$i,
'UnsupportedPlaceholder',
array( $match )
);
}
}
}
unset( $match, $matches );
}
/*
* Analyse the query for quoted placeholders.
*/
$regex = '`(' . $regex_quote . ')%[dfFs]\1`';
if ( preg_match_all( $regex, $content, $matches ) > 0 ) {
if ( ! empty( $matches[0] ) ) {
foreach ( $matches[0] as $match ) {
$this->phpcsFile->addError(
'Simple placeholders should not be quoted in the query string in $wpdb->prepare(). Found: %s.',
$i,
'QuotedSimplePlaceholder',
array( $match )
);
}
}
unset( $match, $matches );
}
/*
* Analyse the query for unquoted complex placeholders.
*/
$regex = '`(?<!' . $regex_quote . ')' . self::PREPARE_PLACEHOLDER_REGEX . '(?!' . $regex_quote . ')`x';
if ( preg_match_all( $regex, $content, $matches ) > 0 ) {
if ( ! empty( $matches[0] ) ) {
foreach ( $matches[0] as $match ) {
if ( preg_match( '`%[dfFs]`', $match ) !== 1 ) {
$this->phpcsFile->addWarning(
'Complex placeholders used for values in the query string in $wpdb->prepare() will NOT be quoted automagically. Found: %s.',
$i,
'UnquotedComplexPlaceholder',
array( $match )
);
}
}
}
unset( $match, $matches );
}
/*
* Check for an ` IN (%s)` clause.
*/
$found_in = preg_match_all( '`\s+IN\s*\(\s*%s\s*\)`i', $content, $matches );
if ( $found_in > 0 ) {
$valid_in_clauses['uses_in'] += $found_in;
}
unset( $found_in );
}
if ( false === $text_string_tokens_found ) {
// Query string passed in as a variable or function call, nothing to examine.
return;
}
$count_diff_whitelisted = $this->has_whitelist_comment(
'PreparedSQLPlaceholders replacement count',
$stackPtr
);
if ( 0 === $total_placeholders ) {
if ( 1 === $total_parameters ) {
if ( false === $variable_found && false === $sql_wildcard_found ) {
/*
* Only throw this warning if the PreparedSQL sniff won't throw one about
* variables being found.
* Also don't throw it if we just advised to use a replacement variable to pass a
* string containing an SQL wildcard.
*/
$this->phpcsFile->addWarning(
'It is not necessary to prepare a query which doesn\'t use variable replacement.',
$i,
'UnnecessaryPrepare'
);
}
} elseif ( false === $count_diff_whitelisted && 0 === $valid_in_clauses['uses_in'] ) {
$this->phpcsFile->addWarning(
'Replacement variables found, but no valid placeholders found in the query.',
$i,
'UnfinishedPrepare'
);
}
return;
}
if ( 1 === $total_parameters ) {
$this->phpcsFile->addError(
'Placeholders found in the query passed to $wpdb->prepare(), but no replacements found. Expected %d replacement(s) parameters.',
$stackPtr,
'MissingReplacements',
array( $total_placeholders )
);
return;
}
if ( true === $count_diff_whitelisted ) {
return;
}
$replacements = $parameters;
array_shift( $replacements ); // Remove the query.
// The parameters may have been passed as an array in parameter 2.
if ( isset( $parameters[2] ) && 2 === $total_parameters ) {
$next = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$parameters[2]['start'],
( $parameters[2]['end'] + 1 ),
true
);
if ( false !== $next
&& ( \T_ARRAY === $this->tokens[ $next ]['code']
|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $next ]['code'] )
) {
$replacements = $this->get_function_call_parameters( $next );
}
}
$total_replacements = \count( $replacements );
$total_placeholders -= $valid_in_clauses['adjustment_count'];
// Bow out when `IN` clauses have been used which appear to be correct.
if ( $valid_in_clauses['uses_in'] > 0
&& $valid_in_clauses['uses_in'] === $valid_in_clauses['implode_fill']
&& 1 === $total_replacements
) {
return;
}
/*
* Verify that the correct amount of replacements have been passed.
*/
if ( $total_replacements !== $total_placeholders ) {
$this->phpcsFile->addWarning(
'Incorrect number of replacements passed to $wpdb->prepare(). Found %d replacement parameters, expected %d.',
$stackPtr,
'ReplacementsWrongNumber',
array( $total_replacements, $total_placeholders )
);
}
}
/**
* Retrieve a regex snippet to recognize and remember quotes based on the quote style
* used in the original string (if any).
*
* This allows for recognizing `"` and `\'` in single quoted strings,
* recognizing `'` and `\"` in double quotes strings and `'` and `"`when the quote
* style is unknown or it is a non-quoted string (heredoc/nowdoc and such).
*
* @since 0.14.0
*
* @param string $stripped_content Text string content without surrounding quotes.
* @param string $original_content Original content for the same text string.
*
* @return string
*/
protected function get_regex_quote_snippet( $stripped_content, $original_content ) {
$regex_quote = $this->regex_quote;
if ( $original_content !== $stripped_content ) {
$quote_style = $original_content[0];
if ( '"' === $quote_style ) {
$regex_quote = '\\\\"|\'';
} elseif ( "'" === $quote_style ) {
$regex_quote = '"|\\\\\'';
}
}
return $regex_quote;
}
/**
* Analyse a sprintf() query wrapper to see if it contains a specific code pattern
* to deal correctly with `IN` queries.
*
* The pattern we are searching for is:
* `sprintf( 'query ....', implode( ',', array_fill( 0, count( $something ), '%s' ) ) )`
*
* @since 0.14.0
*
* @param array $sprintf_params Parameters details for the sprintf call.
*
* @return int The number of times the pattern was found in the replacements.
*/
protected function analyse_sprintf( $sprintf_params ) {
$found = 0;
unset( $sprintf_params[1] );
foreach ( $sprintf_params as $sprintf_param ) {
if ( strpos( strtolower( $sprintf_param['raw'] ), 'implode' ) === false ) {
continue;
}
$implode = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$sprintf_param['start'],
$sprintf_param['end'],
true
);
if ( \T_STRING === $this->tokens[ $implode ]['code']
&& 'implode' === strtolower( $this->tokens[ $implode ]['content'] )
) {
if ( $this->analyse_implode( $implode ) === true ) {
++$found;
}
}
}
return $found;
}
/**
* Analyse an implode() function call to see if it contains a specific code pattern
* to dynamically create placeholders.
*
* The pattern we are searching for is:
* `implode( ',', array_fill( 0, count( $something ), '%s' ) )`
*
* This pattern presumes unquoted placeholders!
*
* @since 0.14.0
*
* @param int $implode_token The stackPtr to the implode function call.
*
* @return bool True if the pattern is found, false otherwise.
*/
protected function analyse_implode( $implode_token ) {
$implode_params = $this->get_function_call_parameters( $implode_token );
if ( empty( $implode_params ) || \count( $implode_params ) !== 2 ) {
return false;
}
if ( preg_match( '`^(["\']), ?\1$`', $implode_params[1]['raw'] ) !== 1 ) {
return false;
}
if ( strpos( strtolower( $implode_params[2]['raw'] ), 'array_fill' ) === false ) {
return false;
}
$array_fill = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$implode_params[2]['start'],
$implode_params[2]['end'],
true
);
if ( \T_STRING !== $this->tokens[ $array_fill ]['code']
|| 'array_fill' !== strtolower( $this->tokens[ $array_fill ]['content'] )
) {
return false;
}
$array_fill_params = $this->get_function_call_parameters( $array_fill );
if ( empty( $array_fill_params ) || \count( $array_fill_params ) !== 3 ) {
return false;
}
return (bool) preg_match( '`^(["\'])%[dfFs]\1$`', $array_fill_params[3]['raw'] );
}
}

View file

@ -0,0 +1,210 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Sniff for prepared SQL.
*
* Makes sure that variables aren't directly interpolated into SQL statements.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#formatting-sql-statements
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.8.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `WP` category to the `DB` category.
*/
class PreparedSQLSniff extends Sniff {
/**
* The lists of $wpdb methods.
*
* @since 0.8.0
* @since 0.11.0 Changed from static to non-static.
*
* @var array
*/
protected $methods = array(
'get_var' => true,
'get_col' => true,
'get_row' => true,
'get_results' => true,
'prepare' => true,
'query' => true,
);
/**
* Tokens that we don't flag when they are found in a $wpdb method call.
*
* @since 0.9.0
*
* @var array
*/
protected $ignored_tokens = array(
\T_OBJECT_OPERATOR => true,
\T_OPEN_PARENTHESIS => true,
\T_CLOSE_PARENTHESIS => true,
\T_STRING_CONCAT => true,
\T_CONSTANT_ENCAPSED_STRING => true,
\T_OPEN_SQUARE_BRACKET => true,
\T_CLOSE_SQUARE_BRACKET => true,
\T_COMMA => true,
\T_LNUMBER => true,
\T_START_HEREDOC => true,
\T_END_HEREDOC => true,
\T_START_NOWDOC => true,
\T_NOWDOC => true,
\T_END_NOWDOC => true,
\T_INT_CAST => true,
\T_DOUBLE_CAST => true,
\T_BOOL_CAST => true,
\T_NS_SEPARATOR => true,
);
/**
* A loop pointer.
*
* It is a property so that we can access it in all of our methods.
*
* @since 0.9.0
*
* @var int
*/
protected $i;
/**
* The loop end marker.
*
* It is a property so that we can access it in all of our methods.
*
* @since 0.9.0
*
* @var int
*/
protected $end;
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.8.0
*
* @return array
*/
public function register() {
$this->ignored_tokens += Tokens::$emptyTokens;
return array(
\T_VARIABLE,
\T_STRING,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.8.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( ! $this->is_wpdb_method_call( $stackPtr, $this->methods ) ) {
return;
}
if ( $this->has_whitelist_comment( 'unprepared SQL', $stackPtr ) ) {
return;
}
for ( $this->i; $this->i < $this->end; $this->i++ ) {
if ( isset( $this->ignored_tokens[ $this->tokens[ $this->i ]['code'] ] ) ) {
continue;
}
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $this->i ]['code']
|| \T_HEREDOC === $this->tokens[ $this->i ]['code']
) {
$bad_variables = array_filter(
$this->get_interpolated_variables( $this->tokens[ $this->i ]['content'] ),
function ( $symbol ) {
return ( 'wpdb' !== $symbol );
}
);
foreach ( $bad_variables as $bad_variable ) {
$this->phpcsFile->addError(
'Use placeholders and $wpdb->prepare(); found interpolated variable $%s at %s',
$this->i,
'InterpolatedNotPrepared',
array(
$bad_variable,
$this->tokens[ $this->i ]['content'],
)
);
}
continue;
}
if ( \T_VARIABLE === $this->tokens[ $this->i ]['code'] ) {
if ( '$wpdb' === $this->tokens[ $this->i ]['content'] ) {
$this->is_wpdb_method_call( $this->i, $this->methods );
continue;
}
if ( $this->is_safe_casted( $this->i ) ) {
continue;
}
}
if ( \T_STRING === $this->tokens[ $this->i ]['code'] ) {
if (
isset( $this->SQLEscapingFunctions[ $this->tokens[ $this->i ]['content'] ] )
|| isset( $this->SQLAutoEscapedFunctions[ $this->tokens[ $this->i ]['content'] ] )
) {
// Find the opening parenthesis.
$opening_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->i + 1 ), null, true, null, true );
if ( false !== $opening_paren
&& \T_OPEN_PARENTHESIS === $this->tokens[ $opening_paren ]['code']
&& isset( $this->tokens[ $opening_paren ]['parenthesis_closer'] )
) {
// Skip past the end of the function.
$this->i = $this->tokens[ $opening_paren ]['parenthesis_closer'];
continue;
}
} elseif ( isset( $this->formattingFunctions[ $this->tokens[ $this->i ]['content'] ] ) ) {
continue;
}
}
$this->phpcsFile->addError(
'Use placeholders and $wpdb->prepare(); found %s',
$this->i,
'NotPrepared',
array( $this->tokens[ $this->i ]['content'] )
);
}
return $this->end;
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\AbstractClassRestrictionsSniff;
/**
* Verifies that no database related PHP classes are used.
*
* "Avoid touching the database directly. If there is a defined function that can get
* the data you need, use it. Database abstraction (using functions instead of queries)
* helps keep your code forward-compatible and, in cases where results are cached in memory,
* it can be many times faster."
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class RestrictedClassesSniff extends AbstractClassRestrictionsSniff {
/**
* Groups of classes to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Avoid direct calls to the database.',
* 'classes' => array( 'PDO', '\Namespace\Classname' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'mysql' => array(
'type' => 'error',
'message' => 'Accessing the database directly should be avoided. Please use the $wpdb object and associated functions instead. Found: %s.',
'classes' => array(
'mysqli',
'PDO',
'PDOStatement',
),
),
);
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Verifies that no database related PHP functions are used.
*
* "Avoid touching the database directly. If there is a defined function that can get
* the data you need, use it. Database abstraction (using functions instead of queries)
* helps keep your code forward-compatible and, in cases where results are cached in memory,
* it can be many times faster."
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'mysql' => array(
'type' => 'error',
'message' => 'Accessing the database directly should be avoided. Please use the $wpdb object and associated functions instead. Found: %s.',
'functions' => array(
'mysql_*',
'mysqli_*',
'mysqlnd_ms_*',
'mysqlnd_qc_*',
'mysqlnd_uh_*',
'mysqlnd_memcache_*',
'maxdb_*',
),
'whitelist' => array(
'mysql_to_rfc3339' => true,
),
),
);
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff;
/**
* Flag potentially slow queries.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#uncached-pageload
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.12.0 Introduced new and more intuitively named 'slow query' whitelist
* comment, replacing the 'tax_query' whitelist comment which is now
* deprecated.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `VIP` category to the `DB` category.
*/
class SlowDBQuerySniff extends AbstractArrayAssignmentRestrictionsSniff {
/**
* Groups of variables to restrict.
*
* @return array
*/
public function getGroups() {
return array(
'slow_db_query' => array(
'type' => 'warning',
'message' => 'Detected usage of %s, possible slow query.',
'keys' => array(
'tax_query',
'meta_query',
'meta_key',
'meta_value',
),
),
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.10.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( $this->has_whitelist_comment( 'slow query', $stackPtr ) ) {
return;
} elseif ( $this->has_whitelist_comment( 'tax_query', $stackPtr ) ) {
return;
}
return parent::process_token( $stackPtr );
}
/**
* Callback to process each confirmed key, to check value.
* This must be extended to add the logic to check assignment value.
*
* @param string $key Array index / key.
* @param mixed $val Assigned value.
* @param int $line Token line.
* @param array $group Group definition.
* @return mixed FALSE if no match, TRUE if matches, STRING if matches
* with custom error message passed to ->process().
*/
public function callback( $key, $val, $line, $group ) {
return true;
}
}

View file

@ -0,0 +1,174 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DateTime;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Don't use current_time() to get a (timezone corrected) "timestamp".
*
* Disallow using the current_time() function to get "timestamps" as it
* doesn't produce a *real* timestamp, but a "WordPress timestamp", i.e.
* a Unix timestamp with current timezone offset, not a Unix timestamp ansich.
*
* @link https://developer.wordpress.org/reference/functions/current_time/
* @link https://make.wordpress.org/core/2019/09/23/date-time-improvements-wp-5-3/
* @link https://core.trac.wordpress.org/ticket/40657
* @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1791
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class CurrentTimeTimestampSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 2.2.0
*
* @var string
*/
protected $group_name = 'current_time';
/**
* List of functions to examine.
*
* @since 2.2.0
*
* @var array <string function_name> => <bool always needed ?>
*/
protected $target_functions = array(
'current_time' => true,
);
/**
* Process the parameters of a matched function.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
/*
* We already know there will be valid open & close parentheses as otherwise the parameter
* retrieval function call would have returned an empty array, so no additional checks needed.
*/
$open_parens = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $stackPtr );
$close_parens = $this->tokens[ $open_parens ]['parenthesis_closer'];
/*
* Check whether the first parameter is a timestamp format.
*/
for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
if ( isset( Tokens::$textStringTokens[ $this->tokens[ $i ]['code'] ] ) ) {
$content_first = trim( $this->strip_quotes( $this->tokens[ $i ]['content'] ) );
if ( 'U' !== $content_first && 'timestamp' !== $content_first ) {
// Most likely valid use of current_time().
return;
}
continue;
}
if ( isset( Tokens::$heredocTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
/*
* If we're still here, we've encountered an unexpected token, like a variable or
* function call. Bow out as we can't determine the runtime value.
*/
return;
}
$gmt_true = false;
/*
* Check whether the second parameter, $gmt, is a set to `true` or `1`.
*/
if ( isset( $parameters[2] ) ) {
$content_second = '';
if ( 'true' === $parameters[2]['raw'] || '1' === $parameters[2]['raw'] ) {
$content_second = $parameters[2]['raw'];
$gmt_true = true;
} else {
// Do a more extensive parameter check.
for ( $i = $parameters[2]['start']; $i <= $parameters[2]['end']; $i++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
$content_second .= $this->tokens[ $i ]['content'];
}
if ( 'true' === $content_second || '1' === $content_second ) {
$gmt_true = true;
}
}
}
/*
* Non-UTC timestamp requested.
*/
if ( false === $gmt_true ) {
$this->phpcsFile->addWarning(
'Calling current_time() with a $type of "timestamp" or "U" is strongly discouraged as it will not return a Unix (UTC) timestamp. Please consider using a non-timestamp format or otherwise refactoring this code.',
$stackPtr,
'Requested'
);
return;
}
/*
* UTC timestamp requested. Should use time() instead.
*/
$has_comment = $this->phpcsFile->findNext( Tokens::$commentTokens, ( $stackPtr + 1 ), ( $close_parens + 1 ) );
$error = 'Don\'t use current_time() for retrieving a Unix (UTC) timestamp. Use time() instead. Found: %s';
$error_code = 'RequestedUTC';
$code_snippet = "current_time( '" . $content_first . "'";
if ( isset( $content_second ) ) {
$code_snippet .= ', ' . $content_second;
}
$code_snippet .= ' )';
if ( false !== $has_comment ) {
// If there are comments, we don't auto-fix as it would remove those comments.
$this->phpcsFile->addError( $error, $stackPtr, $error_code, array( $code_snippet ) );
return;
}
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code, array( $code_snippet ) );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $stackPtr + 1 ); $i < $close_parens; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->replaceToken( $stackPtr, 'time(' );
$this->phpcsFile->fixer->endChangeset();
}
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DateTime;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Forbids the use of various native DateTime related PHP/WP functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* @return array
*/
public function getGroups() {
return array(
/*
* Disallow the changing the timezone.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#manipulating-the-timezone-server-side
*/
'timezone_change' => array(
'type' => 'error',
'message' => 'Using %s() and similar isn\'t allowed, instead use WP internal timezone support.',
'functions' => array(
'date_default_timezone_set',
),
),
/*
* Use gmdate(), not date().
* Don't rely on the current PHP time zone as it might have been changed by third party code.
*
* @link https://make.wordpress.org/core/2019/09/23/date-time-improvements-wp-5-3/
* @link https://core.trac.wordpress.org/ticket/46438
* @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1713
*/
'date' => array(
'type' => 'error',
'message' => '%s() is affected by runtime timezone changes which can cause date/time to be incorrectly displayed. Use gmdate() instead.',
'functions' => array(
'date',
),
),
);
}
}

View file

@ -0,0 +1,248 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Files;
use WordPressCS\WordPress\Sniff;
/**
* Ensures filenames do not contain underscores.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.1.0
* @since 0.11.0 - This sniff will now also check for all lowercase file names.
* - This sniff will now also verify that files containing a class start with `class-`.
* - This sniff will now also verify that files in `wp-includes` containing
* template tags end in `-template`. Based on @subpackage file DocBlock tag.
* - This sniff will now allow for underscores in file names for certain theme
* specific exceptions if the `$is_theme` property is set to `true`.
* @since 0.12.0 Now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* @uses \WordPressCS\WordPress\Sniff::$custom_test_class_whitelist
*/
class FileNameSniff extends Sniff {
/**
* Regex for the theme specific exceptions.
*
* N.B. This regex currently does not allow for mimetype sublevel only file names,
* such as `plain.php`.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-post
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#custom-taxonomies
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#custom-post-types
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#embeds
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#attachment
* @link https://developer.wordpress.org/themes/template-files-section/partial-and-miscellaneous-template-files/#content-slug-php
* @link https://wphierarchy.com/
* @link https://en.wikipedia.org/wiki/Media_type#Naming
*
* @since 0.11.0
*
* @var string
*/
const THEME_EXCEPTIONS_REGEX = '`
^ # Anchor to the beginning of the string.
(?:
# Template prefixes which can have exceptions.
(?:archive|category|content|embed|page|single|tag|taxonomy)
-[^\.]+ # These need to be followed by a dash and some chars.
|
(?:application|audio|example|image|message|model|multipart|text|video) #Top-level mime-types
(?:_[^\.]+)? # Optionally followed by an underscore and a sub-type.
)\.(?:php|inc)$ # End in .php (or .inc for the test files) and anchor to the end of the string.
`Dx';
/**
* Whether the codebase being sniffed is a theme.
*
* If true, it will allow for certain typical theme specific exceptions to the filename rules.
*
* @since 0.11.0
*
* @var bool
*/
public $is_theme = false;
/**
* Whether to apply strict class file name rules.
*
* If true, it demands that classes are prefixed with `class-` and that the rest of the
* file name reflects the class name.
*
* @since 0.11.0
*
* @var bool
*/
public $strict_class_file_names = true;
/**
* Historical exceptions in WP core to the class name rule.
*
* @since 0.11.0
*
* @var array
*/
private $class_exceptions = array(
'class.wp-dependencies.php' => true,
'class.wp-scripts.php' => true,
'class.wp-styles.php' => true,
);
/**
* Unit test version of the historical exceptions in WP core.
*
* @since 0.11.0
*
* @var array
*/
private $unittest_class_exceptions = array(
'class.wp-dependencies.inc' => true,
'class.wp-scripts.inc' => true,
'class.wp-styles.inc' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
if ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) ) {
$this->class_exceptions = array_merge( $this->class_exceptions, $this->unittest_class_exceptions );
}
return array(
\T_OPEN_TAG,
\T_OPEN_TAG_WITH_ECHO,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Usage of `strip_quotes` is to ensure `stdin_path` passed by IDEs does not include quotes.
$file = $this->strip_quotes( $this->phpcsFile->getFileName() );
if ( 'STDIN' === $file ) {
return;
}
// Respect phpcs:disable comments as long as they are not accompanied by an enable (PHPCS 3.2+).
if ( \defined( '\T_PHPCS_DISABLE' ) && \defined( '\T_PHPCS_ENABLE' ) ) {
$i = -1;
while ( $i = $this->phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) {
if ( empty( $this->tokens[ $i ]['sniffCodes'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] )
) {
do {
$i = $this->phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) );
} while ( false !== $i
&& ! empty( $this->tokens[ $i ]['sniffCodes'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] ) );
if ( false === $i ) {
// The entire (rest of the) file is disabled.
return;
}
}
}
}
$fileName = basename( $file );
$expected = strtolower( str_replace( '_', '-', $fileName ) );
/*
* Generic check for lowercase hyphenated file names.
*/
if ( $fileName !== $expected && ( false === $this->is_theme || 1 !== preg_match( self::THEME_EXCEPTIONS_REGEX, $fileName ) ) ) {
$this->phpcsFile->addError(
'Filenames should be all lowercase with hyphens as word separators. Expected %s, but found %s.',
0,
'NotHyphenatedLowercase',
array( $expected, $fileName )
);
}
unset( $expected );
/*
* Check files containing a class for the "class-" prefix and that the rest of
* the file name reflects the class name.
*/
if ( true === $this->strict_class_file_names ) {
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );
if ( false !== $has_class && false === $this->is_test_class( $has_class ) ) {
$class_name = $this->phpcsFile->getDeclarationName( $has_class );
$expected = 'class-' . strtolower( str_replace( '_', '-', $class_name ) );
if ( substr( $fileName, 0, -4 ) !== $expected && ! isset( $this->class_exceptions[ $fileName ] ) ) {
$this->phpcsFile->addError(
'Class file names should be based on the class name with "class-" prepended. Expected %s, but found %s.',
0,
'InvalidClassFileName',
array(
$expected . '.php',
$fileName,
)
);
}
unset( $expected );
}
}
/*
* Check non-class files in "wp-includes" with a "@subpackage Template" tag for a "-template" suffix.
*/
if ( false !== strpos( $file, \DIRECTORY_SEPARATOR . 'wp-includes' . \DIRECTORY_SEPARATOR ) ) {
$subpackage_tag = $this->phpcsFile->findNext( \T_DOC_COMMENT_TAG, $stackPtr, null, false, '@subpackage' );
if ( false !== $subpackage_tag ) {
$subpackage = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, $subpackage_tag );
if ( false !== $subpackage ) {
$fileName_end = substr( $fileName, -13 );
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );
if ( ( 'Template' === trim( $this->tokens[ $subpackage ]['content'] )
&& $this->tokens[ $subpackage_tag ]['line'] === $this->tokens[ $subpackage ]['line'] )
&& ( ( ! \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.php' !== $fileName_end )
|| ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.inc' !== $fileName_end ) )
&& false === $has_class
) {
$this->phpcsFile->addError(
'Files containing template tags should have "-template" appended to the end of the file name. Expected %s, but found %s.',
0,
'InvalidTemplateTagFileName',
array(
substr( $fileName, 0, -4 ) . '-template.php',
$fileName,
)
);
}
}
}
}
// Only run this sniff once per file, no need to run it again.
return ( $this->phpcsFile->numTokens + 1 );
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,187 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
use PHP_CodeSniffer\Standards\PEAR\Sniffs\NamingConventions\ValidFunctionNameSniff as PHPCS_PEAR_ValidFunctionNameSniff;
use PHP_CodeSniffer\Files\File;
use WordPressCS\WordPress\Sniff;
/**
* Enforces WordPress function name and method name format, based upon Squiz code.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.1.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 2.0.0 The `get_name_suggestion()` method has been moved to the
* WordPress native `Sniff` base class as `get_snake_case_name_suggestion()`.
* @since 2.2.0 Will now ignore functions and methods which are marked as @deprecated.
*
* Last synced with parent class December 2018 up to commit ee167761d7756273b8ad0ad68bf3db1f2c211bb8.
* @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php
*
* {@internal While this class extends the PEAR parent, it does not actually use the checks
* contained in the parent. It only uses the properties and the token registration from the parent.}}
*/
class ValidFunctionNameSniff extends PHPCS_PEAR_ValidFunctionNameSniff {
/**
* Processes the tokens outside the scope.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
* @param int $stackPtr The position where this token was
* found.
*
* @return void
*/
protected function processTokenOutsideScope( File $phpcsFile, $stackPtr ) {
if ( Sniff::is_function_deprecated( $phpcsFile, $stackPtr ) === true ) {
/*
* Deprecated functions don't have to comply with the naming conventions,
* otherwise functions deprecated in favour of a function with a compliant
* name would still trigger an error.
*/
return;
}
$functionName = $phpcsFile->getDeclarationName( $stackPtr );
if ( ! isset( $functionName ) ) {
// Ignore closures.
return;
}
if ( '' === ltrim( $functionName, '_' ) ) {
// Ignore special functions, like __().
return;
}
$functionNameLc = strtolower( $functionName );
// Is this a magic function ? I.e., it is prefixed with "__" ?
// Outside class scope this basically just means __autoload().
if ( 0 === strpos( $functionName, '__' ) ) {
$magicPart = substr( $functionNameLc, 2 );
if ( isset( $this->magicFunctions[ $magicPart ] ) ) {
return;
}
$error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
$errorData = array( $functionName );
$phpcsFile->addError( $error, $stackPtr, 'FunctionDoubleUnderscore', $errorData );
}
if ( $functionNameLc !== $functionName ) {
$error = 'Function name "%s" is not in snake case format, try "%s"';
$errorData = array(
$functionName,
Sniff::get_snake_case_name_suggestion( $functionName ),
);
$phpcsFile->addError( $error, $stackPtr, 'FunctionNameInvalid', $errorData );
}
}
/**
* Processes the tokens within the scope.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
* @param int $stackPtr The position where this token was
* found.
* @param int $currScope The position of the current scope.
*
* @return void
*/
protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) {
$tokens = $phpcsFile->getTokens();
// Determine if this is a function which needs to be examined.
$conditions = $tokens[ $stackPtr ]['conditions'];
end( $conditions );
$deepestScope = key( $conditions );
if ( $deepestScope !== $currScope ) {
return;
}
if ( Sniff::is_function_deprecated( $phpcsFile, $stackPtr ) === true ) {
/*
* Deprecated functions don't have to comply with the naming conventions,
* otherwise functions deprecated in favour of a function with a compliant
* name would still trigger an error.
*/
return;
}
$methodName = $phpcsFile->getDeclarationName( $stackPtr );
if ( ! isset( $methodName ) ) {
// Ignore closures.
return;
}
$className = $phpcsFile->getDeclarationName( $currScope );
if ( isset( $className ) === false ) {
$className = '[Anonymous Class]';
}
$methodNameLc = strtolower( $methodName );
$classNameLc = strtolower( $className );
// Ignore special functions.
if ( '' === ltrim( $methodName, '_' ) ) {
return;
}
// PHP4 constructors are allowed to break our rules.
if ( $methodNameLc === $classNameLc ) {
return;
}
// PHP4 destructors are allowed to break our rules.
if ( '_' . $classNameLc === $methodNameLc ) {
return;
}
$extended = $phpcsFile->findExtendedClassName( $currScope );
$interfaces = $phpcsFile->findImplementedInterfaceNames( $currScope );
// If this is a child class or interface implementation, it may have to use camelCase or double underscores.
if ( ! empty( $extended ) || ! empty( $interfaces ) ) {
return;
}
// Is this a magic method ? I.e. is it prefixed with "__" ?
if ( 0 === strpos( $methodName, '__' ) ) {
$magicPart = substr( $methodNameLc, 2 );
if ( isset( $this->magicMethods[ $magicPart ] ) ) {
return;
}
$error = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
$errorData = array( $className . '::' . $methodName );
$phpcsFile->addError( $error, $stackPtr, 'MethodDoubleUnderscore', $errorData );
}
// Check for all lowercase.
if ( $methodNameLc !== $methodName ) {
$error = 'Method name "%s" in class %s is not in snake case format, try "%s"';
$errorData = array(
$methodName,
$className,
Sniff::get_snake_case_name_suggestion( $methodName ),
);
$phpcsFile->addError( $error, $stackPtr, 'MethodNameInvalid', $errorData );
}
}
}

View file

@ -0,0 +1,289 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Use lowercase letters in action and filter names. Separate words via underscores.
*
* This sniff is only testing the hook invoke functions as when using 'add_action'/'add_filter'
* you can't influence the hook name.
*
* Hook names invoked with `do_action_deprecated()` and `apply_filters_deprecated()` are ignored.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.11.0 Extends the WordPressCS native `AbstractFunctionParameterSniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class ValidHookNameSniff extends AbstractFunctionParameterSniff {
/**
* Additional word separators.
*
* This public variable allows providing additional word separators which
* will be allowed in hook names via a property in the phpcs.xml config file.
*
* Example usage:
* <rule ref="WordPress.NamingConventions.ValidHookName">
* <properties>
* <property name="additionalWordDelimiters" value="-"/>
* </properties>
* </rule>
*
* Provide several extra delimiters as one string:
* <rule ref="WordPress.NamingConventions.ValidHookName">
* <properties>
* <property name="additionalWordDelimiters" value="-/."/>
* </properties>
* </rule>
*
* @var string
*/
public $additionalWordDelimiters = '';
/**
* Regular expression to test for correct punctuation of a hook name.
*
* The placeholder will be replaced by potentially provided additional
* word delimiters in the `prepare_regex()` method.
*
* @var string
*/
protected $punctuation_regex = '`[^\w%s]`';
/**
* Groups of functions to restrict.
*
* @since 0.11.0
*
* @return array
*/
public function getGroups() {
$this->target_functions = $this->hookInvokeFunctions;
// No need to examine the names of deprecated hooks.
unset(
$this->target_functions['do_action_deprecated'],
$this->target_functions['apply_filters_deprecated']
);
return parent::getGroups();
}
/**
* Process the parameters of a matched function.
*
* @since 0.11.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
$regex = $this->prepare_regex();
$case_errors = 0;
$underscores = 0;
$content = array();
$expected = array();
for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
// Skip past comment tokens.
if ( isset( Tokens::$commentTokens[ $this->tokens[ $i ]['code'] ] ) !== false ) {
continue;
}
$content[ $i ] = $this->tokens[ $i ]['content'];
$expected[ $i ] = $this->tokens[ $i ]['content'];
// Skip past potential variable array access: $var['Key'].
if ( \T_VARIABLE === $this->tokens[ $i ]['code'] ) {
do {
$open_bracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( false === $open_bracket
|| \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code']
|| ! isset( $this->tokens[ $open_bracket ]['bracket_closer'] )
) {
continue 2;
}
$i = $this->tokens[ $open_bracket ]['bracket_closer'];
} while ( isset( $this->tokens[ $i ] ) && $i <= $parameters[1]['end'] );
continue;
}
// Skip past non-string tokens.
if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
continue;
}
$string = $this->strip_quotes( $this->tokens[ $i ]['content'] );
/*
* Here be dragons - a double quoted string can contain extrapolated variables
* which don't have to comply with these rules.
*/
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) {
$transform = $this->transform_complex_string( $string, $regex );
$case_transform = $this->transform_complex_string( $string, $regex, 'case' );
$punct_transform = $this->transform_complex_string( $string, $regex, 'punctuation' );
} else {
$transform = $this->transform( $string, $regex );
$case_transform = $this->transform( $string, $regex, 'case' );
$punct_transform = $this->transform( $string, $regex, 'punctuation' );
}
if ( $string === $transform ) {
continue;
}
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) {
$expected[ $i ] = '"' . $transform . '"';
} else {
$expected[ $i ] = '\'' . $transform . '\'';
}
if ( $string !== $case_transform ) {
$case_errors++;
}
if ( $string !== $punct_transform ) {
$underscores++;
}
}
$first_non_empty = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$parameters[1]['start'],
( $parameters[1]['end'] + 1 ),
true
);
$data = array(
trim( implode( '', $expected ) ),
trim( implode( '', $content ) ),
);
if ( $case_errors > 0 ) {
$error = 'Hook names should be lowercase. Expected: %s, but found: %s.';
$this->phpcsFile->addError( $error, $first_non_empty, 'NotLowercase', $data );
}
if ( $underscores > 0 ) {
$error = 'Words in hook names should be separated using underscores. Expected: %s, but found: %s.';
$this->phpcsFile->addWarning( $error, $first_non_empty, 'UseUnderscores', $data );
}
}
/**
* Prepare the punctuation regular expression.
*
* Merges the existing regular expression with potentially provided extra word delimiters to allow.
* This is done 'late' and for each found token as otherwise inline `phpcs:set` directives
* would be ignored.
*
* @return string
*/
protected function prepare_regex() {
$extra = '';
if ( '' !== $this->additionalWordDelimiters && \is_string( $this->additionalWordDelimiters ) ) {
$extra = preg_quote( $this->additionalWordDelimiters, '`' );
}
return sprintf( $this->punctuation_regex, $extra );
}
/**
* Transform an arbitrary string to lowercase and replace punctuation and spaces with underscores.
*
* @param string $string The target string.
* @param string $regex The punctuation regular expression to use.
* @param string $transform_type Whether to a partial or complete transform.
* Valid values are: 'full', 'case', 'punctuation'.
* @return string
*/
protected function transform( $string, $regex, $transform_type = 'full' ) {
switch ( $transform_type ) {
case 'case':
return strtolower( $string );
case 'punctuation':
return preg_replace( $regex, '_', $string );
case 'full':
default:
return preg_replace( $regex, '_', strtolower( $string ) );
}
}
/**
* Transform a complex string which may contain variable extrapolation.
*
* @param string $string The target string.
* @param string $regex The punctuation regular expression to use.
* @param string $transform_type Whether to a partial or complete transform.
* Valid values are: 'full', 'case', 'punctuation'.
* @return string
*/
protected function transform_complex_string( $string, $regex, $transform_type = 'full' ) {
$output = preg_split( '`([\{\}\$\[\] ])`', $string, -1, \PREG_SPLIT_DELIM_CAPTURE );
$is_variable = false;
$has_braces = false;
$braces = 0;
foreach ( $output as $i => $part ) {
if ( \in_array( $part, array( '$', '{' ), true ) ) {
$is_variable = true;
if ( '{' === $part ) {
$has_braces = true;
$braces++;
}
continue;
}
if ( true === $is_variable ) {
if ( '[' === $part ) {
$has_braces = true;
$braces++;
}
if ( \in_array( $part, array( '}', ']' ), true ) ) {
$braces--;
}
if ( false === $has_braces && ' ' === $part ) {
$is_variable = false;
$output[ $i ] = $this->transform( $part, $regex, $transform_type );
}
if ( ( true === $has_braces && 0 === $braces ) && false === \in_array( $output[ ( $i + 1 ) ], array( '{', '[' ), true ) ) {
$has_braces = false;
$is_variable = false;
}
continue;
}
$output[ $i ] = $this->transform( $part, $regex, $transform_type );
}
return implode( '', $output );
}
}

View file

@ -0,0 +1,208 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Validates post type names.
*
* Checks the post type slug for invalid characters, long function names
* and reserved names.
*
* @link https://developer.wordpress.org/reference/functions/register_post_type/
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class ValidPostTypeSlugSniff extends AbstractFunctionParameterSniff {
/**
* Max length of a post type name is limited by the SQL field.
*
* @since 2.2.0
*
* @var int
*/
const POST_TYPE_MAX_LENGTH = 20;
/**
* Regex that whitelists characters that can be used as the post type slug.
*
* @link https://developer.wordpress.org/reference/functions/register_post_type/
* @since 2.2.0
*
* @var string
*/
const POST_TYPE_CHARACTER_WHITELIST = '/^[a-z0-9_-]+$/';
/**
* Array of functions that must be checked.
*
* @since 2.2.0
*
* @var array List of function names as keys. Value irrelevant.
*/
protected $target_functions = array(
'register_post_type' => true,
);
/**
* Array of reserved post type names which can not be used by themes and plugins.
*
* @since 2.2.0
*
* @var array
*/
protected $reserved_names = array(
'post' => true,
'page' => true,
'attachment' => true,
'revision' => true,
'nav_menu_item' => true,
'custom_css' => true,
'customize_changeset' => true,
'oembed_cache' => true,
'user_request' => true,
'wp_block' => true,
'action' => true,
'author' => true,
'order' => true,
'theme' => true,
);
/**
* All valid tokens for in the first parameter of register_post_type().
*
* Set in `register()`.
*
* @since 2.2.0
*
* @var string
*/
private $valid_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 2.2.0
*
* @return array
*/
public function register() {
$this->valid_tokens = Tokens::$textStringTokens + Tokens::$heredocTokens + Tokens::$emptyTokens;
return parent::register();
}
/**
* Process the parameter of a matched function.
*
* Errors on invalid post type names when reserved keywords are used,
* the post type is too long, or contains invalid characters.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
$string_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $parameters[1]['start'], ( $parameters[1]['end'] + 1 ) );
$has_invalid_tokens = $this->phpcsFile->findNext( $this->valid_tokens, $parameters[1]['start'], ( $parameters[1]['end'] + 1 ), true );
if ( false !== $has_invalid_tokens || false === $string_pos ) {
// Check for non string based slug parameter (we cannot determine if this is valid).
$this->phpcsFile->addWarning(
'The post type slug is not a string literal. It is not possible to automatically determine the validity of this slug. Found: %s.',
$stackPtr,
'NotStringLiteral',
array(
$parameters[1]['raw'],
),
3
);
return;
}
$post_type = $this->strip_quotes( $this->tokens[ $string_pos ]['content'] );
if ( strlen( $post_type ) === 0 ) {
// Error for using empty slug.
$this->phpcsFile->addError(
'register_post_type() called without a post type slug. The slug must be a non-empty string.',
$parameters[1]['start'],
'Empty'
);
return;
}
$data = array(
$this->tokens[ $string_pos ]['content'],
);
// Warn for dynamic parts in the slug parameter.
if ( 'T_DOUBLE_QUOTED_STRING' === $this->tokens[ $string_pos ]['type'] || ( 'T_HEREDOC' === $this->tokens[ $string_pos ]['type'] && strpos( $this->tokens[ $string_pos ]['content'], '$' ) !== false ) ) {
$this->phpcsFile->addWarning(
'The post type slug may, or may not, get too long with dynamic contents and could contain invalid characters. Found: %s.',
$string_pos,
'PartiallyDynamic',
$data
);
$post_type = $this->strip_interpolated_variables( $post_type );
}
if ( preg_match( self::POST_TYPE_CHARACTER_WHITELIST, $post_type ) === 0 ) {
// Error for invalid characters.
$this->phpcsFile->addError(
'register_post_type() called with invalid post type %s. Post type contains invalid characters. Only lowercase alphanumeric characters, dashes, and underscores are allowed.',
$string_pos,
'InvalidCharacters',
$data
);
}
if ( isset( $this->reserved_names[ $post_type ] ) ) {
// Error for using reserved slug names.
$this->phpcsFile->addError(
'register_post_type() called with reserved post type %s. Reserved post types should not be used as they interfere with the functioning of WordPress itself.',
$string_pos,
'Reserved',
$data
);
} elseif ( stripos( $post_type, 'wp_' ) === 0 ) {
// Error for using reserved slug prefix.
$this->phpcsFile->addError(
'The post type passed to register_post_type() uses a prefix reserved for WordPress itself. Found: %s.',
$string_pos,
'ReservedPrefix',
$data
);
}
// Error for slugs that are too long.
if ( strlen( $post_type ) > self::POST_TYPE_MAX_LENGTH ) {
$this->phpcsFile->addError(
'A post type slug must not exceed %d characters. Found: %s (%d characters).',
$string_pos,
'TooLong',
array(
self::POST_TYPE_MAX_LENGTH,
$this->tokens[ $string_pos ]['content'],
strlen( $post_type ),
)
);
}
}
}

View file

@ -0,0 +1,299 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff as PHPCS_AbstractVariableSniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use WordPressCS\WordPress\Sniff;
/**
* Checks the naming of variables and member variables.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.9.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 2.0.0 - Defers to the upstream `$phpReservedVars` property.
* - Now offers name suggestions for variables in violation.
*
* Last synced with base class June 2018 at commit 78ddbae97cac078f09928bf89e3ab9e53ad2ace0.
* @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php
*
* @uses PHP_CodeSniffer\Sniffs\AbstractVariableSniff::$phpReservedVars
*/
class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff {
/**
* Mixed-case variables used by WordPress.
*
* @since 0.11.0
*
* @var array
*/
protected $wordpress_mixed_case_vars = array(
'EZSQL_ERROR' => true,
'GETID3_ERRORARRAY' => true,
'is_IE' => true,
'is_IIS' => true,
'is_macIE' => true,
'is_NS4' => true,
'is_winIE' => true,
'PHP_SELF' => true,
'post_ID' => true,
'tag_ID' => true,
'user_ID' => true,
);
/**
* List of member variables that can have mixed case.
*
* @since 0.9.0
* @since 0.11.0 Changed from public to protected.
*
* @var array
*/
protected $whitelisted_mixed_case_member_var_names = array(
'ID' => true,
'comment_ID' => true,
'comment_post_ID' => true,
'post_ID' => true,
'comment_author_IP' => true,
'cat_ID' => true,
);
/**
* Custom list of properties which can have mixed case.
*
* @since 0.11.0
*
* @var string|string[]
*/
public $customPropertiesWhitelist = array();
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.10.0
* @since 0.11.0 - Name changed from $addedCustomVariables.
* - Changed the format from simple bool to array.
*
* @var array
*/
protected $addedCustomProperties = array(
'properties' => null,
);
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned.
* @param int $stack_ptr The position of the current token in the
* stack passed in $tokens.
*
* @return void
*/
protected function processVariable( File $phpcs_file, $stack_ptr ) {
$tokens = $phpcs_file->getTokens();
$var_name = ltrim( $tokens[ $stack_ptr ]['content'], '$' );
// If it's a php reserved var, then its ok.
if ( isset( $this->phpReservedVars[ $var_name ] ) ) {
return;
}
// Merge any custom variables with the defaults.
$this->mergeWhiteList();
// Likewise if it is a mixed-case var used by WordPress core.
if ( isset( $this->wordpress_mixed_case_vars[ $var_name ] ) ) {
return;
}
$obj_operator = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true );
if ( \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] ) {
// Check to see if we are using a variable from an object.
$var = $phpcs_file->findNext( Tokens::$emptyTokens, ( $obj_operator + 1 ), null, true );
if ( \T_STRING === $tokens[ $var ]['code'] ) {
$bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $var + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS !== $tokens[ $bracket ]['code'] ) {
$obj_var_name = $tokens[ $var ]['content'];
// There is no way for us to know if the var is public or
// private, so we have to ignore a leading underscore if there is
// one and just check the main part of the variable name.
$original_var_name = $obj_var_name;
if ( '_' === substr( $obj_var_name, 0, 1 ) ) {
$obj_var_name = substr( $obj_var_name, 1 );
}
if ( ! isset( $this->whitelisted_mixed_case_member_var_names[ $obj_var_name ] ) && self::isSnakeCase( $obj_var_name ) === false ) {
$error = 'Object property "$%s" is not in valid snake_case format, try "$%s"';
$data = array(
$original_var_name,
Sniff::get_snake_case_name_suggestion( $original_var_name ),
);
$phpcs_file->addError( $error, $var, 'UsedPropertyNotSnakeCase', $data );
}
}
}
}
$in_class = false;
$obj_operator = $phpcs_file->findPrevious( Tokens::$emptyTokens, ( $stack_ptr - 1 ), null, true );
if ( \T_DOUBLE_COLON === $tokens[ $obj_operator ]['code'] || \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] ) {
// The variable lives within a class, and is referenced like
// this: MyClass::$_variable or $class->variable.
$in_class = true;
}
// There is no way for us to know if the var is public or private,
// so we have to ignore a leading underscore if there is one and just
// check the main part of the variable name.
$original_var_name = $var_name;
if ( '_' === substr( $var_name, 0, 1 ) && true === $in_class ) {
$var_name = substr( $var_name, 1 );
}
if ( self::isSnakeCase( $var_name ) === false ) {
if ( $in_class && ! isset( $this->whitelisted_mixed_case_member_var_names[ $var_name ] ) ) {
$error = 'Object property "$%s" is not in valid snake_case format, try "$%s"';
$error_name = 'UsedPropertyNotSnakeCase';
} elseif ( ! $in_class ) {
$error = 'Variable "$%s" is not in valid snake_case format, try "$%s"';
$error_name = 'VariableNotSnakeCase';
}
if ( isset( $error, $error_name ) ) {
$data = array(
$original_var_name,
Sniff::get_snake_case_name_suggestion( $original_var_name ),
);
$phpcs_file->addError( $error, $stack_ptr, $error_name, $data );
}
}
}
/**
* Processes class member variables.
*
* @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned.
* @param int $stack_ptr The position of the current token in the
* stack passed in $tokens.
*
* @return void
*/
protected function processMemberVar( File $phpcs_file, $stack_ptr ) {
$tokens = $phpcs_file->getTokens();
$var_name = ltrim( $tokens[ $stack_ptr ]['content'], '$' );
$member_props = $phpcs_file->getMemberProperties( $stack_ptr );
if ( empty( $member_props ) ) {
// Couldn't get any info about this variable, which
// generally means it is invalid or possibly has a parse
// error. Any errors will be reported by the core, so
// we can ignore it.
return;
}
// Merge any custom variables with the defaults.
$this->mergeWhiteList();
if ( ! isset( $this->whitelisted_mixed_case_member_var_names[ $var_name ] ) && false === self::isSnakeCase( $var_name ) ) {
$error = 'Member variable "$%s" is not in valid snake_case format, try "$%s"';
$data = array(
$var_name,
Sniff::get_snake_case_name_suggestion( $var_name ),
);
$phpcs_file->addError( $error, $stack_ptr, 'PropertyNotSnakeCase', $data );
}
}
/**
* Processes the variable found within a double quoted string.
*
* @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned.
* @param int $stack_ptr The position of the double quoted
* string.
*
* @return void
*/
protected function processVariableInString( File $phpcs_file, $stack_ptr ) {
$tokens = $phpcs_file->getTokens();
if ( preg_match_all( '|[^\\\]\${?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)|', $tokens[ $stack_ptr ]['content'], $matches ) > 0 ) {
// Merge any custom variables with the defaults.
$this->mergeWhiteList();
foreach ( $matches[1] as $var_name ) {
// If it's a php reserved var, then its ok.
if ( isset( $this->phpReservedVars[ $var_name ] ) ) {
continue;
}
// Likewise if it is a mixed-case var used by WordPress core.
if ( isset( $this->wordpress_mixed_case_vars[ $var_name ] ) ) {
return;
}
if ( false === self::isSnakeCase( $var_name ) ) {
$error = 'Variable "$%s" is not in valid snake_case format, try "$%s"';
$data = array(
$var_name,
Sniff::get_snake_case_name_suggestion( $var_name ),
);
$phpcs_file->addError( $error, $stack_ptr, 'InterpolatedVariableNotSnakeCase', $data );
}
}
}
}
/**
* Return whether the variable is in snake_case.
*
* @param string $var_name Variable name.
* @return bool
*/
public static function isSnakeCase( $var_name ) {
return (bool) preg_match( '/^[a-z0-9_]+$/', $var_name );
}
/**
* Merge a custom whitelist provided via a custom ruleset with the predefined whitelist,
* if we haven't already.
*
* @since 0.10.0
* @since 2.0.0 Removed unused $phpcs_file parameter.
*
* @return void
*/
protected function mergeWhiteList() {
if ( $this->customPropertiesWhitelist !== $this->addedCustomProperties['properties'] ) {
// Fix property potentially passed as comma-delimited string.
$customProperties = Sniff::merge_custom_array( $this->customPropertiesWhitelist, array(), false );
$this->whitelisted_mixed_case_member_var_names = Sniff::merge_custom_array(
$customProperties,
$this->whitelisted_mixed_case_member_var_names
);
$this->addedCustomProperties['properties'] = $this->customPropertiesWhitelist;
}
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Restrict the use of various development functions.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class DevelopmentFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'error_log' => array(
'type' => 'warning',
'message' => '%s() found. Debug code should not normally be used in production.',
'functions' => array(
'error_log',
'var_dump',
'var_export',
'print_r',
'trigger_error',
'set_error_handler',
'debug_backtrace',
'debug_print_backtrace',
'wp_debug_backtrace_summary',
),
),
'prevent_path_disclosure' => array(
'type' => 'warning',
'message' => '%s() can lead to full path disclosure.',
'functions' => array(
'error_reporting',
'phpinfo',
),
),
);
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Disallow the use of short ternaries.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#ternary-operator
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class DisallowShortTernarySniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 2.2.0
*
* @return array
*/
public function register() {
return array( \T_INLINE_THEN );
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false === $nextNonEmpty ) {
// Live coding or parse error.
return;
}
if ( \T_INLINE_ELSE !== $this->tokens[ $nextNonEmpty ]['code'] ) {
return;
}
$this->phpcsFile->addError(
'Using short ternaries is not allowed',
$stackPtr,
'Found'
);
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of various native PHP functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 `create_function` was moved to the PHP.RestrictedFunctions sniff.
*/
class DiscouragedPHPFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to discourage.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'serialize' => array(
'type' => 'warning',
'message' => '%s() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection',
'functions' => array(
'serialize',
'unserialize',
),
),
'urlencode' => array(
'type' => 'warning',
'message' => '%s() should only be used when dealing with legacy applications rawurlencode() should now be used instead. See http://php.net/manual/en/function.rawurlencode.php and http://www.faqs.org/rfcs/rfc3986.html',
'functions' => array(
'urlencode',
),
),
'runtime_configuration' => array(
'type' => 'warning',
'message' => '%s() found. Changing configuration values at runtime is strongly discouraged.',
'functions' => array(
'error_reporting',
'ini_restore',
'apache_setenv',
'putenv',
'set_include_path',
'restore_include_path',
// This alias was DEPRECATED in PHP 5.3.0, and REMOVED as of PHP 7.0.0.
'magic_quotes_runtime',
// Warning This function was DEPRECATED in PHP 5.3.0, and REMOVED as of PHP 7.0.0.
'set_magic_quotes_runtime',
// Warning This function was removed from most SAPIs in PHP 5.3.0, and was removed from PHP-FPM in PHP 7.0.0.
'dl',
),
),
'system_calls' => array(
'type' => 'warning',
'message' => '%s() found. PHP system calls are often disabled by server admins.',
'functions' => array(
'exec',
'passthru',
'proc_open',
'shell_exec',
'system',
'popen',
),
),
'obfuscation' => array(
'type' => 'warning',
'message' => '%s() can be used to obfuscate code which is strongly discouraged. Please verify that the function is used for benign reasons.',
'functions' => array(
'base64_decode',
'base64_encode',
'convert_uudecode',
'convert_uuencode',
'str_rot13',
),
),
);
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Restricts the usage of extract().
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#dont-extract
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0 Previously this check was contained within the
* `WordPress.VIP.RestrictedFunctions` sniff.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `Functions` category to the `PHP` category.
*/
class DontExtractSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'extract' => array(
'type' => 'error',
'message' => '%s() usage is highly discouraged, due to the complexity and unintended issues it might cause.',
'functions' => array(
'extract',
),
),
);
}
}

View file

@ -0,0 +1,177 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
/**
* Detect use of the `ini_set()` function.
*
* - Won't throw notices for "safe" ini directives as listed in the whitelist.
* - Throws errors for ini directives listed in the blacklist.
* - A warning will be thrown in all other cases.
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.1.0
*/
class IniSetSniff extends AbstractFunctionParameterSniff {
/**
* Array of functions that must be checked.
*
* @since 2.1.0
*
* @var array Multidimensional array with parameter details.
* $target_functions = array(
* (string) Function name.
* );
*/
protected $target_functions = array(
'ini_set' => true,
'ini_alter' => true,
);
/**
* Array of PHP configuration options that are allowed to be manipulated.
*
* @since 2.1.0
*
* @var array Multidimensional array with parameter details.
* $whitelisted_options = array(
* (string) option name. = array(
* (string[]) 'valid_values' = array()
* )
* );
*/
protected $whitelisted_options = array(
'auto_detect_line_endings' => array(),
'highlight.bg' => array(),
'highlight.comment' => array(),
'highlight.default' => array(),
'highlight.html' => array(),
'highlight.keyword' => array(),
'highlight.string' => array(),
'short_open_tag' => array(
'valid_values' => array( 'true', '1', 'on' ),
),
);
/**
* Array of PHP configuration options that are not allowed to be manipulated.
*
* @since 2.1.0
*
* @var array Multidimensional array with parameter details.
* $blacklisted_options = array(
* (string) option name. = array(
* (string[]) 'invalid_values' = array()
* (string) 'message'
* )
* );
*/
protected $blacklisted_options = array(
'bcmath.scale' => array(
'message' => 'Use `bcscale()` instead.',
),
'display_errors' => array(
'message' => 'Use `WP_DEBUG_DISPLAY` instead.',
),
'error_reporting' => array(
'message' => 'Use `WP_DEBUG` instead.',
),
'filter.default' => array(
'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.',
),
'filter.default_flags' => array(
'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.',
),
'iconv.input_encoding' => array(
'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.',
),
'iconv.internal_encoding' => array(
'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.',
),
'iconv.output_encoding' => array(
'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.',
),
'ignore_user_abort' => array(
'message' => 'Use `ignore_user_abort()` instead.',
),
'log_errors' => array(
'message' => 'Use `WP_DEBUG_LOG` instead.',
),
'max_execution_time' => array(
'message' => 'Use `set_time_limit()` instead.',
),
'memory_limit' => array(
'message' => 'Use `wp_raise_memory_limit()` or hook into the filters in that function.',
),
'short_open_tag' => array(
'invalid_values' => array( 'false', '0', 'off' ),
'message' => 'Turning off short_open_tag is prohibited as it can break other plugins.',
),
);
/**
* Process the parameter of a matched function.
*
* Errors if an option is found in the blacklist. Warns as
* 'risky' when the option is not found in the whitelist.
*
* @since 2.1.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
$option_name = $this->strip_quotes( $parameters[1]['raw'] );
$option_value = $this->strip_quotes( $parameters[2]['raw'] );
if ( isset( $this->whitelisted_options[ $option_name ] ) ) {
$whitelisted_option = $this->whitelisted_options[ $option_name ];
if ( ! isset( $whitelisted_option['valid_values'] ) || in_array( strtolower( $option_value ), $whitelisted_option['valid_values'], true ) ) {
return;
}
}
if ( isset( $this->blacklisted_options[ $option_name ] ) ) {
$blacklisted_option = $this->blacklisted_options[ $option_name ];
if ( ! isset( $blacklisted_option['invalid_values'] ) || in_array( strtolower( $option_value ), $blacklisted_option['invalid_values'], true ) ) {
$this->phpcsFile->addError(
'%s(%s, %s) found. %s',
$stackPtr,
$this->string_to_errorcode( $option_name . '_Blacklisted' ),
array(
$matched_content,
$parameters[1]['raw'],
$parameters[2]['raw'],
$blacklisted_option['message'],
)
);
return;
}
}
$this->phpcsFile->addWarning(
'%s(%s, %s) found. Changing configuration values at runtime is strongly discouraged.',
$stackPtr,
'Risky',
array(
$matched_content,
$parameters[1]['raw'],
$parameters[2]['raw'],
)
);
}
}

View file

@ -0,0 +1,239 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Discourage the use of the PHP error silencing operator.
*
* This sniff allows the error operator to be used with a select list
* of whitelisted functions, as no amount of error checking can prevent
* PHP from throwing errors when those functions are used.
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.1.0
*/
class NoSilencedErrorsSniff extends Sniff {
/**
* Number of tokens to display in the error message to show
* the error silencing context.
*
* @since 1.1.0
*
* @var int
*/
public $context_length = 6;
/**
* Whether or not the `$function_whitelist` should be used.
*
* Defaults to true.
*
* This property only affects whether the standard function whitelist is
* used. The custom whitelist, if set, will always be respected.
*
* @since 1.1.0
*
* @var bool
*/
public $use_default_whitelist = true;
/**
* User defined whitelist.
*
* Allows users to pass a list of additional functions to whitelist
* from their custom ruleset.
*
* @since 1.1.0
*
* @var array
*/
public $custom_whitelist = array();
/**
* PHP native function whitelist.
*
* Errors caused by calls to any of these native PHP functions
* are allowed to be silenced as file system permissions and such
* can cause E_WARNINGs to be thrown which cannot be prevented via
* error checking.
*
* Note: only calls to global functions - in contrast to class methods -
* are taken into account.
*
* Only functions for which the PHP manual annotates that an
* error will be thrown on failure are accepted into this list.
*
* @since 1.1.0
*
* @var array <string function name> => <bool true>
*/
protected $function_whitelist = array(
// Directory extension.
'chdir' => true,
'opendir' => true,
'scandir' => true,
// File extension.
'file_exists' => true,
'file_get_contents' => true,
'file' => true,
'fileatime' => true,
'filectime' => true,
'filegroup' => true,
'fileinode' => true,
'filemtime' => true,
'fileowner' => true,
'fileperms' => true,
'filesize' => true,
'filetype' => true,
'fopen' => true,
'is_dir' => true,
'is_executable' => true,
'is_file' => true,
'is_link' => true,
'is_readable' => true,
'is_writable' => true,
'is_writeable' => true,
'lstat' => true,
'mkdir' => true,
'move_uploaded_file' => true,
'readfile' => true,
'readlink' => true,
'rename' => true,
'rmdir' => true,
'stat' => true,
'unlink' => true,
// FTP extension.
'ftp_chdir' => true,
'ftp_login' => true,
'ftp_rename' => true,
// Stream extension.
'stream_select' => true,
'stream_set_chunk_size' => true,
// Zlib extension.
'deflate_add' => true,
'deflate_init' => true,
'inflate_add' => true,
'inflate_init' => true,
'readgzfile' => true,
// Miscellaneous other functions.
'imagecreatefromstring' => true,
'parse_url' => true, // Pre-PHP 5.3.3 an E_WARNING was thrown when URL parsing failed.
'unserialize' => true,
);
/**
* Tokens which are regarded as empty for the purpose of determining
* the name of the called function.
*
* This property is set from within the register() method.
*
* @since 1.1.0
*
* @var array
*/
private $empty_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 1.1.0
*
* @return array
*/
public function register() {
$this->empty_tokens = Tokens::$emptyTokens;
$this->empty_tokens[ \T_NS_SEPARATOR ] = \T_NS_SEPARATOR;
$this->empty_tokens[ \T_BITWISE_AND ] = \T_BITWISE_AND;
return array(
\T_ASPERAND,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.1.0
*
* @param int $stackPtr The position of the current token in the stack.
*/
public function process_token( $stackPtr ) {
// Handle the user-defined custom function whitelist.
$this->custom_whitelist = $this->merge_custom_array( $this->custom_whitelist, array(), false );
$this->custom_whitelist = array_map( 'strtolower', $this->custom_whitelist );
/*
* Check if the error silencing is done for one of the whitelisted functions.
*
* @internal The function call name determination is done even when there is no whitelist active
* to allow the metrics to be more informative.
*/
$next_non_empty = $this->phpcsFile->findNext( $this->empty_tokens, ( $stackPtr + 1 ), null, true, null, true );
if ( false !== $next_non_empty && \T_STRING === $this->tokens[ $next_non_empty ]['code'] ) {
$has_parenthesis = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true );
if ( false !== $has_parenthesis && \T_OPEN_PARENTHESIS === $this->tokens[ $has_parenthesis ]['code'] ) {
$function_name = strtolower( $this->tokens[ $next_non_empty ]['content'] );
if ( ( true === $this->use_default_whitelist
&& isset( $this->function_whitelist[ $function_name ] ) === true )
|| ( ! empty( $this->custom_whitelist )
&& in_array( $function_name, $this->custom_whitelist, true ) === true )
) {
$this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', 'whitelisted function call: ' . $function_name );
return;
}
}
}
$this->context_length = (int) $this->context_length;
$context_length = $this->context_length;
if ( $this->context_length <= 0 ) {
$context_length = 2;
}
// Prepare the "Found" string to display.
$end_of_statement = $this->phpcsFile->findEndOfStatement( $stackPtr, \T_COMMA );
if ( ( $end_of_statement - $stackPtr ) < $context_length ) {
$context_length = ( $end_of_statement - $stackPtr );
}
$found = $this->phpcsFile->getTokensAsString( $stackPtr, $context_length );
$found = str_replace( array( "\t", "\n", "\r" ), ' ', $found ) . '...';
$error_msg = 'Silencing errors is strongly discouraged. Use proper error checking instead.';
$data = array();
if ( $this->context_length > 0 ) {
$error_msg .= ' Found: %s';
$data[] = $found;
}
$this->phpcsFile->addWarning(
$error_msg,
$stackPtr,
'Discouraged',
$data
);
if ( isset( $function_name ) ) {
$this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', '@' . $function_name );
} else {
$this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', $found );
}
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Perl compatible regular expressions (PCRE, preg_ functions) should be used in preference
* to their POSIX counterparts.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#regular-expressions
* @link http://php.net/manual/en/ref.regex.php
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0 Previously this check was contained within the
* `WordPress.VIP.RestrictedFunctions` and the
* `WordPress.PHP.DiscouragedPHPFunctions` sniffs.
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class POSIXFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'ereg' => array(
'type' => 'error',
'message' => '%s() has been deprecated since PHP 5.3 and removed in PHP 7.0, please use preg_match() instead.',
'functions' => array(
'ereg',
'eregi',
'sql_regcase',
),
),
'ereg_replace' => array(
'type' => 'error',
'message' => '%s() has been deprecated since PHP 5.3 and removed in PHP 7.0, please use preg_replace() instead.',
'functions' => array(
'ereg_replace',
'eregi_replace',
),
),
'split' => array(
'type' => 'error',
'message' => '%s() has been deprecated since PHP 5.3 and removed in PHP 7.0, please use explode(), str_split() or preg_split() instead.',
'functions' => array(
'split',
'spliti',
),
),
);
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
/**
* Flag calling preg_quote() without the second ($delimiter) parameter.
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.0.0
*/
class PregQuoteDelimiterSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 1.0.0
*
* @var string
*/
protected $group_name = 'preg_quote';
/**
* List of functions this sniff should examine.
*
* @link http://php.net/preg_quote
*
* @since 1.0.0
*
* @var array <string function_name> => <bool>
*/
protected $target_functions = array(
'preg_quote' => true,
);
/**
* Process the parameters of a matched function.
*
* @since 1.0.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
* @param array $parameters Array with information about the parameters.
*
* @return void
*/
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
if ( \count( $parameters ) > 1 ) {
return;
}
$this->phpcsFile->addWarning(
'Passing the $delimiter as the second parameter to preg_quote() is strongly recommended.',
$stackPtr,
'Missing'
);
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Forbids the use of various native PHP functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*/
class RestrictedPHPFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to forbid.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'create_function' => array(
'type' => 'error',
'message' => '%s() is deprecated as of PHP 7.2, please use full fledged functions or anonymous functions instead.',
'functions' => array(
'create_function',
),
),
);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
/**
* Enforces Strict Comparison checks, based upon Squiz code.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.4.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* Last synced with base class ?[unknown date]? at commit ?[unknown commit]?.
* It is currently unclear whether this sniff is actually based on Squiz code on whether the above
* reference to it is a copy/paste oversight.
* @link Possibly: https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php
*/
class StrictComparisonsSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_IS_EQUAL,
\T_IS_NOT_EQUAL,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
if ( ! $this->has_whitelist_comment( 'loose comparison', $stackPtr ) ) {
$error = 'Found: ' . $this->tokens[ $stackPtr ]['content'] . '. Use strict comparisons (=== or !==).';
$this->phpcsFile->addWarning( $error, $stackPtr, 'LooseComparison' );
}
}
}

Some files were not shown because too many files have changed in this diff Show more