From 3bac93505eabb2e5dd9558632e6b1c7bd275c60b Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Thu, 31 Aug 2023 07:23:50 +0200 Subject: [PATCH 1/5] Modernize sniffs & helpers and embrace PHPCSUtils See #72, #73 - Split PHPCSHelpers class in multiple smaller classes, easier to test and maintain - Make use of new helpers and PHPCSUtils in all sniffs - Modernize sniffs and their tests for modern PHP feautures - Reorganize tests and improve bootstrapping - Standardize file headers --- Inpsyde/Helpers/Boundaries.php | 123 +++ Inpsyde/Helpers/FunctionDocBlock.php | 191 +++++ Inpsyde/Helpers/FunctionReturnStatement.php | 145 ++++ Inpsyde/Helpers/Functions.php | 244 ++++++ Inpsyde/Helpers/Misc.php | 144 ++++ Inpsyde/Helpers/Names.php | 171 ++++ Inpsyde/Helpers/Objects.php | 199 +++++ Inpsyde/Helpers/WpHooks.php | 92 +++ Inpsyde/PhpcsHelpers.php | 734 ------------------ .../ArgumentTypeDeclarationSniff.php | 118 ++- .../CodeQuality/DisableCallUserFuncSniff.php | 32 +- .../DisableMagicSerializeSniff.php | 34 +- .../CodeQuality/DisallowShortOpenTagSniff.php | 23 +- .../ElementNameMinimalLengthSniff.php | 47 +- .../ForbiddenPublicPropertySniff.php | 28 +- .../CodeQuality/FunctionBodyStartSniff.php | 24 + .../CodeQuality/FunctionLengthSniff.php | 24 + .../CodeQuality/HookClosureReturnSniff.php | 36 +- .../Sniffs/CodeQuality/LineLengthSniff.php | 24 + .../Sniffs/CodeQuality/NestingLevelSniff.php | 24 + .../Sniffs/CodeQuality/NoAccessorsSniff.php | 28 +- Inpsyde/Sniffs/CodeQuality/NoElseSniff.php | 24 + .../NoRootNamespaceFunctionsSniff.php | 35 +- .../CodeQuality/NoTopLevelDefineSniff.php | 28 +- .../PropertyPerClassLimitSniff.php | 35 +- Inpsyde/Sniffs/CodeQuality/Psr4Sniff.php | 52 +- .../ReturnTypeDeclarationSniff.php | 477 ++++++------ .../Sniffs/CodeQuality/StaticClosureSniff.php | 41 +- .../Sniffs/CodeQuality/VariablesNameSniff.php | 28 +- Inpsyde/ruleset.xml | 16 +- LICENSE | 2 +- composer.json | 5 + phpcs.xml | 6 +- phpunit.xml.dist | 2 +- tests/autoload.php | 28 +- tests/bootstrap.php | 25 +- tests/cases/FixturesTest.php | 30 +- tests/cases/Helpers/FunctionDocBlocTest.php | 209 +++++ .../Helpers/FunctionReturnStatementTest.php | 128 +++ tests/cases/Helpers/FunctionsTest.php | 168 ++++ tests/cases/Helpers/MiscTest.php | 115 +++ tests/cases/Helpers/NamesTest.php | 93 +++ tests/cases/Helpers/ObjectsTest.php | 118 +++ tests/cases/Helpers/WpHooksTest.php | 83 ++ tests/cases/PhpcsHelpersTest.php | 598 -------------- tests/fixtures/argument-type-declaration.php | 6 +- .../fixtures/element-name-minimal-length.php | 105 +-- tests/fixtures/return-type-declaration.php | 94 ++- tests/fixtures/static-closure.php | 36 + tests/src/FixtureContentParser.php | 29 +- tests/src/SniffMessages.php | 29 +- tests/src/SniffMessagesExtractor.php | 29 +- tests/src/TestCase.php | 62 ++ 53 files changed, 3373 insertions(+), 1848 deletions(-) create mode 100644 Inpsyde/Helpers/Boundaries.php create mode 100644 Inpsyde/Helpers/FunctionDocBlock.php create mode 100644 Inpsyde/Helpers/FunctionReturnStatement.php create mode 100644 Inpsyde/Helpers/Functions.php create mode 100644 Inpsyde/Helpers/Misc.php create mode 100644 Inpsyde/Helpers/Names.php create mode 100644 Inpsyde/Helpers/Objects.php create mode 100644 Inpsyde/Helpers/WpHooks.php delete mode 100644 Inpsyde/PhpcsHelpers.php create mode 100644 tests/cases/Helpers/FunctionDocBlocTest.php create mode 100644 tests/cases/Helpers/FunctionReturnStatementTest.php create mode 100644 tests/cases/Helpers/FunctionsTest.php create mode 100644 tests/cases/Helpers/MiscTest.php create mode 100644 tests/cases/Helpers/NamesTest.php create mode 100644 tests/cases/Helpers/ObjectsTest.php create mode 100644 tests/cases/Helpers/WpHooksTest.php delete mode 100644 tests/cases/PhpcsHelpersTest.php create mode 100644 tests/src/TestCase.php diff --git a/Inpsyde/Helpers/Boundaries.php b/Inpsyde/Helpers/Boundaries.php new file mode 100644 index 0000000..3a568f2 --- /dev/null +++ b/Inpsyde/Helpers/Boundaries.php @@ -0,0 +1,123 @@ +> $tokens */ + $tokens = $file->getTokens(); + + if ( + !in_array( + $tokens[$position]['code'] ?? null, + array_keys(Collections::functionDeclarationTokens()), + true + ) + ) { + return [-1, -1]; + } + + return static::startEnd($file, $position); + } + + /** + * @param File $file + * @param int $position + * @return list{int, int} + */ + public static function objectBoundaries(File $file, int $position): array + { + /** @var array> $tokens */ + $tokens = $file->getTokens(); + + if (!in_array(($tokens[$position]['code'] ?? null), Tokens::$ooScopeTokens, true)) { + return [-1, -1]; + } + + return static::startEnd($file, $position); + } + + /** + * @param File $file + * @param int $position + * @return list{int, int} + */ + public static function arrayBoundaries(File $file, int $position): array + { + $openClose = Arrays::getOpenClose($file, $position); + if ( + !is_array($openClose) + || !is_int($openClose['opener'] ?? null) + || !is_int($openClose['closer'] ?? null) + ) { + return [-1, -1]; + } + + return [(int)$openClose['opener'], (int)$openClose['closer']]; + } + + /** + * @param File $file + * @param int $position + * @return list{int, int} + */ + private static function startEnd(File $file, int $position): array + { + /** @var array $token */ + $token = $file->getTokens()[$position] ?? []; + if (($token['code'] ?? '') === T_FN) { + $start = $file->findNext(T_FN_ARROW, $position + 1, null, false, null, true); + if (!$start) { + return [-1, -1]; + } + + return [$start + 1, $file->findEndOfStatement($start)]; + } + + $start = (int)($token['scope_opener'] ?? 0); + $end = (int)($token['scope_closer'] ?? 0); + if (($start <= 0) || ($end <= 0) || ($start >= ($end - 1))) { + return [-1, -1]; + } + + return [$start, $end]; + } +} diff --git a/Inpsyde/Helpers/FunctionDocBlock.php b/Inpsyde/Helpers/FunctionDocBlock.php new file mode 100644 index 0000000..ac0b1fc --- /dev/null +++ b/Inpsyde/Helpers/FunctionDocBlock.php @@ -0,0 +1,191 @@ +> + * + * phpcs:disable Inpsyde.CodeQuality.FunctionLength + * phpcs:disable Generic.Metrics.CyclomaticComplexity + */ + public static function allTags( + File $file, + int $position, + bool $normalizeContent = true + ): array { + // phpcs:enable Inpsyde.CodeQuality.FunctionLength + // phpcs:enable Generic.Metrics.CyclomaticComplexity + + /** @var array> $tokens */ + $tokens = $file->getTokens(); + + if ( + !array_key_exists($position, $tokens) + || !in_array($tokens[$position]['code'], [T_FUNCTION, T_CLOSURE, T_FN], true) + ) { + return []; + } + + $closeType = T_DOC_COMMENT_CLOSE_TAG; + $closeTag = $file->findPrevious($closeType, $position - 1, null, false, null, true); + + if (!$closeTag || empty($tokens[$closeTag]['comment_opener'])) { + return []; + } + + $functionLine = (int)($tokens[$position]['line'] ?? -1); + $closeLine = (int)($tokens[$closeTag]['line'] ?? -1); + if ($closeLine !== ($functionLine - 1)) { + return []; + } + + /** @var array $tags */ + $tags = []; + $start = (int)$tokens[$closeTag]['comment_opener'] + 1; + $key = -1; + $inTag = false; + + for ($i = $start; $i < $closeTag; $i++) { + $code = $tokens[$i]['code']; + if ($code === T_DOC_COMMENT_STAR) { + continue; + } + + $content = (string)$tokens[$i]['content']; + if (($tokens[$i]['code'] === T_DOC_COMMENT_TAG)) { + $inTag = true; + $key++; + $tags[$key] = [$content, '']; + continue; + } + + if ($inTag) { + $tags[$key][1] .= $content; + } + } + + $normalizedTags = []; + static $rand; + $rand or $rand = bin2hex(random_bytes(3)); + foreach ($tags as [$tagName, $tagContent]) { + empty($normalizedTags[$tagName]) and $normalizedTags[$tagName] = []; + if (!$normalizeContent) { + $normalizedTags[$tagName][] = $tagContent; + continue; + } + + $lines = array_filter(array_map('trim', explode("\n", $tagContent))); + $normalized = preg_replace('~\s+~', ' ', implode("%LB%{$rand}%LB%", $lines)) ?? ''; + $normalizedTags[$tagName][] = trim(str_replace("%LB%{$rand}%LB%", "\n", $normalized)); + } + + return $normalizedTags; + } + + /** + * @param string $tag + * @param File $file + * @param int $position + * @return list + */ + public static function tag(string $tag, File $file, int $position): array + { + $tagName = '@' . ltrim($tag, '@'); + $tags = static::allTags($file, $position); + if (empty($tags[$tagName])) { + return []; + } + + return $tags[$tagName]; + } + + /** + * @param File $file + * @param int $functionPosition + * @return array> + */ + public static function allParamTypes(File $file, int $functionPosition): array + { + $params = static::tag('@param', $file, $functionPosition); + if (!$params) { + return []; + } + + $types = []; + foreach ($params as $param) { + preg_match('~^([^$]+)\s*(\$\S+)~', trim($param), $matches); + if (($matches[1] ?? null) && ($matches[2] ?? null)) { + $types[$matches[2]] = static::normalizeTypesString($matches[1]); + } + } + + return $types; + } + + /** + * @param File $file + * @param int $functionPosition + * @return list + */ + public static function normalizeTypesString(string $typesString): array + { + $typesString = preg_replace('~\s+~', '', $typesString); + $splitTypes = explode('|', $typesString ?? ''); + $normalized = []; + $hasNull = false; + foreach ($splitTypes as $splitType) { + if (strpos($splitType, '&') !== false) { + $splitType = rtrim(ltrim($splitType, '('), ')'); + } elseif (strpos($splitType, '?') === 0) { + $splitType = substr($splitType, 1); + $hasNull = $splitType !== false; + } + if ($splitType === false) { + continue; + } + if (strtolower($splitType) === 'null') { + $hasNull = true; + continue; + } + $normalized[] = $splitType; + } + $ordered = array_values(array_unique($normalized)); + sort($ordered, SORT_STRING); + $hasNull and $ordered[] = 'null'; + + return $ordered; + } +} diff --git a/Inpsyde/Helpers/FunctionReturnStatement.php b/Inpsyde/Helpers/FunctionReturnStatement.php new file mode 100644 index 0000000..96ed6a3 --- /dev/null +++ b/Inpsyde/Helpers/FunctionReturnStatement.php @@ -0,0 +1,145 @@ + 0, 'void' => 0, 'null' => 0, 'total' => -1]; + + [$start, $end] = Boundaries::functionBoundaries($file, $position); + if (($start < 0) || ($end <= 0)) { + return $returnCount; + } + + /** @var array> $tokens */ + $tokens = $file->getTokens(); + + if ($tokens[$position]['code'] === T_FN) { + $returnCount['total'] = 1; + $key = static::isNull($file, $position) ? 'null' : 'nonEmpty'; + $returnCount[$key] = 1; + + return $returnCount; + } + + $returnCount['total'] = 0; + + $pos = $start + 1; + while ($pos < $end) { + [, $innerFunctionEnd] = Boundaries::functionBoundaries($file, $pos); + [, $innerClassEnd] = Boundaries::objectBoundaries($file, $pos); + if (($innerFunctionEnd > 0) || ($innerClassEnd > 0)) { + $pos = ($innerFunctionEnd > 0) ? $innerFunctionEnd + 1 : $innerClassEnd + 1; + continue; + } + + if ($tokens[$pos]['code'] === T_RETURN) { + $returnCount['total']++; + $void = static::isVoid($file, $pos); + $null = !$void && static::isNull($file, $pos); + $void and $returnCount['void']++; + $null and $returnCount['null']++; + (!$void && !$null) and $returnCount['nonEmpty']++; + } + + $pos++; + } + + return $returnCount; + } + + /** + * @param File $file + * @param int $position + * @return bool + */ + public static function isVoid(File $file, int $position): bool + { + /** @var array> $tokens */ + $tokens = $file->getTokens(); + + if (($tokens[$position]['code'] ?? '') !== T_RETURN) { + return false; + } + + $exclude = Tokens::$emptyTokens; + + $nextToReturn = $file->findNext($exclude, $position + 1, null, true, null, true); + + return ($tokens[$nextToReturn]['code'] ?? '') === T_SEMICOLON; + } + + /** + * @param File $file + * @param int $position + * @return bool + */ + public static function isNull(File $file, int $position): bool + { + /** @var array> $tokens */ + $tokens = $file->getTokens(); + + $code = $tokens[$position]['code'] ?? ''; + + if (($code !== T_RETURN) && ($code !== T_FN)) { + return false; + } + + if ($code === T_FN) { + $position = $file->findNext(T_FN_ARROW, $position + 1, null, false, null, true); + if (!$position) { + return false; + } + } + + $returnString = Misc::tokensSubsetToString( + $position + 1, + $file->findEndOfStatement($position + 1) - 1, + $file, + Tokens::$emptyTokens, + true + ); + + return strtolower($returnString) === 'null'; + } +} diff --git a/Inpsyde/Helpers/Functions.php b/Inpsyde/Helpers/Functions.php new file mode 100644 index 0000000..e49e817 --- /dev/null +++ b/Inpsyde/Helpers/Functions.php @@ -0,0 +1,244 @@ +> $tokens */ + $tokens = $file->getTokens(); + + $code = $tokens[$position]['code'] ?? -1; + $types = array_keys(Tokens::$functionNameTokens); + $types[] = T_VARIABLE; + + if ( + !in_array($code, $types, true) + || (($code === T_VARIABLE) && Scopes::isOOProperty($file, $position)) + ) { + return false; + } + + $callOpen = $file->findNext(Tokens::$emptyTokens, $position + 1, null, true, null, true); + if (!$callOpen || $tokens[$callOpen]['code'] !== T_OPEN_PARENTHESIS) { + return false; + } + + $prevExclude = Tokens::$emptyTokens; + $prevMeaningful = $file->findPrevious($prevExclude, $position - 1, null, true, null, true); + + if ($prevMeaningful && ($tokens[$prevMeaningful]['code'] ?? -1) === T_NS_SEPARATOR) { + $prevExclude = array_merge($prevExclude, [T_STRING, T_NS_SEPARATOR]); + $prevStart = $prevMeaningful - 1; + $prevMeaningful = $file->findPrevious($prevExclude, $prevStart, null, true, null, true); + } + + $prevMeaningfulCode = $prevMeaningful ? $tokens[$prevMeaningful]['code'] : null; + if ($prevMeaningfulCode && in_array($prevMeaningfulCode, [T_NEW, T_FUNCTION], true)) { + return false; + } + + $callClose = $file->findNext([T_CLOSE_PARENTHESIS], $callOpen + 1, null, false, null, true); + $expectedCallClose = $tokens[$callOpen]['parenthesis_closer'] ?? -1; + + return $callClose && ($callClose === $expectedCallClose); + } + + /** + * @param File $file + * @param int $position + * @return bool + */ + public static function isArrayAccess(File $file, int $position): bool + { + $methods = ['offsetSet', 'offsetGet', 'offsetUnset', 'offsetExists']; + + return Scopes::isOOMethod($file, $position) + && in_array(FunctionDeclarations::getName($file, $position), $methods, true); + } + + /** + * @param File $file + * @param int $position + * @return string + */ + public static function bodyContent(File $file, int $position): string + { + [$start, $end] = Boundaries::functionBoundaries($file, $position); + + if (($start < 0) || ($end < 0)) { + return ''; + } + + return Misc::tokensSubsetToString($start + 1, $end - 1, $file, []); + } + + /** + * @param File $file + * @param int $position + * @return int + */ + public static function countYieldInBody(File $file, int $position): int + { + /** @var array> $tokens */ + $tokens = $file->getTokens(); + if ($tokens[$position]['code'] === T_FN) { + return 0; + } + + [$start, $end] = Boundaries::functionBoundaries($file, $position); + if (($start < 0) || ($end <= 0)) { + return 0; + } + + $found = 0; + + $pos = $start + 1; + while ($pos < $end) { + [, $innerFunctionEnd] = Boundaries::functionBoundaries($file, $pos); + [, $innerClassEnd] = Boundaries::objectBoundaries($file, $pos); + if (($innerFunctionEnd > 0) || ($innerClassEnd > 0)) { + $pos = ($innerFunctionEnd > 0) ? $innerFunctionEnd + 1 : $innerClassEnd + 1; + continue; + } + + if (($tokens[$pos]['code'] === T_YIELD) || ($tokens[$pos]['code'] === T_YIELD_FROM)) { + $found++; + } + + $pos++; + } + + return $found; + } + + /** + * @param File $file + * @param int $position + * @return bool + */ + public static function isPsrMethod(File $file, int $position): bool + { + if (!Scopes::isOOMethod($file, $position)) { + return false; + } + + $tokens = $file->getTokens(); + $scopes = [T_CLASS, T_ANON_CLASS]; + + $classPos = Conditions::getLastCondition($file, $position, $scopes); + $type = is_int($classPos) ? ($tokens[$classPos]['code'] ?? null) : null; + if (!in_array($type, $scopes, true)) { + return false; + } + + /** @var int $classPos */ + $interfaces = Objects::allInterfacesFullyQualifiedNames($file, $classPos) ?? []; + foreach ($interfaces as $interface) { + if (stripos($interface, '\\Psr\\') === 0) { + return true; + } + } + + return false; + } + + /** + * Sometimes we don't declare the type because we can't, e.g. is the type is "mixed" or + * it is union, and we are using PHP 7.4. + * In those cases, we expect to document the type via doc bloc, and this functions aims + * to return true. + * + * @param list $docTypes + * @param bool $return + * @return bool + */ + public static function isNonDeclarableDocBlockType(array $docTypes, bool $return): bool + { + if ($docTypes === []) { + return false; + } + + $count = count($docTypes); + + $minVer = Misc::minPhpTestVersion(); + $is80 = version_compare($minVer, '8.0', '>='); + $is81 = version_compare($minVer, '8.1', '>='); + $is82 = version_compare($minVer, '8.2', '>='); + $isIntersection = strpos(implode('|', $docTypes), '&') !== false; + + // If "never" is there, this is valid for return types and PHP < 8.1, + // not valid for argument types. + if (in_array('never', $docTypes, true)) { + return $return && !$is81; + } + + if ($count > 1) { + // Union type with "mixed" make no sense, just use "mixed" + if (in_array('mixed', $docTypes, true)) { + return false; + } + // Union type without null, valid if we're not on PHP < 8.0, or on PHP < 8.2 and + // there's an intersection (DNF) + if (!in_array('null', $docTypes, true)) { + return !$is80 || (!$is82 && $isIntersection); + } + $docTypes = array_diff($docTypes, ['null']); + $count = count($docTypes); + } + + // Union type with "null" plus something else, valid if we're not on PHP < 8.0 or + // on PHP < 8.2 and there's an intersection (DNF) + if ($count > 1) { + return !$is80 || (!$is82 && $isIntersection); + } + + $singleDocType = reset($docTypes); + + // If the single type is "mixed" is valid if we are on PHP < 8.0. + // If the single type is "null" is valid if we are on PHP < 8.2. + // If the single is an intersection, is valid if we are on PHP < 8.1 + return (($singleDocType === 'mixed') && !$is80) + || (($singleDocType === 'null') && !$is82) + || ($isIntersection && !$is81); + } +} diff --git a/Inpsyde/Helpers/Misc.php b/Inpsyde/Helpers/Misc.php new file mode 100644 index 0000000..0fff6d5 --- /dev/null +++ b/Inpsyde/Helpers/Misc.php @@ -0,0 +1,144 @@ + self::MAX_SUPPORTED_MAJOR_VERSION) + || ($major < self::MIN_SUPPORTED_MAJOR_VERSION) + || version_compare("{$major}.{$minor}", self::MIN_SUPPORTED_VERSION, '<') + ) { + return self::MIN_SUPPORTED_VERSION; + } + + return "{$major}.{$minor}"; + } + + return self::MIN_SUPPORTED_VERSION; + } + + /** + * @param int $start + * @param int $end + * @param File $file + * @return array> + */ + public static function filterTokens(int $start, int $end, File $file): array + { + /** @var array> $tokens */ + $tokens = $file->getTokens(); + $filtered = []; + foreach ($tokens as $i => $token) { + if (($i >= $start) || ($i <= $end)) { + $filtered[$i] = $token; + } + } + + return $filtered; + } + + /** + * @param int $start + * @param int $end + * @param File $file + * @param array $types + * @param bool $excludeTypes + * @return array> + */ + public static function filterTokensByType( + int $start, + int $end, + File $file, + array $types = [], + bool $excludeTypes = false + ): array { + + /** @var array> $tokens */ + $tokens = $file->getTokens(); + $filtered = []; + foreach ($tokens as $i => $token) { + if (($i < $start) || ($i > $end)) { + continue; + } + $empty = $types === []; + $inArray = !$empty && in_array($token['code'] ?? '', $types, true); + if ($empty || (!$excludeTypes && $inArray) || ($excludeTypes && !$inArray)) { + $filtered[$i] = $token; + } + } + + return $filtered; + } + + /** + * @param int $start + * @param int $end + * @param File $file + * @param array $types + * @param bool $excludeTypes + * @return string + */ + public static function tokensSubsetToString( + int $start, + int $end, + File $file, + array $types, + bool $excludeTypes = false + ): string { + + $filtered = static::filterTokensByType($start, $end, $file, $types, $excludeTypes); + + $content = ''; + foreach ($filtered as $token) { + $content .= (string)($token['content'] ?? ''); + } + + return $content; + } +} diff --git a/Inpsyde/Helpers/Names.php b/Inpsyde/Helpers/Names.php new file mode 100644 index 0000000..de18c67 --- /dev/null +++ b/Inpsyde/Helpers/Names.php @@ -0,0 +1,171 @@ +> $tokens */ + $tokens = $file->getTokens(); + $code = $tokens[$position]['code'] ?? null; + + if (!in_array($code, self::NAMEABLE_TOKENS, true)) { + return null; + } elseif ($code === T_VARIABLE) { + $name = ltrim((string)($tokens[$position]['content'] ?? ''), '$'); + + return ($name === '') ? null : $name; + } + + if ($code === T_NAMESPACE) { + return Namespaces::isDeclaration($file, $position) + ? (Namespaces::getDeclaredName($file, $position) ?: '') + : null; + } + + $namePosition = $file->findNext(T_STRING, $position, null, false, null, true); + $name = ($namePosition === false) ? null : (string)$tokens[$namePosition]['content']; + + return ($name === '') ? null : $name; + } + + /** + * @param File $file + * @param int $position + * @return string + * + * phpcs:disable Inpsyde.CodeQuality.FunctionLength + * phpcs:disable Generic.Metrics.CyclomaticComplexity + */ + public static function tokenTypeName(File $file, int $position): string + { + // phpcs:enable Inpsyde.CodeQuality.FunctionLength + // phpcs:enable Generic.Metrics.CyclomaticComplexity + + /** @var array> $tokens */ + $tokens = $file->getTokens(); + $code = $tokens[$position]['code'] ?? -1; + + switch ($code) { + case T_CLASS: + case T_ANON_CLASS: + return 'Class'; + case T_ENUM: + return 'Enum'; + case T_ENUM_CASE: + return 'Enum case'; + case T_TRAIT: + return 'Trait'; + case T_INTERFACE: + return 'Interface'; + case T_CONST: + return 'Constant'; + case T_FUNCTION: + return 'Function'; + case T_VARIABLE: + return Scopes::isOOProperty($file, $position) ? 'Property' : 'Variable'; + case T_LNUMBER: + case T_DNUMBER: + return 'Number'; + case T_STRING: + return 'String'; + case T_THIS: + return 'Property'; + case T_WHITESPACE: + return 'White space'; + } + + $operators = array_merge( + array_keys(\PHP_CodeSniffer\Util\Tokens::$arithmeticTokens), + array_keys(\PHP_CodeSniffer\Util\Tokens::$assignmentTokens), + array_keys(\PHP_CodeSniffer\Util\Tokens::$equalityTokens), + array_keys(\PHP_CodeSniffer\Util\Tokens::$arithmeticTokens), + array_keys(\PHP_CodeSniffer\Util\Tokens::$operators), + array_keys(\PHP_CodeSniffer\Util\Tokens::$booleanOperators), + array_keys(\PHP_CodeSniffer\Util\Tokens::$castTokens), + array_keys(\PHP_CodeSniffer\Util\Tokens::$bracketTokens), + array_keys(\PHP_CodeSniffer\Util\Tokens::$heredocTokens), + array_keys(Collections::objectOperators()), + array_keys(Collections::incrementDecrementOperators()), + array_keys(Collections::phpOpenTags()), + array_keys(Collections::namespaceDeclarationClosers()), + [ + T_COMMA, + T_ASPERAND, + T_BACKTICK, + T_STRING_CONCAT, + T_COLON, + T_FN_ARROW, + T_MATCH_ARROW, + T_TYPE_UNION, + T_ATTRIBUTE_END, + T_TYPE_INTERSECTION, + T_ELLIPSIS, + ], + ); + + switch (true) { + case in_array($code, $operators, true): + return 'Operator'; + case in_array($code, \PHP_CodeSniffer\Util\Tokens::$textStringTokens, true): + return 'Text'; + case in_array($code, \PHP_CodeSniffer\Util\Tokens::$commentTokens, true): + return 'Comment'; + } + + return 'Keyword'; + } +} diff --git a/Inpsyde/Helpers/Objects.php b/Inpsyde/Helpers/Objects.php new file mode 100644 index 0000000..0609724 --- /dev/null +++ b/Inpsyde/Helpers/Objects.php @@ -0,0 +1,199 @@ +> $tokens */ + $tokens = $file->getTokens(); + if ( + !in_array( + $tokens[$position]['code'] ?? null, + Collections::ooPropertyScopes(), + true + ) + ) { + return 0; + } + + [$start, $end] = Boundaries::objectBoundaries($file, $position); + if (($start < 0) || ($end < 0)) { + return 0; + } + + + $found = 0; + + $next = $start + 1; + while ($next < $end) { + [, $innerFunctionEnd] = Boundaries::functionBoundaries($file, $next); + if ($innerFunctionEnd > 0) { + $next = $innerFunctionEnd + 1; + continue; + } + + if ( + (($tokens[$next]['code'] ?? '') === T_VARIABLE) + && Scopes::isOOProperty($file, $next) + ) { + $found++; + } + + $next++; + } + + return $found; + } + + /** + * @param File $file + * @param int $position + * @return array + * + * phpcs:disable Generic.Metrics.CyclomaticComplexity + */ + public static function findAllImportUses(File $file, int $position): array + { + // phpcs:enable Generic.Metrics.CyclomaticComplexity + $usePositions = []; + $nextUse = $file->findPrevious(T_NAMESPACE, $position - 1) ?: 0; + + while (true) { + $nextUse = $file->findNext(T_USE, $nextUse + 1, $position - 1); + if (!$nextUse) { + break; + } + if (!UseStatements::isImportUse($file, $nextUse)) { + continue; + } + $usePositions[] = $nextUse; + } + + if (!$usePositions) { + return []; + } + + $tokens = $file->getTokens(); + $uses = []; + $useNameEnd = $file->findEndOfStatement(end($usePositions)); + foreach ($usePositions as $i => $usePosition) { + $end = ($i === (count($usePositions)) - 1) ? $useNameEnd : $usePositions[$i + 1]; + $asPos = $file->findNext(T_AS, $usePosition + 1, $end, false, null, true); + $useName = Misc::tokensSubsetToString( + $usePosition + 1, + ($asPos ?: $end) - 1, + $file, + [T_STRING, T_NS_SEPARATOR] + ); + $useName = trim($useName, '\\'); + $useNameParts = explode('\\', $useName); + $key = end($useNameParts); + if ($asPos) { + $keyPos = $file->findNext(T_STRING, $asPos + 1, null, false, null, true); + /** @var string $key */ + $key = $tokens[$keyPos]['content'] ?? ''; + } + $uses[$key] = $useName; + } + + return $uses; + } + + /** + * @param File $file + * @param int $position + * @return list|null + */ + public static function allInterfacesFullyQualifiedNames(File $file, int $position): ?array + { + $tokens = $file->getTokens(); + $code = $tokens[$position]['code'] ?? null; + if (!in_array($code, Collections::ooCanImplement(), true)) { + return null; + } + + $implementsPos = $file->findNext(T_IMPLEMENTS, $position, null, false, null, true); + if (!$implementsPos) { + return null; + } + + $namesEnd = $file->findNext( + [T_OPEN_CURLY_BRACKET, T_EXTENDS], + $position, + null, + false, + null, + true + ); + + if (!$namesEnd) { + return null; + } + + $uses = static::findAllImportUses($file, $position - 1); + /** @var non-empty-list|false $names */ + $names = ObjectDeclarations::findImplementedInterfaceNames($file, $position); + if (!$names) { + return []; + } + + $fqns = []; + foreach ($names as $name) { + if (strpos($name, '\\') === 0) { + $fqns[] = $name; + continue; + } + $parts = explode('\\', $name); + $first = $parts[0]; + if (isset($uses[$first])) { + array_shift($parts); + $fqns[] = rtrim('\\' . $uses[$first] . '\\' . implode('\\', $parts), '\\'); + continue; + } + $namespace = Namespaces::determineNamespace($file, $position); + $fqns[] = $namespace ? "\\{$namespace}\\{$name}" : "\\{$name}"; + } + + return $fqns; + } +} diff --git a/Inpsyde/Helpers/WpHooks.php b/Inpsyde/Helpers/WpHooks.php new file mode 100644 index 0000000..f315002 --- /dev/null +++ b/Inpsyde/Helpers/WpHooks.php @@ -0,0 +1,92 @@ +> $tokens */ + $tokens = $file->getTokens(); + + if (!in_array(($tokens[$position]['code'] ?? ''), [T_CLOSURE, T_FN], true)) { + return false; + } + + $empty = Tokens::$emptyTokens; + + $exclude = $empty; + $exclude[] = T_STATIC; + $commaPos = $file->findPrevious($exclude, $position - 1, null, true, null, true); + if (!$commaPos || ($tokens[$commaPos]['code'] ?? '') !== T_COMMA) { + return false; + } + + $openType = [T_OPEN_PARENTHESIS]; + $openCallPos = $file->findPrevious($openType, $commaPos - 2, null, false, null, true); + if (!$openCallPos) { + return false; + } + + $functionCallPos = $file->findPrevious($empty, $openCallPos - 1, null, true, null, true); + if (!$functionCallPos || $tokens[$functionCallPos]['code'] !== T_STRING) { + return false; + } + + $actions = []; + $lookForFilters and $actions[] = 'add_filter'; + $lookForActions and $actions[] = 'add_action'; + + return in_array($tokens[$functionCallPos]['content'] ?? '', $actions, true); + } + + /** + * @param File $file + * @param int $position + * @return bool + */ + public static function isHookFunction(File $file, int $position): bool + { + return (bool)FunctionDocBlock::tag('@wp-hook', $file, $position); + } +} diff --git a/Inpsyde/PhpcsHelpers.php b/Inpsyde/PhpcsHelpers.php deleted file mode 100644 index 39b887c..0000000 --- a/Inpsyde/PhpcsHelpers.php +++ /dev/null @@ -1,734 +0,0 @@ - - */ - public static function allPropertiesTokenPositions(File $file, int $position): array - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - $code = $tokens[$position]['code'] ?? ''; - - if (!in_array($code, Tokens::$ooScopeTokens, true)) { - return []; - } - - $opener = (int)($tokens[$position]['scope_opener'] ?? -1); - $closer = (int)($tokens[$position]['scope_closer'] ?? -1); - - if ($opener <= 0 || $closer <= 0 || $closer <= $opener || $closer <= $position) { - return []; - } - - $propertyList = []; - $pos = $opener + 1; - while ($pos < $closer) { - if ($tokens[$pos]['code'] === T_CLASS || $tokens[$pos]['code'] === T_ANON_CLASS) { - $pos = ((int)($tokens[$pos]['scope_closer'] ?? $pos)) + 1; - continue; - } - - if (self::variableIsProperty($file, $pos)) { - $propertyList[] = $pos; - } - - $pos++; - } - - return $propertyList; - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function variableIsProperty(File $file, int $position): bool - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if ( - (($tokens[$position]['code'] ?? '') !== T_VARIABLE) - || !static::hasOopCondition($file, $position) - ) { - return false; - } - - $prev = $file->findPrevious(Tokens::$emptyTokens, $position - 1, null, true, null, true); - - $modifiers = [T_PRIVATE, T_PUBLIC, T_PROTECTED, T_STATIC, T_VAR]; - - return $prev && in_array($tokens[$prev]['code'], $modifiers, true); - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function functionIsMethod(File $file, int $position): bool - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - return (($tokens[$position]['code'] ?? '') === T_FUNCTION) - && static::hasOopCondition($file, $position); - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function hasOopCondition(File $file, int $position): bool - { - return static::findOopContext($file, $position) !== 0; - } - - /** - * @param File $file - * @param int $position - * @return int - */ - public static function findOopContext(File $file, int $position): int - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if ( - empty($tokens[$position]['conditions']) - || ((int)($tokens[$position]['level'] ?? 0) <= 0) - || !is_array($tokens[$position]['conditions']) - ) { - return 0; - } - - $targetLevel = (int)$tokens[$position]['level'] - 1; - - foreach ($tokens[$position]['conditions'] as $condPosition => $condCode) { - assert(is_int($condPosition)); - $condLevel = (int)($tokens[$condPosition]['level'] ?? -1); - - if ( - in_array($condCode, Tokens::$ooScopeTokens, true) - && ($condLevel === $targetLevel) - ) { - return $condPosition; - } - } - - return 0; - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function functionIsArrayAccess(File $file, int $position): bool - { - $methods = ['offsetSet', 'offsetGet', 'offsetUnset', 'offsetExists']; - - return self::functionIsMethod($file, $position) - && in_array($file->getDeclarationName($position), $methods, true); - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function looksLikeFunctionCall(File $file, int $position): bool - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - $code = $tokens[$position]['code'] ?? -1; - if (!in_array($code, [T_VARIABLE, T_STRING], true)) { - return false; - } - - $empty = Tokens::$emptyTokens; - - $callOpen = $file->findNext($empty, $position + 1, null, true, null, true); - if (!$callOpen || $tokens[$callOpen]['code'] !== T_OPEN_PARENTHESIS) { - return false; - } - - $prevExclude = $empty; - $prevMeaningful = $file->findPrevious($prevExclude, $position - 1, null, true, null, true); - - if ($prevMeaningful && ($tokens[$prevMeaningful]['code'] ?? -1) === T_NS_SEPARATOR) { - $prevExclude = array_merge($prevExclude, [T_STRING, T_NS_SEPARATOR]); - $prevStart = $prevMeaningful - 1; - $prevMeaningful = $file->findPrevious($prevExclude, $prevStart, null, true, null, true); - } - - $prevMeaningfulCode = $prevMeaningful ? $tokens[$prevMeaningful]['code'] : null; - if ($prevMeaningfulCode && in_array($prevMeaningfulCode, [T_NEW, T_FUNCTION], true)) { - return false; - } - - $callClose = $file->findNext([T_CLOSE_PARENTHESIS], $callOpen + 1, null, false, null, true); - $expectedCallClose = $tokens[$callOpen]['parenthesis_closer'] ?? -1; - - return $callClose && $callClose === $expectedCallClose; - } - - /** - * @param File $file - * @param int $position - * @return string - */ - public static function tokenTypeName(File $file, int $position): string - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - switch ((int)($tokens[$position]['code'] ?? -1)) { - case T_CLASS: - case T_ANON_CLASS: - return 'Class'; - case T_TRAIT: - return 'Trait'; - case T_INTERFACE: - return 'Interface'; - case T_CONST: - return 'Constant'; - case T_FUNCTION: - return 'Function'; - case T_VARIABLE: - return self::variableIsProperty($file, $position) ? 'Property' : 'Variable'; - } - - return ''; - } - - /** - * @param File $file - * @param int $position - * @return string - */ - public static function tokenName(File $file, int $position): string - { - static $nameable; - $nameable or $nameable = [T_CLASS, T_TRAIT, T_INTERFACE, T_CONST, T_FUNCTION, T_VARIABLE]; - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - $code = $tokens[$position]['code'] ?? null; - - if (!in_array($code, (array)$nameable, true)) { - return ''; - } elseif ($code === T_VARIABLE) { - return ltrim((string)($tokens[$position]['content'] ?? ''), '$'); - } - - $namePosition = $file->findNext(T_STRING, $position, null, false, null, true); - - return $namePosition === false ? '' : (string)$tokens[$namePosition]['content']; - } - - /** - * @param int $start - * @param int $end - * @param File $file - * @param int|string ...$types - * @return array> - * - * phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration - */ - public static function filterTokensByType(int $start, int $end, File $file, ...$types): array - { - // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - $filtered = []; - foreach ($tokens as $position => $token) { - if ( - ($position >= $start) - && ($position <= $end) - && in_array($token['code'] ?? '', $types, true) - ) { - $filtered[$position] = $token; - } - } - - return $filtered; - } - - /** - * @param File $file - * @param int $position - * @param bool $lookForFilters - * @param bool $lookForActions - * @return bool - */ - public static function isHookClosure( - File $file, - int $position, - bool $lookForFilters = true, - bool $lookForActions = true - ): bool { - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if (!in_array(($tokens[$position]['code'] ?? ''), [T_CLOSURE, T_FN], true)) { - return false; - } - - $empty = Tokens::$emptyTokens; - - $exclude = $empty; - $exclude[] = T_STATIC; - $commaPos = $file->findPrevious($exclude, $position - 1, null, true, null, true); - if (!$commaPos || ($tokens[$commaPos]['code'] ?? '') !== T_COMMA) { - return false; - } - - $openType = [T_OPEN_PARENTHESIS]; - $openCallPos = $file->findPrevious($openType, $commaPos - 2, null, false, null, true); - if (!$openCallPos) { - return false; - } - - $functionCallPos = $file->findPrevious($empty, $openCallPos - 1, null, true, null, true); - if (!$functionCallPos || $tokens[$functionCallPos]['code'] !== T_STRING) { - return false; - } - - $actions = []; - $lookForFilters and $actions[] = 'add_filter'; - $lookForActions and $actions[] = 'add_action'; - - return in_array($tokens[$functionCallPos]['content'] ?? '', $actions, true); - } - - /** - * @param File $file - * @param int $position - * @param bool $normalizeContent - * @return array> - * - * phpcs:disable Inpsyde.CodeQuality.FunctionLength - * phpcs:disable Generic.Metrics.CyclomaticComplexity - */ - public static function functionDocBlockTags( - File $file, - int $position, - bool $normalizeContent = true - ): array { - // phpcs:enable Inpsyde.CodeQuality.FunctionLength - // phpcs:enable Generic.Metrics.CyclomaticComplexity - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if ( - !array_key_exists($position, $tokens) - || !in_array($tokens[$position]['code'], [T_FUNCTION, T_CLOSURE, T_FN], true) - ) { - return []; - } - - $closeType = T_DOC_COMMENT_CLOSE_TAG; - $closeTag = $file->findPrevious($closeType, $position - 1, null, false, null, true); - - if (!$closeTag || empty($tokens[$closeTag]['comment_opener'])) { - return []; - } - - $functionLine = (int)($tokens[$position]['line'] ?? -1); - $closeLine = (int)($tokens[$closeTag]['line'] ?? -1); - if ($closeLine !== ($functionLine - 1)) { - return []; - } - - /** @var array $tags */ - $tags = []; - $start = (int)$tokens[$closeTag]['comment_opener'] + 1; - $key = -1; - $inTag = false; - - for ($i = $start; $i < $closeTag; $i++) { - $code = $tokens[$i]['code']; - if ($code === T_DOC_COMMENT_STAR) { - continue; - } - - $content = (string)$tokens[$i]['content']; - if (($tokens[$i]['code'] === T_DOC_COMMENT_TAG)) { - $inTag = true; - $key++; - $tags[$key] = [$content, '']; - continue; - } - - if ($inTag) { - $tags[$key][1] .= $content; - } - } - - $normalizedTags = []; - static $rand; - $rand or $rand = bin2hex(random_bytes(3)); - foreach ($tags as [$tagName, $tagContent]) { - empty($normalizedTags[$tagName]) and $normalizedTags[$tagName] = []; - if (!$normalizeContent) { - $normalizedTags[$tagName][] = $tagContent; - continue; - } - - $lines = array_filter(array_map('trim', explode("\n", $tagContent))); - $normalized = preg_replace('~\s+~', ' ', implode("%LB%{$rand}%LB%", $lines)) ?? ''; - $normalizedTags[$tagName][] = trim(str_replace("%LB%{$rand}%LB%", "\n", $normalized)); - } - - return $normalizedTags; - } - - /** - * @param string $tag - * @param File $file - * @param int $position - * @return array - */ - public static function functionDocBlockTag(string $tag, File $file, int $position): array - { - $tagName = '@' . ltrim($tag, '@'); - $tags = static::functionDocBlockTags($file, $position); - if (empty($tags[$tagName])) { - return []; - } - - return $tags[$tagName]; - } - - /** - * @param File $file - * @param int $functionPosition - * @return array> - */ - public static function functionDocBlockParamTypes(File $file, int $functionPosition): array - { - $params = PhpcsHelpers::functionDocBlockTag('@param', $file, $functionPosition); - if (!$params) { - return []; - } - - $types = []; - foreach ($params as $param) { - preg_match('~^([^$]+)\s*(\$\S+)~', trim($param), $matches); - if (empty($matches[1]) || empty($matches[2])) { - continue; - } - - $types[$matches[2]] = array_map('trim', explode('|', $matches[1])); - } - - return $types; - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function isHookFunction(File $file, int $position): bool - { - return (bool)self::functionDocBlockTag('@wp-hook', $file, $position); - } - - /** - * @param File $file - * @param int $position - * @return string - */ - public static function functionBody(File $file, int $position): string - { - [$start, $end] = static::functionBoundaries($file, $position); - if ($start < 0 || $end < 0) { - return ''; - } - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - $body = ''; - for ($i = $start + 1; $i < $end; $i++) { - $body .= (string)($tokens[$i]['content'] ?? ''); - } - - return $body; - } - - /** - * @param File $file - * @param int $position - * @return list{int, int} - */ - public static function functionBoundaries(File $file, int $position): array - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if (!in_array(($tokens[$position]['code'] ?? null), [T_FUNCTION, T_CLOSURE, T_FN], true)) { - return [-1, -1]; - } - - return static::boundaries($tokens, $position); - } - - /** - * @param File $file - * @param int $position - * @return list{int, int} - */ - public static function classBoundaries(File $file, int $position): array - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if (!in_array(($tokens[$position]['code'] ?? null), Tokens::$ooScopeTokens, true)) { - return [-1, -1]; - } - - return static::boundaries($tokens, $position); - } - - /** - * @param File $file - * @param int $position - * @return array{nonEmpty:int, void:int, null:int, total:int} - */ - public static function returnsCountInfo(File $file, int $position): array - { - $returnCount = ['nonEmpty' => 0, 'void' => 0, 'null' => 0, 'total' => 0]; - - [$start, $end] = self::functionBoundaries($file, $position); - if ($start < 0 || $end <= 0) { - return $returnCount; - } - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if (T_FN === ($tokens[$position]['code'] ?? null)) { - return ['nonEmpty' => 1, 'void' => 0, 'null' => 0, 'total' => 1]; - } - - $pos = $start + 1; - while ($pos < $end) { - [, $innerFunctionEnd] = self::functionBoundaries($file, $pos); - [, $innerClassEnd] = self::classBoundaries($file, $pos); - if ($innerFunctionEnd > 0 || $innerClassEnd > 0) { - $pos = ($innerFunctionEnd > 0) ? $innerFunctionEnd + 1 : $innerClassEnd + 1; - continue; - } - - if ($tokens[$pos]['code'] === T_RETURN) { - $returnCount['total']++; - $void = PhpcsHelpers::isVoidReturn($file, $pos); - $null = PhpcsHelpers::isNullReturn($file, $pos); - $void and $returnCount['void']++; - $null and $returnCount['null']++; - (!$void && !$null) and $returnCount['nonEmpty']++; - } - - $pos++; - } - - return $returnCount; - } - - /** - * @param File $file - * @param int $returnPosition - * @param bool $includeNull - * @return bool - */ - public static function isVoidReturn( - File $file, - int $returnPosition, - bool $includeNull = false - ): bool { - - /** @var array> $tokens */ - $tokens = $file->getTokens(); - - if (($tokens[$returnPosition]['code'] ?? '') !== T_RETURN) { - return false; - } - - $exclude = Tokens::$emptyTokens; - $includeNull and $exclude[] = T_NULL; - - $nextToReturn = $file->findNext($exclude, $returnPosition + 1, null, true, null, true); - - return ($tokens[$nextToReturn]['code'] ?? '') === T_SEMICOLON; - } - - /** - * @param File $file - * @param int $returnPosition - * @return bool - */ - public static function isNullReturn(File $file, int $returnPosition): bool - { - return !self::isVoidReturn($file, $returnPosition, false) - && self::isVoidReturn($file, $returnPosition, true); - } - - /** - * @param File $file - * @param int $position - * @return array{null, null}|array{int, string} - */ - public static function findNamespace(File $file, int $position): array - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); - $namespacePos = $file->findPrevious([T_NAMESPACE], $position - 1); - if (!$namespacePos || !array_key_exists($namespacePos, $tokens)) { - return [null, null]; - } - - $end = $file->findNext( - [T_SEMICOLON, T_OPEN_CURLY_BRACKET], - $namespacePos + 1, - null, - false, - null, - true - ); - - if (!$end) { - return [null, null]; - } - - if ( - $tokens[$end]['code'] === T_OPEN_CURLY_BRACKET - && !empty($tokens[$end]['scope_closer']) - && $tokens[$end]['scope_closer'] < $position - ) { - return [null, null]; - } - - $namespace = ''; - for ($i = $namespacePos + 1; $i < $end; $i++) { - $code = $tokens[$i]['code'] ?? null; - if (in_array($code, [T_STRING, T_NS_SEPARATOR], true)) { - $namespace .= (string)($tokens[$i]['content'] ?? ''); - } - } - - return [$namespacePos, $namespace]; - } - - /** - * @return string - */ - public static function minPhpTestVersion(): string - { - $testVersion = trim(Config::getConfigData('testVersion') ?: ''); - if (!$testVersion) { - return ''; - } - - preg_match('`^(\d+\.\d+)(?:\s*-\s*(?:\d+\.\d+)?)?$`', $testVersion, $matches); - - return $matches[1] ?? ''; - } - - /** - * @param File $file - * @param int $position - * @return bool - */ - public static function isUntypedPsrMethod(File $file, int $position): bool - { - $tokens = $file->getTokens(); - - if (($tokens[$position]['type'] ?? '') !== 'T_FUNCTION') { - return false; - } - - $classPos = static::findOopContext($file, $position); - $type = $tokens[$classPos]['type'] ?? null; - if (!$classPos || !in_array($type, ['T_CLASS', 'T_ANON_CLASS'], true)) { - return false; - } - - $names = $file->findImplementedInterfaceNames($classPos); - - if (!$names) { - return false; - } - - /** @var array $psrInterfaces */ - static $psrInterfaces; - $psrInterfaces or $psrInterfaces = [ - 'LoggerInterface', - 'CacheItemInterface', - 'CacheItemPoolInterface', - 'MessageInterface', - 'RequestInterface', - 'ServerRequestInterface', - 'ResponseInterface', - 'StreamInterface', - 'UriInterface', - 'UploadedFileInterface', - 'ContainerInterface', - 'LinkInterface', - 'EvolvableLinkInterface', - 'LinkProviderInterface', - 'EvolvableLinkProviderInterface', - 'CacheInterface', - 'RequestFactoryInterface', - 'ResponseFactoryInterface', - 'ServerRequestFactoryInterface', - 'StreamFactoryInterface', - ]; - - /** @var string $name */ - foreach ($names as $name) { - $lastName = array_slice(explode('\\', $name), -1, 1)[0]; - if (in_array($lastName, $psrInterfaces, true)) { - return true; - } - } - - return false; - } - - /** - * @param array> $tokens - * @param int $position - * @return list{int, int} - */ - private static function boundaries(array $tokens, int $position): array - { - $start = (int)($tokens[$position]['scope_opener'] ?? 0); - $end = (int)($tokens[$position]['scope_closer'] ?? 0); - if ($start <= 0 || $end <= 0 || $start >= ($end - 1)) { - return [-1, -1]; - } - - return [$start, $end]; - } -} diff --git a/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php b/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php index 7afdbb0..1963730 100644 --- a/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php @@ -1,31 +1,43 @@ > $tokens */ $tokens = $phpcsFile->getTokens(); @@ -59,34 +69,32 @@ public function process(File $phpcsFile, $stackPtr): void return; } - $paramsStart = (int)($tokens[$stackPtr]['parenthesis_opener'] ?? 0); - $paramsEnd = (int)($tokens[$stackPtr]['parenthesis_closer'] ?? 0); - - if (!$paramsStart || !$paramsEnd || $paramsStart >= ($paramsEnd - 1)) { - return; - } + /** @var array $parameters */ + $parameters = FunctionDeclarations::getParameters($phpcsFile, $stackPtr); + $docBlockTypes = FunctionDocBlock::allParamTypes($phpcsFile, $stackPtr); - $docBlockTypes = PhpcsHelpers::functionDocBlockParamTypes($phpcsFile, $stackPtr); - $variables = PhpcsHelpers::filterTokensByType($paramsStart, $paramsEnd, $phpcsFile, T_VARIABLE); - - foreach ($variables as $varPosition => $varToken) { - // Not triggering error for variable explicitly declared as mixed (or mixed|null) - if ($this->isMixed((string)($varToken['content'] ?? ''), $docBlockTypes)) { + $errors = []; + foreach ($parameters as $parameter) { + if ($parameter['type_hint'] ?? null) { continue; } - $typePosition = $phpcsFile->findPrevious( - [T_WHITESPACE, T_ELLIPSIS, T_BITWISE_AND], - $varPosition - 1, - $paramsStart + 1, - true - ); - - $type = $tokens[$typePosition] ?? null; - if ($type && !in_array($type['code'] ?? '', self::TYPE_CODES, true)) { - $phpcsFile->addWarning('Argument type is missing', $stackPtr, 'NoArgumentType'); + $docTypes = $docBlockTypes[$parameter['name']] ?? []; + if (!Functions::isNonDeclarableDocBlockType($docTypes, false)) { + $errors[] = $parameter['name']; } } + + if (!$errors) { + return; + } + + $allErrors = implode('", "', $errors); + $phpcsFile->addWarning( + sprintf('Argument type is missing for parameter(s) "%s"', $allErrors), + $stackPtr, + 'NoArgumentType' + ); } /** @@ -102,40 +110,16 @@ private function shouldIgnore(File $phpcsFile, $stackPtr, array $tokens): bool // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration $tokenCode = $tokens[$stackPtr]['code'] ?? ''; - $name = ($tokenCode !== T_FN) ? ($phpcsFile->getDeclarationName($stackPtr) ?: '') : ''; + $name = ($tokenCode !== T_FN) ? FunctionDeclarations::getName($phpcsFile, $stackPtr) : ''; - return PhpcsHelpers::functionIsArrayAccess($phpcsFile, $stackPtr) - || PhpcsHelpers::isHookClosure($phpcsFile, $stackPtr) - || PhpcsHelpers::isHookFunction($phpcsFile, $stackPtr) - || PhpcsHelpers::isUntypedPsrMethod($phpcsFile, $stackPtr) + return Functions::isArrayAccess($phpcsFile, $stackPtr) + || WpHooks::isHookClosure($phpcsFile, $stackPtr) + || WpHooks::isHookFunction($phpcsFile, $stackPtr) + || Functions::isPsrMethod($phpcsFile, $stackPtr) + || FunctionDeclarations::isSpecialMethod($phpcsFile, $stackPtr) || ( - PhpcsHelpers::functionIsMethod($phpcsFile, $stackPtr) + Scopes::isOOMethod($phpcsFile, $stackPtr) && in_array($name, self::METHODS_WHITELIST, true) ); } - - /** - * @param string $paramName - * @param array> $docBlockTypes - * @return bool - */ - private function isMixed(string $paramName, array $docBlockTypes): bool - { - $paramDocBlockTypes = $paramName ? ($docBlockTypes[$paramName] ?? null) : null; - if (!$paramDocBlockTypes) { - return false; - } - - $paramDocBlockTypesCount = count($paramDocBlockTypes); - if ($paramDocBlockTypesCount !== 1 && $paramDocBlockTypesCount !== 2) { - return false; - } - - $paramDocBlockTypes = array_map('trim', $paramDocBlockTypes); - if (!in_array('mixed', $paramDocBlockTypes, true)) { - return false; - } - - return ($paramDocBlockTypesCount === 1) || in_array('null', $paramDocBlockTypes, true); - } } diff --git a/Inpsyde/Sniffs/CodeQuality/DisableCallUserFuncSniff.php b/Inpsyde/Sniffs/CodeQuality/DisableCallUserFuncSniff.php index 9f6428c..4ea3744 100644 --- a/Inpsyde/Sniffs/CodeQuality/DisableCallUserFuncSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/DisableCallUserFuncSniff.php @@ -1,21 +1,27 @@ */ public array $allowedShortNames = [ + 'an', 'as', 'at', 'be', + 'by', + 'c', 'db', 'do', 'ex', 'go', + 'he', + 'hi', 'i', 'id', 'if', @@ -31,14 +60,19 @@ class ElementNameMinimalLengthSniff implements Sniff 'is', 'it', 'js', + 'me', 'my', 'no', 'of', 'ok', 'on', 'or', + 'pi', + 'so', + 'sh', 'to', 'up', + 'we', 'wp', ]; @@ -46,11 +80,11 @@ class ElementNameMinimalLengthSniff implements Sniff public array $additionalAllowedNames = []; /** - * @return list + * @return list */ public function register(): array { - return [T_CLASS, T_TRAIT, T_INTERFACE, T_CONST, T_FUNCTION, T_VARIABLE]; + return Names::NAMEABLE_TOKENS; } /** @@ -64,14 +98,17 @@ public function process(File $phpcsFile, $stackPtr): void { // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration - $elementName = PhpcsHelpers::tokenName($phpcsFile, $stackPtr); + $elementName = Names::nameableTokenName($phpcsFile, $stackPtr); + if (($elementName === '') || ($elementName === null)) { + return; + } $elementNameLength = mb_strlen($elementName); if ($this->shouldBeSkipped($elementNameLength, $elementName)) { return; } - $typeName = PhpcsHelpers::tokenTypeName($phpcsFile, $stackPtr); + $typeName = Names::tokenTypeName($phpcsFile, $stackPtr); $message = sprintf( '%s name "%s" is only %d chars long. Must be at least %d.', $typeName, diff --git a/Inpsyde/Sniffs/CodeQuality/ForbiddenPublicPropertySniff.php b/Inpsyde/Sniffs/CodeQuality/ForbiddenPublicPropertySniff.php index 04a3365..d3941c8 100644 --- a/Inpsyde/Sniffs/CodeQuality/ForbiddenPublicPropertySniff.php +++ b/Inpsyde/Sniffs/CodeQuality/ForbiddenPublicPropertySniff.php @@ -1,13 +1,37 @@ addError( diff --git a/Inpsyde/Sniffs/CodeQuality/LineLengthSniff.php b/Inpsyde/Sniffs/CodeQuality/LineLengthSniff.php index 0473033..3869360 100644 --- a/Inpsyde/Sniffs/CodeQuality/LineLengthSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/LineLengthSniff.php @@ -1,5 +1,29 @@ maxCount) { return; } $message = sprintf( '"%s" has too many properties: %d. Can be up to %d properties.', - PhpcsHelpers::tokenTypeName($phpcsFile, $stackPtr), + Names::tokenTypeName($phpcsFile, $stackPtr), $count, $this->maxCount ); diff --git a/Inpsyde/Sniffs/CodeQuality/Psr4Sniff.php b/Inpsyde/Sniffs/CodeQuality/Psr4Sniff.php index dc7a5b4..3368a34 100644 --- a/Inpsyde/Sniffs/CodeQuality/Psr4Sniff.php +++ b/Inpsyde/Sniffs/CodeQuality/Psr4Sniff.php @@ -1,12 +1,37 @@ + * @return list */ public function register(): array { - return [T_CLASS, T_INTERFACE, T_TRAIT]; + return [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM]; } /** @@ -32,7 +57,7 @@ public function process(File $phpcsFile, $stackPtr): void { // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration - $className = (string)$phpcsFile->getDeclarationName($stackPtr); + $className = ObjectDeclarations::getName($phpcsFile, $stackPtr) ?? ''; /** @var array> $tokens */ $tokens = $phpcsFile->getTokens(); @@ -65,7 +90,7 @@ private function checkFilenameOnly( int $position, string $className, string $entityType - ) { + ): void { if (basename($file->getFilename()) === "{$className}.php") { return; @@ -85,13 +110,20 @@ private function checkFilenameOnly( } /** + * @param File $file + * @param int $position + * @param string $className + * @param string $entityType * @return void */ - private function checkPsr4(File $file, int $position, string $className, string $entityType) - { - [, $namespace] = PhpcsHelpers::findNamespace($file, $position); - $namespace = is_string($namespace) ? "{$namespace}\\" : ''; - $namespace = rtrim($namespace, '\\'); + private function checkPsr4( + File $file, + int $position, + string $className, + string $entityType + ): void { + + $namespace = Namespaces::determineNamespace($file, $position); $fullyQualifiedName = $namespace . "\\{$className}"; diff --git a/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php b/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php index c0862d5..e679d95 100644 --- a/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php @@ -1,12 +1,42 @@ > $tokens */ + $tokens = $phpcsFile->getTokens(); + + $data = FunctionDeclarations::getProperties($phpcsFile, $stackPtr); + if (!$data['has_body']) { return; } - [$functionStart, $functionEnd] = PhpcsHelpers::functionBoundaries($phpcsFile, $stackPtr); + $returnType = $data['return_type'] ?? null; + $returnTypes = $returnType ? $this->normalizeReturnTypes($phpcsFile, $data) : []; + $returnInfo = FunctionReturnStatement::allInfo($phpcsFile, $stackPtr); + + if ($returnTypes) { + $this->checkNonEmptyReturnTypes($phpcsFile, $stackPtr, $returnTypes, $returnInfo); - if (($functionStart < 0) || ($functionEnd <= 0)) { return; } - [ - $hasNonVoidReturnType, - $hasVoidReturnType, - $hasNoReturnType, - $hasNullableReturn, - $returnsGenerator - ] = $this->returnTypeInfo($phpcsFile, $stackPtr); - - $returnData = PhpcsHelpers::returnsCountInfo($phpcsFile, $stackPtr); - $nonVoidReturnCount = $returnData['nonEmpty']; - $voidReturnCount = $returnData['void']; - $nullReturnCount = $returnData['null']; - - $yieldCount = $this->countYield($functionStart, $functionEnd, $phpcsFile); - - if ($yieldCount || $returnsGenerator) { - $this->maybeGeneratorErrors( - $yieldCount, - $returnsGenerator, - $nonVoidReturnCount, - $phpcsFile, - $stackPtr - ); + if ($this->checkMissingGeneratorReturnType($phpcsFile, $stackPtr)) { + return; + } + + $docTags = FunctionDocBlock::tag('return', $phpcsFile, $stackPtr); + $docTypes = (count($docTags) === 1) + ? FunctionDocBlock::normalizeTypesString(reset($docTags)) + : []; + + if ( + !Functions::isNonDeclarableDocBlockType($docTypes, true) + && !$this->shouldIgnore($phpcsFile, $stackPtr, $tokens) + ) { + $phpcsFile->addWarning('Return type is missing', $stackPtr, 'NoReturnType'); return; } - $this->maybeErrors( - $hasNonVoidReturnType, - $hasVoidReturnType, - $hasNoReturnType, - $hasNullableReturn, - $nonVoidReturnCount, - $nullReturnCount, - $voidReturnCount, - $phpcsFile, - $stackPtr - ); + $this->checkNonEmptyReturnTypes($phpcsFile, $stackPtr, $docTypes, $returnInfo); } /** - * @param bool $hasNonVoidReturnType - * @param bool $hasVoidReturnType - * @param bool $hasNoReturnType - * @param bool $hasNullableReturn - * @param int $nonVoidReturnCount - * @param int $nullReturnCount - * @param int $voidReturnCount * @param File $file * @param int $position - * @return void - * - * phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh + * @param array> $tokens + * @return bool */ - private function maybeErrors( - bool $hasNonVoidReturnType, - bool $hasVoidReturnType, - bool $hasNoReturnType, - bool $hasNullableReturn, - int $nonVoidReturnCount, - int $nullReturnCount, - int $voidReturnCount, - File $file, - int $position - ): void { - - $hasNullableReturn - ? $nonVoidReturnCount += $nullReturnCount - : $voidReturnCount += $nullReturnCount; - - if ($hasNonVoidReturnType && ($nonVoidReturnCount === 0 || $voidReturnCount > 0)) { - $msg = 'Return type with'; - $file->addError( - $nonVoidReturnCount === 0 ? "{$msg} no return" : "{$msg} void return", - $position, - $nonVoidReturnCount === 0 ? 'MissingReturn' : 'IncorrectVoidReturn' + private function shouldIgnore(File $file, int $position, array $tokens): bool + { + $tokenCode = $tokens[$position]['code'] ?? ''; + $name = ($tokenCode !== T_FN) ? FunctionDeclarations::getName($file, $position) : ''; + + return Functions::isArrayAccess($file, $position) + || Functions::isPsrMethod($file, $position) + || FunctionDeclarations::isSpecialMethod($file, $position) + || WpHooks::isHookClosure($file, $position) + || WpHooks::isHookFunction($file, $position) + || ( + Scopes::isOOMethod($file, $position) + && in_array($name, self::METHODS_WHITELIST, true) ); - } + } - if ($nonVoidReturnCount <= 0) { - return; - } + /** + * @param File $file + * @param $returnType + * @param array $data + * @return list + */ + private function normalizeReturnTypes(File $file, array $data): array + { + /** @var int $start */ + $start = is_int($data['return_type_token'] ?? null) ? $data['return_type_token'] : -1; + /** @var int $end */ + $end = is_int($data['return_type_end_token'] ?? null) ? $data['return_type_end_token'] : -1; + + if (($start > 0) && ($end > 0)) { + $returnTypesStr = Misc::tokensSubsetToString($start, $end, $file, []); + if ($data['nullable_return_type'] ?? false) { + $returnTypesStr .= '|null'; + } - if ($hasVoidReturnType) { - $file->addError( - 'Void return type when returning non-void', - $position, - 'IncorrectVoidReturnType' - ); + return FunctionDocBlock::normalizeTypesString($returnTypesStr); } - $docBlock = $this->hasReturnNullOrMixedDocBloc($file, $position); - - if ( - $docBlock['mixed'] - || PhpcsHelpers::isHookClosure($file, $position) - || PhpcsHelpers::isHookFunction($file, $position) - ) { - return; - } + return []; + } - if (!$this->areNullableReturnTypesSupported() && $docBlock['null']) { - return; - } + /** + * @param File $file + * @param int $position + * @param list $returnTypes + * @param array $returnInfo + * @return void + */ + private function checkNonEmptyReturnTypes( + File $file, + int $position, + array $returnTypes, + array $returnInfo + ): void { - $tokenCode = $file->getTokens()[$position]['code'] ?? ''; - $name = ($tokenCode !== T_FN) ? ($file->getDeclarationName($position) ?: '') : ''; + if (($returnTypes === ['void']) || ($returnTypes === ['null'])) { + $this->checkIsActualVoid($file, $position, $returnInfo, $returnTypes === ['null']); - if ( - PhpcsHelpers::functionIsMethod($file, $position) - && (in_array($name, self::METHODS_WHITELIST, true) || strpos($name, '__') === 0) - ) { return; } - if ($hasNoReturnType) { - $file->addWarning('Return type is missing', $position, 'NoReturnType'); - } + $this->checkInvalidGenerator($file, $position, $returnTypes, $returnInfo) + || $this->checkMissingReturn($file, $position, $returnTypes, $returnInfo) + || $this->checkIncorrectVoid($file, $position, $returnTypes, $returnInfo); } /** - * @param int $yieldCount - * @param bool $returnsGenerator - * @param int $nonVoidReturnCount * @param File $file * @param int $position + * @param array $returnInfo + * @param bool $checkNull * @return void */ - private function maybeGeneratorErrors( - int $yieldCount, - bool $returnsGenerator, - int $nonVoidReturnCount, + private function checkIsActualVoid( File $file, - int $position + int $position, + array $returnInfo, + bool $checkNull ): void { - if ($nonVoidReturnCount > 1) { - $file->addWarning( - 'A generator should only contain a single return point.', - $position, - 'InvalidGeneratorManyReturns' - ); - } + $key = $checkNull ? 'null' : 'void'; - if ($yieldCount && $returnsGenerator) { + if (($returnInfo['total'] >= 0) && ($returnInfo['total'] === $returnInfo[$key])) { return; } - if (!$yieldCount) { - $file->addError( - 'Found a generator return type in non-yielding function.', - $position, - 'GeneratorReturnTypeWithoutYield' - ); + $file->addError( + sprintf( + 'Return type is declared "%s" but incompatible return statement(s) found', + $checkNull ? 'null' : 'void' + ), + $position, + $checkNull ? 'IncorrectNullReturnType' : 'IncorrectVoidReturnType' + ); + } - return; - } + /** + * @param File $file + * @param int $position + * @param list $returnTypes + * @param array $returnInfo + * @return bool + */ + private function checkIncorrectVoid( + File $file, + int $position, + array $returnTypes, + array $returnInfo + ): bool { - $returnType = $this->returnTypeContent($file, $position); - if (in_array($returnType, ['Traversable', 'Iterator', 'iterable'], true)) { - return; - } + $hasReturnNull = $returnInfo['null'] > 0; - if (!$nonVoidReturnCount) { - $file->addWarning( - 'Found a function that yield values but missing compatible return type.', + if ( + ($hasReturnNull && !in_array('null', $returnTypes, true)) + || (!in_array('void', $returnTypes, true) && ($returnInfo['void'] > 0)) + ) { + $file->addError( + sprintf( + 'Return type %s but %s found', + $hasReturnNull ? 'is not nullable' : 'contains no void', + $hasReturnNull ? 'return null' : 'void return', + ), $position, - 'NoGeneratorReturnType' + $hasReturnNull ? 'IncorrectNullReturn' : 'IncorrectVoidReturn' ); - return; + return true; } - $file->addError( - 'Found a function that yield values using a return type incompatible with Generator.', - $position, - 'IncorrectReturnTypeForGenerator' - ); + return false; } /** * @param File $file - * @param int $functionPosition - * @return string + * @param int $position + * @param list $returnTypes + * @param array $returnInfo + * @return bool */ - private function returnTypeContent(File $file, int $functionPosition): string - { - $info = $file->getMethodProperties($functionPosition); - if (array_key_exists('return_type', $info) && is_string($info['return_type'])) { - return ltrim($info['return_type'], '\\'); + private function checkMissingReturn( + File $file, + int $position, + array $returnTypes, + array $returnInfo + ): bool { + + $nonEmptyTypes = array_diff($returnTypes, ['void', 'null', 'never']); + if ($nonEmptyTypes !== $returnTypes) { + return false; } - /** @var array> $tokens */ - $tokens = $file->getTokens(); - $returnTypeToken = $file->findNext( - [T_RETURN_TYPE], - $functionPosition + 3, // 3: open parenthesis, close parenthesis, colon - (int)($tokens[$functionPosition]['scope_opener'] ?? 0) - 1 - ); + $hasNull = $returnInfo['null'] > 0; + $hasVoid = $returnInfo['void'] > 0; + + if ($hasNull || $hasVoid) { + $file->addError( + sprintf( + 'Non-empty return type declared, but %s return found', + $hasNull ? ($hasVoid ? 'empty' : 'null') : 'empty' + ), + $position, + $hasNull ? 'IncorrectNullReturn' : 'IncorrectVoidReturn' + ); - $returnType = $tokens[$returnTypeToken] ?? null; - if (!$returnType || $returnType['code'] !== T_RETURN_TYPE) { - return ''; + return true; } - return ltrim((string)($returnType['content'] ?? ''), '\\'); + return false; } /** * @param File $file - * @param int $functionPosition - * @return array{bool, bool, bool, bool, bool} + * @param int $position + * @param list $returnTypes + * @param array $returnInfo + * @return bool */ - private function returnTypeInfo(File $file, int $functionPosition): array - { - /** @var array> $tokens */ - $tokens = $file->getTokens(); + private function checkInvalidGenerator( + File $file, + int $position, + array $returnTypes, + array $returnInfo + ): bool { + + $hasGenerator = false; + while (!$hasGenerator && $returnTypes) { + $returnType = explode('&', rtrim(ltrim(array_shift($returnTypes), '('), ')')); + $hasGenerator = in_array('Generator', $returnType, true) + || in_array('\Generator', $returnType, true) + || in_array('Traversable', $returnType, true) + || in_array('\Traversable', $returnType, true) + || in_array('Iterator', $returnType, true) + || in_array('\Iterator', $returnType, true) + || in_array('iterable', $returnType, true); + } - $returnTypeContent = $this->returnTypeContent($file, $functionPosition); + $yieldCount = Functions::countYieldInBody($file, $position); - if (!$returnTypeContent) { - return [false, false, true, false, false]; - } + $return = false; - $start = (int)($tokens[$functionPosition]['parenthesis_closer']) + 1; - $end = (int)($tokens[$functionPosition]['scope_opener']); - $hasNullable = false; - for ($i = $start; $i < $end; $i++) { - if ($tokens[$i]['code'] === T_NULLABLE) { - $hasNullable = true; - break; + if ($hasGenerator || ($yieldCount > 0)) { + if ($returnInfo['total'] > 1) { + $file->addError( + 'A function returning a Generator should only contain a single return point.', + $position, + 'InvalidGeneratorManyReturns' + ); } + $return = true; } - $hasNonVoidReturnType = $returnTypeContent !== 'void'; - $hasVoidReturnType = $returnTypeContent === 'void'; - $returnsGenerator = $returnTypeContent === 'Generator'; - - return [$hasNonVoidReturnType, $hasVoidReturnType, false, $hasNullable, $returnsGenerator]; - } + if ($hasGenerator && ($yieldCount === 0)) { + $file->addError( + 'Return type contains "Generator" but no yield found in the function body', + $position, + 'GeneratorReturnTypeWithoutYield' + ); - /** - * @param File $file - * @param int $functionPosition - * @return array{'mixed': bool, 'null': bool} - */ - private function hasReturnNullOrMixedDocBloc(File $file, int $functionPosition): array - { - $return = PhpcsHelpers::functionDocBlockTag('@return', $file, $functionPosition); - if (!$return) { - return ['mixed' => false, 'null' => false]; + return true; } - $returnContentParts = preg_split('~\s+~', reset($return), PREG_SPLIT_NO_EMPTY); - if (!$returnContentParts) { - return ['mixed' => false, 'null' => false]; - } + if (!$hasGenerator && ($yieldCount > 0)) { + $file->addError( + 'Return type does not contain "Generator" but yield found in the function body', + $position, + 'NoGeneratorReturnType' + ); - $returnTypes = array_map('strtolower', explode('|', reset($returnContentParts))); - $returnTypes = array_map('trim', $returnTypes); - $returnTypesCount = count($returnTypes); - // Only if 1 or 2 types - if ($returnTypesCount !== 1 && $returnTypesCount !== 2) { - return ['mixed' => false, 'null' => false]; + return true; } - return [ - 'mixed' => in_array('mixed', $returnTypes, true), - 'null' => in_array('null', $returnTypes, true), - ]; + return $return; } /** + * @param File $file + * @param int $position * @return bool */ - private function areNullableReturnTypesSupported(): bool + private function checkMissingGeneratorReturnType(File $file, int $position): bool { - $min = PhpcsHelpers::minPhpTestVersion(); - - return $min && version_compare($min, '7.1', '>='); - } + $yield = Functions::countYieldInBody($file, $position); + if ($yield > 0) { + $file->addError( + 'Return type does not contain "Generator" but yield found in the function body', + $position, + 'NoGeneratorReturnType' + ); - /** - * @param int $functionStart - * @param int $functionEnd - * @param File $file - * @return int - */ - private function countYield(int $functionStart, int $functionEnd, File $file): int - { - $count = 0; - /** @var array> $tokens */ - $tokens = $file->getTokens(); - for ($i = ($functionStart + 1); $i < $functionEnd; $i++) { - if ($tokens[$i]['code'] === T_CLOSURE) { - $i = (int)($tokens[$i]['scope_closer'] ?? -1); - continue; - } - if ($tokens[$i]['code'] === T_YIELD || $tokens[$i]['code'] === T_YIELD_FROM) { - $count++; - } + return true; } - return $count; + return false; } } diff --git a/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php b/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php index e9d62bd..e763802 100644 --- a/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php @@ -1,10 +1,35 @@ getTokens(); while (!$thisFound && ($i < $functionEnd)) { $token = $tokens[$i]; - $thisFound = ($token['code'] === T_VARIABLE) && ($token['content'] === '$this'); + $thisFound = (($token['code'] === T_VARIABLE) && ($token['content'] === '$this')) + || ( + in_array($token['code'], [T_DOUBLE_QUOTED_STRING, T_HEREDOC], true) + && (strpos($token['content'], '$this->') !== false) + ); $i++; } @@ -54,12 +83,12 @@ public function process(File $phpcsFile, $stackPtr): void return; } - $boundDoc = PhpcsHelpers::functionDocBlockTag('@bound', $phpcsFile, $stackPtr); + $boundDoc = FunctionDocBlock::tag('@bound', $phpcsFile, $stackPtr); if ($boundDoc) { return; } - $varDoc = PhpcsHelpers::functionDocBlockTag('@var', $phpcsFile, $stackPtr); + $varDoc = FunctionDocBlock::tag('@var', $phpcsFile, $stackPtr); foreach ($varDoc as $content) { if (preg_match('~(?:^|\s+)\$this(?:$|\s+)~', $content)) { return; @@ -79,7 +108,7 @@ public function process(File $phpcsFile, $stackPtr): void * @param File $file * @return void */ - private function fix(int $position, File $file) + private function fix(int $position, File $file): void { $fixer = $file->fixer; $fixer->beginChangeset(); diff --git a/Inpsyde/Sniffs/CodeQuality/VariablesNameSniff.php b/Inpsyde/Sniffs/CodeQuality/VariablesNameSniff.php index dbe8502..88a4945 100644 --- a/Inpsyde/Sniffs/CodeQuality/VariablesNameSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/VariablesNameSniff.php @@ -1,12 +1,36 @@ arePropertiesIgnored()) diff --git a/Inpsyde/ruleset.xml b/Inpsyde/ruleset.xml index d51b281..c7ba5fd 100644 --- a/Inpsyde/ruleset.xml +++ b/Inpsyde/ruleset.xml @@ -3,8 +3,6 @@ PHP 7+ coding standards for Inpsyde WordPress projects. - ./PhpcsHelpers.php - + warning @@ -128,6 +130,7 @@ + warning @@ -136,7 +139,9 @@ - + + warning + @@ -149,6 +154,7 @@ + warning @@ -161,6 +167,9 @@ + + warning + @@ -168,6 +177,7 @@ + warning diff --git a/LICENSE b/LICENSE index 49dba23..2fd7a20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Inpsyde GmbH +Copyright (c) 2023 Inpsyde GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 0b63cc2..29971d7 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,11 @@ "phpunit/phpunit": "^9.6.11", "vimeo/psalm": "^5.15.0" }, + "autoload": { + "psr-4": { + "Inpsyde\\Helpers\\": "Inpsyde/Helpers/" + } + }, "autoload-dev": { "psr-4": { "Inpsyde\\CodingStandard\\Tests\\": [ diff --git a/phpcs.xml b/phpcs.xml index 0cfa7e3..deef883 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -3,8 +3,8 @@ PHP 7+ coding standards for Inpsyde WordPress projects. + ./Inpsyde/Helpers ./Inpsyde/Sniffs - ./Inpsyde/PhpcsHelpers.php ./tests/src ./tests/cases @@ -23,4 +23,8 @@ + + ./tests/cases/ + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d997102..826c5fa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ tests/cases/FixturesTest.php - tests/cases/PhpcsHelpersTest.php + tests/cases/Helpers diff --git a/tests/autoload.php b/tests/autoload.php index 3b6fc50..93af35c 100644 --- a/tests/autoload.php +++ b/tests/autoload.php @@ -1,5 +1,31 @@ factoryFile($php); + + $oneFuncPos = $file->findNext(T_FUNCTION, 1); + $twoFuncPos = $file->findNext(T_CLOSURE, $oneFuncPos + 1); + + $tagsOne = FunctionDocBlock::allTags($file, $oneFuncPos); + $tagsTwo = FunctionDocBlock::allTags($file, $twoFuncPos); + + static::assertSame( + [ + '@param' => ['?string $foo', 'bool $bool'], + '@return' => ['string'], + ], + $tagsOne + ); + + static::assertSame( + [ + '@param' => ['string $foo'], + '@return' => ['string'], + '@custom' => ['Hello World'], + '@wp-hook' => [''], + ], + $tagsTwo + ); + } + + /** + * @test + */ + public function testTag(): void + { + $php = <<<'PHP' +factoryFile($php); + + $oneFuncPos = $file->findNext(T_FUNCTION, 1); + $twoFuncPos = $file->findNext(T_FUNCTION, $oneFuncPos + 1); + $threeFuncPos = $file->findNext(T_FUNCTION, $twoFuncPos + 1); + + static::assertSame('one', $file->getDeclarationName($oneFuncPos)); + static::assertSame('two', $file->getDeclarationName($twoFuncPos)); + static::assertSame('three', $file->getDeclarationName($threeFuncPos)); + + $oneCustomFull = FunctionDocBlock::tag('customFull', $file, $oneFuncPos); + $oneCustomEmpty = FunctionDocBlock::tag('customEmpty', $file, $oneFuncPos); + + $twoCustomFull = FunctionDocBlock::tag('customFull', $file, $twoFuncPos); + $twoCustomEmpty = FunctionDocBlock::tag('customEmpty', $file, $twoFuncPos); + + $threeCustomFull = FunctionDocBlock::tag('customFull', $file, $threeFuncPos); + $threeCustomEmpty = FunctionDocBlock::tag('customEmpty', $file, $threeFuncPos); + + static::assertSame(["Hello There Foo\nnext line"], $oneCustomFull); + static::assertSame([''], $oneCustomEmpty); + + static::assertSame([], $twoCustomFull); + static::assertSame([], $twoCustomEmpty); + + static::assertSame(['Third', 'Third Again'], $threeCustomFull); + static::assertSame(['', ''], $threeCustomEmpty); + } + + /** + * @test + */ + public function testAllParamTypes(): void + { + $php = <<<'PHP' +factoryFile($php); + + $oneFuncPos = $file->findNext(T_FUNCTION, 1); + $twoFuncPos = $file->findNext(T_CLOSURE, $oneFuncPos + 1); + + $paramsOne = FunctionDocBlock::allParamTypes($file, $oneFuncPos); + $paramsTwo = FunctionDocBlock::allParamTypes($file, $twoFuncPos); + + static::assertSame( + [ + '$foo' => ['string'], + '$bar' => ['\SomeClass', 'int', 'string'], + ], + $paramsOne + ); + + static::assertSame( + [ + '$foo' => ['\SomeClass', 'int', 'string', 'null'], + '$bar' => ['int', 'string', 'null'], + ], + $paramsTwo + ); + } +} diff --git a/tests/cases/Helpers/FunctionReturnStatementTest.php b/tests/cases/Helpers/FunctionReturnStatementTest.php new file mode 100644 index 0000000..9108a01 --- /dev/null +++ b/tests/cases/Helpers/FunctionReturnStatementTest.php @@ -0,0 +1,128 @@ +something(1); + } + + return true; + } +} +PHP; + + $file = $this->factoryFile($php); + + $functionPos = $file->findNext(T_FUNCTION, 1); + $info = FunctionReturnStatement::allInfo($file, $functionPos); + + static::assertSame('countInfo', $file->getDeclarationName($functionPos)); + static::assertSame(['nonEmpty' => 2, 'void' => 1, 'null' => 1, 'total' => 4], $info); + } + + /** + * @test + */ + public function testAllInfoShortForClosure(): void + { + $php = <<<'PHP' + true; +fn () => null; +fn () => 'x'; +PHP; + + $file = $this->factoryFile($php); + + $fn1Pos = $file->findNext(T_FN, 1); + $fn2Pos = $file->findNext(T_FN, $fn1Pos + 1); + $fn3Pos = $file->findNext(T_FN, $fn2Pos + 1); + $info1 = FunctionReturnStatement::allInfo($file, $fn1Pos); + $info2 = FunctionReturnStatement::allInfo($file, $fn2Pos); + $info3 = FunctionReturnStatement::allInfo($file, $fn3Pos); + + static::assertSame(['nonEmpty' => 1, 'void' => 0, 'null' => 0, 'total' => 1], $info1); + static::assertSame(['nonEmpty' => 0, 'void' => 0, 'null' => 1, 'total' => 1], $info2); + static::assertSame(['nonEmpty' => 1, 'void' => 0, 'null' => 0, 'total' => 1], $info3); + } +} diff --git a/tests/cases/Helpers/FunctionsTest.php b/tests/cases/Helpers/FunctionsTest.php new file mode 100644 index 0000000..fd02264 --- /dev/null +++ b/tests/cases/Helpers/FunctionsTest.php @@ -0,0 +1,168 @@ +x(); +// four: `y()` +$y = (new Test())->y(); +// five: `$y` +$y(); + +// six: `require` +require('foo.php'); + +PHP; + + $file = $this->factoryFile($php); + $tokens = $file->getTokens(); + + $functionCallContents = []; + foreach ($tokens as $pos => $token) { + if (Functions::looksLikeFunctionCall($file, $pos)) { + $functionCallContents[] = $token['content']; + } + } + + static::assertSame(['x', 'sprintf', 'x', 'y', '$y', 'require'], $functionCallContents); + } + + /** + * @test + */ + public function testIsUntypedPsrMethodWithClass(): void + { + $php = <<<'PHP' +data[$id] ?? null; + } + + public function has($id) + { + return isset($this->data[$id]); + } +} +PHP; + $file = $this->factoryFile($php); + + $getFunc = $file->findNext(T_FUNCTION, 1); + $hasFunc = $file->findNext(T_FUNCTION, $getFunc + 2); + + static::assertSame('get', $file->getDeclarationName($getFunc)); + static::assertSame('has', $file->getDeclarationName($hasFunc)); + + $isPsrGet = Functions::isPsrMethod($file, $getFunc); + $isPsrHas = Functions::isPsrMethod($file, $hasFunc); + + static::assertTrue($isPsrGet); + static::assertTrue($isPsrHas); + } + + /** + * @test + */ + public function testIsUntypedPsrMethodWithAnonClass(): void + { + $php = <<<'PHP' +data[$id] ?? null; + } + + public function has($id) + { + return isset($this->data[$id]); + } +}; +PHP; + $file = $this->factoryFile($php); + + $getFunc = $file->findNext(T_FUNCTION, 1); + $hasFunc = $file->findNext(T_FUNCTION, $getFunc + 1); + + static::assertSame('get', $file->getDeclarationName($getFunc)); + static::assertSame('has', $file->getDeclarationName($hasFunc)); + + $isPsrGet = Functions::isPsrMethod($file, $getFunc); + $isPsrHas = Functions::isPsrMethod($file, $hasFunc); + + static::assertTrue($isPsrGet); + static::assertTrue($isPsrHas); + } +} diff --git a/tests/cases/Helpers/MiscTest.php b/tests/cases/Helpers/MiscTest.php new file mode 100644 index 0000000..32f6104 --- /dev/null +++ b/tests/cases/Helpers/MiscTest.php @@ -0,0 +1,115 @@ +factoryFile('factoryFile($php); + + $tokens = $file->getTokens(); + $start = $file->findNext(T_FUNCTION, 1); + $end = count($tokens) - 1; + + $actual = Misc::tokensSubsetToString($start, $end, $file, [], true); + $expected = <<<'PHP' +function x(): string { + return ("foo + bar"); +} +PHP; + static::assertSame($expected, $actual); + } + + /** + * @test + */ + public function tokensSubsetToStringExclude(): void + { + $php = <<<'PHP' +factoryFile($php); + + $tokens = $file->getTokens(); + $start = $file->findNext(T_FUNCTION, 1); + $end = count($tokens) - 1; + $exclude = array_keys(Tokens::$emptyTokens); + $exclude[] = T_RETURN; + + $actual = Misc::tokensSubsetToString($start + 1, $end, $file, $exclude, true); + $expected = 'x():string{("foo + bar");}'; + + static::assertSame($expected, $actual); + } +} diff --git a/tests/cases/Helpers/NamesTest.php b/tests/cases/Helpers/NamesTest.php new file mode 100644 index 0000000..6464785 --- /dev/null +++ b/tests/cases/Helpers/NamesTest.php @@ -0,0 +1,93 @@ +l()); + $s = 's'; +} + +namespace t { + +} +PHP; + $file = $this->factoryFile($php); + $tokens = $file->getTokens(); + + $names = []; + foreach ($tokens as $pos => $token) { + $name = Names::nameableTokenName($file, $pos); + $name and $names[] = $name; + } + + static::assertSame(range('a', 't'), $names); + } +} diff --git a/tests/cases/Helpers/ObjectsTest.php b/tests/cases/Helpers/ObjectsTest.php new file mode 100644 index 0000000..ca1fdb5 --- /dev/null +++ b/tests/cases/Helpers/ObjectsTest.php @@ -0,0 +1,118 @@ +factoryFile($php); + $class1 = $file->findNext(T_CLASS, 0); + $class2 = $file->findNext(T_CLASS, $class1 + 1); + + $names1 = Objects::allInterfacesFullyQualifiedNames($file, $class1); + $names2 = Objects::allInterfacesFullyQualifiedNames($file, $class2); + + static::assertSame( + ['\\Foo\\Bar', '\\X', '\Y\Y', '\\X\\Partial\\Sub', '\\Y\\Full'], + $names1 + ); + + static::assertSame( + ['\\Foo\\Bar', '\\X', '\Y\Y', '\\X\\Partial\\Sub', '\\Y\\Full'], + $names2 + ); + } + + /** + * @test + */ + public function testCountProperties(): void + { + $php = <<<'PHP' +var1 = $bar; + + return new class() { + static private $x1; + public static $x2; + static $x3; + var $x4; + }; + } + + static private readonly Test $var6; + + function foo($foo, int $bar) { + var $x4; + } + + var $var7; +} +PHP; + + $file = $this->factoryFile($php); + + $classPos = $file->findNext(T_CLASS, 1); + static::assertSame(7, Objects::countProperties($file, $classPos)); + } +} diff --git a/tests/cases/Helpers/WpHooksTest.php b/tests/cases/Helpers/WpHooksTest.php new file mode 100644 index 0000000..1aa080c --- /dev/null +++ b/tests/cases/Helpers/WpHooksTest.php @@ -0,0 +1,83 @@ + 'find me short!'; +); + +add_action('x', '__return_false'); + +function theHookPrefix() { + return 'x_'; +} + +add_action /* x */ (theHookPrefix() . 'xx', + static + function /* add_action('x', function () {}) */ + () { + return 'find me!'; + } +); + +function add_action($x, $y) { + return function () { + return function() { + add_action('x', 'theHookPrefix'); + }; + }; +} +PHP; + + $file = $this->factoryFile($php); + $tokens = $file->getTokens(); + + $bodies = []; + foreach ($tokens as $pos => $token) { + $isHookClosure = WpHooks::isHookClosure($file, $pos); + $isHookClosure and $bodies[] = trim(Functions::bodyContent($file, $pos)); + } + + static::assertSame(["'find me short!'", "return 'find me!';"], $bodies); + } +} diff --git a/tests/cases/PhpcsHelpersTest.php b/tests/cases/PhpcsHelpersTest.php deleted file mode 100644 index bbdc19e..0000000 --- a/tests/cases/PhpcsHelpersTest.php +++ /dev/null @@ -1,598 +0,0 @@ -var1 = $bar; - - return new class() { - static private $var7; - public static $var8; - static $var9; - var $var10; - }; - } -} -PHP; - - $file = $this->factoryFile($php); - $tokens = $file->getTokens(); - - $classPos = $file->findNext(T_CLASS, 1); - $classLine = $tokens[$classPos]['line']; - $list = PhpcsHelpers::allPropertiesTokenPositions($file, $classPos); - - $actualNames = []; - $propLine = $classLine; - foreach ($list as $propPos) { - $propLine++; - $actualNames[$propLine] = $tokens[$propPos]['content']; - } - - $expectedNames = [ - ($classLine + 1) => '$var1', - ($classLine + 2) => '$var2', - ($classLine + 3) => '$var3', - ($classLine + 4) => '$var4', - ($classLine + 5) => '$var5', - ]; - - static::assertTrue(is_int($classPos) && $classPos > 0); - static::assertSame('Test', $file->getDeclarationName($classPos)); - static::assertSame($expectedNames, $actualNames); - } - - /** - * @test - */ - public function testClassMethods(): void - { - $php = <<<'PHP' -var1 = $bar; - } -} -class Two { - use One; - private function methodTwo($foo, int $bar) { - return new class() { - public function methodThree() { - return function() { - return function() { - }; - }; - } - }; - } -} -function test() { - return function () { - return function() { - return new Two(); - }; - }; -} -function testTest() { - return (test()()())->var1; -} -PHP; - - $file = $this->factoryFile($php); - $tokens = $file->getTokens(); - - $methodNames = []; - foreach ($tokens as $pos => $token) { - if (PhpcsHelpers::functionIsMethod($file, $pos)) { - $methodNames[] = $file->getDeclarationName($pos); - } - } - - static::assertSame(['methodOne', 'methodTwo', 'methodThree'], $methodNames); - } - - /** - * @test - */ - public function testFunctionCall(): void - { - $php = <<<'PHP' -x(); -// four: `y()` -$y = (new Test())->y(); -// five: `$y` -$y(); - -PHP; - - $file = $this->factoryFile($php); - $tokens = $file->getTokens(); - - $functionCallContents = []; - foreach ($tokens as $pos => $token) { - if (PhpcsHelpers::looksLikeFunctionCall($file, $pos)) { - $functionCallContents[] = $tokens[$pos]['content']; - } - } - - static::assertSame(['x', 'sprintf', 'x', 'y', '$y'], $functionCallContents); - } - - /** - * @test - */ - public function testTokenName(): void - { - $php = <<<'PHP' -l()); -PHP; - - $file = $this->factoryFile($php); - $tokens = $file->getTokens(); - - $names = []; - foreach ($tokens as $pos => $token) { - $name = PhpcsHelpers::tokenName($file, $pos); - $name and $names[] = $name; - } - - static::assertSame(range('a', 'l'), $names); - } - - /** - * @test - */ - public function testHookClosure(): void - { - $php = <<<'PHP' -factoryFile($php); - $tokens = $file->getTokens(); - - $bodies = []; - foreach ($tokens as $pos => $token) { - $isHookClosure = PhpcsHelpers::isHookClosure($file, $pos); - $isHookClosure and $bodies[] = trim(PhpcsHelpers::functionBody($file, $pos)); - } - - static::assertSame(["return 'find me!';"], $bodies); - } - - /** - * @test - */ - public function testReturnCount(): void - { - $php = <<<'PHP' -something(1); - } - - return true; - } -} -PHP; - - $file = $this->factoryFile($php); - - $functionPos = $file->findNext(T_FUNCTION, 1); - $info = PhpcsHelpers::returnsCountInfo($file, $functionPos); - - static::assertSame('countInfo', $file->getDeclarationName($functionPos)); - static::assertSame(['nonEmpty' => 2, 'void' => 1, 'null' => 1, 'total' => 4], $info); - } - - /** - * @test - */ - public function testDocBlocTag() - { - $php = <<<'PHP' -factoryFile($php); - - $oneFuncPos = $file->findNext(T_FUNCTION, 1); - $twoFuncPos = $file->findNext(T_FUNCTION, $oneFuncPos + 1); - $threeFuncPos = $file->findNext(T_FUNCTION, $twoFuncPos + 1); - - static::assertSame('one', $file->getDeclarationName($oneFuncPos)); - static::assertSame('two', $file->getDeclarationName($twoFuncPos)); - static::assertSame('three', $file->getDeclarationName($threeFuncPos)); - - $oneCustomFull = PhpcsHelpers::functionDocBlockTag('customFull', $file, $oneFuncPos); - $oneCustomEmpty = PhpcsHelpers::functionDocBlockTag('customEmpty', $file, $oneFuncPos); - - $twoCustomFull = PhpcsHelpers::functionDocBlockTag('customFull', $file, $twoFuncPos); - $twoCustomEmpty = PhpcsHelpers::functionDocBlockTag('customEmpty', $file, $twoFuncPos); - - $threeCustomFull = PhpcsHelpers::functionDocBlockTag('customFull', $file, $threeFuncPos); - $threeCustomEmpty = PhpcsHelpers::functionDocBlockTag('customEmpty', $file, $threeFuncPos); - - static::assertSame(["Hello There Foo\nnext line"], $oneCustomFull); - static::assertSame([''], $oneCustomEmpty); - - static::assertSame([], $twoCustomFull); - static::assertSame([], $twoCustomEmpty); - - static::assertSame(['Third', 'Third Again'], $threeCustomFull); - static::assertSame(['', ''], $threeCustomEmpty); - } - - /** - * @test - */ - public function testDocBlocTags() - { - $php = <<<'PHP' -factoryFile($php); - - $oneFuncPos = $file->findNext(T_FUNCTION, 1); - $twoFuncPos = $file->findNext(T_CLOSURE, $oneFuncPos + 1); - - $tagsOne = PhpcsHelpers::functionDocBlockTags($file, $oneFuncPos); - $tagsTwo = PhpcsHelpers::functionDocBlockTags($file, $twoFuncPos); - - static::assertSame( - [ - '@param' => ['string $foo', 'bool $bool'], - '@return' => ['string'], - ], - $tagsOne - ); - - static::assertSame( - [ - '@param' => ['string $foo'], - '@return' => ['string'], - '@custom' => ['Hello World'], - '@wp-hook' => [''], - ], - $tagsTwo - ); - } - - /** - * @test - */ - public function testParamTypes(): void - { - $php = <<<'PHP' -factoryFile($php); - - $oneFuncPos = $file->findNext(T_FUNCTION, 1); - $twoFuncPos = $file->findNext(T_CLOSURE, $oneFuncPos + 1); - - $paramsOne = PhpcsHelpers::functionDocBlockParamTypes($file, $oneFuncPos); - $paramsTwo = PhpcsHelpers::functionDocBlockParamTypes($file, $twoFuncPos); - - static::assertSame( - [ - '$foo' => ['string'], - '$bar' => ['string', 'int', '\SomeClass'], - ], - $paramsOne - ); - - static::assertSame( - ['$foo' => ['string', 'int', '\SomeClass']], - $paramsTwo - ); - } - - /** - * @test - */ - public function testIsUntypedPsrMethodWithClass(): void - { - $php = <<<'PHP' -data[$id] ?? null; - } - - public function has($id) - { - return isset($this->data[$id]); - } -} -PHP; - $file = $this->factoryFile($php); - - $getFunc = $file->findNext(T_FUNCTION, 1); - $hasFunc = $file->findNext(T_FUNCTION, $getFunc + 1); - - static::assertSame('get', $file->getDeclarationName($getFunc)); - static::assertSame('has', $file->getDeclarationName($hasFunc)); - - $isPsrGet = PhpcsHelpers::isUntypedPsrMethod($file, $getFunc); - $isPsrHas = PhpcsHelpers::isUntypedPsrMethod($file, $hasFunc); - - static::assertTrue($isPsrGet); - static::assertTrue($isPsrHas); - } - - /** - * @test - */ - public function testIsUntypedPsrMethodWithAnonClass(): void - { - $php = <<<'PHP' -data[$id] ?? null; - } - - public function has($id) - { - return isset($this->data[$id]); - } -}; -PHP; - $file = $this->factoryFile($php); - - $getFunc = $file->findNext(T_FUNCTION, 1); - $hasFunc = $file->findNext(T_FUNCTION, $getFunc + 1); - - static::assertSame('get', $file->getDeclarationName($getFunc)); - static::assertSame('has', $file->getDeclarationName($hasFunc)); - - $isPsrGet = PhpcsHelpers::isUntypedPsrMethod($file, $getFunc); - $isPsrHas = PhpcsHelpers::isUntypedPsrMethod($file, $hasFunc); - - static::assertTrue($isPsrGet); - static::assertTrue($isPsrHas); - } - - /** - * @param string $content - * @return File - */ - private function factoryFile(string $content): File - { - $config = new Config(); - $config->standards = []; - $config->extensions = ['php' => 'PHP']; - $config->dieOnUnknownArg = false; - $config->setCommandLineValues([]); - /** @var Ruleset $ruleset */ - $ruleset = (new \ReflectionClass(Ruleset::class))->newInstanceWithoutConstructor(); - - $file = new DummyFile($content, $ruleset, $config); - $file->parse(); - - return $file; - } -} diff --git a/tests/fixtures/argument-type-declaration.php b/tests/fixtures/argument-type-declaration.php index 1410ec2..499be2b 100644 --- a/tests/fixtures/argument-type-declaration.php +++ b/tests/fixtures/argument-type-declaration.php @@ -1,6 +1,8 @@ 2) { @@ -28,6 +42,7 @@ function c(): void return true; } +// @phpcsWarningCodeOnNextLine NoReturnType function aa($foo) { return; @@ -43,7 +58,7 @@ function d($foo): void return; } -// @phpcsErrorCodeOnNextLine MissingReturn +// @phpcsErrorCodeOnNextLine IncorrectVoidReturn function e(): bool { if (true) { @@ -78,10 +93,15 @@ function h(): void return null === true; } +// @phpcsErrorCodeOnNextLine IncorrectVoidReturnType function hh(): void { return null; } +function hhh(): void { + return; +} + function hhComment(): void { return /* I return void */ ; } @@ -96,7 +116,7 @@ function gen(string $content): \Generator } // @phpcsErrorCodeOnNextLine GeneratorReturnTypeWithoutYield -function genNoYield(string $content): \Generator +function genNoYield1(string $content): \Generator { $line = strtok($content, "\n"); while ($line !== false) { @@ -107,8 +127,30 @@ function genNoYield(string $content): \Generator return true; } -// @phpcsWarningCodeOnNextLine NoGeneratorReturnType -function yieldNoGen(string $content) +// @phpcsErrorCodeOnNextLine GeneratorReturnTypeWithoutYield +function genNoYield2(string $content): (Generator&Countable)|bool +{ + $line = strtok($content, "\n"); + while ($line !== false) { + $line = strtok("\n"); + is_string($line) ? trim($line) : ''; + } + + return true; +} + +// @phpcsErrorCodeOnNextLine NoGeneratorReturnType +function yieldNoGen1(string $content): Foo +{ + $line = strtok($content, "\n"); + while ($line !== false) { + $line = strtok("\n"); + yield is_string($line) ? trim($line) : ''; + } +} + +// @phpcsErrorCodeOnNextLine NoGeneratorReturnType +function yieldNoGen2(string $content) { $line = strtok($content, "\n"); while ($line !== false) { @@ -117,7 +159,7 @@ function yieldNoGen(string $content) } } -// @phpcsErrorCodeOnNextLine IncorrectReturnTypeForGenerator +// @phpcsErrorCodeOnNextLine NoGeneratorReturnType function yieldWrongReturn(string $content): int { $line = strtok($content, "\n"); @@ -160,7 +202,7 @@ function genFrom(): \Generator yield from $data; } -// @phpcsWarningCodeOnNextLine InvalidGeneratorManyReturns +// @phpcsErrorCodeOnNextLine InvalidGeneratorManyReturns function genMultiReturn(): \Generator { if (defined('MEH_MEH')) { @@ -176,24 +218,12 @@ function genMultiReturn(): \Generator return 2; } -add_filter('x', function () { - return ''; -}); - -add_filter('x', function (): string { - return ''; -}); - -add_filter('x', static function () { - return ''; -}); - // @phpcsErrorCodeOnNextLine IncorrectVoidReturnType add_filter('x', function (): void { return '0'; }); -// @phpcsErrorCodeOnNextLine MissingReturn +// @phpcsErrorCodeOnNextLine IncorrectVoidReturn add_filter('x', function (): string { return; }); @@ -205,7 +235,6 @@ function genMultiReturn(): \Generator function filter_wrapper(): bool { - // @phpcsWarningCodeOnNextLine NoReturnType foo('x', function () { return ''; @@ -235,7 +264,7 @@ function hookCallback() * @return bool * @wp-hook Meh */ -function badHookCallback(): bool // @phpcsErrorCodeOnThisLine MissingReturn +function badHookCallback(): bool // @phpcsErrorCodeOnThisLine IncorrectVoidReturn { return; } @@ -249,7 +278,7 @@ function noHookCallback() // @phpcsWarningCodeOnThisLine NoReturnType } /** - * @return mixed|null + * @return mixed */ function mixed() { return $GLOBALS['thing'] ?? null; @@ -275,6 +304,7 @@ public function filterWrapper(string $x, int $y): bool return true; } + // @phpcsWarningCodeOnNextLine NoReturnType public function register() { add_filter('foo_bar', function (array $a): array { @@ -374,7 +404,7 @@ public function offsetUnset($offset) /** * @return \ArrayAccess|null */ - public function iMaybeReturnNull() + public function iMaybeReturnNull() // @phpcsWarningOnThisLine { if (rand(1, 4) === 3) { return null; @@ -390,7 +420,7 @@ public function iMaybeReturnNull() /** * @return \ArrayAccess|null */ - public function iShouldReturnNullButReturnVoid() + public function iShouldReturnNullButReturnVoid() // @phpcsWarningOnThisLine { if (rand(1, 4) === 3) { return null; @@ -406,7 +436,7 @@ public function iShouldReturnNullButReturnVoid() /** * @return \ArrayAccess|null */ - public function iShouldReturnNull() + public function iShouldReturnNull() // @phpcsWarningOnThisLine { return new \ArrayObject(); } @@ -414,7 +444,7 @@ public function iShouldReturnNull() /** * @return \ArrayAccess|null|\ArrayObject */ - public function iReturnALotOfStuff() // @phpcsWarningCodeOnThisLine NoReturnType + public function iReturnALotOfStuff() { if (rand(1, 4) > 2) { return null; @@ -423,7 +453,7 @@ public function iReturnALotOfStuff() // @phpcsWarningCodeOnThisLine NoReturnType return new \ArrayObject(); } - // @phpcsErrorCodeOnNextLine IncorrectVoidReturn + // @phpcsErrorCodeOnNextLine IncorrectNullReturn public function iReturnWrongNull(): \ArrayAccess { if (rand(1, 4) > 2) { @@ -452,7 +482,7 @@ function returnTraversable(): Traversable } } -class Container implements \Psr\Container\ContainerInterface { +class Container implements PsrContainer { private $data = []; @@ -469,17 +499,17 @@ public function has($id) class JsonSerializeTest implements \JsonSerializable, \Serializable { - public function jsonSerialize() + public function jsonSerialize(): string { return ''; } - public function serialize() + public function serialize(): string { return ''; } - public function unserialize($serialized) + public function unserialize($serialized): void { } } diff --git a/tests/fixtures/static-closure.php b/tests/fixtures/static-closure.php index 3aa137c..d4b2fc3 100644 --- a/tests/fixtures/static-closure.php +++ b/tests/fixtures/static-closure.php @@ -96,3 +96,39 @@ function () { add_filter('x', fn() => 'y'); // @phpcsWarningOnThisLine add_filter('x', static fn() => 'y'); + +class Test { + + public static function new(string $basePath): Test + { + return new self($basePath); + } + + private function __construct(private string $basePath) + { + } + + public function foo(): array + { + return [ + 'a' => fn () => Test::new("{$this->basePath}/resources/templates"), + 'b' => fn () => Test::new("$this->basePath/resources/templates"), + 'c' => fn () => Test::new($this->basePath . "/resources/templates"), + ]; + } +} + +function () { + $value = <<x + PHP; + print $value; +}; + +// @phpcsWarningOnNextLine +function () { + $value = <<<'PHP' + $this->x + PHP; + print $value; +}; diff --git a/tests/src/FixtureContentParser.php b/tests/src/FixtureContentParser.php index 54b1dee..42de807 100644 --- a/tests/src/FixtureContentParser.php +++ b/tests/src/FixtureContentParser.php @@ -1,22 +1,33 @@ standards = []; + $config->extensions = ['php' => 'PHP']; + $config->setCommandLineValues([]); + /** @var Ruleset $ruleset */ + $ruleset = (new \ReflectionClass(Ruleset::class))->newInstanceWithoutConstructor(); + + $file = new DummyFile($content, $ruleset, $config); + $file->parse(); + + return $file; + } +} From 4f6cff180e2f8a3171df8446c659a895feffb409 Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Thu, 31 Aug 2023 12:19:45 +0200 Subject: [PATCH 2/5] Fix Psalm --- Inpsyde/Helpers/FunctionDocBlock.php | 6 +++--- Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Inpsyde/Helpers/FunctionDocBlock.php b/Inpsyde/Helpers/FunctionDocBlock.php index ac0b1fc..72749a9 100644 --- a/Inpsyde/Helpers/FunctionDocBlock.php +++ b/Inpsyde/Helpers/FunctionDocBlock.php @@ -170,10 +170,10 @@ public static function normalizeTypesString(string $typesString): array if (strpos($splitType, '&') !== false) { $splitType = rtrim(ltrim($splitType, '('), ')'); } elseif (strpos($splitType, '?') === 0) { - $splitType = substr($splitType, 1); - $hasNull = $splitType !== false; + $splitType = substr($splitType, 1) ?: ''; + $hasNull = $hasNull || ($splitType !== ''); } - if ($splitType === false) { + if (!$splitType) { continue; } if (strtolower($splitType) === 'null') { diff --git a/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php b/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php index e763802..16f7a6f 100644 --- a/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php @@ -71,10 +71,11 @@ public function process(File $phpcsFile, $stackPtr): void $tokens = $phpcsFile->getTokens(); while (!$thisFound && ($i < $functionEnd)) { $token = $tokens[$i]; - $thisFound = (($token['code'] === T_VARIABLE) && ($token['content'] === '$this')) + $content = (string)($token['content'] ?? ''); + $thisFound = (($token['code'] === T_VARIABLE) && ($content === '$this')) || ( in_array($token['code'], [T_DOUBLE_QUOTED_STRING, T_HEREDOC], true) - && (strpos($token['content'], '$this->') !== false) + && (strpos($content, '$this->') !== false) ); $i++; } From e3a34886bb736a8ad6971492b31c4928451100a6 Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Mon, 4 Sep 2023 20:25:29 +0200 Subject: [PATCH 3/5] Simplify condition in Inpsyde/Helpers/Names Co-authored-by: Thorsten Frommen Signed-off-by: Giuseppe Mazzapica --- Inpsyde/Helpers/Names.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Inpsyde/Helpers/Names.php b/Inpsyde/Helpers/Names.php index de18c67..6eca699 100644 --- a/Inpsyde/Helpers/Names.php +++ b/Inpsyde/Helpers/Names.php @@ -64,7 +64,9 @@ public static function nameableTokenName(File $file, int $position): ?string if (!in_array($code, self::NAMEABLE_TOKENS, true)) { return null; - } elseif ($code === T_VARIABLE) { + } + + if ($code === T_VARIABLE) { $name = ltrim((string)($tokens[$position]['content'] ?? ''), '$'); return ($name === '') ? null : $name; From 3aa3eaac0e8fdfbc216bd84dca25a8a0687ee321 Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Mon, 4 Sep 2023 20:49:13 +0200 Subject: [PATCH 4/5] Rename Inpsyde\Helpers to Inpsyde\CodingStandard\Helpers --- Inpsyde/Helpers/Boundaries.php | 2 +- Inpsyde/Helpers/FunctionDocBlock.php | 2 +- Inpsyde/Helpers/FunctionReturnStatement.php | 2 +- Inpsyde/Helpers/Functions.php | 2 +- Inpsyde/Helpers/Misc.php | 2 +- Inpsyde/Helpers/Names.php | 2 +- Inpsyde/Helpers/Objects.php | 2 +- Inpsyde/Helpers/WpHooks.php | 2 +- .../CodeQuality/ArgumentTypeDeclarationSniff.php | 6 +++--- .../CodeQuality/ElementNameMinimalLengthSniff.php | 2 +- Inpsyde/Sniffs/CodeQuality/HookClosureReturnSniff.php | 6 +++--- Inpsyde/Sniffs/CodeQuality/NoTopLevelDefineSniff.php | 2 +- .../Sniffs/CodeQuality/PropertyPerClassLimitSniff.php | 4 ++-- .../Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php | 10 +++++----- Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php | 4 ++-- composer.json | 2 +- tests/cases/Helpers/FunctionDocBlocTest.php | 2 +- tests/cases/Helpers/FunctionReturnStatementTest.php | 2 +- tests/cases/Helpers/FunctionsTest.php | 2 +- tests/cases/Helpers/MiscTest.php | 2 +- tests/cases/Helpers/NamesTest.php | 2 +- tests/cases/Helpers/ObjectsTest.php | 2 +- tests/cases/Helpers/WpHooksTest.php | 4 ++-- 23 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Inpsyde/Helpers/Boundaries.php b/Inpsyde/Helpers/Boundaries.php index 3a568f2..12de08b 100644 --- a/Inpsyde/Helpers/Boundaries.php +++ b/Inpsyde/Helpers/Boundaries.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; diff --git a/Inpsyde/Helpers/FunctionDocBlock.php b/Inpsyde/Helpers/FunctionDocBlock.php index 72749a9..0ead0cb 100644 --- a/Inpsyde/Helpers/FunctionDocBlock.php +++ b/Inpsyde/Helpers/FunctionDocBlock.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; diff --git a/Inpsyde/Helpers/FunctionReturnStatement.php b/Inpsyde/Helpers/FunctionReturnStatement.php index 96ed6a3..e85405a 100644 --- a/Inpsyde/Helpers/FunctionReturnStatement.php +++ b/Inpsyde/Helpers/FunctionReturnStatement.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; diff --git a/Inpsyde/Helpers/Functions.php b/Inpsyde/Helpers/Functions.php index e49e817..b6f3610 100644 --- a/Inpsyde/Helpers/Functions.php +++ b/Inpsyde/Helpers/Functions.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; diff --git a/Inpsyde/Helpers/Misc.php b/Inpsyde/Helpers/Misc.php index 0fff6d5..dbb66f7 100644 --- a/Inpsyde/Helpers/Misc.php +++ b/Inpsyde/Helpers/Misc.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; diff --git a/Inpsyde/Helpers/Names.php b/Inpsyde/Helpers/Names.php index 6eca699..ad2b8ce 100644 --- a/Inpsyde/Helpers/Names.php +++ b/Inpsyde/Helpers/Names.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; use PHPCSUtils\Tokens\Collections; diff --git a/Inpsyde/Helpers/Objects.php b/Inpsyde/Helpers/Objects.php index 0609724..e84084d 100644 --- a/Inpsyde/Helpers/Objects.php +++ b/Inpsyde/Helpers/Objects.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; use PHPCSUtils\Tokens\Collections; diff --git a/Inpsyde/Helpers/WpHooks.php b/Inpsyde/Helpers/WpHooks.php index f315002..d8e56c0 100644 --- a/Inpsyde/Helpers/WpHooks.php +++ b/Inpsyde/Helpers/WpHooks.php @@ -26,7 +26,7 @@ declare(strict_types=1); -namespace Inpsyde\Helpers; +namespace Inpsyde\CodingStandard\Helpers; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; diff --git a/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php b/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php index 1963730..0bc51f1 100644 --- a/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php @@ -28,9 +28,9 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\FunctionDocBlock; -use Inpsyde\Helpers\Functions; -use Inpsyde\Helpers\WpHooks; +use Inpsyde\CodingStandard\Helpers\FunctionDocBlock; +use Inpsyde\CodingStandard\Helpers\Functions; +use Inpsyde\CodingStandard\Helpers\WpHooks; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; use PHPCSUtils\Utils\FunctionDeclarations; diff --git a/Inpsyde/Sniffs/CodeQuality/ElementNameMinimalLengthSniff.php b/Inpsyde/Sniffs/CodeQuality/ElementNameMinimalLengthSniff.php index 6aefe4e..4566845 100644 --- a/Inpsyde/Sniffs/CodeQuality/ElementNameMinimalLengthSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/ElementNameMinimalLengthSniff.php @@ -28,7 +28,7 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\Names; +use Inpsyde\CodingStandard\Helpers\Names; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; diff --git a/Inpsyde/Sniffs/CodeQuality/HookClosureReturnSniff.php b/Inpsyde/Sniffs/CodeQuality/HookClosureReturnSniff.php index 38c6d2f..5092a09 100644 --- a/Inpsyde/Sniffs/CodeQuality/HookClosureReturnSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/HookClosureReturnSniff.php @@ -28,9 +28,9 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\Boundaries; -use Inpsyde\Helpers\FunctionReturnStatement; -use Inpsyde\Helpers\WpHooks; +use Inpsyde\CodingStandard\Helpers\Boundaries; +use Inpsyde\CodingStandard\Helpers\FunctionReturnStatement; +use Inpsyde\CodingStandard\Helpers\WpHooks; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; diff --git a/Inpsyde/Sniffs/CodeQuality/NoTopLevelDefineSniff.php b/Inpsyde/Sniffs/CodeQuality/NoTopLevelDefineSniff.php index 96e3aae..895c166 100644 --- a/Inpsyde/Sniffs/CodeQuality/NoTopLevelDefineSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/NoTopLevelDefineSniff.php @@ -28,7 +28,7 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\Functions; +use Inpsyde\CodingStandard\Helpers\Functions; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; diff --git a/Inpsyde/Sniffs/CodeQuality/PropertyPerClassLimitSniff.php b/Inpsyde/Sniffs/CodeQuality/PropertyPerClassLimitSniff.php index dc41b79..bf6876f 100644 --- a/Inpsyde/Sniffs/CodeQuality/PropertyPerClassLimitSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/PropertyPerClassLimitSniff.php @@ -28,8 +28,8 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\Names; -use Inpsyde\Helpers\Objects; +use Inpsyde\CodingStandard\Helpers\Names; +use Inpsyde\CodingStandard\Helpers\Objects; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHPCSUtils\Tokens\Collections; diff --git a/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php b/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php index e679d95..3fb9591 100644 --- a/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php @@ -28,11 +28,11 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\FunctionDocBlock; -use Inpsyde\Helpers\FunctionReturnStatement; -use Inpsyde\Helpers\Functions; -use Inpsyde\Helpers\Misc; -use Inpsyde\Helpers\WpHooks; +use Inpsyde\CodingStandard\Helpers\FunctionDocBlock; +use Inpsyde\CodingStandard\Helpers\FunctionReturnStatement; +use Inpsyde\CodingStandard\Helpers\Functions; +use Inpsyde\CodingStandard\Helpers\Misc; +use Inpsyde\CodingStandard\Helpers\WpHooks; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; use PHPCSUtils\Utils\FunctionDeclarations; diff --git a/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php b/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php index 16f7a6f..d8a80c0 100644 --- a/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php +++ b/Inpsyde/Sniffs/CodeQuality/StaticClosureSniff.php @@ -28,8 +28,8 @@ namespace Inpsyde\Sniffs\CodeQuality; -use Inpsyde\Helpers\Boundaries; -use Inpsyde\Helpers\FunctionDocBlock; +use Inpsyde\CodingStandard\Helpers\Boundaries; +use Inpsyde\CodingStandard\Helpers\FunctionDocBlock; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; diff --git a/composer.json b/composer.json index 29971d7..1763330 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ }, "autoload": { "psr-4": { - "Inpsyde\\Helpers\\": "Inpsyde/Helpers/" + "Inpsyde\\CodingStandard\\Helpers\\": "Inpsyde/Helpers/" } }, "autoload-dev": { diff --git a/tests/cases/Helpers/FunctionDocBlocTest.php b/tests/cases/Helpers/FunctionDocBlocTest.php index e7020f0..badbe48 100644 --- a/tests/cases/Helpers/FunctionDocBlocTest.php +++ b/tests/cases/Helpers/FunctionDocBlocTest.php @@ -29,7 +29,7 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\FunctionDocBlock; +use Inpsyde\CodingStandard\Helpers\FunctionDocBlock; class FunctionDocBlocTest extends TestCase { diff --git a/tests/cases/Helpers/FunctionReturnStatementTest.php b/tests/cases/Helpers/FunctionReturnStatementTest.php index 9108a01..24052b9 100644 --- a/tests/cases/Helpers/FunctionReturnStatementTest.php +++ b/tests/cases/Helpers/FunctionReturnStatementTest.php @@ -29,7 +29,7 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\FunctionReturnStatement; +use Inpsyde\CodingStandard\Helpers\FunctionReturnStatement; class FunctionReturnStatementTest extends TestCase { diff --git a/tests/cases/Helpers/FunctionsTest.php b/tests/cases/Helpers/FunctionsTest.php index fd02264..44c4b23 100644 --- a/tests/cases/Helpers/FunctionsTest.php +++ b/tests/cases/Helpers/FunctionsTest.php @@ -29,7 +29,7 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\Functions; +use Inpsyde\CodingStandard\Helpers\Functions; class FunctionsTest extends TestCase { diff --git a/tests/cases/Helpers/MiscTest.php b/tests/cases/Helpers/MiscTest.php index 32f6104..39d6ce1 100644 --- a/tests/cases/Helpers/MiscTest.php +++ b/tests/cases/Helpers/MiscTest.php @@ -29,7 +29,7 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\Misc; +use Inpsyde\CodingStandard\Helpers\Misc; use PHP_CodeSniffer\Util\Tokens; class MiscTest extends TestCase diff --git a/tests/cases/Helpers/NamesTest.php b/tests/cases/Helpers/NamesTest.php index 6464785..c114613 100644 --- a/tests/cases/Helpers/NamesTest.php +++ b/tests/cases/Helpers/NamesTest.php @@ -29,7 +29,7 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\Names; +use Inpsyde\CodingStandard\Helpers\Names; class NamesTest extends TestCase { diff --git a/tests/cases/Helpers/ObjectsTest.php b/tests/cases/Helpers/ObjectsTest.php index ca1fdb5..59960e7 100644 --- a/tests/cases/Helpers/ObjectsTest.php +++ b/tests/cases/Helpers/ObjectsTest.php @@ -29,7 +29,7 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\Objects; +use Inpsyde\CodingStandard\Helpers\Objects; class ObjectsTest extends TestCase { diff --git a/tests/cases/Helpers/WpHooksTest.php b/tests/cases/Helpers/WpHooksTest.php index 1aa080c..8be5483 100644 --- a/tests/cases/Helpers/WpHooksTest.php +++ b/tests/cases/Helpers/WpHooksTest.php @@ -29,8 +29,8 @@ namespace Inpsyde\CodingStandard\Tests\Helpers; use Inpsyde\CodingStandard\Tests\TestCase; -use Inpsyde\Helpers\Functions; -use Inpsyde\Helpers\WpHooks; +use Inpsyde\CodingStandard\Helpers\Functions; +use Inpsyde\CodingStandard\Helpers\WpHooks; class WpHooksTest extends TestCase { From bb11b0254c6ef7d2682fd8cb3f2c9b60e1f7f88c Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Tue, 5 Sep 2023 20:31:35 +0200 Subject: [PATCH 5/5] Update automattic/vipwpcs to 3.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1763330..bbffe06 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "minimum-stability": "stable", "require": { "php": ">=7.4", - "automattic/vipwpcs": "dev-develop", + "automattic/vipwpcs": "^3.0.0", "dealerdirect/phpcodesniffer-composer-installer": "~1.0.0", "phpcompatibility/php-compatibility": "^9.3.5", "phpcsstandards/phpcsextra": "^1.1",