diff --git a/composer.json b/composer.json index fa9a9e4c..39f80afd 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,8 @@ "spatie/phpunit-snapshot-assertions": "^2.2.1", "squizlabs/php_codesniffer": "^3.2.2", "symfony/var-dumper": "^4.4.1", - "theseer/phpdox": "*" + "theseer/phpdox": "*", + "vimeo/psalm": "^3.7" }, "scripts": { "build": "vendor/bin/robo build", @@ -58,10 +59,11 @@ "build:js": "cd public && npm run build:js && cd ..", "clean": "vendor/bin/robo clean", "coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build", - "phpstan": "phpstan analyse -l 4 -c phpstan.neon src tests ./console index.php", + "phpstan": "phpstan analyse -c phpstan.neon", "watch:css": "cd public && npm run watch:css", "watch:js": "cd public && npm run watch:js", - "test": "vendor/bin/phpunit" + "test": "vendor/bin/phpunit -c build --no-coverage", + "test-update": "vendor/bin/phpunit -c build --no-coverage -d --update-snapshots" }, "scripts-descriptions": { "build": "Generate the api docs", diff --git a/phpstan.neon b/phpstan.neon index 964eb301..46125efa 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,18 @@ parameters: + checkGenericClassInNonGenericObjectType: false + inferPrivatePropertyTypeFromConstructor: true + level: 7 autoload_files: - %rootDir%/../../../tests/mocks.php + paths: + - src + - tests + - ./console + - index.php ignoreErrors: - '#Access to an undefined property Aviat\\\Ion\\\Friend::\$[a-zA-Z0-9_]+#' - '#Call to an undefined method Aviat\\\Ion\\\Friend::[a-zA-Z0-9_]+\(\)#' - '#Call to an undefined method Aura\\\Html\\\HelperLocator::[a-zA-Z0-9_]+\(\)#' - - '#Undefined variable: \$var#' - '#Property Amp\\Artax\\Internal\\RequestCycle::\$[a-zA-Z0-9_]+#' + excludes_analyse: + - tests/mocks.php \ No newline at end of file diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..42f355b2 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 3edf9476..d42fdf81 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -65,7 +65,7 @@ class APIRequestBuilder { /** * The current request - * @var \Amp\Artax\Request + * @var Request */ protected $request; @@ -219,14 +219,14 @@ class APIRequestBuilder { /** * Return the promise for the current request * + * @return Request * @throws \Throwable - * @return \Amp\Artax\Request */ public function getFullRequest(): Request { $this->buildUri(); - if ($this->logger) + if ($this->logger !== NULL) { $this->logger->debug('API Request', [ 'request_url' => $this->request->getUri(), diff --git a/src/API/Anilist/Model.php b/src/API/Anilist/Model.php index fe6516f5..d3b51f82 100644 --- a/src/API/Anilist/Model.php +++ b/src/API/Anilist/Model.php @@ -25,6 +25,8 @@ use Aviat\AnimeClient\API\Anilist; use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; use Aviat\AnimeClient\Types\FormItem; use Aviat\Ion\Json; +use Aviat\Ion\Di\Exception\ContainerException; +use Aviat\Ion\Di\Exception\NotFoundException; /** * Anilist API Model @@ -92,8 +94,8 @@ final class Model * * @param string $type * @return array - * @throws \Aviat\Ion\Di\Exception\ContainerException - * @throws \Aviat\Ion\Di\Exception\NotFoundException + * @throws ContainerException + * @throws NotFoundException */ public function getSyncList(string $type = 'anime'): array { @@ -144,7 +146,7 @@ final class Model ]; } - return $this->listItem->create($createData, $type); + return $this->listItem->create($createData); } /** diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php index 3acad63c..6718561c 100644 --- a/src/API/Kitsu/Auth.php +++ b/src/API/Kitsu/Auth.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\API\Kitsu; +use Aura\Session\Segment; + use const Aviat\AnimeClient\SESSION_SEGMENT; use Aviat\AnimeClient\API\{ @@ -23,7 +25,6 @@ use Aviat\AnimeClient\API\{ Kitsu as K }; use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; -use Exception; /** * Kitsu API Authentication @@ -42,7 +43,7 @@ final class Auth { /** * Session object * - * @var \Aura\Session\Segment + * @var Segment */ private $segment; diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index 8871c615..8d685feb 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -137,6 +137,9 @@ final class ListItem implements ListItemInterface { return $request->getFullRequest(); } + /** + * @return false|string + */ private function getAuthHeader() { $cache = $this->getContainer()->get('cache'); diff --git a/src/API/Kitsu/Transformer/CharacterTransformer.php b/src/API/Kitsu/Transformer/CharacterTransformer.php index 8bfc7206..adf7d9b7 100644 --- a/src/API/Kitsu/Transformer/CharacterTransformer.php +++ b/src/API/Kitsu/Transformer/CharacterTransformer.php @@ -142,23 +142,24 @@ final class CharacterTransformer extends AbstractTransformer { foreach ($role['relationships']['person']['people'] as $pid => $peoples) { $p = $peoples; + + $person = $p['attributes']; + $person['id'] = $pid; + $person['image'] = $person['image']['original']; + + uasort($role['relationships']['media']['anime'], static function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + + $item = [ + 'person' => $person, + 'series' => $role['relationships']['media']['anime'] + ]; + + $output[$roleName][$language][] = $item; } - - $person = $p['attributes']; - $person['id'] = $pid; - $person['image'] = $person['image']['original']; - - uasort($role['relationships']['media']['anime'], function ($a, $b) { - return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; - }); - - $item = [ - 'person' => $person, - 'series' => $role['relationships']['media']['anime'] - ]; - - $output[$roleName][$language][] = $item; - } else + } + else { foreach ($role['relationships']['person']['people'] as $pid => $person) { diff --git a/src/API/Kitsu/Transformer/MangaTransformer.php b/src/API/Kitsu/Transformer/MangaTransformer.php index a6e0fb8b..f268248f 100644 --- a/src/API/Kitsu/Transformer/MangaTransformer.php +++ b/src/API/Kitsu/Transformer/MangaTransformer.php @@ -130,6 +130,9 @@ final class MangaTransformer extends AbstractTransformer { ]); } + /** + * @return int|null|string + */ private function count(int $value = NULL) { return ((int)$value === 0) diff --git a/src/Command/UpdateThumbnails.php b/src/Command/UpdateThumbnails.php index afdfc01b..ff7fe97e 100644 --- a/src/Command/UpdateThumbnails.php +++ b/src/Command/UpdateThumbnails.php @@ -62,7 +62,12 @@ final class UpdateThumbnails extends ClearThumbnails { $this->echoBox('Finished regenerating all thumbnails'); } - public function getImageList() + /** + * @return array-key[][] + * + * @psalm-return array{anime: list, manga: list} + */ + public function getImageList(): array { $mangaList = $this->kitsuModel->getFullRawMangaList(); $includes = JsonAPI::organizeIncludes($mangaList['included']); diff --git a/src/Controller/Images.php b/src/Controller/Images.php index 55a17b69..4e047e63 100644 --- a/src/Controller/Images.php +++ b/src/Controller/Images.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\Controller; +use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; + use function Amp\Promise\wait; use function Aviat\AnimeClient\getResponse; use function Aviat\AnimeClient\createPlaceholderImage; @@ -32,17 +34,14 @@ final class Images extends BaseController { * @param string $type The category of image * @param string $file The filename to look for * @param bool $display Whether to output the image to the server - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @throws \TypeError - * @throws \Error - * @throws \Throwable * @return void + * @throws NotFoundException + * @throws \Throwable + * @throws ContainerException */ public function cache(string $type, string $file, $display = TRUE): void { - $currentUrl = $this->request->getUri()->__toString(); + $currentUrl = (string)$this->request->getUri(); $kitsuUrl = 'https://media.kitsu.io/'; $fileName = str_replace('-original', '', $file); diff --git a/src/Types/AbstractType.php b/src/Types/AbstractType.php index b6353894..f15371a1 100644 --- a/src/Types/AbstractType.php +++ b/src/Types/AbstractType.php @@ -37,7 +37,7 @@ abstract class AbstractType implements ArrayAccess, Countable { * * @param mixed $data */ - public function __construct($data = []) + final public function __construct($data = []) { $typeKeys = array_keys((array)$this); $dataKeys = array_keys((array)$data); @@ -187,7 +187,7 @@ abstract class AbstractType implements ArrayAccess, Countable { { $object = $parent ?? $this; - if (is_scalar($object) || empty($object)) + if (is_scalar($object) || $object === NULL) { return $object; } diff --git a/src/Util.php b/src/Util.php index b04f5ff6..565f589a 100644 --- a/src/Util.php +++ b/src/Util.php @@ -16,7 +16,8 @@ namespace Aviat\AnimeClient; -use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; +use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; +use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; /** * Utility method class @@ -45,8 +46,8 @@ class Util { * Set up the Util class * * @param ContainerInterface $container - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws ContainerException + * @throws NotFoundException */ public function __construct(ContainerInterface $container) { @@ -80,8 +81,8 @@ class Util { /** * Determine whether to show the sub-menu * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws ContainerException + * @throws NotFoundException * @return bool */ public function isViewPage(): bool @@ -98,8 +99,8 @@ class Util { * Determine whether the page is a page with a form, and * not suitable for redirection * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws ContainerException + * @throws NotFoundException * @return bool */ public function isFormPage(): bool diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testTransform__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testTransform__1.php index 8fb7c0d6..7559803d 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testTransform__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testTransform__1.php @@ -36,10 +36,10 @@ 1 => '小林さんちのメイドラゴン', ), )), - 'watching_status' => 'current', 'notes' => NULL, + 'private' => false, 'rewatching' => false, 'rewatched' => 0, 'user_rating' => '-', - 'private' => false, + 'watching_status' => 'current', )); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php deleted file mode 100644 index 4b97af5e..00000000 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php +++ /dev/null @@ -1,15 +0,0 @@ - 14047981, - 'anilist_item_id' => NULL, - 'mal_id' => NULL, - 'data' => - Aviat\AnimeClient\Types\FormItemData::__set_state(array( - 'notes' => 'Very formulaic.', - 'private' => false, - 'progress' => 38, - 'ratingTwenty' => 16, - 'reconsumeCount' => 0, - 'reconsuming' => false, - 'status' => 'current', - )), -)); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php deleted file mode 100644 index ca751d41..00000000 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php +++ /dev/null @@ -1,15 +0,0 @@ - 14047981, - 'anilist_item_id' => NULL, - 'mal_id' => '12345', - 'data' => - Aviat\AnimeClient\Types\FormItemData::__set_state(array( - 'notes' => 'Very formulaic.', - 'private' => true, - 'progress' => 38, - 'ratingTwenty' => 16, - 'reconsumeCount' => 0, - 'reconsuming' => true, - 'status' => 'current', - )), -)); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php deleted file mode 100644 index 9375de10..00000000 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php +++ /dev/null @@ -1,14 +0,0 @@ - 14047983, - 'anilist_item_id' => NULL, - 'mal_id' => '12347', - 'data' => - Aviat\AnimeClient\Types\FormItemData::__set_state(array( - 'notes' => '', - 'private' => true, - 'progress' => 12, - 'reconsumeCount' => 0, - 'reconsuming' => true, - 'status' => 'current', - )), -)); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php b/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php deleted file mode 100644 index 8d1c950f..00000000 --- a/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php +++ /dev/null @@ -1,201 +0,0 @@ - - Aviat\AnimeClient\Types\MangaListItem::__set_state(array( - 'id' => '15084773', - 'mal_id' => '26769', - 'chapters' => - array ( - 'read' => 67, - 'total' => '-', - ), - 'volumes' => - array ( - 'read' => '-', - 'total' => '-', - ), - 'manga' => - Aviat\AnimeClient\Types\MangaListItemDetail::__set_state(array( - 'genres' => - array ( - 0 => 'Comedy', - 1 => 'Romance', - 2 => 'School', - 3 => 'Slice of Life', - 4 => 'Thriller', - ), - 'id' => '20286', - 'image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999', - 'slug' => 'bokura-wa-minna-kawaisou', - 'title' => 'Bokura wa Minna Kawaisou', - 'titles' => - array ( - ), - 'type' => 'Manga', - 'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou', - )), - 'reading_status' => 'current', - 'notes' => '', - 'rereading' => false, - 'reread' => 0, - 'user_rating' => 9, - )), - 1 => - Aviat\AnimeClient\Types\MangaListItem::__set_state(array( - 'id' => '15085607', - 'mal_id' => '16', - 'chapters' => - array ( - 'read' => 17, - 'total' => 120, - ), - 'volumes' => - array ( - 'read' => '-', - 'total' => 14, - ), - 'manga' => - Aviat\AnimeClient\Types\MangaListItemDetail::__set_state(array( - 'genres' => - array ( - 0 => 'Comedy', - 1 => 'Ecchi', - 2 => 'Harem', - 3 => 'Romance', - 4 => 'Sports', - ), - 'id' => '47', - 'image' => 'https://media.kitsu.io/manga/poster_images/47/small.jpg?1434249493', - 'slug' => 'love-hina', - 'title' => 'Love Hina', - 'titles' => - array ( - ), - 'type' => 'Manga', - 'url' => 'https://kitsu.io/manga/love-hina', - )), - 'reading_status' => 'current', - 'notes' => '', - 'rereading' => false, - 'reread' => 0, - 'user_rating' => 7, - )), - 2 => - Aviat\AnimeClient\Types\MangaListItem::__set_state(array( - 'id' => '15084529', - 'mal_id' => '35003', - 'chapters' => - array ( - 'read' => 16, - 'total' => '-', - ), - 'volumes' => - array ( - 'read' => '-', - 'total' => '-', - ), - 'manga' => - Aviat\AnimeClient\Types\MangaListItemDetail::__set_state(array( - 'genres' => - array ( - 0 => 'Comedy', - 1 => 'Ecchi', - 2 => 'Gender Bender', - 3 => 'Romance', - 4 => 'School', - 5 => 'Sports', - 6 => 'Supernatural', - ), - 'id' => '11777', - 'image' => 'https://media.kitsu.io/manga/poster_images/11777/small.jpg?1438784325', - 'slug' => 'yamada-kun-to-7-nin-no-majo', - 'title' => 'Yamada-kun to 7-nin no Majo', - 'titles' => - array ( - 0 => 'Yamada-kun and the Seven Witches', - ), - 'type' => 'Manga', - 'url' => 'https://kitsu.io/manga/yamada-kun-to-7-nin-no-majo', - )), - 'reading_status' => 'current', - 'notes' => '', - 'rereading' => false, - 'reread' => 0, - 'user_rating' => 9, - )), - 3 => - Aviat\AnimeClient\Types\MangaListItem::__set_state(array( - 'id' => '15312827', - 'mal_id' => '78523', - 'chapters' => - array ( - 'read' => 68, - 'total' => '-', - ), - 'volumes' => - array ( - 'read' => '-', - 'total' => '-', - ), - 'manga' => - Aviat\AnimeClient\Types\MangaListItemDetail::__set_state(array( - 'genres' => - array ( - 0 => 'Romance', - 1 => 'School', - 2 => 'Slice of Life', - ), - 'id' => '27175', - 'image' => 'https://media.kitsu.io/manga/poster_images/27175/small.jpg?1464379411', - 'slug' => 'relife', - 'title' => 'ReLIFE', - 'titles' => - array ( - ), - 'type' => 'Manga', - 'url' => 'https://kitsu.io/manga/relife', - )), - 'reading_status' => 'current', - 'notes' => '', - 'rereading' => false, - 'reread' => 0, - 'user_rating' => '-', - )), - 4 => - Aviat\AnimeClient\Types\MangaListItem::__set_state(array( - 'id' => '15084769', - 'mal_id' => '60815', - 'chapters' => - array ( - 'read' => 43, - 'total' => '-', - ), - 'volumes' => - array ( - 'read' => '-', - 'total' => '-', - ), - 'manga' => - Aviat\AnimeClient\Types\MangaListItemDetail::__set_state(array( - 'genres' => - array ( - 0 => 'Comedy', - 1 => 'School', - 2 => 'Slice of Life', - ), - 'id' => '25491', - 'image' => 'https://media.kitsu.io/manga/poster_images/25491/small.jpg?1434305043', - 'slug' => 'joshikausei', - 'title' => 'Joshikausei', - 'titles' => - array ( - ), - 'type' => 'Manga', - 'url' => 'https://kitsu.io/manga/joshikausei', - )), - 'reading_status' => 'current', - 'notes' => '', - 'rereading' => false, - 'reread' => 0, - 'user_rating' => 8, - )), -); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.yml b/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.yml deleted file mode 100644 index 87effff0..00000000 --- a/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.yml +++ /dev/null @@ -1,5 +0,0 @@ -- null -- null -- null -- null -- null