|
@@ -24,16 +24,51 @@ use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
|
|
|
|
|
|
/**
|
|
|
* @author Andreas Möller <am@localheinz.com>
|
|
|
+ * @author SpacePossum
|
|
|
*/
|
|
|
final class NativeFunctionInvocationFixer extends AbstractFixer implements ConfigurableFixerInterface
|
|
|
{
|
|
|
+ /**
|
|
|
+ * @internal
|
|
|
+ */
|
|
|
+ const SET_ALL = '@all';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Subset of SET_INTERNAL.
|
|
|
+ *
|
|
|
+ * Change function call to functions known to be optimized by the Zend engine.
|
|
|
+ * For details:
|
|
|
+ * - @see https://github.com/php/php-src/blob/php-7.2.6/Zend/zend_compile.c "zend_try_compile_special_func"
|
|
|
+ * - @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c
|
|
|
+ *
|
|
|
+ * @internal
|
|
|
+ */
|
|
|
+ const SET_COMPILER_OPTIMIZED = '@compiler_optimized';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @internal
|
|
|
+ */
|
|
|
+ const SET_INTERNAL = '@internal';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var callable
|
|
|
+ */
|
|
|
+ private $functionFilter;
|
|
|
+
|
|
|
+ public function configure(array $configuration = null)
|
|
|
+ {
|
|
|
+ parent::configure($configuration);
|
|
|
+
|
|
|
+ $this->functionFilter = $this->getFunctionFilter();
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* {@inheritdoc}
|
|
|
*/
|
|
|
public function getDefinition()
|
|
|
{
|
|
|
return new FixerDefinition(
|
|
|
- 'Add leading `\` before function invocation of internal function to speed up resolving.',
|
|
|
+ 'Add leading `\` before function invocation to speed up resolving.',
|
|
|
[
|
|
|
new CodeSample(
|
|
|
'<?php
|
|
@@ -66,6 +101,56 @@ function baz($options)
|
|
|
],
|
|
|
]
|
|
|
),
|
|
|
+ new CodeSample(
|
|
|
+ '<?php
|
|
|
+namespace space1 {
|
|
|
+ echo count([1]);
|
|
|
+}
|
|
|
+namespace {
|
|
|
+ echo count([1]);
|
|
|
+}
|
|
|
+',
|
|
|
+ ['scope' => 'all']
|
|
|
+ ),
|
|
|
+ new CodeSample(
|
|
|
+ '<?php
|
|
|
+namespace space1 {
|
|
|
+ echo count([1]);
|
|
|
+}
|
|
|
+namespace {
|
|
|
+ echo count([1]);
|
|
|
+}
|
|
|
+',
|
|
|
+ ['scope' => 'namespaced']
|
|
|
+ ),
|
|
|
+ new CodeSample(
|
|
|
+ '<?php
|
|
|
+myGlobalFunction();
|
|
|
+count();
|
|
|
+',
|
|
|
+ ['include' => ['myGlobalFunction']]
|
|
|
+ ),
|
|
|
+ new CodeSample(
|
|
|
+ '<?php
|
|
|
+myGlobalFunction();
|
|
|
+count();
|
|
|
+',
|
|
|
+ ['include' => ['@all']]
|
|
|
+ ),
|
|
|
+ new CodeSample(
|
|
|
+ '<?php
|
|
|
+myGlobalFunction();
|
|
|
+count();
|
|
|
+',
|
|
|
+ ['include' => ['@internal']]
|
|
|
+ ),
|
|
|
+ new CodeSample(
|
|
|
+ '<?php
|
|
|
+$a .= str_repeat($a, 4);
|
|
|
+$c = get_class($d);
|
|
|
+',
|
|
|
+ ['include' => ['@compiler_optimized']]
|
|
|
+ ),
|
|
|
],
|
|
|
null,
|
|
|
'Risky when any of the functions are overridden.'
|
|
@@ -93,22 +178,89 @@ function baz($options)
|
|
|
*/
|
|
|
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
|
|
|
{
|
|
|
- $functionNames = $this->getFunctionNames();
|
|
|
+ if ('all' === $this->configuration['scope']) {
|
|
|
+ $this->fixFunctionCalls($tokens, $this->functionFilter, 0, \count($tokens) - 1);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 'scope' is 'namespaced' here
|
|
|
+ foreach (\array_reverse($this->getUserDefinedNamespaces($tokens)) as $namespace) {
|
|
|
+ $this->fixFunctionCalls($tokens, $this->functionFilter, $namespace['open'], $namespace['close']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritdoc}
|
|
|
+ */
|
|
|
+ protected function createConfigurationDefinition()
|
|
|
+ {
|
|
|
+ return new FixerConfigurationResolver([
|
|
|
+ (new FixerOptionBuilder('exclude', 'List of functions to ignore.'))
|
|
|
+ ->setAllowedTypes(['array'])
|
|
|
+ ->setAllowedValues([static function (array $value) {
|
|
|
+ foreach ($value as $functionName) {
|
|
|
+ if (!\is_string($functionName) || '' === \trim($functionName) || \trim($functionName) !== $functionName) {
|
|
|
+ throw new InvalidOptionsException(\sprintf(
|
|
|
+ 'Each element must be a non-empty, trimmed string, got "%s" instead.',
|
|
|
+ \is_object($functionName) ? \get_class($functionName) : \gettype($functionName)
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }])
|
|
|
+ ->setDefault([])
|
|
|
+ ->getOption(),
|
|
|
+ (new FixerOptionBuilder('include', 'List of function names or sets to fix. Defined sets are `@internal` (all native functions), `@all` (all global functions) and `@compiler_optimized` (functions that are specially optimized by Zend).'))
|
|
|
+ ->setAllowedTypes(['array'])
|
|
|
+ ->setAllowedValues([static function (array $value) {
|
|
|
+ foreach ($value as $functionName) {
|
|
|
+ if (!\is_string($functionName) || '' === \trim($functionName) || \trim($functionName) !== $functionName) {
|
|
|
+ throw new InvalidOptionsException(\sprintf(
|
|
|
+ 'Each element must be a non-empty, trimmed string, got "%s" instead.',
|
|
|
+ \is_object($functionName) ? \get_class($functionName) : \gettype($functionName)
|
|
|
+ ));
|
|
|
+ }
|
|
|
|
|
|
- $indexes = [];
|
|
|
+ $sets = [
|
|
|
+ self::SET_ALL,
|
|
|
+ self::SET_INTERNAL,
|
|
|
+ self::SET_COMPILER_OPTIMIZED,
|
|
|
+ ];
|
|
|
|
|
|
- for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) {
|
|
|
- $token = $tokens[$index];
|
|
|
+ if ('@' === $value[0] && !\in_array($value, $sets, true)) {
|
|
|
+ throw new InvalidOptionsException(\sprintf('Unknown set "%s", known sets are "%s".', $value, \implode('", "', $sets)));
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- $tokenContent = $token->getContent();
|
|
|
+ return true;
|
|
|
+ }])
|
|
|
+ ->setDefault([self::SET_INTERNAL])
|
|
|
+ ->getOption(),
|
|
|
+ (new FixerOptionBuilder('scope', 'Only fix function calls that are made within a namespace or fix all.'))
|
|
|
+ ->setAllowedValues(['all', 'namespaced'])
|
|
|
+ ->setDefault('all')
|
|
|
+ ->getOption(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
|
|
|
+ /**
|
|
|
+ * @param Tokens $tokens
|
|
|
+ * @param callable $functionFilter
|
|
|
+ * @param int $start
|
|
|
+ * @param int $end
|
|
|
+ */
|
|
|
+ private function fixFunctionCalls(Tokens $tokens, callable $functionFilter, $start, $end)
|
|
|
+ {
|
|
|
+ $insertAtIndexes = [];
|
|
|
+ for ($index = $start; $index < $end; ++$index) {
|
|
|
// test if we are at a function call
|
|
|
- if (!$token->isGivenKind(T_STRING)) {
|
|
|
+ if (!$tokens[$index]->isGivenKind(T_STRING)) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- $next = $tokens->getNextMeaningfulToken($index);
|
|
|
- if (!$tokens[$next]->equals('(')) {
|
|
|
+ if (!$tokens[$tokens->getNextMeaningfulToken($index)]->equals('(')) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
@@ -117,81 +269,188 @@ function baz($options)
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- if ($tokens[$functionNamePrefix]->isGivenKind(T_NS_SEPARATOR)) {
|
|
|
- // skip if the call is to a constructor or to a function in a namespace other than the default
|
|
|
- $prev = $tokens->getPrevMeaningfulToken($functionNamePrefix);
|
|
|
- if ($tokens[$prev]->isGivenKind([T_STRING, T_NEW])) {
|
|
|
- continue;
|
|
|
- }
|
|
|
+ if (
|
|
|
+ $tokens[$functionNamePrefix]->isGivenKind(T_NS_SEPARATOR)
|
|
|
+ && $tokens[$tokens->getPrevMeaningfulToken($functionNamePrefix)]->isGivenKind([T_STRING, T_NEW])
|
|
|
+ ) {
|
|
|
+ continue; // skip if the call is to a constructor or to a function in a namespace other than the default
|
|
|
}
|
|
|
|
|
|
- $lowerFunctionName = \strtolower($tokenContent);
|
|
|
-
|
|
|
- if (!\in_array($lowerFunctionName, $functionNames, true)) {
|
|
|
+ if (!$functionFilter($tokens[$index]->getContent())) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- // do not bother if previous token is already namespace separator
|
|
|
if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_NS_SEPARATOR)) {
|
|
|
- continue;
|
|
|
+ continue; // do not bother if previous token is already namespace separator
|
|
|
}
|
|
|
|
|
|
- $indexes[] = $index;
|
|
|
+ $insertAtIndexes[] = $index;
|
|
|
}
|
|
|
|
|
|
- $indexes = \array_reverse($indexes);
|
|
|
- foreach ($indexes as $index) {
|
|
|
+ foreach (\array_reverse($insertAtIndexes) as $index) {
|
|
|
$tokens->insertAt($index, new Token([T_NS_SEPARATOR, '\\']));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * @return callable
|
|
|
*/
|
|
|
- protected function createConfigurationDefinition()
|
|
|
+ private function getFunctionFilter()
|
|
|
{
|
|
|
- return new FixerConfigurationResolver([
|
|
|
- (new FixerOptionBuilder('exclude', 'List of functions to ignore.'))
|
|
|
- ->setAllowedTypes(['array'])
|
|
|
- ->setAllowedValues([static function ($value) {
|
|
|
- foreach ($value as $functionName) {
|
|
|
- if (!\is_string($functionName) || '' === \trim($functionName) || \trim($functionName) !== $functionName) {
|
|
|
- throw new InvalidOptionsException(\sprintf(
|
|
|
- 'Each element must be a non-empty, trimmed string, got "%s" instead.',
|
|
|
- \is_object($functionName) ? \get_class($functionName) : \gettype($functionName)
|
|
|
- ));
|
|
|
- }
|
|
|
- }
|
|
|
+ $exclude = $this->normalizeFunctionNames($this->configuration['exclude']);
|
|
|
|
|
|
- return true;
|
|
|
- }])
|
|
|
- ->setDefault([])
|
|
|
- ->getOption(),
|
|
|
+ if (\in_array(self::SET_ALL, $this->configuration['include'], true)) {
|
|
|
+ if (\count($exclude) > 0) {
|
|
|
+ return static function ($functionName) use ($exclude) {
|
|
|
+ return !isset($exclude[\strtolower($functionName)]);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return static function () {
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ $include = [];
|
|
|
+ if (\in_array(self::SET_INTERNAL, $this->configuration['include'], true)) {
|
|
|
+ $include = $this->getAllInternalFunctionsNormalized();
|
|
|
+ } elseif (\in_array(self::SET_COMPILER_OPTIMIZED, $this->configuration['include'], true)) {
|
|
|
+ $include = $this->getAllCompilerOptimizedFunctionsNormalized(); // if `@internal` is set all compiler optimized function are already loaded
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($this->configuration['include'] as $additional) {
|
|
|
+ if ('@' !== $additional[0]) {
|
|
|
+ $include[\strtolower($additional)] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (\count($exclude) > 0) {
|
|
|
+ return static function ($functionName) use ($include, $exclude) {
|
|
|
+ return isset($include[\strtolower($functionName)]) && !isset($exclude[\strtolower($functionName)]);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return static function ($functionName) use ($include) {
|
|
|
+ return isset($include[\strtolower($functionName)]);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return array<string, true> normalized function names of which the PHP compiler optimizes
|
|
|
+ */
|
|
|
+ private function getAllCompilerOptimizedFunctionsNormalized()
|
|
|
+ {
|
|
|
+ return $this->normalizeFunctionNames([
|
|
|
+ // @see https://github.com/php/php-src/blob/php-7.2.6/Zend/zend_compile.c "zend_try_compile_special_func"
|
|
|
+ 'array_slice',
|
|
|
+ 'assert',
|
|
|
+ 'boolval',
|
|
|
+ 'call_user_func',
|
|
|
+ 'call_user_func_array',
|
|
|
+ 'chr',
|
|
|
+ 'count',
|
|
|
+ 'defined',
|
|
|
+ 'doubleval',
|
|
|
+ 'floatval',
|
|
|
+ 'func_get_args',
|
|
|
+ 'func_num_args',
|
|
|
+ 'get_called_class',
|
|
|
+ 'get_class',
|
|
|
+ 'gettype',
|
|
|
+ 'in_array',
|
|
|
+ 'intval',
|
|
|
+ 'is_array',
|
|
|
+ 'is_bool',
|
|
|
+ 'is_double',
|
|
|
+ 'is_float',
|
|
|
+ 'is_int',
|
|
|
+ 'is_integer',
|
|
|
+ 'is_long',
|
|
|
+ 'is_null',
|
|
|
+ 'is_object',
|
|
|
+ 'is_real',
|
|
|
+ 'is_resource',
|
|
|
+ 'is_string',
|
|
|
+ 'ord',
|
|
|
+ 'strlen',
|
|
|
+ 'strval',
|
|
|
+ // @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c
|
|
|
+ 'constant',
|
|
|
+ 'define',
|
|
|
+ 'dirname',
|
|
|
+ 'extension_loaded',
|
|
|
+ 'function_exists',
|
|
|
+ 'is_callable',
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @return string[]
|
|
|
+ * @return array<string, true> normalized function names of all internal defined functions
|
|
|
*/
|
|
|
- private function getFunctionNames()
|
|
|
+ private function getAllInternalFunctionsNormalized()
|
|
|
{
|
|
|
- $definedFunctions = \get_defined_functions();
|
|
|
+ return $this->normalizeFunctionNames(\get_defined_functions()['internal']);
|
|
|
+ }
|
|
|
|
|
|
- return \array_diff(
|
|
|
- $this->normalizeFunctionNames($definedFunctions['internal']),
|
|
|
- \array_unique($this->normalizeFunctionNames($this->configuration['exclude']))
|
|
|
- );
|
|
|
+ /**
|
|
|
+ * Returns array<'open'|'close', int>[].
|
|
|
+ *
|
|
|
+ * @param Tokens $tokens
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function getUserDefinedNamespaces(Tokens $tokens)
|
|
|
+ {
|
|
|
+ $namespaces = [];
|
|
|
+ for ($index = 1, $count = \count($tokens); $index < $count; ++$index) {
|
|
|
+ if (!$tokens[$index]->isGivenKind(T_NAMESPACE)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $index = $tokens->getNextMeaningfulToken($index);
|
|
|
+ if ($tokens[$index]->equals('{')) { // global namespace
|
|
|
+ $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ while (!$tokens[++$index]->equalsAny(['{', ';', [T_CLOSE_TAG]])) {
|
|
|
+ // no-op
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($tokens[$index]->equals('{')) {
|
|
|
+ // namespace ends at block end of `{`
|
|
|
+ $namespaces[] = ['open' => $index, 'close' => $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index)];
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // namespace ends at next T_NAMESPACE or EOF
|
|
|
+ $close = $tokens->getNextTokenOfKind($index, [[T_NAMESPACE]], false);
|
|
|
+ if (null === $close) {
|
|
|
+ $namespaces[] = ['open' => $index, 'close' => \count($tokens) - 1];
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $namespaces[] = ['open' => $index, 'close' => $close];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $namespaces;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param string[] $functionNames
|
|
|
*
|
|
|
- * @return string[]
|
|
|
+ * @return array<string, true> all function names lower cased
|
|
|
*/
|
|
|
private function normalizeFunctionNames(array $functionNames)
|
|
|
{
|
|
|
- return \array_map(static function ($functionName) {
|
|
|
- return \strtolower($functionName);
|
|
|
- }, $functionNames);
|
|
|
+ foreach ($functionNames as $index => $functionName) {
|
|
|
+ $functionNames[\strtolower($functionName)] = true;
|
|
|
+ unset($functionNames[$index]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $functionNames;
|
|
|
}
|
|
|
}
|