diff --git a/.github/workflows/test-repositories.yml b/.github/workflows/test-repositories.yml index 13e29f3..f42b14c 100644 --- a/.github/workflows/test-repositories.yml +++ b/.github/workflows/test-repositories.yml @@ -65,7 +65,7 @@ jobs: - name: Generate OpenAPI if: matrix.repositories != 'nextcloud/server' working-directory: temp-repository/ - run: ../generate-spec + run: ../bin/generate-spec - name: Generate OpenAPI - Server if: matrix.repositories == 'nextcloud/server' @@ -73,7 +73,7 @@ jobs: run: | for path in core apps/*; do if [ ! -f "$path/.noopenapi" ]; then - ../generate-spec "$path" "$path/openapi.json" || exit 1 + ../bin/generate-spec "$path" "$path/openapi.json" || exit 1 fi done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a116f50..248d408 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,7 +50,7 @@ jobs: - name: Generate OpenAPI working-directory: tests/ - run: ../generate-spec + run: ../bin/generate-spec - name: Check openapi changes run: | diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 59cf5fb..14406a2 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -15,5 +15,5 @@ ->ignoreVCSIgnored(true) ->notPath('vendor') ->in(__DIR__) - ->append(['generate-spec', 'merge-specs']); + ->append(['generate-spec.php', 'merge-specs.php']); return $config; diff --git a/bin/generate-spec b/bin/generate-spec new file mode 100755 index 0000000..be166ec --- /dev/null +++ b/bin/generate-spec @@ -0,0 +1,11 @@ +#!/usr/bin/env php +arguments('dir out') ->option('--first-status-code', 'Only output the first status code') @@ -84,11 +84,7 @@ $appIsCore = false; $appID = (string)$xml->id; - if ($xml->namespace) { - $readableAppID = (string)$xml->namespace; - } else { - $readableAppID = Helpers::generateReadableAppID($appID); - } + $readableAppID = $xml->namespace ? (string)$xml->namespace : Helpers::generateReadableAppID($appID); $appSummary = (string)$xml->summary; $appVersion = (string)$xml->version; $appLicence = (string)$xml->licence; @@ -171,7 +167,7 @@ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($iterator as $file) { $path = $file->getPathname(); - if (str_ends_with($path, 'Capabilities.php')) { + if (str_ends_with((string)$path, 'Capabilities.php')) { $capabilitiesFiles[] = $path; } } @@ -182,8 +178,8 @@ * @var Class_ $node */ foreach ($nodeFinder->findInstanceOf($astParser->parse(file_get_contents($path)), Class_::class) as $node) { - $implementsCapability = count(array_filter($node->implements, fn (Name $name) => $name->getLast() == 'ICapability')) > 0; - $implementsPublicCapability = count(array_filter($node->implements, fn (Name $name) => $name->getLast() == 'IPublicCapability')) > 0; + $implementsCapability = array_filter($node->implements, fn (Name $name): bool => $name->getLast() === 'ICapability') !== []; + $implementsPublicCapability = array_filter($node->implements, fn (Name $name): bool => $name->getLast() === 'IPublicCapability') !== []; if (!$implementsCapability && !$implementsPublicCapability) { continue; } @@ -327,7 +323,7 @@ } } -if (count($parsedRoutes) === 0) { +if ($parsedRoutes === []) { Logger::warning('Routes', 'No routes were loaded'); } @@ -343,26 +339,26 @@ foreach ($value as $route) { $routeName = $route['name']; - $postfix = array_key_exists('postfix', $route) ? $route['postfix'] : null; + $postfix = $route['postfix'] ?? null; $verb = array_key_exists('verb', $route) ? $route['verb'] : 'GET'; $requirements = array_key_exists('requirements', $route) ? $route['requirements'] : []; $defaults = array_key_exists('defaults', $route) ? $route['defaults'] : []; $root = array_key_exists('root', $route) ? $route['root'] : ($appIsCore ? '' : '/apps/' . $appID); $url = $route['url']; - if (!str_starts_with($url, '/')) { + if (!str_starts_with((string)$url, '/')) { $url = '/' . $url; } - if (str_ends_with($url, '/')) { - $url = substr($url, 0, -1); + if (str_ends_with((string)$url, '/')) { + $url = substr((string)$url, 0, -1); } $url = $pathPrefix . $root . $url; - $methodName = lcfirst(str_replace('_', '', ucwords(explode('#', $routeName)[1], '_'))); - if ($methodName == 'preflightedCors') { + $methodName = lcfirst(str_replace('_', '', ucwords(explode('#', (string)$routeName)[1], '_'))); + if ($methodName === 'preflightedCors') { continue; } - $controllerName = ucfirst(str_replace('_', '', ucwords(explode('#', $routeName)[0], '_'))); + $controllerName = ucfirst(str_replace('_', '', ucwords(explode('#', (string)$routeName)[0], '_'))); $controllerClass = null; /** @var Class_ $class */ foreach ($nodeFinder->findInstanceOf($controllers[$controllerName] ?? [], Class_::class) as $class) { @@ -390,7 +386,7 @@ $controllerScopes = Helpers::getOpenAPIAttributeScopes($controllerClass, $routeName); if (Helpers::classMethodHasAnnotationOrAttribute($controllerClass, 'IgnoreOpenAPI')) { - if (count($controllerScopes) === 0 || (in_array('ignore', $controllerScopes, true) && count($controllerScopes) === 1)) { + if ($controllerScopes === [] || (in_array('ignore', $controllerScopes, true) && count($controllerScopes) === 1)) { Logger::debug($routeName, "Controller '" . $controllerName . "' ignored because of IgnoreOpenAPI attribute"); continue; } @@ -409,24 +405,24 @@ $tagName = implode('_', array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase($controllerName))); $doc = $controllerClass->getDocComment()?->getText(); - if ($doc != null && count(array_filter($tags, fn (array $tag) => $tag['name'] == $tagName)) == 0) { + if ($doc != null && count(array_filter($tags, fn (array $tag): bool => $tag['name'] === $tagName)) == 0) { $classDescription = []; $docNodes = $phpDocParser->parse(new TokenIterator($lexer->tokenize($doc)))->children; foreach ($docNodes as $docNode) { if ($docNode instanceof PhpDocTextNode) { $block = Helpers::cleanDocComment($docNode->text); - if ($block == '') { + if ($block === '') { continue; } $classDescription[] = $block; } } - if (count($classDescription) > 0) { + if ($classDescription !== []) { $tags[] = [ 'name' => $tagName, - 'description' => join("\n", $classDescription), + 'description' => implode("\n", $classDescription), ]; } } @@ -454,7 +450,7 @@ $scopes = Helpers::getOpenAPIAttributeScopes($classMethod, $routeName); if ($isIgnored) { - if (count($scopes) === 0 || (in_array('ignore', $scopes, true) && count($scopes) === 1)) { + if ($scopes === [] || (in_array('ignore', $scopes, true) && count($scopes) === 1)) { Logger::debug($routeName, 'Route ignored because of IgnoreOpenAPI attribute'); continue; } @@ -471,8 +467,8 @@ Logger::panic($routeName, 'Route is marked as ignore but also has other scopes'); } - if (empty($scopes)) { - if (!empty($controllerScopes)) { + if ($scopes === []) { + if ($controllerScopes !== []) { $scopes = $controllerScopes; } elseif ($isExApp) { $scopes = ['ex_app']; @@ -503,7 +499,7 @@ } $classMethodInfo = ControllerMethod::parse($routeName, $definitions, $methodFunction, $isAdmin, $isDeprecated, $isPasswordConfirmation); - if (count($classMethodInfo->returns) > 0) { + if ($classMethodInfo->returns !== []) { Logger::error($routeName, 'Returns an invalid response'); continue; } @@ -524,10 +520,10 @@ } } - $docStatusCodes = array_map(fn (ControllerMethodResponse $response) => $response->statusCode, array_filter($classMethodInfo->responses, fn (?ControllerMethodResponse $response) => $response != null)); - $missingDocStatusCodes = array_unique(array_filter(array_diff($codeStatusCodes, $docStatusCodes), fn (int $code) => $code < 500)); + $docStatusCodes = array_map(fn (ControllerMethodResponse $response): int => $response->statusCode, array_filter($classMethodInfo->responses, fn (?ControllerMethodResponse $response): bool => $response != null)); + $missingDocStatusCodes = array_unique(array_filter(array_diff($codeStatusCodes, $docStatusCodes), fn (int $code): bool => $code < 500)); - if (count($missingDocStatusCodes) > 0) { + if ($missingDocStatusCodes !== []) { Logger::error($routeName, 'Returns undocumented status codes: ' . implode(', ', $missingDocStatusCodes)); continue; } @@ -570,7 +566,7 @@ $tagNames = []; if ($useTags) { - foreach ($routes as $scope => $scopeRoutes) { + foreach ($routes as $scopeRoutes) { foreach ($scopeRoutes as $route) { foreach ($route->tags as $tag) { if (!in_array($tag, $tagNames)) { @@ -589,13 +585,11 @@ $urlParameters = []; preg_match_all('/{[^}]*}/', $route->url, $urlParameters); - $urlParameters = array_map(fn (string $name) => substr($name, 1, -1), $urlParameters[0]); + $urlParameters = array_map(fn (string $name): string => substr($name, 1, -1), $urlParameters[0]); foreach ($urlParameters as $urlParameter) { - $matchingParameters = array_filter($route->controllerMethod->parameters, function (ControllerMethodParameter $param) use ($urlParameter) { - return $param->name == $urlParameter; - }); - $requirement = array_key_exists($urlParameter, $route->requirements) ? $route->requirements[$urlParameter] : null; + $matchingParameters = array_filter($route->controllerMethod->parameters, fn (ControllerMethodParameter $param): bool => $param->name == $urlParameter); + $requirement = $route->requirements[$urlParameter] ?? null; if (count($matchingParameters) == 1) { $parameter = $matchingParameters[array_keys($matchingParameters)[0]]; if ($parameter?->methodParameter == null && ($route->requirements == null || !array_key_exists($urlParameter, $route->requirements))) { @@ -613,11 +607,11 @@ } if ($requirement != null) { - if (!str_starts_with($requirement, '^')) { + if (!str_starts_with((string)$requirement, '^')) { $requirement = '^' . $requirement; } - if (!str_ends_with($requirement, '$')) { - $requirement = $requirement . '$'; + if (!str_ends_with((string)$requirement, '$')) { + $requirement .= '$'; } } @@ -627,7 +621,7 @@ Logger::error($route->name, 'Missing requirement for apiVersion'); continue; } - preg_match("/^\^\(([v0-9-.|]*)\)\\$$/m", $requirement, $matches); + preg_match("/^\^\(([v0-9-.|]*)\)\\$$/m", (string)$requirement, $matches); if (count($matches) == 2) { $enum = explode('|', $matches[1]); } else { @@ -678,12 +672,12 @@ } $mergedResponses = []; - foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->statusCode, array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response) => $response != null))) as $statusCode) { - if ($firstStatusCode && count($mergedResponses) > 0) { + foreach (array_unique(array_map(fn (ControllerMethodResponse $response): int => $response->statusCode, array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response): bool => $response != null))) as $statusCode) { + if ($firstStatusCode && $mergedResponses !== []) { break; } - $statusCodeResponses = array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response) => $response != null && $response->statusCode == $statusCode); + $statusCodeResponses = array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response): bool => $response != null && $response->statusCode == $statusCode); $headers = []; foreach ($statusCodeResponses as $response) { if ($response->headers !== null) { @@ -692,16 +686,16 @@ } $mergedContentTypeResponses = []; - foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->contentType, array_filter($statusCodeResponses, fn (ControllerMethodResponse $response) => $response->contentType != null))) as $contentType) { - if ($firstContentType && count($mergedContentTypeResponses) > 0) { + foreach (array_unique(array_map(fn (ControllerMethodResponse $response): ?string => $response->contentType, array_filter($statusCodeResponses, fn (ControllerMethodResponse $response): bool => $response->contentType != null))) as $contentType) { + if ($firstContentType && $mergedContentTypeResponses !== []) { break; } /** @var ControllerMethodResponse[] $contentTypeResponses */ - $contentTypeResponses = array_values(array_filter($statusCodeResponses, fn (ControllerMethodResponse $response) => $response->contentType == $contentType)); + $contentTypeResponses = array_values(array_filter($statusCodeResponses, fn (ControllerMethodResponse $response): bool => $response->contentType == $contentType)); - $hasEmpty = count(array_filter($contentTypeResponses, fn (ControllerMethodResponse $response) => $response->type == null)) > 0; - $uniqueResponses = array_values(array_intersect_key($contentTypeResponses, array_unique(array_map(fn (ControllerMethodResponse $response) => $response->type->toArray(), array_filter($contentTypeResponses, fn (ControllerMethodResponse $response) => $response->type != null)), SORT_REGULAR))); + $hasEmpty = array_filter($contentTypeResponses, fn (ControllerMethodResponse $response): bool => $response->type == null) !== []; + $uniqueResponses = array_values(array_intersect_key($contentTypeResponses, array_unique(array_map(fn (ControllerMethodResponse $response): array|\stdClass => $response->type->toArray(), array_filter($contentTypeResponses, fn (ControllerMethodResponse $response): bool => $response->type != null)), SORT_REGULAR))); if (count($uniqueResponses) == 1) { if ($hasEmpty) { $mergedContentTypeResponses[$contentType] = []; @@ -712,7 +706,7 @@ } else { $mergedContentTypeResponses[$contentType] = [ 'schema' => [ - [$hasEmpty ? 'anyOf' : 'oneOf' => array_map(function (ControllerMethodResponse $response) use ($route) { + [$hasEmpty ? 'anyOf' : 'oneOf' => array_map(function (ControllerMethodResponse $response) use ($route): \stdClass|array { $schema = Helpers::cleanEmptyResponseArray($response->type->toArray()); return Helpers::wrapOCSResponse($route, $response, $schema); }, $uniqueResponses)], @@ -724,18 +718,18 @@ $response = [ 'description' => array_key_exists($statusCode, $route->controllerMethod->responseDescription) ? $route->controllerMethod->responseDescription[$statusCode] : '', ]; - if (count($headers) > 0) { + if ($headers !== []) { $response['headers'] = array_combine( array_keys($headers), array_map( - fn (OpenApiType $type) => [ + fn (OpenApiType $type): array => [ 'schema' => $type->toArray(), ], array_values($headers), ), ); } - if (count($mergedContentTypeResponses) > 0) { + if ($mergedContentTypeResponses !== []) { $response['content'] = $mergedContentTypeResponses; } $mergedResponses[$statusCode] = $response; @@ -759,7 +753,7 @@ if ($route->controllerMethod->summary !== null) { $operation['summary'] = $route->controllerMethod->summary; } - if (count($route->controllerMethod->description) > 0) { + if ($route->controllerMethod->description !== []) { $operation['description'] = implode("\n", $route->controllerMethod->description); } if ($route->controllerMethod->isDeprecated) { @@ -768,11 +762,9 @@ if ($useTags) { $operation['tags'] = $route->tags; } - if (count($security) > 0) { - $operation['security'] = $security; - } + $operation['security'] = $security; - if (count($bodyParameters) > 0) { + if ($bodyParameters !== []) { $requiredBodyParameters = []; foreach ($bodyParameters as $bodyParameter) { @@ -782,7 +774,7 @@ } } - $required = count($requiredBodyParameters) > 0; + $required = $requiredBodyParameters !== []; $schema = [ 'type' => 'object', @@ -833,7 +825,7 @@ ], ]; } - if (count($parameters) > 0) { + if ($parameters !== []) { $operation['parameters'] = $parameters; } @@ -929,7 +921,7 @@ if (!$hasSingleScope) { $scopePaths['full'] = []; -} elseif (count($scopePaths) === 0) { +} elseif ($scopePaths === []) { if (isset($schemas['Capabilities']) || isset($schemas['PublicCapabilities'])) { Logger::debug('app', 'Generating default scope without routes to populate capabilities'); $scopePaths['default'] = []; @@ -954,14 +946,14 @@ $openapiScope['components']['schemas'] = $schemas; } else { $usedSchemas = []; - foreach ($paths as $url => $urlRoutes) { - foreach ($urlRoutes as $httpMethod => $routeData) { - foreach ($routeData['responses'] as $statusCode => $responseData) { - if (!empty($responseData['content'])) { + foreach ($paths as $urlRoutes) { + foreach ($urlRoutes as $routeData) { + foreach ($routeData['responses'] as $responseData) { + if (isset($responseData['content']) && $responseData['content'] !== []) { $usedSchemas[] = Helpers::collectUsedRefs($responseData['content']); } } - if (!empty($routeData['requestBody']['content'])) { + if (isset($routeData['requestBody']['content']) && $routeData['requestBody']['content'] !== []) { $usedSchemas[] = Helpers::collectUsedRefs($routeData['requestBody']['content']); } } @@ -971,11 +963,11 @@ $scopedSchemas = []; while ($usedSchema = array_shift($usedSchemas)) { - if (!str_starts_with($usedSchema, '#/components/schemas/')) { + if (!str_starts_with((string)$usedSchema, '#/components/schemas/')) { continue; } - $schemaName = substr($usedSchema, strlen('#/components/schemas/')); + $schemaName = substr((string)$usedSchema, strlen('#/components/schemas/')); if (!isset($schemas[$schemaName])) { Logger::error('app', "Schema $schemaName used by scope $scope is not defined"); @@ -983,7 +975,7 @@ $newRefs = Helpers::collectUsedRefs($schemas[$schemaName]); foreach ($newRefs as $newRef) { - if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { + if (!isset($scopedSchemas[substr((string)$newRef, strlen('#/components/schemas/'))])) { $usedSchemas[] = $newRef; } } @@ -998,7 +990,7 @@ $scopedSchemas['PublicCapabilities'] = $schemas['PublicCapabilities']; } - if (count($scopedSchemas) === 0) { + if ($scopedSchemas === []) { $scopedSchemas = new stdClass(); } else { ksort($scopedSchemas); diff --git a/merge-specs b/merge-specs.php similarity index 89% rename from merge-specs rename to merge-specs.php index 9a86749..38de5af 100755 --- a/merge-specs +++ b/merge-specs.php @@ -18,7 +18,7 @@ use Ahc\Cli\Input\Command; use stdClass; -$command = new Command('merge-specs', 'Merge multiple Nextcloud OpenAPI specs into one'); +$command = new Command('merge-specs.php', 'Merge multiple Nextcloud OpenAPI specs into one'); $command ->option('--merged ') ->option('--core ') @@ -97,7 +97,7 @@ ['properties'] ['capabilities'] ['anyOf'] - = array_map(fn (string $capability) => ['$ref' => '#/components/schemas/' . $capability], $capabilities); + = array_map(fn (string $capability): array => ['$ref' => '#/components/schemas/' . $capability], $capabilities); function loadSpec(string $path): array { return rewriteRefs(json_decode(file_get_contents($path), true)); @@ -105,7 +105,7 @@ function loadSpec(string $path): array { function rewriteRefs(array $spec): array { $readableAppID = Helpers::generateReadableAppID($spec['info']['title']); - array_walk_recursive($spec, function (mixed &$item, string $key) use ($readableAppID) { + array_walk_recursive($spec, function (mixed &$item, string $key) use ($readableAppID): void { if ($key === '$ref' && $item !== '#/components/schemas/OCSMeta') { $item = str_replace('#/components/schemas/', '#/components/schemas/' . $readableAppID, $item); } @@ -129,7 +129,7 @@ function rewriteSchemaNames(array $spec): array { $schemas = $spec['components']['schemas']; $readableAppID = Helpers::generateReadableAppID($spec['info']['title']); return array_combine( - array_map(fn (string $key) => $key == 'OCSMeta' ? $key : $readableAppID . $key, array_keys($schemas)), + array_map(fn (string $key): string => $key === 'OCSMeta' ? $key : $readableAppID . $key, array_keys($schemas)), array_values($schemas), ); } @@ -154,7 +154,7 @@ function rewriteOperations(array $spec): array { $operation['operationId'] = $spec['info']['title'] . '-' . $operation['operationId']; } if (array_key_exists('tags', $operation)) { - $operation['tags'] = array_map(fn (string $tag) => $spec['info']['title'] . '/' . $tag, $operation['tags']); + $operation['tags'] = array_map(fn (string $tag): string => $spec['info']['title'] . '/' . $tag, $operation['tags']); } else { $operation['tags'] = [$spec['info']['title']]; } @@ -164,7 +164,8 @@ function rewriteOperations(array $spec): array { $operation['responses'] = [$value => $operation['responses'][$value]]; } if (array_key_exists('security', $operation)) { - for ($i = 0; $i < count($operation['security']); $i++) { + $counter = count($operation['security']); + for ($i = 0; $i < $counter; $i++) { if (count($operation['security'][$i]) == 0) { $operation['security'][$i] = new stdClass(); // When reading {} will be converted to [], so we have to fix it } diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..e590488 --- /dev/null +++ b/rector.php @@ -0,0 +1,21 @@ +withPaths([__DIR__]) + ->withSkipPath(__DIR__ . '/vendor') + ->withPhpSets() + ->withPreparedSets( + deadCode: true, + codeQuality: true, + typeDeclarations: true, + strictBooleans: true, + ); diff --git a/src/ControllerMethod.php b/src/ControllerMethod.php index a3e1727..7b17b58 100644 --- a/src/ControllerMethod.php +++ b/src/ControllerMethod.php @@ -54,13 +54,14 @@ public static function parse(string $context, array $definitions, ClassMethod $m foreach ($docNodes as $docNode) { if ($docNode instanceof PhpDocTextNode) { $block = Helpers::cleanDocComment($docNode->text); - if ($block == '') { + if ($block === '') { continue; } - $pattern = '/([0-9]{3}): /'; + $pattern = '/(\d{3}): /'; if (preg_match($pattern, $block)) { $parts = preg_split($pattern, $block, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - for ($i = 0; $i < count($parts); $i += 2) { + $counter = count($parts); + for ($i = 0; $i < $counter; $i += 2) { $statusCode = intval($parts[$i]); $responseDescriptions[$statusCode] = trim($parts[$i + 1]); } @@ -108,8 +109,8 @@ public static function parse(string $context, array $definitions, ClassMethod $m } if (!$allowMissingDocs) { - foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->statusCode, array_filter($responses, fn (?ControllerMethodResponse $response) => $response != null))) as $statusCode) { - if ($statusCode < 500 && (!array_key_exists($statusCode, $responseDescriptions) || $responseDescriptions[$statusCode] == '')) { + foreach (array_unique(array_map(fn (ControllerMethodResponse $response): int => $response->statusCode, array_filter($responses, fn (?ControllerMethodResponse $response): bool => $response != null))) as $statusCode) { + if ($statusCode < 500 && (!array_key_exists($statusCode, $responseDescriptions) || $responseDescriptions[$statusCode] === '')) { Logger::error($context, 'Missing description for status code ' . $statusCode); } } @@ -136,7 +137,7 @@ public static function parse(string $context, array $definitions, ClassMethod $m } } - if ($paramTag !== null && $psalmParamTag !== null) { + if ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode && $psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { // Use all the type information from @psalm-param because it is more specific, // but pull the description from @param and @psalm-param because usually only one of them has it. if ($psalmParamTag->description !== '') { @@ -176,10 +177,10 @@ public static function parse(string $context, array $definitions, ClassMethod $m } $param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type); - } elseif ($psalmParamTag !== null) { + } elseif ($psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { $type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $psalmParamTag); $param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type); - } elseif ($paramTag !== null) { + } elseif ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) { $type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $paramTag); $param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type); } elseif ($allowMissingDocs) { diff --git a/src/Helpers.php b/src/Helpers.php index a01e900..a1adb17 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -26,7 +26,7 @@ class Helpers { public const OPENAPI_ATTRIBUTE_CLASSNAME = 'OpenAPI'; public static function generateReadableAppID(string $appID): string { - return implode('', array_map(fn (string $s) => ucfirst($s), explode('_', $appID))); + return implode('', array_map(fn (string $s): string => ucfirst($s), explode('_', $appID))); } public static function securitySchemes(): array { @@ -62,7 +62,7 @@ public static function jsonFlags(): int { } public static function cleanDocComment(string $comment): string { - return trim(preg_replace("/\s+/", ' ', $comment)); + return trim((string)preg_replace("/\s+/", ' ', $comment)); } public static function splitOnUppercaseFollowedByNonUppercase(string $str): array { @@ -70,10 +70,10 @@ public static function splitOnUppercaseFollowedByNonUppercase(string $str): arra } public static function mergeSchemas(array $schemas): mixed { - if (!in_array(true, array_map(fn ($schema) => is_array($schema), $schemas))) { + if (!in_array(true, array_map(fn ($schema): bool => is_array($schema), $schemas))) { $results = array_values(array_unique($schemas)); if (count($results) > 1) { - throw new Exception('Incompatibles types: ' . join(', ', $results)); + throw new Exception('Incompatibles types: ' . implode(', ', $results)); } return $results[0]; } @@ -168,7 +168,7 @@ public static function classMethodHasAnnotationOrAttribute(ClassMethod|Class_|No public static function cleanSchemaName(string $name): string { global $readableAppID; - return substr($name, strlen($readableAppID)); + return substr($name, strlen((string)$readableAppID)); } protected static function getScopeNameFromAttributeArgument(Arg $arg, int $key, string $routeName): ?string { @@ -205,7 +205,7 @@ public static function getOpenAPIAttributeScopes(ClassMethod|Class_|Node $node, foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->getLast() === self::OPENAPI_ATTRIBUTE_CLASSNAME) { - if (empty($attr->args)) { + if ($attr->args === []) { $scopes[] = 'default'; continue; } @@ -230,7 +230,7 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->getLast() === self::OPENAPI_ATTRIBUTE_CLASSNAME) { - if (empty($attr->args)) { + if ($attr->args === []) { $tags[$defaultScope] = [$defaultTag]; continue; } @@ -252,7 +252,7 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n if ($item?->value instanceof String_) { $tag = $item->value->value; $pattern = '/^[0-9a-zA-Z_-]+$/'; - if (!preg_match($pattern, $tag)) { + if (in_array(preg_match($pattern, $tag), [0, false], true)) { Logger::error($routeName, 'Tag "' . $tag . '" has to match pattern "' . $pattern . '"'); } $foundTags[] = $tag; @@ -260,8 +260,8 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n } } - if (!empty($foundTags)) { - $tags[$foundScopeName ?: $defaultScope] = $foundTags; + if ($foundTags !== []) { + $tags[$foundScopeName !== null && $foundScopeName !== '' && $foundScopeName !== '0' ? $foundScopeName : $defaultScope] = $foundTags; } } } @@ -272,7 +272,7 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n public static function collectUsedRefs(array $data): array { $refs = []; - array_walk_recursive($data, function ($value, $key) use (&$refs) { + array_walk_recursive($data, function ($value, $key) use (&$refs): void { if ($key === '$ref') { $refs[] = $value; } diff --git a/src/OpenApiType.php b/src/OpenApiType.php index fbae1b3..63ce147 100644 --- a/src/OpenApiType.php +++ b/src/OpenApiType.php @@ -67,7 +67,7 @@ public function toArray(bool $isParameter = false): array|stdClass { type: 'integer', nullable: $this->nullable, hasDefaultValue: $this->hasDefaultValue, - defaultValue: !$this->hasDefaultValue ? null : ($this->defaultValue === true ? 1 : 0), + defaultValue: $this->hasDefaultValue ? ($this->defaultValue === true ? 1 : 0) : (null), description: $this->description, enum: [0, 1], ))->toArray($isParameter); @@ -99,11 +99,7 @@ enum: [0, 1], $values['nullable'] = true; } if ($this->hasDefaultValue && $this->defaultValue !== null) { - if ($this->type === 'object' && empty($this->defaultValue)) { - $values['default'] = new stdClass(); - } else { - $values['default'] = $this->defaultValue; - } + $values['default'] = $this->type === 'object' && empty($this->defaultValue) ? new stdClass() : $this->defaultValue; } if ($this->enum !== null) { $values['enum'] = $this->enum; @@ -111,7 +107,7 @@ enum: [0, 1], if ($this->description !== null && $this->description !== '' && !$isParameter) { $values['description'] = Helpers::cleanDocComment($this->description); } - if ($this->items !== null) { + if ($this->items instanceof \OpenAPIExtractor\OpenApiType) { $values['items'] = $this->items->toArray(); } if ($this->minLength !== null) { @@ -135,9 +131,9 @@ enum: [0, 1], if ($this->required !== null) { $values['required'] = $this->required; } - if ($this->properties !== null && count($this->properties) > 0) { + if ($this->properties !== null && $this->properties !== []) { $values['properties'] = array_combine(array_keys($this->properties), - array_map(static fn (OpenApiType $property) => $property->toArray(), array_values($this->properties)), + array_map(static fn (OpenApiType $property): array|\stdClass => $property->toArray(), array_values($this->properties)), ); } if ($this->additionalProperties !== null) { @@ -148,16 +144,16 @@ enum: [0, 1], } } if ($this->oneOf !== null) { - $values['oneOf'] = array_map(fn (OpenApiType $type) => $type->toArray(), $this->oneOf); + $values['oneOf'] = array_map(fn (OpenApiType $type): array|\stdClass => $type->toArray(), $this->oneOf); } if ($this->anyOf !== null) { - $values['anyOf'] = array_map(fn (OpenApiType $type) => $type->toArray(), $this->anyOf); + $values['anyOf'] = array_map(fn (OpenApiType $type): array|\stdClass => $type->toArray(), $this->anyOf); } if ($this->allOf !== null) { - $values['allOf'] = array_map(fn (OpenApiType $type) => $type->toArray(), $this->allOf); + $values['allOf'] = array_map(fn (OpenApiType $type): array|\stdClass => $type->toArray(), $this->allOf); } - return count($values) > 0 ? $values : new stdClass(); + return $values !== [] ? $values : new stdClass(); } public static function resolve(string $context, array $definitions, ParamTagValueNode|NodeAbstract|TypeNode $node): OpenApiType { @@ -220,7 +216,7 @@ public static function resolve(string $context, array $definitions, ParamTagValu context: $context, type: 'object', properties: $properties, - required: count($required) > 0 ? $required : null, + required: $required !== [] ? $required : null, ); } @@ -263,14 +259,14 @@ public static function resolve(string $context, array $definitions, ParamTagValu $isUnion = $node instanceof UnionTypeNode || $node instanceof UnionType; $isIntersection = $node instanceof IntersectionTypeNode || $node instanceof IntersectionType; - if ($isUnion && count($node->types) == count(array_filter($node->types, fn ($type) => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprStringNode))) { + if ($isUnion && count($node->types) === count(array_filter($node->types, fn ($type): bool => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprStringNode))) { $values = []; /** @var ConstTypeNode $type */ foreach ($node->types as $type) { $values[] = $type->constExpr->value; } - if (count(array_filter($values, fn (string $value) => $value == '')) > 0) { + if (array_filter($values, fn (string $value): bool => $value === '') !== []) { // Not a valid enum return new OpenApiType( context: $context, @@ -284,14 +280,14 @@ public static function resolve(string $context, array $definitions, ParamTagValu enum: $values, ); } - if ($isUnion && count($node->types) == count(array_filter($node->types, fn ($type) => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprIntegerNode))) { + if ($isUnion && count($node->types) === count(array_filter($node->types, fn ($type): bool => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprIntegerNode))) { $values = []; /** @var ConstTypeNode $type */ foreach ($node->types as $type) { $values[] = (int)$type->constExpr->value; } - if (count(array_filter($values, fn (string $value) => $value == '')) > 0) { + if (array_filter($values, fn (string $value): bool => $value === '') !== []) { // Not a valid enum return new OpenApiType( context: $context, @@ -340,14 +336,14 @@ enum: $values, ); } - $itemTypes = array_map(static function (OpenApiType $item) { + $itemTypes = array_map(static function (OpenApiType $item): ?string { if ($item->type === 'integer') { return 'number'; } return $item->type; }, $items); - if (!empty(array_filter($itemTypes, static fn (?string $type) => $type === null)) || count($itemTypes) !== count(array_unique($itemTypes))) { + if (array_filter($itemTypes, static fn (?string $type): bool => $type === null) !== [] || count($itemTypes) !== count(array_unique($itemTypes))) { return new OpenApiType( context: $context, nullable: $nullable, @@ -414,20 +410,20 @@ private static function mergeEnums(string $context, array $types): array { } } - foreach (array_map(static fn (OpenApiType $type) => $type->type, $nonEnums) as $type) { + foreach (array_map(static fn (OpenApiType $type): ?string => $type->type, $nonEnums) as $type) { if (array_key_exists($type, $enums)) { unset($enums[$type]); } } - return array_merge($nonEnums, array_map(static fn (string $type) => new OpenApiType( + return array_merge($nonEnums, array_map(static fn (string $type): \OpenAPIExtractor\OpenApiType => new OpenApiType( context: $context, type: $type, enum: $enums[$type], ), array_keys($enums))); } private static function resolveIdentifier(string $context, array $definitions, string $name): OpenApiType { - if ($name == 'array') { + if ($name === 'array') { Logger::error($context, "Instead of 'array' use:\n'new stdClass()' for empty objects\n'array' for non-empty objects\n'array' for empty lists\n'array' for lists"); } if (str_starts_with($name, '\\')) { diff --git a/src/ResponseType.php b/src/ResponseType.php index e50c810..8c3492c 100644 --- a/src/ResponseType.php +++ b/src/ResponseType.php @@ -163,8 +163,6 @@ public static function getAll(): array { } /** - * @param string $context - * @param TypeNode $obj * @return list * @throws Exception */ @@ -195,14 +193,14 @@ public static function resolve(string $context, TypeNode $obj): array { if ($className == 'void') { $responses[] = null; } else { - if (count(array_filter($responseTypes, fn ($responseType) => $responseType->className == $className)) == 0) { + if (count(array_filter($responseTypes, fn ($responseType): bool => $responseType->className == $className)) == 0) { Logger::error($context, "Invalid return type '" . $obj . "'"); return []; } foreach ($responseTypes as $responseType) { if ($responseType->className == $className) { // +2 for status code and headers which are always present - $expectedArgs = count(array_filter([$responseType->hasContentTypeTemplate, $responseType->hasTypeTemplate], fn ($value) => $value)) + 2; + $expectedArgs = count(array_filter([$responseType->hasContentTypeTemplate, $responseType->hasTypeTemplate], fn ($value): bool => $value)) + 2; if (count($args) != $expectedArgs) { Logger::error($context, "'" . $className . "' needs " . $expectedArgs . ' parameters'); continue; @@ -219,7 +217,7 @@ public static function resolve(string $context, TypeNode $obj): array { } elseif ($args[$i] instanceof UnionTypeNode) { $contentTypes = array_map(fn ($arg) => $arg->constExpr->value, $args[$i]->types); } else { - Logger::panic($context, 'Unable to parse content type from ' . get_class($args[$i])); + Logger::panic($context, 'Unable to parse content type from ' . $args[$i]::class); } $i++; } else { @@ -245,11 +243,7 @@ public static function resolve(string $context, TypeNode $obj): array { if (array_key_exists('Content-Type', $headers)) { /** @var OpenApiType $value */ $values = $headers['Content-Type']; - if ($values->oneOf != null) { - $values = $values->oneOf; - } else { - $values = [$values]; - } + $values = $values->oneOf != null ? $values->oneOf : [$values]; foreach ($values as $value) { if ($value->type == 'string' && $value->enum != null) { @@ -266,8 +260,8 @@ public static function resolve(string $context, TypeNode $obj): array { foreach ($statusCodes as $statusCode) { if ($statusCode === 204 || $statusCode === 304) { if ($statusCode === 304) { - $customHeaders = array_filter(array_keys($headers), static fn (string $header) => str_starts_with(strtolower($header), 'x-')); - if (!empty($customHeaders)) { + $customHeaders = array_filter(array_keys($headers), static fn (string $header): bool => str_starts_with(strtolower($header), 'x-')); + if ($customHeaders !== []) { Logger::error($context, 'Custom headers are not allowed for responses with status code 304. Found: ' . implode(', ', $customHeaders)); } } diff --git a/src/Route.php b/src/Route.php index 6e2a460..5b07332 100644 --- a/src/Route.php +++ b/src/Route.php @@ -34,7 +34,7 @@ public static function parseRoutes(string $path): array { return include($path); } elseif (str_contains($content, 'registerRoutes')) { preg_match_all('/registerRoutes\(.*?\$this,.*?(\[[^;]*)\);/s', $content, $matches); - return array_merge(...array_map(fn (string $match) => self::includeRoutes(" self::includeRoutes("context . ': Unable to parse Expr: ' . get_class($this->expr)); + parent::__construct($this->context . ': Unable to parse Expr: ' . $this->expr::class); } }