diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index e86da5cd..67c66a05 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,6 +23,7 @@ class Application extends App implements IBootstrap { public const APP_ID = 'suspicious_login'; + /** @psalm-mutation-free */ public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); } diff --git a/lib/Command/ETL.php b/lib/Command/ETL.php index f61ef796..9b9d8a87 100644 --- a/lib/Command/ETL.php +++ b/lib/Command/ETL.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** @psalm-immutable */ class ETL extends Command { public function __construct( diff --git a/lib/Command/Optimize.php b/lib/Command/Optimize.php index ce688e90..c4caf745 100644 --- a/lib/Command/Optimize.php +++ b/lib/Command/Optimize.php @@ -19,6 +19,7 @@ use function extension_loaded; use function time; +/** @psalm-immutable */ class Optimize extends Command { use ModelStatistics; diff --git a/lib/Command/Predict.php b/lib/Command/Predict.php index 8df773a6..181ab50f 100644 --- a/lib/Command/Predict.php +++ b/lib/Command/Predict.php @@ -20,6 +20,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** @psalm-immutable */ class Predict extends Command { public function __construct( diff --git a/lib/Command/Seed.php b/lib/Command/Seed.php index 4005b1af..1e48d722 100644 --- a/lib/Command/Seed.php +++ b/lib/Command/Seed.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** @psalm-immutable */ class Seed extends Command { use ModelStatistics; diff --git a/lib/Command/Train.php b/lib/Command/Train.php index e4bf5923..8e2a1366 100644 --- a/lib/Command/Train.php +++ b/lib/Command/Train.php @@ -24,6 +24,7 @@ use function extension_loaded; use function time; +/** @psalm-immutable */ class Train extends Command { use ModelStatistics; diff --git a/lib/Db/LoginAddressAggregatedSeeder.php b/lib/Db/LoginAddressAggregatedSeeder.php index f97b365a..1c329e94 100644 --- a/lib/Db/LoginAddressAggregatedSeeder.php +++ b/lib/Db/LoginAddressAggregatedSeeder.php @@ -17,6 +17,7 @@ class LoginAddressAggregatedSeeder { /** @var IDBConnection */ private $db; + /** @psalm-mutation-free */ public function __construct( IDBConnection $db, private readonly LoginAddressAggregatedMapper $mapper, diff --git a/lib/Db/Model.php b/lib/Db/Model.php index b4a26f06..028ec125 100644 --- a/lib/Db/Model.php +++ b/lib/Db/Model.php @@ -65,6 +65,7 @@ class Model extends Entity implements JsonSerializable { protected $createdAt; protected $addressType; + /** @psalm-mutation-free */ #[\Override] #[ReturnTypeWillChange] public function jsonSerialize(): array { diff --git a/lib/Exception/InsufficientDataException.php b/lib/Exception/InsufficientDataException.php index 479c6e66..2680b111 100644 --- a/lib/Exception/InsufficientDataException.php +++ b/lib/Exception/InsufficientDataException.php @@ -10,6 +10,7 @@ namespace OCA\SuspiciousLogin\Exception; class InsufficientDataException extends ServiceException { + /** @psalm-mutation-free */ public function __construct(string $message = '') { parent::__construct( $message === '' ? 'Insufficient data' : "Insufficient data: $message" diff --git a/lib/Listener/LoginListener.php b/lib/Listener/LoginListener.php index f668d657..b1d7a41b 100644 --- a/lib/Listener/LoginListener.php +++ b/lib/Listener/LoginListener.php @@ -21,6 +21,7 @@ * @implements IEventListener */ class LoginListener implements IEventListener { + /** @psalm-mutation-free */ public function __construct( private readonly IRequest $request, private readonly ITimeFactory $timeFactory, diff --git a/lib/Listener/LoginMailListener.php b/lib/Listener/LoginMailListener.php index f037cc6e..9956fd64 100644 --- a/lib/Listener/LoginMailListener.php +++ b/lib/Listener/LoginMailListener.php @@ -25,6 +25,7 @@ */ class LoginMailListener implements IEventListener { + /** @psalm-mutation-free */ public function __construct( private readonly LoggerInterface $logger, private readonly IMailer $mailer, diff --git a/lib/Listener/LoginNotificationListener.php b/lib/Listener/LoginNotificationListener.php index 93e5c75d..427feea0 100644 --- a/lib/Listener/LoginNotificationListener.php +++ b/lib/Listener/LoginNotificationListener.php @@ -21,6 +21,7 @@ */ class LoginNotificationListener implements IEventListener { + /** @psalm-mutation-free */ public function __construct( private readonly IManager $notificationManager, private readonly ITimeFactory $timeFactory, diff --git a/lib/Migration/Version4002Date20220922094803.php b/lib/Migration/Version4002Date20220922094803.php index 5646aca7..479ff064 100644 --- a/lib/Migration/Version4002Date20220922094803.php +++ b/lib/Migration/Version4002Date20220922094803.php @@ -22,6 +22,7 @@ class Version4002Date20220922094803 extends SimpleMigrationStep { */ protected $connection; + /** @psalm-mutation-free */ public function __construct(IDBConnection $connection) { $this->connection = $connection; } diff --git a/lib/Migration/Version9000Date20250114095826.php b/lib/Migration/Version9000Date20250114095826.php index 98ec2f9a..a8b594a7 100644 --- a/lib/Migration/Version9000Date20250114095826.php +++ b/lib/Migration/Version9000Date20250114095826.php @@ -18,6 +18,7 @@ class Version9000Date20250114095826 extends SimpleMigrationStep { + /** @psalm-mutation-free */ public function __construct( private readonly IJobList $jobList, ) { diff --git a/lib/Notifications/Notifier.php b/lib/Notifications/Notifier.php index b411dcda..dcc2d448 100644 --- a/lib/Notifications/Notifier.php +++ b/lib/Notifications/Notifier.php @@ -33,6 +33,7 @@ class Notifier implements INotifier { /** @var IURLGenerator */ protected $url; + /** @psalm-mutation-free */ public function __construct(IFactory $factory, IRequest $request, IConfig $config, IURLGenerator $urlGenerator) { $this->config = $config; $this->factory = $factory; @@ -40,6 +41,7 @@ public function __construct(IFactory $factory, IRequest $request, IConfig $confi $this->url = $urlGenerator; } + /** @psalm-pure */ #[\Override] public function getID(): string { return Application::APP_ID; diff --git a/lib/Service/AClassificationStrategy.php b/lib/Service/AClassificationStrategy.php index 519ffb2e..b9373286 100644 --- a/lib/Service/AClassificationStrategy.php +++ b/lib/Service/AClassificationStrategy.php @@ -20,11 +20,14 @@ use function str_split; abstract class AClassificationStrategy { + /** @psalm-pure */ abstract public static function getTypeName(): string; + /** @psalm-impure */ abstract public function hasSufficientData(LoginAddressAggregatedMapper $loginAddressMapper, int $validationDays): bool; /** + * @psalm-impure * @return LoginAddressAggregated[][] */ abstract public function findHistoricAndRecent(LoginAddressAggregatedMapper $loginAddressMapper, int $validationThreshold, int $maxAge): array; @@ -47,6 +50,7 @@ public function generateRandomIpVector(): array { } /** + * @psalm-pure * @param string $ip * * @return int[] @@ -79,6 +83,7 @@ protected function numStringsToBitArray(array $strings, int $base, int $padding) return array_map(fn ($x) => (int)$x, $converted); } + /** @psalm-pure */ protected function numStringToBitArray(string $s, int $base, int $padding): array { $bin = base_convert($s, $base, 2); // make sure we get 00000000 to 11111111 @@ -86,9 +91,12 @@ protected function numStringToBitArray(string $s, int $base, int $padding): arra return str_split($padded); } + /** @psalm-impure */ abstract public function generateRandomIp(): string; + /** @psalm-pure */ abstract public function getSize(): int; + /** @psalm-mutation-free */ abstract public function getDefaultMlpConfig(): Config; } diff --git a/lib/Service/CollectedData.php b/lib/Service/CollectedData.php index 4f6ed114..08f3f3ef 100644 --- a/lib/Service/CollectedData.php +++ b/lib/Service/CollectedData.php @@ -11,7 +11,9 @@ use Rubix\ML\Datasets\Labeled; +/** @psalm-immutable */ class CollectedData { + /** @psalm-mutation-free */ public function __construct( private readonly Labeled $trainingPositives, private readonly Labeled $validationPositives, diff --git a/lib/Service/DataLoader.php b/lib/Service/DataLoader.php index eba1cb09..c0c2f6cf 100644 --- a/lib/Service/DataLoader.php +++ b/lib/Service/DataLoader.php @@ -28,6 +28,7 @@ class DataLoader { private const MAX_SAMPLES_POSITIVES = 15_000; private const MAX_SAMPLES_VALIDATE_POSITIVES = 3_000; + /** @psalm-mutation-free */ public function __construct( private readonly LoginAddressAggregatedMapper $loginAddressMapper, private readonly NegativeSampleGenerator $negativeSampleGenerator, diff --git a/lib/Service/ETLService.php b/lib/Service/ETLService.php index a2f98c6f..d20cbf18 100644 --- a/lib/Service/ETLService.php +++ b/lib/Service/ETLService.php @@ -20,6 +20,7 @@ class ETLService { public const MAX_BATCH_SIZE = 10000; + /** @psalm-mutation-free */ public function __construct( private readonly IDBConnection $db, private readonly LoginAddressAggregatedMapper $aggregatedMapper, diff --git a/lib/Service/EstimatorService.php b/lib/Service/EstimatorService.php index 8632e881..e73045d9 100644 --- a/lib/Service/EstimatorService.php +++ b/lib/Service/EstimatorService.php @@ -17,6 +17,7 @@ class EstimatorService { + /** @psalm-mutation-free */ public function __construct( private readonly ModelStore $modelStore, private readonly LoggerInterface $logger, diff --git a/lib/Service/IpV6Strategy.php b/lib/Service/IpV6Strategy.php index 0ae1f4ad..7303aa79 100644 --- a/lib/Service/IpV6Strategy.php +++ b/lib/Service/IpV6Strategy.php @@ -22,6 +22,7 @@ use function substr; class IpV6Strategy extends AClassificationStrategy { + /** @psalm-pure */ #[\Override] public static function getTypeName(): string { return 'ipv6'; @@ -37,6 +38,7 @@ public function findHistoricAndRecent(LoginAddressAggregatedMapper $loginAddress return $loginAddressMapper->findHistoricAndRecentIpv6($validationThreshold, $maxAge); } + /** @psalm-pure */ #[\Override] protected function ipToVec(string $ip): array { $strippedIp = IPv6AddressParser::stripScopeIdentifier($ip); @@ -61,11 +63,13 @@ public function generateRandomIp(): string { return implode(':', array_map(fn (int $index) => base_convert((string)random_int(0, 2 ** 16 - 1), 10, 16), range(0, 7))); } + /** @psalm-pure */ #[\Override] public function getSize(): int { return 16 + 64; } + /** @psalm-mutation-free */ #[\Override] public function getDefaultMlpConfig(): Config { return Config::default()->setEpochs(20); diff --git a/lib/Service/Ipv4Strategy.php b/lib/Service/Ipv4Strategy.php index 242a510a..a71bdd4d 100644 --- a/lib/Service/Ipv4Strategy.php +++ b/lib/Service/Ipv4Strategy.php @@ -15,6 +15,7 @@ use function explode; class Ipv4Strategy extends AClassificationStrategy { + /** @psalm-pure */ #[\Override] public static function getTypeName(): string { return 'ipv4'; @@ -47,11 +48,13 @@ public function generateRandomIp(): string { return implode('.', array_map(fn (int $index) => random_int(0, 255), range(0, 3))); } + /** @psalm-pure */ #[\Override] public function getSize(): int { return 16 + 32; } + /** @psalm-mutation-free */ #[\Override] public function getDefaultMlpConfig(): Config { return Config::default(); diff --git a/lib/Service/LoginClassifier.php b/lib/Service/LoginClassifier.php index e7580437..3556dc97 100644 --- a/lib/Service/LoginClassifier.php +++ b/lib/Service/LoginClassifier.php @@ -28,6 +28,7 @@ class LoginClassifier { + /** @psalm-mutation-free */ public function __construct( private readonly EstimatorService $estimator, private readonly IRequest $request, diff --git a/lib/Service/LoginDataCollector.php b/lib/Service/LoginDataCollector.php index fb281db1..8ff183d2 100644 --- a/lib/Service/LoginDataCollector.php +++ b/lib/Service/LoginDataCollector.php @@ -14,6 +14,7 @@ class LoginDataCollector { + /** @psalm-mutation-free */ public function __construct( private readonly LoginAddressMapper $addressMapper, ) { diff --git a/lib/Service/MLP/Config.php b/lib/Service/MLP/Config.php index ea4496cd..15d276d6 100644 --- a/lib/Service/MLP/Config.php +++ b/lib/Service/MLP/Config.php @@ -23,6 +23,7 @@ public function __construct( ) { } + /** @psalm-pure */ public static function default() { return new static( 330, diff --git a/lib/Service/MLP/OptimizerService.php b/lib/Service/MLP/OptimizerService.php index 6e2165d2..4794b8ba 100644 --- a/lib/Service/MLP/OptimizerService.php +++ b/lib/Service/MLP/OptimizerService.php @@ -38,12 +38,14 @@ class OptimizerService { 'learningRate' => [0.0001, 0.01], ]; + /** @psalm-mutation-free */ public function __construct( private readonly DataLoader $loader, private readonly Trainer $trainer, ) { } + /** @psalm-mutation-free */ private function printConfig(int $epoch, float $stepWidth, Config $config, diff --git a/lib/Service/MLP/Trainer.php b/lib/Service/MLP/Trainer.php index 61071103..2d3872e4 100644 --- a/lib/Service/MLP/Trainer.php +++ b/lib/Service/MLP/Trainer.php @@ -33,6 +33,7 @@ class Trainer { /** @var ITimeFactory */ private $timeFactory; + /** @psalm-mutation-free */ public function __construct(ITimeFactory $timeFactory) { $this->timeFactory = $timeFactory; } diff --git a/lib/Service/ModelStore.php b/lib/Service/ModelStore.php index cfaff7e4..934e1dcc 100644 --- a/lib/Service/ModelStore.php +++ b/lib/Service/ModelStore.php @@ -34,6 +34,7 @@ class ModelStore { public const APPDATA_MODELS_FOLDER = 'models'; + /** @psalm-mutation-free */ public function __construct( private readonly ModelMapper $modelMapper, private readonly IAppData $appData, @@ -59,6 +60,7 @@ public function loadLatest(AClassificationStrategy $strategy): Estimator { return $this->load($latestModel->getId()); } + /** @psalm-pure */ private function getCacheKey(int $id): string { return "suspicious_login_model_$id"; } diff --git a/lib/Service/NotificationState.php b/lib/Service/NotificationState.php index 698c4139..003a740c 100644 --- a/lib/Service/NotificationState.php +++ b/lib/Service/NotificationState.php @@ -7,6 +7,7 @@ */ namespace OCA\SuspiciousLogin\Service; +/** @psalm-immutable */ class NotificationState { public const SENT = 1; public const NOT_SENT_DUPLICATE = 2; diff --git a/lib/Service/Statistics/AppStatistics.php b/lib/Service/Statistics/AppStatistics.php index 002a7084..611c5ed1 100644 --- a/lib/Service/Statistics/AppStatistics.php +++ b/lib/Service/Statistics/AppStatistics.php @@ -21,6 +21,7 @@ class AppStatistics implements JsonSerializable { + /** @psalm-mutation-free */ public function __construct( private readonly bool $active, /** @var Model[] */ @@ -58,6 +59,7 @@ public function getTrainingDataStatistics(): TrainingDataStatistics { return $this->trainingDataStatistics; } + /** @psalm-mutation-free */ #[\Override] #[ReturnTypeWillChange] public function jsonSerialize(): array { diff --git a/lib/Service/Statistics/TrainingDataStatistics.php b/lib/Service/Statistics/TrainingDataStatistics.php index 8617b01e..c94b0d0f 100644 --- a/lib/Service/Statistics/TrainingDataStatistics.php +++ b/lib/Service/Statistics/TrainingDataStatistics.php @@ -14,6 +14,7 @@ class TrainingDataStatistics implements JsonSerializable { + /** @psalm-mutation-free */ public function __construct( private readonly int $loginsCaptured, private readonly int $loginsAggregated, @@ -34,6 +35,7 @@ public function getLoginsAggregated(): int { return $this->loginsAggregated; } + /** @psalm-mutation-free */ #[\Override] #[ReturnTypeWillChange] public function jsonSerialize(): array { diff --git a/lib/Service/StatisticsService.php b/lib/Service/StatisticsService.php index 5c5d92e2..b5e09f44 100644 --- a/lib/Service/StatisticsService.php +++ b/lib/Service/StatisticsService.php @@ -18,6 +18,7 @@ class StatisticsService { + /** @psalm-mutation-free */ public function __construct( private readonly LoginAddressMapper $loginAddressMapper, private readonly LoginAddressAggregatedMapper $loginAddressAggregatedMapper, diff --git a/lib/Service/TrainService.php b/lib/Service/TrainService.php index 5a83ca1a..a911d9d9 100644 --- a/lib/Service/TrainService.php +++ b/lib/Service/TrainService.php @@ -17,6 +17,7 @@ class TrainService { + /** @psalm-mutation-free */ public function __construct( private readonly DataLoader $dataLoader, private readonly Trainer $trainer, diff --git a/lib/Service/TrainingDataConfig.php b/lib/Service/TrainingDataConfig.php index 8c2b3f8b..810bf2bc 100644 --- a/lib/Service/TrainingDataConfig.php +++ b/lib/Service/TrainingDataConfig.php @@ -15,6 +15,7 @@ class TrainingDataConfig implements JsonSerializable { + /** @psalm-mutation-free */ public function __construct( private int $maxAge, private int $threshold, @@ -80,6 +81,7 @@ public function setNow(int $now): TrainingDataConfig { return $clone; } + /** @psalm-mutation-free */ #[\Override] #[ReturnTypeWillChange] public function jsonSerialize(): array { diff --git a/lib/Service/TrainingResult.php b/lib/Service/TrainingResult.php index e074c293..cfc36465 100644 --- a/lib/Service/TrainingResult.php +++ b/lib/Service/TrainingResult.php @@ -13,8 +13,10 @@ use Rubix\ML\Learner; use Rubix\ML\Report; +/** @psalm-immutable */ class TrainingResult { + /** @psalm-mutation-free */ public function __construct( private readonly Learner $classifier, private readonly Model $model, diff --git a/lib/Task/TrainTask.php b/lib/Task/TrainTask.php index 6ba70229..fa36a1ca 100644 --- a/lib/Task/TrainTask.php +++ b/lib/Task/TrainTask.php @@ -21,6 +21,7 @@ class TrainTask implements Task { + /** @psalm-mutation-free */ public function __construct( private readonly Config $config, private readonly CollectedData $dataSet, diff --git a/lib/Util/AddressClassifier.php b/lib/Util/AddressClassifier.php index fcf90dac..3945c7d7 100644 --- a/lib/Util/AddressClassifier.php +++ b/lib/Util/AddressClassifier.php @@ -11,11 +11,14 @@ use function filter_var; +/** @psalm-immutable */ class AddressClassifier { + /** @psalm-pure */ public static function isIpV4(string $address): bool { return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false; } + /** @psalm-pure */ public static function isIpV6(string $address): bool { $strippedAddress = IPv6AddressParser::stripScopeIdentifier($address); diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json index bc3b652f..c88b7213 100644 --- a/vendor-bin/psalm/composer.json +++ b/vendor-bin/psalm/composer.json @@ -6,7 +6,7 @@ }, "require-dev": { "nextcloud/ocp": "dev-master", - "psalm/phar": "6.16.1", + "psalm/phar": "7.0.0-beta19", "psr/clock": "^1.0" } } diff --git a/vendor-bin/psalm/composer.lock b/vendor-bin/psalm/composer.lock index 0d39bf3b..288c2219 100644 --- a/vendor-bin/psalm/composer.lock +++ b/vendor-bin/psalm/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2b60204de428a474fada6676dda6c383", + "content-hash": "3919b8f1dd1290912f8dd5c4b933c956", "packages": [], "packages-dev": [ { @@ -58,16 +58,16 @@ }, { "name": "psalm/phar", - "version": "6.16.1", + "version": "7.0.0-beta19", "source": { "type": "git", "url": "https://github.com/psalm/phar.git", - "reference": "11c6b55449667837fc07bb2a456c45a137c05ecd" + "reference": "8f65c84236950ddb2aa1f9e44d8d4a3a073e685a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/psalm/phar/zipball/11c6b55449667837fc07bb2a456c45a137c05ecd", - "reference": "11c6b55449667837fc07bb2a456c45a137c05ecd", + "url": "https://api.github.com/repos/psalm/phar/zipball/8f65c84236950ddb2aa1f9e44d8d4a3a073e685a", + "reference": "8f65c84236950ddb2aa1f9e44d8d4a3a073e685a", "shasum": "" }, "require": { @@ -87,9 +87,9 @@ "description": "Composer-based Psalm Phar", "support": { "issues": "https://github.com/psalm/phar/issues", - "source": "https://github.com/psalm/phar/tree/6.16.1" + "source": "https://github.com/psalm/phar/tree/7.0.0-beta19" }, - "time": "2026-03-19T11:11:23+00:00" + "time": "2026-04-15T20:55:22+00:00" }, { "name": "psr/clock", @@ -296,7 +296,8 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "nextcloud/ocp": 20 + "nextcloud/ocp": 20, + "psalm/phar": 10 }, "prefer-stable": false, "prefer-lowest": false,