diff --git a/generate-spec.php b/generate-spec.php index acca389..744814b 100755 --- a/generate-spec.php +++ b/generate-spec.php @@ -16,7 +16,6 @@ } use Ahc\Cli\Input\Command; -use DirectoryIterator; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\New_; @@ -25,6 +24,8 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Throw_; use PhpParser\NodeFinder; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; @@ -69,6 +70,9 @@ $astParser = (new ParserFactory())->createForNewestSupportedVersion(); $nodeFinder = new NodeFinder; +$nameResolver = new NameResolver; +$nodeTraverser = new NodeTraverser; +$nodeTraverser->addVisitor($nameResolver); $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true, 'comments' => true]); $lexer = new Lexer($config); @@ -84,9 +88,12 @@ Logger::panic('appinfo', 'info.xml file at ' . $infoXMLPath . ' is not parsable'); } + $rawNamespace = $xml->namespace ?? ucfirst($xml->id); + $appIsCore = false; + $appNamespace = 'OCA\\' . $rawNamespace; $appID = (string)$xml->id; - $readableAppID = $xml->namespace ? (string)$xml->namespace : Helpers::generateReadableAppID($appID); + $readableAppID = (string)$rawNamespace; $appSummary = (string)$xml->summary; $appVersion = (string)$xml->version; $appLicence = (string)$xml->licence; @@ -104,6 +111,7 @@ } $appIsCore = true; + $appNamespace = 'OC\\Core'; $appID = 'core'; $readableAppID = 'Core'; $appSummary = 'Core functionality of Nextcloud'; @@ -242,9 +250,9 @@ $controllers = []; $controllersDir = $sourceDir . '/Controller'; if (file_exists($controllersDir)) { - $dir = new DirectoryIterator($controllersDir); + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($controllersDir)); $controllerFiles = []; - foreach ($dir as $file) { + foreach ($iterator as $file) { $filePath = $file->getPathname(); if (!str_ends_with($filePath, 'Controller.php')) { continue; @@ -254,7 +262,10 @@ sort($controllerFiles); foreach ($controllerFiles as $filePath) { - $controllers[basename($filePath, 'Controller.php')] = $astParser->parse(file_get_contents($filePath)); + $offset = strlen($controllersDir . '/'); + $name = substr($filePath, $offset, strlen($filePath) - $offset - strlen('Controller.php')); + $name = str_replace('/', '\\', $name); + $controllers[$name] = $nodeTraverser->traverse($astParser->parse(file_get_contents($filePath))); } } @@ -263,7 +274,7 @@ $controllerClass = null; /** @var Class_ $class */ foreach ($nodeFinder->findInstanceOf($stmts, Class_::class) as $class) { - if ($class->name->name === $controllerName . 'Controller') { + if ($class->namespacedName->name === $appNamespace . '\\Controller\\' . $controllerName . 'Controller') { $controllerClass = $class; break; } @@ -369,7 +380,7 @@ $controllerClass = null; /** @var Class_ $class */ foreach ($nodeFinder->findInstanceOf($controllers[$controllerName] ?? [], Class_::class) as $class) { - if ($class->name == $controllerName . 'Controller') { + if ($class->namespacedName->name === $appNamespace . '\\Controller\\' . $controllerName . 'Controller') { $controllerClass = $class; break; } @@ -410,7 +421,7 @@ Logger::panic($routeName, "Controller '" . $controllerName . "' is marked as ignore but also has other scopes"); } - $tagName = implode('_', array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase($controllerName))); + $tagName = implode('_', array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase(str_replace('\\', '', $controllerName)))); $doc = $controllerClass->getDocComment()?->getText(); if ($doc != null && count(array_filter($tags, fn (array $tag): bool => $tag['name'] === $tagName)) == 0) { $classDescription = []; diff --git a/tests/appinfo/routes.php b/tests/appinfo/routes.php index bfc2147..8969f21 100644 --- a/tests/appinfo/routes.php +++ b/tests/appinfo/routes.php @@ -85,5 +85,6 @@ ['name' => 'Settings#deprecatedRouteAndParameterGet', 'url' => '/api/{apiVersion}/deprecated-route-parameter-get', 'verb' => 'GET', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#samePathGet', 'url' => '/api/{apiVersion}/same-path', 'verb' => 'GET', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#samePathPost', 'url' => '/api/{apiVersion}/same-path', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], + ['name' => 'V1\SubDir#subDirRoute', 'url' => '/sub-dir', 'verb' => 'GET'], ], ]; diff --git a/tests/lib/Controller/V1/SubDirController.php b/tests/lib/Controller/V1/SubDirController.php new file mode 100644 index 0000000..5023e10 --- /dev/null +++ b/tests/lib/Controller/V1/SubDirController.php @@ -0,0 +1,29 @@ +, array{}> + * + * 200: Personal settings updated + */ + #[NoAdminRequired] + public function subDirRoute(): DataResponse { + return new DataResponse(); + } +} diff --git a/tests/openapi-full.json b/tests/openapi-full.json index 3386143..9426e5a 100644 --- a/tests/openapi-full.json +++ b/tests/openapi-full.json @@ -6533,6 +6533,65 @@ } } }, + "/ocs/v2.php/apps/notifications/sub-dir": { + "get": { + "operationId": "v1_sub_dir-sub-dir-route", + "summary": "A route in a controller in a subdir.", + "tags": [ + "v1_sub_dir" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/index.php/apps/notifications/plain/with-scope": { "get": { "operationId": "plain-with-scope", diff --git a/tests/openapi.json b/tests/openapi.json index bac0f19..930e35e 100644 --- a/tests/openapi.json +++ b/tests/openapi.json @@ -994,6 +994,65 @@ } } }, + "/ocs/v2.php/apps/notifications/sub-dir": { + "get": { + "operationId": "v1_sub_dir-sub-dir-route", + "summary": "A route in a controller in a subdir.", + "tags": [ + "v1_sub_dir" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/index.php/apps/notifications/plain/with-scope": { "get": { "operationId": "plain-with-scope",