Skip to content

Commit

Permalink
Added FunctionHelper
Browse files Browse the repository at this point in the history
  • Loading branch information
kukulich committed Apr 22, 2016
1 parent 6f15abe commit b411339
Show file tree
Hide file tree
Showing 11 changed files with 573 additions and 0 deletions.
209 changes: 209 additions & 0 deletions SlevomatCodingStandard/Helpers/FunctionHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php

namespace SlevomatCodingStandard\Helpers;

class FunctionHelper
{

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string
*/
public static function getName(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
$tokens = $codeSnifferFile->getTokens();
return $tokens[$codeSnifferFile->findNext(T_STRING, $functionPointer + 1, $tokens[$functionPointer]['parenthesis_opener'])]['content'];
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string
*/
public static function getFullyQualifiedName(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
$name = self::getName($codeSnifferFile, $functionPointer);
$namespace = NamespaceHelper::findCurrentNamespaceName($codeSnifferFile, $functionPointer);

if (self::isMethod($codeSnifferFile, $functionPointer)) {
foreach (array_reverse($codeSnifferFile->getTokens()[$functionPointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) {
if ($conditionTokenCode === T_ANON_CLASS) {
return sprintf('class@anonymous::%s', $name);
} elseif (in_array($conditionTokenCode, [T_CLASS, T_INTERFACE, T_TRAIT])) {
$name = sprintf('%s%s::%s', NamespaceHelper::NAMESPACE_SEPARATOR, ClassHelper::getName($codeSnifferFile, $conditionPointer), $name);
break;
}
}

return $namespace !== null ? sprintf('%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, $name) : $name;
} else {
return $namespace !== null ? sprintf('%s%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name) : $name;
}
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return boolean
*/
public static function isAbstract(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
return !isset($codeSnifferFile->getTokens()[$functionPointer]['scope_opener']);
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return boolean
*/
public static function isMethod(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
foreach (array_reverse($codeSnifferFile->getTokens()[$functionPointer]['conditions']) as $conditionTokenCode) {
if (in_array($conditionTokenCode, [T_CLASS, T_INTERFACE, T_TRAIT, T_ANON_CLASS], true)) {
return true;
}
}

return false;
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string[]
*/
public static function getParametersWithoutTypeHint(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
return array_keys(array_filter(self::getParametersTypeHints($codeSnifferFile, $functionPointer), function ($parameterTypeHint) {
return $parameterTypeHint === null;
}));
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string[]
*/
public static function getParametersTypeHints(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
$tokens = $codeSnifferFile->getTokens();

$parametersTypeHints = [];
for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) {
if ($tokens[$i]['code'] === T_VARIABLE) {
$parameterName = $tokens[$i]['content'];
$parameterTypeHint = null;

$previousToken = $i;
do {
$previousToken = TokenHelper::findPreviousExcluding($codeSnifferFile, [T_WHITESPACE, T_COMMENT, T_BITWISE_AND, T_ELLIPSIS], $previousToken - 1, $tokens[$functionPointer]['parenthesis_opener'] + 1);
$isTypeHint = $previousToken !== null && $tokens[$previousToken]['code'] !== T_COMMA;
if ($isTypeHint) {
$parameterTypeHint = $tokens[$previousToken]['content'] . $parameterTypeHint;
}
} while ($isTypeHint);

$parametersTypeHints[$parameterName] = $parameterTypeHint;
}
}

return $parametersTypeHints;
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return boolean
*/
public static function returnsValue(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
$tokens = $codeSnifferFile->getTokens();

for ($i = $tokens[$functionPointer]['scope_opener'] + 1; $i < $tokens[$functionPointer]['scope_closer']; $i++) {
if ($tokens[$i]['code'] === T_RETURN && $tokens[TokenHelper::findNextNonWhitespace($codeSnifferFile, $i + 1)]['code'] !== T_SEMICOLON) {
foreach (array_reverse($tokens[$i]['conditions'], true) as $conditionPointer => $conditionTokenCode) {
if ($conditionTokenCode === T_CLOSURE || $conditionTokenCode === T_ANON_CLASS) {
continue 2;
} elseif ($conditionPointer === $functionPointer) {
return true;
}
}
}
}

return false;
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string|null
*/
public static function findReturnTypeHint(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
$tokens = $codeSnifferFile->getTokens();

$isAbstract = self::isAbstract($codeSnifferFile, $functionPointer);

$colonToken = $isAbstract
? $codeSnifferFile->findNext(T_COLON, $tokens[$functionPointer]['parenthesis_closer'] + 1, null, false, null, true)
: $codeSnifferFile->findNext(T_COLON, $tokens[$functionPointer]['parenthesis_closer'] + 1, $tokens[$functionPointer]['scope_opener'] - 1);

if ($colonToken === false) {
return null;
}

$returnTypeHint = null;
$nextToken = $colonToken;
do {
$nextToken = $isAbstract
? $codeSnifferFile->findNext([T_WHITESPACE, T_COMMENT, T_SEMICOLON], $nextToken + 1, null, true, null, true)
: $codeSnifferFile->findNext([T_WHITESPACE, T_COMMENT], $nextToken + 1, $tokens[$functionPointer]['scope_opener'] - 1, true);

$isTypeHint = $nextToken !== false;
if ($isTypeHint) {
$returnTypeHint .= $tokens[$nextToken]['content'];
}
} while ($isTypeHint);

return $returnTypeHint;
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return boolean
*/
public static function hasReturnTypeHint(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
return self::findReturnTypeHint($codeSnifferFile, $functionPointer) !== null;
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string[]
*/
public static function getParametersAnnotations(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
return AnnotationHelper::getAnnotationsByName($codeSnifferFile, $functionPointer, '@param');
}

/**
* @param \PHP_CodeSniffer_File $codeSnifferFile
* @param integer $functionPointer
* @return string|null
*/
public static function findReturnAnnotation(\PHP_CodeSniffer_File $codeSnifferFile, $functionPointer)
{
$returnAnnotations = AnnotationHelper::getAnnotationsByName($codeSnifferFile, $functionPointer, '@return');

if (count($returnAnnotations) === 0) {
return null;
}

return $returnAnnotations[0];
}

}
161 changes: 161 additions & 0 deletions tests/Helpers/FunctionHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

namespace SlevomatCodingStandard\Helpers;

class FunctionHelperTest extends \SlevomatCodingStandard\Helpers\TestCase
{

public function testNameWithNamespace()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionWithNamespace.php');
$this->assertSame('\FooNamespace\fooFunction', FunctionHelper::getFullyQualifiedName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooFunction')));
$this->assertSame('fooFunction', FunctionHelper::getName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooFunction')));
$this->assertSame('\FooNamespace\FooClass::fooMethod', FunctionHelper::getFullyQualifiedName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
$this->assertSame('fooMethod', FunctionHelper::getName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
}

public function testNameWithoutNamespace()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionWithoutNamespace.php');
$this->assertSame('fooFunction', FunctionHelper::getFullyQualifiedName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooFunction')));
$this->assertSame('fooFunction', FunctionHelper::getName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooFunction')));
$this->assertSame('\FooClass::fooMethod', FunctionHelper::getFullyQualifiedName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
$this->assertSame('fooMethod', FunctionHelper::getName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
}

public function testNameInAnonymousClass()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/php7/functionInAnonymousClass.php');
$this->assertSame('class@anonymous::fooMethod', FunctionHelper::getFullyQualifiedName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
$this->assertSame('fooMethod', FunctionHelper::getName($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
}

public function testAbstractOrNot()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionAbstractOrNot.php');
$this->assertFalse(FunctionHelper::isAbstract($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
$this->assertTrue(FunctionHelper::isAbstract($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooAbstractMethod')));
$this->assertTrue(FunctionHelper::isAbstract($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooInterfaceMethod')));
}

public function testFunctionOrMethod()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionOrMethod.php');
$this->assertTrue(FunctionHelper::isMethod($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooMethod')));
$this->assertFalse(FunctionHelper::isMethod($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'fooFunction')));
}

public function testParametersTypeHints()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/php7/functionParametersTypeHints.php');

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'allParametersWithTypeHints');
$parametersTypeHints = FunctionHelper::getParametersTypeHints($codeSnifferFile, $functionPointer);
$parametersWithoutTypeHints = FunctionHelper::getParametersWithoutTypeHint($codeSnifferFile, $functionPointer);
$this->assertCount(7, $parametersTypeHints);
$this->assertSame([
'$string' => 'string',
'$int' => 'int',
'$bool' => 'bool',
'$float' => 'float',
'$callable' => 'callable',
'$array' => 'array',
'$object' => '\FooNamespace\FooClass',
], $parametersTypeHints);
$this->assertCount(0, $parametersWithoutTypeHints);

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'allParametersWithoutTypeHints');
$parametersTypeHints = FunctionHelper::getParametersTypeHints($codeSnifferFile, $functionPointer);
$parametersWithoutTypeHints = FunctionHelper::getParametersWithoutTypeHint($codeSnifferFile, $functionPointer);
$this->assertCount(7, $parametersTypeHints);
$this->assertSame([
'$string' => null,
'$int' => null,
'$bool' => null,
'$float' => null,
'$callable' => null,
'$array' => null,
'$object' => null,
], $parametersTypeHints);
$this->assertCount(7, $parametersWithoutTypeHints);
$this->assertSame([
'$string',
'$int',
'$bool',
'$float',
'$callable',
'$array',
'$object',
], $parametersWithoutTypeHints);

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'someParametersWithoutTypeHints');
$parametersTypeHints = FunctionHelper::getParametersTypeHints($codeSnifferFile, $functionPointer);
$parametersWithoutTypeHints = FunctionHelper::getParametersWithoutTypeHint($codeSnifferFile, $functionPointer);
$this->assertCount(7, $parametersTypeHints);
$this->assertSame([
'$string' => 'string',
'$int' => null,
'$bool' => 'bool',
'$float' => null,
'$callable' => 'callable',
'$array' => null,
'$object' => '\FooNamespace\FooClass',
], $parametersTypeHints);
$this->assertCount(3, $parametersWithoutTypeHints);
$this->assertSame([
'$int',
'$float',
'$array',
], $parametersWithoutTypeHints);
}

public function testReturnsValueOrNot()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionReturnsValueOrNot.php');
$this->assertTrue(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'returnsValue')));
$this->assertTrue(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'returnsVariable')));
$this->assertTrue(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'returnsValueInCondition')));
$this->assertFalse(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'noReturn')));
$this->assertFalse(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'returnsVoid')));
$this->assertFalse(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'containsClosure')));
$this->assertFalse(FunctionHelper::returnsValue($codeSnifferFile, $this->findFunctionPointerByName($codeSnifferFile, 'containsAnonymousClass')));
}

public function testReturnTypeHint()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/php7/functionReturnTypeHint.php');

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'withReturnTypeHint');
$this->assertTrue(FunctionHelper::hasReturnTypeHint($codeSnifferFile, $functionPointer));
$this->assertSame('\FooNamespace\FooInterface', FunctionHelper::findReturnTypeHint($codeSnifferFile, $functionPointer));

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'withoutReturnTypeHint');
$this->assertFalse(FunctionHelper::hasReturnTypeHint($codeSnifferFile, $functionPointer));
$this->assertNull(FunctionHelper::findReturnTypeHint($codeSnifferFile, $functionPointer));

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'abstractWithReturnTypeHint');
$this->assertTrue(FunctionHelper::hasReturnTypeHint($codeSnifferFile, $functionPointer));
$this->assertSame('bool', FunctionHelper::findReturnTypeHint($codeSnifferFile, $functionPointer));

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'abstractWithoutReturnTypeHint');
$this->assertFalse(FunctionHelper::hasReturnTypeHint($codeSnifferFile, $functionPointer));
$this->assertNull(FunctionHelper::findReturnTypeHint($codeSnifferFile, $functionPointer));
}

public function testAnnotations()
{
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionAnnotations.php');

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'withAnnotations');
$this->assertSame([
'string $a',
'integer $b',
], FunctionHelper::getParametersAnnotations($codeSnifferFile, $functionPointer));
$this->assertSame('boolean', FunctionHelper::findReturnAnnotation($codeSnifferFile, $functionPointer));

$functionPointer = $this->findFunctionPointerByName($codeSnifferFile, 'withoutAnnotations');
$this->assertCount(0, FunctionHelper::getParametersAnnotations($codeSnifferFile, $functionPointer));
$this->assertNull(FunctionHelper::findReturnAnnotation($codeSnifferFile, $functionPointer));
}

}
25 changes: 25 additions & 0 deletions tests/Helpers/data/functionAbstractOrNot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

class FooClass
{

public function fooMethod()
{

}

}

abstract class FooAbstractClass
{

abstract public function fooAbstractMethod();

}

interface FooInterface
{

public function fooInterfaceMethod();

}

0 comments on commit b411339

Please sign in to comment.