Kaynağa Gözat

PHPStan level 6 for all PDO and Exception classes (#5239)

* PHPStan level 6 for all PDO and Exception classes
Contributes to https://github.com/FreshRSS/FreshRSS/issues/4112

* Fix type

* Now also our remaining own librairies

* Motivation for a few more files

* A few more DAO classes

* Last interface
Alexandre Alapetite 3 yıl önce
ebeveyn
işleme
288ed04ccc
46 değiştirilmiş dosya ile 183 ekleme ve 169 silme
  1. 3 2
      app/Controllers/feedController.php
  2. 2 1
      app/Controllers/tagController.php
  3. 4 2
      app/Controllers/userController.php
  4. 4 2
      app/Exceptions/AlreadySubscribedException.php
  5. 1 1
      app/Exceptions/BadUrlException.php
  6. 4 2
      app/Exceptions/FeedNotAddedException.php
  7. 4 2
      app/Exceptions/ZipException.php
  8. 1 1
      app/Mailers/UserMailer.php
  9. 6 5
      app/Models/BooleanSearch.php
  10. 2 1
      app/Models/CategoryDAO.php
  11. 2 1
      app/Models/CategoryDAOSQLite.php
  12. 14 4
      app/Models/DatabaseDAO.php
  13. 5 0
      app/Models/DatabaseDAOPGSQL.php
  14. 5 0
      app/Models/DatabaseDAOSQLite.php
  15. 2 1
      app/Models/FeedDAO.php
  16. 2 1
      app/Models/FeedDAOSQLite.php
  17. 13 42
      app/Models/ReadingMode.php
  18. 2 1
      app/Models/TagDAO.php
  19. 2 1
      app/Models/TagDAOSQLite.php
  20. 7 6
      app/Models/UserDAO.php
  21. 13 4
      cli/i18n/I18nCompletionValidator.php
  22. 13 4
      cli/i18n/I18nUsageValidator.php
  23. 3 10
      cli/i18n/I18nValidatorInterface.php
  24. 1 1
      lib/Minz/ActionException.php
  25. 1 1
      lib/Minz/Configuration.php
  26. 1 1
      lib/Minz/ConfigurationException.php
  27. 1 1
      lib/Minz/ControllerNotActionControllerException.php
  28. 1 1
      lib/Minz/ControllerNotExistException.php
  29. 1 1
      lib/Minz/CurrentPagePaginationException.php
  30. 2 2
      lib/Minz/Error.php
  31. 1 1
      lib/Minz/Exception.php
  32. 1 1
      lib/Minz/ExtensionException.php
  33. 1 1
      lib/Minz/FileNotExistException.php
  34. 2 0
      lib/Minz/Helper.php
  35. 3 0
      lib/Minz/Mailer.php
  36. 1 0
      lib/Minz/ModelPdo.php
  37. 1 1
      lib/Minz/PDOConnectionException.php
  38. 20 5
      lib/Minz/Pdo.php
  39. 6 2
      lib/Minz/PdoMysql.php
  40. 2 1
      lib/Minz/PdoPgsql.php
  41. 6 2
      lib/Minz/PdoSqlite.php
  42. 1 1
      lib/Minz/PermissionDeniedException.php
  43. 5 4
      lib/favicons.php
  44. 6 6
      lib/lib_date.php
  45. 5 4
      lib/lib_install.php
  46. 0 38
      tests/phpstan-next.txt

+ 3 - 2
app/Controllers/feedController.php

@@ -47,10 +47,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		$url = trim($url);
 
 		/** @var string|null $url */
-		$url = Minz_ExtensionManager::callHook('check_url_before_add', $url);
-		if (null === $url) {
+		$urlHooked = Minz_ExtensionManager::callHook('check_url_before_add', $url);
+		if ($urlHooked === $url) {
 			throw new FreshRSS_FeedNotAdded_Exception($url);
 		}
+		$url = $urlHooked;
 
 		$cat = null;
 		if ($cat_id > 0) {

+ 2 - 1
app/Controllers/tagController.php

@@ -126,7 +126,8 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 		$sourceId = Minz_Request::param('id_tag');
 
 		if ($targetName == '' || $sourceId == '') {
-			return Minz_Error::error(400);
+			Minz_Error::error(400);
+			return;
 		}
 
 		$tagDAO = FreshRSS_Factory::createTagDao();

+ 4 - 2
app/Controllers/userController.php

@@ -431,11 +431,13 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		} elseif (FreshRSS_Auth::hasAccess()) {
 			$user_config = FreshRSS_Context::$user_conf;
 		} else {
-			return Minz_Error::error(403);
+			Minz_Error::error(403);
+			return;
 		}
 
 		if (!FreshRSS_UserDAO::exists($username) || $user_config === null) {
-			return Minz_Error::error(404);
+			Minz_Error::error(404);
+			return;
 		}
 
 		if ($user_config->email_validation_token === '') {

+ 4 - 2
app/Exceptions/AlreadySubscribedException.php

@@ -1,14 +1,16 @@
 <?php
 
 class FreshRSS_AlreadySubscribed_Exception extends Exception {
+
+	/** @var string */
 	private $feedName = '';
 
-	public function __construct($url, $feedName) {
+	public function __construct(string $url, string $feedName) {
 		parent::__construct('Already subscribed! ' . $url, 2135);
 		$this->feedName = $feedName;
 	}
 
-	public function feedName() {
+	public function feedName(): string {
 		return $this->feedName;
 	}
 }

+ 1 - 1
app/Exceptions/BadUrlException.php

@@ -2,7 +2,7 @@
 
 class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {
 
-	public function __construct($url) {
+	public function __construct(string $url) {
 		parent::__construct('`' . $url . '` is not a valid URL');
 	}
 

+ 4 - 2
app/Exceptions/FeedNotAddedException.php

@@ -1,14 +1,16 @@
 <?php
 
 class FreshRSS_FeedNotAdded_Exception extends Exception {
+
+	/** @var string */
 	private $url = '';
 
-	public function __construct($url) {
+	public function __construct(string $url) {
 		parent::__construct('Feed not added! ' . $url, 2147);
 		$this->url = $url;
 	}
 
-	public function url() {
+	public function url(): string {
 		return $this->url;
 	}
 }

+ 4 - 2
app/Exceptions/ZipException.php

@@ -1,14 +1,16 @@
 <?php
 
 class FreshRSS_Zip_Exception extends Exception {
+
+	/** @var int */
 	private $zipErrorCode = 0;
 
-	public function __construct($zipErrorCode) {
+	public function __construct(int $zipErrorCode) {
 		parent::__construct('ZIP error!', 2141);
 		$this->zipErrorCode = $zipErrorCode;
 	}
 
-	public function zipErrorCode() {
+	public function zipErrorCode(): int {
 		return $this->zipErrorCode;
 	}
 }

+ 1 - 1
app/Mailers/UserMailer.php

@@ -10,7 +10,7 @@ class FreshRSS_User_Mailer extends Minz_Mailer {
 	 */
 	protected $view;
 
-	public function send_email_need_validation($username, $user_config) {
+	public function send_email_need_validation(string $username, FreshRSS_UserConfiguration $user_config): bool {
 		Minz_Translate::reset($user_config->language);
 
 		$this->view->_path('user_mailer/email_need_validation.txt.php');

+ 6 - 5
app/Models/BooleanSearch.php

@@ -10,10 +10,11 @@ class FreshRSS_BooleanSearch {
 	/** @var array<FreshRSS_BooleanSearch|FreshRSS_Search> */
 	private $searches = array();
 
-	/** @var string 'AND' or 'OR' or 'AND NOT' */
+	/** @var 'AND'|'OR'|'AND NOT' */
 	private $operator;
 
-	public function __construct(string $input, int $level = 0, $operator = 'AND') {
+	/** @param 'AND'|'OR'|'AND NOT' $operator */
+	public function __construct(string $input, int $level = 0, string $operator = 'AND') {
 		$this->operator = $operator;
 		$input = trim($input);
 		if ($input == '') {
@@ -221,7 +222,7 @@ class FreshRSS_BooleanSearch {
 		return false;
 	}
 
-	private function parseOrSegments(string $input) {
+	private function parseOrSegments(string $input): void {
 		$input = trim($input);
 		if ($input == '') {
 			return;
@@ -258,13 +259,13 @@ class FreshRSS_BooleanSearch {
 		return $this->searches;
 	}
 
-	/** @return string 'AND' or 'OR' depending on how this BooleanSearch should be combined */
+	/** @return 'AND'|'OR'|'AND NOT' depending on how this BooleanSearch should be combined */
 	public function operator(): string {
 		return $this->operator;
 	}
 
 	/** @param FreshRSS_BooleanSearch|FreshRSS_Search $search */
-	public function add($search) {
+	public function add($search): void {
 		$this->searches[] = $search;
 	}
 

+ 2 - 1
app/Models/CategoryDAO.php

@@ -75,7 +75,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 		return false;
 	}
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
 				$errorLines = explode("\n", $errorInfo[2], 2);	// The relevant column name is on the first line, other lines are noise

+ 2 - 1
app/Models/CategoryDAOSQLite.php

@@ -2,7 +2,8 @@
 
 class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) {
 			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
 			foreach (['kind', 'lastUpdate', 'error', 'attributes'] as $column) {

+ 14 - 4
app/Models/DatabaseDAO.php

@@ -63,13 +63,15 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		return count(array_keys($tables, true, true)) == count($tables);
 	}
 
+	/** @return array<array<string,string|bool>> */
 	public function getSchema(string $table): array {
 		$sql = 'DESC `_' . $table . '`';
 		$stm = $this->pdo->query($sql);
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
-	public function checkTable(string $table, $schema): bool {
+	/** @param array<string> $schema */
+	public function checkTable(string $table, array $schema): bool {
 		$columns = $this->getSchema($table);
 
 		$ok = (count($columns) == count($schema));
@@ -120,6 +122,10 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		));
 	}
 
+	/**
+	 * @param array<string,string> $dao
+	 * @return array<string,string|bool>
+	 */
 	public function daoToSchema(array $dao): array {
 		return array(
 			'name' => $dao['Field'],
@@ -129,7 +135,11 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		);
 	}
 
-	public function listDaoToSchema($listDAO): array {
+	/**
+	 * @param array<array<string,string>> $listDAO
+	 * @return array<array<string,string|bool>>
+	 */
+	public function listDaoToSchema(array $listDAO): array {
 		$list = array();
 
 		foreach ($listDAO as $dao) {
@@ -185,14 +195,14 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 		return $ok;
 	}
 
-	public function minorDbMaintenance() {
+	public function minorDbMaintenance(): void {
 		$catDAO = FreshRSS_Factory::createCategoryDao();
 		$catDAO->resetDefaultCategoryName();
 
 		$this->ensureCaseInsensitiveGuids();
 	}
 
-	private static function stdError($error): bool {
+	private static function stdError(string $error): bool {
 		if (defined('STDERR')) {
 			fwrite(STDERR, $error . "\n");
 		}

+ 5 - 0
app/Models/DatabaseDAOPGSQL.php

@@ -33,6 +33,7 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
 		return count(array_keys($tables, true, true)) == count($tables);
 	}
 
+	/** @return array<array<string,string|bool>> */
 	public function getSchema(string $table): array {
 		$sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
 		$stm = $this->pdo->prepare($sql);
@@ -40,6 +41,10 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
 		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
+	/**
+	 * @param array<string,string> $dao
+	 * @return array<string,string|bool>
+	 */
 	public function daoToSchema(array $dao): array {
 		return array(
 			'name' => $dao['field'],

+ 5 - 0
app/Models/DatabaseDAOSQLite.php

@@ -25,6 +25,7 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 		return count(array_keys($tables, true, true)) == count($tables);
 	}
 
+	/** @return array<array<string,string|bool>> */
 	public function getSchema(string $table): array {
 		$sql = 'PRAGMA table_info(' . $table . ')';
 		$stm = $this->pdo->query($sql);
@@ -45,6 +46,10 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 		));
 	}
 
+	/**
+	 * @param array<string,string> $dao
+	 * @return array<string,string|bool>
+	 */
 	public function daoToSchema(array $dao): array {
 		return [
 			'name'    => $dao['name'],

+ 2 - 1
app/Models/FeedDAO.php

@@ -19,7 +19,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		return false;
 	}
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
 				$errorLines = explode("\n", $errorInfo[2], 2);	// The relevant column name is on the first line, other lines are noise

+ 2 - 1
app/Models/FeedDAOSQLite.php

@@ -2,7 +2,8 @@
 
 class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) {
 			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
 			foreach (['attributes', 'kind'] as $column) {

+ 13 - 42
app/Models/ReadingMode.php

@@ -28,12 +28,9 @@ class FreshRSS_ReadingMode {
 
 	/**
 	 * ReadingMode constructor.
-	 * @param string $id
-	 * @param string $title
-	 * @param string[] $urlParams
-	 * @param bool $active
+	 * @param array<string> $urlParams
 	 */
-	public function __construct($id, $title, $urlParams, $active) {
+	public function __construct(string $id, string $title, array $urlParams, bool $active) {
 		$this->id = $id;
 		$this->name = _i($id);
 		$this->title = $title;
@@ -41,41 +38,24 @@ class FreshRSS_ReadingMode {
 		$this->isActive = $active;
 	}
 
-	/**
-	 * @return string
-	 */
-	public function getId() {
+	public function getId(): string {
 		return $this->id;
 	}
 
-	/**
-	 * @return string
-	 */
-	public function getName() {
+	public function getName(): string {
 		return $this->name;
 	}
 
-	/**
-	 * @param string $name
-	 * @return FreshRSS_ReadingMode
-	 */
-	public function setName($name) {
+	public function setName(string $name): FreshRSS_ReadingMode {
 		$this->name = $name;
 		return $this;
 	}
 
-	/**
-	 * @return string
-	 */
-	public function getTitle() {
+	public function getTitle(): string {
 		return $this->title;
 	}
 
-	/**
-	 * @param string $title
-	 * @return FreshRSS_ReadingMode
-	 */
-	public function setTitle($title) {
+	public function setTitle(string $title): FreshRSS_ReadingMode {
 		$this->title = $title;
 		return $this;
 	}
@@ -83,40 +63,31 @@ class FreshRSS_ReadingMode {
 	/**
 	 * @return array<string>
 	 */
-	public function getUrlParams() {
+	public function getUrlParams(): array {
 		return $this->urlParams;
 	}
 
 	/**
 	 * @param array<string> $urlParams
-	 * @return FreshRSS_ReadingMode
 	 */
-	public function setUrlParams($urlParams) {
+	public function setUrlParams(array $urlParams): FreshRSS_ReadingMode {
 		$this->urlParams = $urlParams;
 		return $this;
 	}
 
-	/**
-	 * @return bool
-	 */
-	public function isActive() {
+	public function isActive(): bool {
 		return $this->isActive;
 	}
 
-	/**
-	 * @param bool $isActive
-	 * @return FreshRSS_ReadingMode
-	 */
-	public function setIsActive($isActive) {
+	public function setIsActive(bool $isActive): FreshRSS_ReadingMode {
 		$this->isActive = $isActive;
 		return $this;
 	}
 
 	/**
-	 * Returns the built-in reading modes.
-	 * return ReadingMode[]
+	 * @return array<FreshRSS_ReadingMode> the built-in reading modes
 	 */
-	public static function getReadingModes() {
+	public static function getReadingModes(): array {
 		$actualView = Minz_Request::actionName();
 		$defaultCtrl = Minz_Request::defaultControllerName();
 		$isDefaultCtrl = Minz_Request::controllerName() === $defaultCtrl;

+ 2 - 1
app/Models/TagDAO.php

@@ -30,7 +30,8 @@ class FreshRSS_TagDAO extends Minz_ModelPdo {
 		return $ok;
 	}
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
 				if (stripos($errorInfo[2], 'tag') !== false) {

+ 2 - 1
app/Models/TagDAOSQLite.php

@@ -6,7 +6,8 @@ class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
 		return 'OR IGNORE';
 	}
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
 			$showCreate = $tableInfo->fetchColumn();
 			if (stripos($showCreate, 'tag') === false) {

+ 7 - 6
app/Models/UserDAO.php

@@ -1,7 +1,8 @@
 <?php
 
 class FreshRSS_UserDAO extends Minz_ModelPdo {
-	public function createUser() {
+
+	public function createUser(): bool {
 		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 
 		try {
@@ -21,7 +22,7 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		}
 	}
 
-	public function deleteUser() {
+	public function deleteUser(): bool {
 		if (defined('STDERR')) {
 			fwrite(STDERR, 'Deleting SQL data for user “' . $this->current_user . "”…\n");
 		}
@@ -38,18 +39,18 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		}
 	}
 
-	public static function exists($username) {
+	public static function exists(string $username): bool {
 		return is_dir(USERS_PATH . '/' . $username);
 	}
 
-	public static function touch($username = '') {
+	public static function touch(string $username = ''): bool {
 		if (!FreshRSS_user_Controller::checkUsername($username)) {
 			$username = Minz_User::name() ?? Minz_User::INTERNAL_USER;
 		}
 		return touch(USERS_PATH . '/' . $username . '/config.php');
 	}
 
-	public static function mtime($username) {
-		return @filemtime(USERS_PATH . '/' . $username . '/config.php');
+	public static function mtime(string $username): int {
+		return @(int)filemtime(USERS_PATH . '/' . $username . '/config.php');
 	}
 }

+ 13 - 4
cli/i18n/I18nCompletionValidator.php

@@ -4,18 +4,27 @@ require_once __DIR__ . '/I18nValidatorInterface.php';
 
 class I18nCompletionValidator implements I18nValidatorInterface {
 
+	/** @var array<string,array<string,I18nValue>> */
 	private $reference;
+	/** @var array<string,array<string,I18nValue>> */
 	private $language;
+	/** @var int */
 	private $totalEntries = 0;
+	/** @var int */
 	private $passEntries = 0;
+	/** @var string */
 	private $result = '';
 
-	public function __construct($reference, $language) {
+	/**
+	 * @param array<string,array<string,I18nValue>> $reference
+	 * @param array<string,array<string,I18nValue>> $language
+	 */
+	public function __construct(array $reference, array $language) {
 		$this->reference = $reference;
 		$this->language = $language;
 	}
 
-	public function displayReport() {
+	public function displayReport(): string {
 		if ($this->passEntries > $this->totalEntries) {
 			throw new \RuntimeException('The number of translated strings cannot be higher than the number of strings');
 		}
@@ -25,11 +34,11 @@ class I18nCompletionValidator implements I18nValidatorInterface {
 		return sprintf('Translation is %5.1f%% complete.', $this->passEntries / $this->totalEntries * 100) . PHP_EOL;
 	}
 
-	public function displayResult() {
+	public function displayResult(): string {
 		return $this->result;
 	}
 
-	public function validate() {
+	public function validate(): bool {
 		foreach ($this->reference as $file => $data) {
 			foreach ($data as $refKey => $refValue) {
 				$this->totalEntries++;

+ 13 - 4
cli/i18n/I18nUsageValidator.php

@@ -4,18 +4,27 @@ require_once __DIR__ . '/I18nValidatorInterface.php';
 
 class I18nUsageValidator implements I18nValidatorInterface {
 
+	/** @var array<string> */
 	private $code;
+	/** @var array<string,array<string,string>> */
 	private $reference;
+	/** @var int */
 	private $totalEntries = 0;
+	/** @var int */
 	private $failedEntries = 0;
+	/** @var string */
 	private $result = '';
 
-	public function __construct($reference, $code) {
+	/**
+	 * @param array<string,array<string,string>> $reference
+	 * @param array<string> $code
+	 */
+	public function __construct(array $reference, array $code) {
 		$this->code = $code;
 		$this->reference = $reference;
 	}
 
-	public function displayReport() {
+	public function displayReport(): string {
 		if ($this->failedEntries > $this->totalEntries) {
 			throw new \RuntimeException('The number of unused strings cannot be higher than the number of strings');
 		}
@@ -25,11 +34,11 @@ class I18nUsageValidator implements I18nValidatorInterface {
 		return sprintf('%5.1f%% of translation keys are unused.', $this->failedEntries / $this->totalEntries * 100) . PHP_EOL;
 	}
 
-	public function displayResult() {
+	public function displayResult(): string {
 		return $this->result;
 	}
 
-	public function validate() {
+	public function validate(): bool {
 		foreach ($this->reference as $file => $data) {
 			foreach ($data as $key => $value) {
 				$this->totalEntries++;

+ 3 - 10
cli/i18n/I18nValidatorInterface.php

@@ -5,21 +5,14 @@ interface I18nValidatorInterface {
 	/**
 	 * Display the validation result.
 	 * Empty if there are no errors.
-	 *
-	 * @return array
 	 */
-	public function displayResult();
+	public function displayResult(): string;
 
-	/**
-	 * @return bool
-	 */
-	public function validate();
+	public function validate(): bool;
 
 	/**
 	 * Display the validation report.
-	 *
-	 * @return string
 	 */
-	public function displayReport();
+	public function displayReport(): string;
 
 }

+ 1 - 1
lib/Minz/ActionException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_ActionException extends Minz_Exception {
-	public function __construct ($controller_name, $action_name, $code = self::ERROR) {
+	public function __construct(string $controller_name, string $action_name, int $code = self::ERROR) {
 		// Just for security, as we are not supposed to get non-alphanumeric characters.
 		$action_name = rawurlencode($action_name);
 

+ 1 - 1
lib/Minz/Configuration.php

@@ -8,7 +8,7 @@
  * @property-read string $environment
  * @property-read array<string> $extensions_enabled
  * @property-read string $mailer
- * @property-read string $smtp
+ * @property-read array<string|int|bool> $smtp
  * @property string $title
  */
 class Minz_Configuration {

+ 1 - 1
lib/Minz/ConfigurationException.php

@@ -1,7 +1,7 @@
 <?php
 
 class Minz_ConfigurationException extends Minz_Exception {
-	public function __construct($error, $code = self::ERROR) {
+	public function __construct(string $error, int $code = self::ERROR) {
 		$message = 'Configuration error: ' . $error;
 		parent::__construct($message, $code);
 	}

+ 1 - 1
lib/Minz/ControllerNotActionControllerException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_ControllerNotActionControllerException extends Minz_Exception {
-	public function __construct ($controller_name, $code = self::ERROR) {
+	public function __construct(string $controller_name, int $code = self::ERROR) {
 		$message = 'Controller `' . $controller_name . '` isn’t instance of ActionController';
 
 		parent::__construct ($message, $code);

+ 1 - 1
lib/Minz/ControllerNotExistException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_ControllerNotExistException extends Minz_Exception {
-	public function __construct ($code = self::ERROR) {
+	public function __construct(int $code = self::ERROR) {
 		$message = 'Controller not found!';
 		parent::__construct ($message, $code);
 	}

+ 1 - 1
lib/Minz/CurrentPagePaginationException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_CurrentPagePaginationException extends Minz_Exception {
-	public function __construct ($page) {
+	public function __construct(int $page) {
 		$message = 'Page number `' . $page . '` doesn\'t exist';
 
 		parent::__construct ($message, self::ERROR);

+ 2 - 2
lib/Minz/Error.php

@@ -19,7 +19,7 @@ class Minz_Error {
 	*      > $logs['notice']
 	* @param bool $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis)
 	*/
-	public static function error ($code = 404, $logs = array (), $redirect = true) {
+	public static function error(int $code = 404, array $logs = [], bool $redirect = true): void {
 		$logs = self::processLogs ($logs);
 		$error_filename = APP_PATH . '/Controllers/errorController.php';
 
@@ -52,7 +52,7 @@ class Minz_Error {
 	 * @param array<string,string>|string $logs logs sorted by category (error, warning, notice)
 	 * @return array<string> list of matching logs, without the category, according to environment preferences (production / development)
 	 */
-	private static function processLogs ($logs) {
+	private static function processLogs($logs) {
 		$conf = Minz_Configuration::get('system');
 		$env = $conf->environment;
 		$logs_ok = array ();

+ 1 - 1
lib/Minz/Exception.php

@@ -4,7 +4,7 @@ class Minz_Exception extends Exception {
 	const WARNING = 10;
 	const NOTICE = 20;
 
-	public function __construct ($message, $code = self::ERROR) {
+	public function __construct(string $message, int $code = self::ERROR) {
 		if ($code != Minz_Exception::ERROR
 		 && $code != Minz_Exception::WARNING
 		 && $code != Minz_Exception::NOTICE) {

+ 1 - 1
lib/Minz/ExtensionException.php

@@ -1,7 +1,7 @@
 <?php
 
 class Minz_ExtensionException extends Minz_Exception {
-	public function __construct ($message, $extension_name = false, $code = self::ERROR) {
+	public function __construct(string $message, ?string $extension_name = null, int $code = self::ERROR) {
 		if ($extension_name) {
 			$message = 'An error occurred in `' . $extension_name . '` extension with the message: ' . $message;
 		} else {

+ 1 - 1
lib/Minz/FileNotExistException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_FileNotExistException extends Minz_Exception {
-	public function __construct ($file_name, $code = self::ERROR) {
+	public function __construct(string $file_name, int $code = self::ERROR) {
 		$message = 'File not found: `' . $file_name.'`';
 
 		parent::__construct ($message, $code);

+ 2 - 0
lib/Minz/Helper.php

@@ -12,6 +12,8 @@ class Minz_Helper {
 	/**
 	 * Wrapper for htmlspecialchars.
 	 * Force UTf-8 value and can be used on array too.
+	 * @param string|array<string> $var
+	 * @return string|array<string>
 	 */
 	public static function htmlspecialchars_utf8($var) {
 		if (is_array($var)) {

+ 3 - 0
lib/Minz/Mailer.php

@@ -32,8 +32,11 @@ class Minz_Mailer {
 	 */
 	protected $view;
 
+	/** @var string */
 	private $mailer;
+	/** @var array<string|int|bool> */
 	private $smtp_config;
+	/** @var int */
 	private $debug_level;
 
 	/**

+ 1 - 0
lib/Minz/ModelPdo.php

@@ -12,6 +12,7 @@ class Minz_ModelPdo {
 
 	/**
 	 * Shares the connection to the database between all instances.
+	 * @var bool
 	 */
 	public static $usesSharedPdo = true;
 

+ 1 - 1
lib/Minz/PDOConnectionException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_PDOConnectionException extends Minz_Exception {
-	public function __construct ($error, $user, $code = self::ERROR) {
+	public function __construct(string $error, string $user, int $code = self::ERROR) {
 		$message = 'Access to database is denied for `' . $user . '`: ' . $error;
 
 		parent::__construct ($message, $code);

+ 20 - 5
lib/Minz/Pdo.php

@@ -6,18 +6,20 @@
  */
 
 abstract class Minz_Pdo extends PDO {
-	public function __construct(string $dsn, $username = null, $passwd = null, $options = null) {
+	/** @param array<int,int|string>|null $options */
+	public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) {
 		parent::__construct($dsn, $username, $passwd, $options);
 		$this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
 	}
 
-	abstract public function dbType();
+	abstract public function dbType(): string;
 
+	/** @var string */
 	private $prefix = '';
 	public function prefix(): string {
 		return $this->prefix;
 	}
-	public function setPrefix(string $prefix) {
+	public function setPrefix(string $prefix): void {
 		$this->prefix = $prefix;
 	}
 
@@ -33,6 +35,10 @@ abstract class Minz_Pdo extends PDO {
 	}
 
 	// PHP8+: PDO::lastInsertId(?string $name = null): string|false
+	/**
+	 * @param string|null $name
+	 * @return string|false
+	 */
 	#[\ReturnTypeWillChange]
 	public function lastInsertId($name = null) {
 		if ($name != null) {
@@ -42,6 +48,11 @@ abstract class Minz_Pdo extends PDO {
 	}
 
 	// PHP8+: PDO::prepare(string $query, array $options = []): PDOStatement|false
+	/**
+	 * @param string $statement
+	 * @param array<int,string>|null $driver_options
+	 * @return PDOStatement|false
+	 */
 	#[\ReturnTypeWillChange]
 	public function prepare($statement, $driver_options = []) {
 		$statement = $this->preSql($statement);
@@ -49,15 +60,19 @@ abstract class Minz_Pdo extends PDO {
 	}
 
 	// PHP8+: PDO::exec(string $statement): int|false
+	/**
+	 * @param string $statement
+	 * @return int|false
+	 */
 	#[\ReturnTypeWillChange]
 	public function exec($statement) {
 		$statement = $this->preSql($statement);
 		return parent::exec($statement);
 	}
 
-	// PHP8+: PDO::query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false
+	/** @return PDOStatement|false */
 	#[\ReturnTypeWillChange]
-	public function query($query, $fetch_mode = null, ...$fetch_mode_args) {
+	public function query(string $query, ?int $fetch_mode = null, ...$fetch_mode_args) {
 		$query = $this->preSql($query);
 		return $fetch_mode ? parent::query($query, $fetch_mode, ...$fetch_mode_args) : parent::query($query);
 	}

+ 6 - 2
lib/Minz/PdoMysql.php

@@ -6,7 +6,8 @@
  */
 
 class Minz_PdoMysql extends Minz_Pdo {
-	public function __construct(string $dsn, $username = null, $passwd = null, $options = null) {
+	/** @param array<int,int|string>|null $options */
+	public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) {
 		parent::__construct($dsn, $username, $passwd, $options);
 		$this->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
 	}
@@ -15,7 +16,10 @@ class Minz_PdoMysql extends Minz_Pdo {
 		return 'mysql';
 	}
 
-	// PHP8+: PDO::lastInsertId(?string $name = null): string|false
+	/**
+	 * @param string|null $name
+	 * @return string|false
+	 */
 	#[\ReturnTypeWillChange]
 	public function lastInsertId($name = null) {
 		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL

+ 2 - 1
lib/Minz/PdoPgsql.php

@@ -6,7 +6,8 @@
  */
 
 class Minz_PdoPgsql extends Minz_Pdo {
-	public function __construct(string $dsn, $username = null, $passwd = null, $options = null) {
+	/** @param array<int,int|string>|null $options */
+	public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) {
 		parent::__construct($dsn, $username, $passwd, $options);
 		$this->exec("SET NAMES 'UTF8';");
 	}

+ 6 - 2
lib/Minz/PdoSqlite.php

@@ -6,7 +6,8 @@
  */
 
 class Minz_PdoSqlite extends Minz_Pdo {
-	public function __construct(string $dsn, $username = null, $passwd = null, $options = null) {
+	/** @param array<int,int|string>|null $options */
+	public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) {
 		parent::__construct($dsn, $username, $passwd, $options);
 		$this->exec('PRAGMA foreign_keys = ON;');
 	}
@@ -15,7 +16,10 @@ class Minz_PdoSqlite extends Minz_Pdo {
 		return 'sqlite';
 	}
 
-	// PHP8+: PDO::lastInsertId(?string $name = null): string|false
+	/**
+	 * @param string|null $name
+	 * @return string|false
+	 */
 	#[\ReturnTypeWillChange]
 	public function lastInsertId($name = null) {
 		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL

+ 1 - 1
lib/Minz/PermissionDeniedException.php

@@ -1,6 +1,6 @@
 <?php
 class Minz_PermissionDeniedException extends Minz_Exception {
-	public function __construct ($file_name, $code = self::ERROR) {
+	public function __construct(string $file_name, int $code = self::ERROR) {
 		$message = 'Permission is denied for `' . $file_name.'`';
 
 		parent::__construct ($message, $code);

+ 5 - 4
lib/favicons.php

@@ -2,7 +2,7 @@
 const FAVICONS_DIR = DATA_PATH . '/favicons/';
 const DEFAULT_FAVICON = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
 
-function isImgMime($content) {
+function isImgMime(string $content): bool {
 	//Based on https://github.com/ArthurHoaro/favicon/blob/3a4f93da9bb24915b21771eb7873a21bde26f5d1/src/Favicon/Favicon.php#L311-L319
 	if ($content == '') {
 		return false;
@@ -21,7 +21,8 @@ function isImgMime($content) {
 	return $isImage;
 }
 
-function downloadHttp(&$url, $curlOptions = array()) {
+/** @param array<int,int|bool> $curlOptions */
+function downloadHttp(string &$url, array $curlOptions = []): string {
 	syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url);
 	$url = checkUrl($url);
 	if (!$url) {
@@ -49,7 +50,7 @@ function downloadHttp(&$url, $curlOptions = array()) {
 	return $info['http_code'] == 200 ? $response : '';
 }
 
-function searchFavicon(&$url) {
+function searchFavicon(string &$url): string {
 	$dom = new DOMDocument();
 	$html = downloadHttp($url);
 	if ($html != '' && @$dom->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) {
@@ -84,7 +85,7 @@ function searchFavicon(&$url) {
 	return '';
 }
 
-function download_favicon($url, $dest) {
+function download_favicon(string $url, string $dest): bool {
 	$url = trim($url);
 	$favicon = searchFavicon($url);
 	if ($favicon == '') {

+ 6 - 6
lib/lib_date.php

@@ -42,13 +42,13 @@ function example($dateInterval) {
 }
 */
 
-function _dateFloor($isoDate) {
+function _dateFloor(string $isoDate): string {
 	$x = explode('T', $isoDate, 2);
 	$t = isset($x[1]) ? str_pad($x[1], 6, '0') : '000000';
 	return str_pad($x[0], 8, '01') . 'T' . $t;
 }
 
-function _dateCeiling($isoDate) {
+function _dateCeiling(string $isoDate): string {
 	$x = explode('T', $isoDate, 2);
 	$t = isset($x[1]) && strlen($x[1]) > 1 ? str_pad($x[1], 6, '59') : '235959';
 	switch (strlen($x[0])) {
@@ -62,11 +62,11 @@ function _dateCeiling($isoDate) {
 	}
 }
 
-function _noDelimit($isoDate) {
+function _noDelimit(?string $isoDate): ?string {
 	return $isoDate === null || $isoDate === '' ? null : str_replace(array('-', ':'), '', $isoDate);	//FIXME: Bug with negative time zone
 }
 
-function _dateRelative($d1, $d2) {
+function _dateRelative(?string $d1, ?string $d2): ?string {
 	if ($d2 === null) {
 		return $d1 !== null && $d1[0] !== 'P' ? $d1 : null;
 	} elseif ($d2 !== '' && $d2[0] != 'P' && $d1 !== null && $d1[0] !== 'P') {
@@ -81,10 +81,10 @@ function _dateRelative($d1, $d2) {
 
 /**
  * Parameter $dateInterval is a string containing an ISO 8601 time interval.
- * Returns an array with the minimum and maximum Unix timestamp of this interval,
+ * @return array{int|null|false,int|null|false} an array with the minimum and maximum Unix timestamp of this interval,
  *  or null if open interval, or false if error.
  */
-function parseDateInterval($dateInterval) {
+function parseDateInterval(string $dateInterval) {
 	$dateInterval = trim($dateInterval);
 	$dateInterval = str_replace('--', '/', $dateInterval);
 	$dateInterval = strtoupper($dateInterval);

+ 5 - 4
lib/lib_install.php

@@ -3,7 +3,8 @@
 Minz_Configuration::register('default_system', join_path(FRESHRSS_PATH, 'config.default.php'));
 Minz_Configuration::register('default_user', join_path(FRESHRSS_PATH, 'config-user.default.php'));
 
-function checkRequirements($dbType = '') {
+/** @return array<string,string> */
+function checkRequirements(string $dbType = ''): array {
 	$php = version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION) >= 0;
 	$curl = extension_loaded('curl');
 	$pdo_mysql = extension_loaded('pdo_mysql');
@@ -74,11 +75,11 @@ function checkRequirements($dbType = '') {
 	);
 }
 
-function generateSalt() {
+function generateSalt(): string {
 	return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__)));
 }
 
-function initDb() {
+function initDb(): string {
 	$conf = FreshRSS_Context::$system_conf;
 	$db = $conf->db;
 	if (empty($db['pdo_options'])) {
@@ -115,7 +116,7 @@ function initDb() {
 	return $databaseDAO->testConnection();
 }
 
-function setupMigrations() {
+function setupMigrations(): bool {
 	$migrations_path = APP_PATH . '/migrations';
 	$migrations_version_path = DATA_PATH . '/applied_migrations.txt';
 

+ 0 - 38
tests/phpstan-next.txt

@@ -8,81 +8,43 @@
 ./app/Controllers/feedController.php
 ./app/Controllers/updateController.php
 ./app/Controllers/userController.php
-./app/Exceptions/AlreadySubscribedException.php
-./app/Exceptions/BadUrlException.php
-./app/Exceptions/FeedNotAddedException.php
-./app/Exceptions/ZipException.php
 ./app/install.php
-./app/Mailers/UserMailer.php
 ./app/Models/Auth.php
-./app/Models/BooleanSearch.php
 ./app/Models/Category.php
 ./app/Models/CategoryDAO.php
-./app/Models/CategoryDAOSQLite.php
 ./app/Models/ConfigurationSetter.php
-./app/Models/DatabaseDAO.php
-./app/Models/DatabaseDAOPGSQL.php
-./app/Models/DatabaseDAOSQLite.php
 ./app/Models/Entry.php
 ./app/Models/Feed.php
 ./app/Models/FeedDAO.php
-./app/Models/FeedDAOSQLite.php
 ./app/Models/FilterAction.php
 ./app/Models/FormAuth.php
-./app/Models/ReadingMode.php
 ./app/Models/Search.php
 ./app/Models/Share.php
 ./app/Models/TagDAO.php
-./app/Models/TagDAOSQLite.php
 ./app/Models/Themes.php
-./app/Models/UserDAO.php
 ./app/Models/View.php
 ./app/Services/ExportService.php
 ./app/Services/ImportService.php
 ./cli/_cli.php
 ./cli/_update-or-create-user.php
 ./cli/check.translation.php
-./cli/i18n/I18nCompletionValidator.php
 ./cli/i18n/I18nData.php
 ./cli/i18n/I18nFile.php
-./cli/i18n/I18nUsageValidator.php
-./cli/i18n/I18nValidatorInterface.php
 ./cli/i18n/I18nValue.php
 ./cli/manipulate.translation.php
 ./lib/core-extensions/Google-Groups/extension.php
 ./lib/core-extensions/Tumblr-GDPR/extension.php
-./lib/favicons.php
 ./lib/http-conditional.php
-./lib/lib_date.php
-./lib/lib_install.php
 ./lib/Minz/ActionController.php
-./lib/Minz/ActionException.php
 ./lib/Minz/Configuration.php
-./lib/Minz/ConfigurationException.php
-./lib/Minz/ControllerNotActionControllerException.php
-./lib/Minz/ControllerNotExistException.php
-./lib/Minz/CurrentPagePaginationException.php
 ./lib/Minz/Dispatcher.php
-./lib/Minz/Error.php
-./lib/Minz/Exception.php
 ./lib/Minz/Extension.php
-./lib/Minz/ExtensionException.php
 ./lib/Minz/ExtensionManager.php
-./lib/Minz/FileNotExistException.php
 ./lib/Minz/FrontController.php
-./lib/Minz/Helper.php
 ./lib/Minz/Log.php
-./lib/Minz/Mailer.php
 ./lib/Minz/Migrator.php
 ./lib/Minz/ModelArray.php
-./lib/Minz/ModelPdo.php
 ./lib/Minz/Paginator.php
-./lib/Minz/Pdo.php
-./lib/Minz/PDOConnectionException.php
-./lib/Minz/PdoMysql.php
-./lib/Minz/PdoPgsql.php
-./lib/Minz/PdoSqlite.php
-./lib/Minz/PermissionDeniedException.php
 ./lib/Minz/Request.php
 ./lib/Minz/Session.php
 ./lib/Minz/Translate.php