diff --git a/.gitattributes b/.gitattributes
index 20b04d1..0faf01d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,3 +6,4 @@
/.travis.yml export-ignore
/composer.lock export-ignore
/phpunit.xml.dist export-ignore
+/psalm.xml export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ce6f607..b2eb5e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -37,5 +37,10 @@ jobs:
- name: Code style checks
run: ./vendor/bin/phpcs .
+ - name: Run psalm
+ if: ${{ matrix.php-versions == '7.4' }}
+ run: |
+ php vendor/bin/psalm --config=psalm.xml --no-progress
+
- name: Unit tests
run: ./vendor/bin/phpunit
diff --git a/composer.json b/composer.json
index 2c40aaa..8769e2c 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
"ext-json": "*"
},
"require-dev": {
+ "vimeo/psalm": "4.*",
"phpunit/phpunit": "^8.5",
"php-coveralls/php-coveralls": "*",
"squizlabs/php_codesniffer": "3.*"
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..cf6bfb0
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Feedback.php b/src/Feedback.php
index 2982e3d..4143524 100644
--- a/src/Feedback.php
+++ b/src/Feedback.php
@@ -16,8 +16,8 @@ class Feedback
{
/**
* @param int $score
- * @param MatchInterface[] $sequence
- * @return array
+ * @param array $sequence
+ * @return array{warning: string, suggestions: array}
*/
public function getFeedback(int $score, array $sequence): array
{
@@ -42,7 +42,7 @@ public function getFeedback(int $score, array $sequence): array
// tie feedback to the longest match for longer sequences
$longestMatch = $sequence[0];
- foreach (array_slice($sequence, 1) as $match) {
+ foreach (\array_slice($sequence, 1) as $match) {
if (mb_strlen($match->token) > mb_strlen($longestMatch->token)) {
$longestMatch = $match;
}
diff --git a/src/Matcher.php b/src/Matcher.php
index c843ab6..bc0f71a 100644
--- a/src/Matcher.php
+++ b/src/Matcher.php
@@ -20,17 +20,20 @@ class Matcher
Matchers\YearMatch::class,
];
+ /**
+ * @var array, class-string>
+ */
private $additionalMatchers = [];
/**
* Get matches for a password.
*
* @param string $password Password string to match
- * @param array $userInputs Array of values related to the user (optional)
+ * @param array $userInputs Array of values related to the user (optional)
* @code array('Alice Smith')
* @endcode
*
- * @return MatchInterface[] Array of Match objects.
+ * @return BaseMatch[]
*
* @see zxcvbn/src/matching.coffee::omnimatch
*/
@@ -39,7 +42,7 @@ public function getMatches(string $password, array $userInputs = []): array
$matches = [];
foreach ($this->getMatchers() as $matcher) {
$matched = $matcher::match($password, $userInputs);
- if (is_array($matched) && !empty($matched)) {
+ if (!empty($matched)) {
$matches[] = $matched;
}
}
@@ -50,6 +53,11 @@ public function getMatches(string $password, array $userInputs = []): array
return $matches;
}
+ /**
+ * @param class-string $className
+ *
+ * @return $this
+ */
public function addMatcher(string $className): self
{
if (!is_a($className, MatchInterface::class, true)) {
@@ -71,8 +79,9 @@ public function addMatcher(string $className): self
* This function taken from https://github.com/vanderlee/PHP-stable-sort-functions
* Copyright © 2015-2018 Martijn van der Lee (http://martijn.vanderlee.com). MIT License applies.
*
- * @param array $array
- * @param callable $value_compare_func
+ * @template TSort
+ * @param array $array
+ * @param callable(TSort,TSort): int $value_compare_func
* @return bool
*/
public static function usortStable(array &$array, callable $value_compare_func): bool
@@ -81,13 +90,23 @@ public static function usortStable(array &$array, callable $value_compare_func):
foreach ($array as &$item) {
$item = [$index++, $item];
}
- $result = usort($array, function ($a, $b) use ($value_compare_func) {
- $result = $value_compare_func($a[1], $b[1]);
- return $result == 0 ? $a[0] - $b[0] : $result;
- });
+ unset($item);
+ $result = usort(
+ $array,
+ /**
+ * @param array $a
+ * @param array $b
+ * @return int
+ */
+ function ($a, $b) use ($value_compare_func) {
+ $result = $value_compare_func($a[1], $b[1]);
+ return $result == 0 ? $a[0] - $b[0] : $result;
+ }
+ );
foreach ($array as &$item) {
$item = $item[1];
}
+ unset($item);
return $result;
}
@@ -103,7 +122,7 @@ public static function compareMatches(BaseMatch $a, BaseMatch $b): int
/**
* Load available Match objects to match against a password.
*
- * @return array Array of classes implementing MatchInterface
+ * @return array>
*/
protected function getMatchers(): array
{
diff --git a/src/Matchers/BaseMatch.php b/src/Matchers/BaseMatch.php
index 527dbc5..8028495 100644
--- a/src/Matchers/BaseMatch.php
+++ b/src/Matchers/BaseMatch.php
@@ -11,27 +11,27 @@
abstract class BaseMatch implements MatchInterface
{
/**
- * @var
+ * @var string
*/
public $password;
/**
- * @var
+ * @var int
*/
public $begin;
/**
- * @var
+ * @var int
*/
public $end;
/**
- * @var
+ * @var string
*/
public $token;
/**
- * @var
+ * @var string
*/
public $pattern;
@@ -46,9 +46,9 @@ public function __construct(string $password, int $begin, int $end, string $toke
/**
* Get feedback to a user based on the match.
*
- * @param bool $isSoleMatch
+ * @param bool $isSoleMatch
* Whether this is the only match in the password
- * @return array
+ * @return array{warning: string, suggestions: array}
* Associative array with warning (string) and suggestions (array of strings)
*/
#[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])]
@@ -62,7 +62,7 @@ abstract public function getFeedback(bool $isSoleMatch): array;
* @param string $regex
* Regular expression with captures.
* @param int $offset
- * @return array
+ * @return array>
* Array of capture groups. Captures in a group have named indexes: 'begin', 'end', 'token'.
* e.g. fishfish /(fish)/
* array(
@@ -82,7 +82,7 @@ public static function findAll(string $string, string $regex, int $offset = 0):
// preg_match_all counts bytes, not characters: to correct this, we need to calculate the byte offset and pass
// that in instead.
$charsBeforeOffset = mb_substr($string, 0, $offset);
- $byteOffset = strlen($charsBeforeOffset);
+ $byteOffset = \strlen($charsBeforeOffset);
$count = preg_match_all($regex, $string, $matches, PREG_SET_ORDER, $byteOffset);
if (!$count) {
@@ -93,7 +93,7 @@ public static function findAll(string $string, string $regex, int $offset = 0):
foreach ($matches as $group) {
$captureBegin = 0;
$match = array_shift($group);
- $matchBegin = mb_strpos($string, $match, $offset);
+ $matchBegin = (int)mb_strpos($string, $match, $offset);
$captures = [
[
'begin' => $matchBegin,
@@ -102,7 +102,7 @@ public static function findAll(string $string, string $regex, int $offset = 0):
],
];
foreach ($group as $capture) {
- $captureBegin = mb_strpos($match, $capture, $captureBegin);
+ $captureBegin = (int)mb_strpos($match, $capture, $captureBegin);
$captures[] = [
'begin' => $matchBegin + $captureBegin,
'end' => $matchBegin + $captureBegin + mb_strlen($capture) - 1,
diff --git a/src/Matchers/Bruteforce.php b/src/Matchers/Bruteforce.php
index cba7bcc..c87efc4 100644
--- a/src/Matchers/Bruteforce.php
+++ b/src/Matchers/Bruteforce.php
@@ -21,7 +21,7 @@ class Bruteforce extends BaseMatch
/**
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @return Bruteforce[]
*/
public static function match(string $password, array $userInputs = []): array
diff --git a/src/Matchers/DateMatch.php b/src/Matchers/DateMatch.php
index f3fdcd9..570a8fb 100644
--- a/src/Matchers/DateMatch.php
+++ b/src/Matchers/DateMatch.php
@@ -20,6 +20,9 @@ class DateMatch extends BaseMatch
public $pattern = 'date';
+ /**
+ * @var array>>
+ */
private static $DATE_SPLITS = [
4 => [ # For length-4 strings, eg 1191 or 9111, two ways to split:
[1, 2], # 1 1 91 (2nd split starts at index 1, 3rd at index 2)
@@ -57,23 +60,39 @@ class DateMatch extends BaseMatch
*/
protected const DATE_WITH_SEPARATOR = '/^(\d{1,4})([\s\/\\\\_.-])(\d{1,2})\2(\d{1,4})$/u';
- /** @var int The day portion of the date in the token. */
+ /**
+ * The day portion of the date in the token.
+ *
+ * @var int
+ */
public $day;
- /** @var int The month portion of the date in the token. */
+ /**
+ * The month portion of the date in the token.
+ *
+ * @var int
+ */
public $month;
- /** @var int The year portion of the date in the token. */
+ /**
+ * The year portion of the date in the token.
+ *
+ * @var int
+ */
public $year;
- /** @var string The separator used for the date in the token. */
+ /**
+ * The separator used for the date in the token.
+ *
+ * @var string
+ */
public $separator;
/**
* Match occurences of dates in a password
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @return DateMatch[]
*/
public static function match(string $password, array $userInputs = []): array
@@ -124,7 +143,7 @@ public function getFeedback(bool $isSoleMatch): array
* @param int $begin
* @param int $end
* @param string $token
- * @param array $params An array with keys: [day, month, year, separator].
+ * @param array{day: int, month: int, year: int, separator: string} $params
*/
public function __construct(string $password, int $begin, int $end, string $token, array $params)
{
@@ -272,7 +291,7 @@ public static function getReferenceYear(): int
/**
* @param int[] $ints Three numbers in an array representing day, month and year (not necessarily in that order).
- * @return array|bool Returns an associative array containing 'day', 'month' and 'year' keys, or false if the
+ * @return array{year: int, month: int, day: int}|false Returns an associative array containing 'day', 'month' and 'year' keys, or false if the
* provided date array is invalid.
*/
protected static function checkDate(array $ints)
@@ -319,7 +338,8 @@ protected static function checkDate(array $ints)
foreach ($possibleYearSplits as [$year, $rest]) {
if ($year >= static::MIN_YEAR && $year <= static::MAX_YEAR) {
- if ($dm = static::mapIntsToDayMonth($rest)) {
+ $dm = static::mapIntsToDayMonth($rest);
+ if ($dm) {
return [
'year' => $year,
'month' => $dm['month'],
@@ -334,7 +354,8 @@ protected static function checkDate(array $ints)
}
foreach ($possibleYearSplits as [$year, $rest]) {
- if ($dm = static::mapIntsToDayMonth($rest)) {
+ $dm = static::mapIntsToDayMonth($rest);
+ if ($dm) {
return [
'year' => static::twoToFourDigitYear($year),
'month' => $dm['month'],
@@ -348,7 +369,9 @@ protected static function checkDate(array $ints)
/**
* @param int[] $ints Two numbers in an array representing day and month (not necessarily in that order).
- * @return array|bool Returns an associative array containing 'day' and 'month' keys, or false if any combination
+ *
+ * @return array{day: int, month: int}|false
+ * Returns an associative array containing 'day' and 'month' keys, or false if any combination
* of the two numbers does not match a day and month.
*/
protected static function mapIntsToDayMonth(array $ints)
diff --git a/src/Matchers/DictionaryMatch.php b/src/Matchers/DictionaryMatch.php
index b936681..607797c 100644
--- a/src/Matchers/DictionaryMatch.php
+++ b/src/Matchers/DictionaryMatch.php
@@ -10,24 +10,39 @@
class DictionaryMatch extends BaseMatch
{
+ /**
+ * @var string
+ */
public $pattern = 'dictionary';
- /** @var string The name of the dictionary that the token was found in. */
+ /**
+ * @var string The name of the dictionary that the token was found in.
+ */
public $dictionaryName;
- /** @var int The rank of the token in the dictionary. */
+ /**
+ * @var int The rank of the token in the dictionary.
+ */
public $rank;
- /** @var string The word that was matched from the dictionary. */
+ /**
+ * @var string The word that was matched from the dictionary.
+ */
public $matchedWord;
- /** @var bool Whether or not the matched word was reversed in the token. */
+ /**
+ * @var bool Whether or not the matched word was reversed in the token.
+ */
public $reversed = false;
- /** @var bool Whether or not the token contained l33t substitutions. */
+ /**
+ * @var bool Whether or not the token contained l33t substitutions.
+ */
public $l33t = false;
- /** @var array A cache of the frequency_lists json file */
+ /**
+ * @var array A cache of the frequency_lists json file
+ */
protected static $rankedDictionaries = [];
protected const START_UPPER = "/^[A-Z][^A-Z]+$/u";
@@ -39,7 +54,7 @@ class DictionaryMatch extends BaseMatch
* Match occurrences of dictionary words in password.
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @param array $rankedDictionaries
* @return DictionaryMatch[]
*/
@@ -75,7 +90,7 @@ public static function match(string $password, array $userInputs = [], array $ra
* @param int $begin
* @param int $end
* @param string $token
- * @param array $params An array with keys: [dictionary_name, matched_word, rank].
+ * @param array{dictionary_name?: string, matched_word?: string, rank?: int} $params
*/
public function __construct(string $password, int $begin, int $end, string $token, array $params = [])
{
@@ -89,7 +104,7 @@ public function __construct(string $password, int $begin, int $end, string $toke
/**
* @param bool $isSoleMatch
- * @return array
+ * @return array{suggestions: array, warning: string}
*/
#[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])]
public function getFeedback(bool $isSoleMatch): array
@@ -186,7 +201,7 @@ protected static function dictionaryMatch(string $password, array $dict): array
protected static function getRankedDictionaries(): array
{
if (empty(self::$rankedDictionaries)) {
- $json = file_get_contents(dirname(__FILE__) . '/frequency_lists.json');
+ $json = file_get_contents(__DIR__ . '/frequency_lists.json');
$data = json_decode($json, true);
$rankedLists = [];
diff --git a/src/Matchers/L33tMatch.php b/src/Matchers/L33tMatch.php
index 6b6288b..f3d6098 100644
--- a/src/Matchers/L33tMatch.php
+++ b/src/Matchers/L33tMatch.php
@@ -14,20 +14,32 @@
*/
class L33tMatch extends DictionaryMatch
{
- /** @var array An array of substitutions made to get from the token to the dictionary word. */
+ /**
+ * An array of substitutions made to get from the token to the dictionary word.
+ *
+ * @var array
+ */
public $sub = [];
- /** @var string A user-readable string that shows which substitutions were detected. */
+ /**
+ * A user-readable string that shows which substitutions were detected.
+ *
+ * @var null|string
+ */
public $subDisplay;
- /** @var bool Whether or not the token contained l33t substitutions. */
+ /**
+ * Whether or not the token contained l33t substitutions.
+ *
+ * @var bool
+ */
public $l33t = true;
/**
* Match occurences of l33t words in password to dictionary words.
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @param array $rankedDictionaries
* @return L33tMatch[]
*/
@@ -87,7 +99,7 @@ public static function match(string $password, array $userInputs = [], array $ra
* @param int $begin
* @param int $end
* @param string $token
- * @param array $params An array with keys: [sub, sub_display].
+ * @param array|array{sub?: array, sub_display?: string} $params
*/
public function __construct(string $password, int $begin, int $end, string $token, array $params = [])
{
@@ -146,7 +158,7 @@ protected static function getL33tSubtable(string $password): array
$table = static::getL33tTable();
foreach ($table as $letter => $substitutions) {
foreach ($substitutions as $sub) {
- if (in_array($sub, $passwordChars)) {
+ if (\in_array($sub, $passwordChars)) {
$subTable[$letter][] = $sub;
}
}
@@ -218,12 +230,28 @@ protected function getL33tVariations(): float
foreach ($this->sub as $substitution => $letter) {
$characters = preg_split('//u', mb_strtolower($this->token), -1, PREG_SPLIT_NO_EMPTY);
- $subbed = count(array_filter($characters, function ($character) use ($substitution) {
- return (string)$character === (string)$substitution;
- }));
- $unsubbed = count(array_filter($characters, function ($character) use ($letter) {
- return (string)$character === (string)$letter;
- }));
+ $subbed = count(array_filter(
+ $characters,
+ /**
+ * @param string $character
+ * @return bool
+ */
+ function ($character) use ($substitution) {
+ /** @psalm-suppress RedundantCastGivenDocblockType | maybe it's needed here? */
+ return (string)$character === (string)$substitution;
+ }
+ ));
+ $unsubbed = count(array_filter(
+ $characters,
+ /**
+ * @param string $character
+ * @return bool
+ */
+ function ($character) use ($letter) {
+ /** @psalm-suppress RedundantCastGivenDocblockType | maybe it's needed here? */
+ return (string)$character === (string)$letter;
+ }
+ ));
if ($subbed === 0 || $unsubbed === 0) {
// for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
diff --git a/src/Matchers/MatchInterface.php b/src/Matchers/MatchInterface.php
index 24f1944..1f25c7d 100644
--- a/src/Matchers/MatchInterface.php
+++ b/src/Matchers/MatchInterface.php
@@ -10,11 +10,11 @@ interface MatchInterface
* Match this password.
*
* @param string $password Password to check for match
- * @param array $userInputs Array of values related to the user (optional)
+ * @param array $userInputs Array of values related to the user (optional)
* @code array('Alice Smith')
* @endcode
*
- * @return array|BaseMatch[] Array of Match objects
+ * @return BaseMatch[]
*/
public static function match(string $password, array $userInputs = []): array;
diff --git a/src/Matchers/RepeatMatch.php b/src/Matchers/RepeatMatch.php
index 3f38da2..039b064 100644
--- a/src/Matchers/RepeatMatch.php
+++ b/src/Matchers/RepeatMatch.php
@@ -16,23 +16,39 @@ class RepeatMatch extends BaseMatch
public $pattern = 'repeat';
- /** @var MatchInterface[] An array of matches for the repeated section itself. */
+ /**
+ * An array of matches for the repeated section itself.
+ *
+ * @var array
+ */
public $baseMatches = [];
- /** @var int The number of guesses required for the repeated section itself. */
+ /**
+ * The number of guesses required for the repeated section itself.
+ *
+ * @var float
+ */
public $baseGuesses;
- /** @var int The number of times the repeated section is repeated. */
+ /**
+ * The number of times the repeated section is repeated.
+ *
+ * @var int
+ */
public $repeatCount;
- /** @var string The string that was repeated in the token. */
+ /**
+ * The string that was repeated in the token.
+ *
+ * @var string
+ */
public $repeatedChar;
/**
* Match 3 or more repeated characters.
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @return RepeatMatch[]
*/
public static function match(string $password, array $userInputs = []): array
@@ -64,7 +80,7 @@ public static function match(string $password, array $userInputs = []): array
$baseMatches = $baseAnalysis['sequence'];
$baseGuesses = $baseAnalysis['guesses'];
- $repeatCount = mb_strlen($match[0]['token']) / mb_strlen($repeatedChar);
+ $repeatCount = (int)(mb_strlen($match[0]['token']) / mb_strlen($repeatedChar));
$matches[] = new static(
$password,
@@ -105,14 +121,14 @@ public function getFeedback(bool $isSoleMatch): array
* @param int $begin
* @param int $end
* @param string $token
- * @param array $params An array with keys: [repeated_char, base_guesses, base_matches, repeat_count].
+ * @param array|array{repeated_char?: string, base_guesses?: float, base_marches?: array, repeat_count?: int} $params
*/
public function __construct(string $password, int $begin, int $end, string $token, array $params = [])
{
parent::__construct($password, $begin, $end, $token);
if (!empty($params)) {
$this->repeatedChar = $params['repeated_char'] ?? '';
- $this->baseGuesses = $params['base_guesses'] ?? 0;
+ $this->baseGuesses = $params['base_guesses'] ?? 0.0;
$this->baseMatches = $params['base_matches'] ?? [];
$this->repeatCount = $params['repeat_count'] ?? 0;
}
diff --git a/src/Matchers/ReverseDictionaryMatch.php b/src/Matchers/ReverseDictionaryMatch.php
index c7b86b6..d1aa110 100644
--- a/src/Matchers/ReverseDictionaryMatch.php
+++ b/src/Matchers/ReverseDictionaryMatch.php
@@ -9,14 +9,18 @@
class ReverseDictionaryMatch extends DictionaryMatch
{
- /** @var bool Whether or not the matched word was reversed in the token. */
+ /**
+ * Whether or not the matched word was reversed in the token.
+ *
+ * @var bool
+ */
public $reversed = true;
/**
* Match occurences of reversed dictionary words in password.
*
- * @param $password
- * @param array $userInputs
+ * @param string $password
+ * @param array $userInputs
* @param array $rankedDictionaries
* @return ReverseDictionaryMatch[]
*/
diff --git a/src/Matchers/SequenceMatch.php b/src/Matchers/SequenceMatch.php
index 1350f2a..cdd96a7 100644
--- a/src/Matchers/SequenceMatch.php
+++ b/src/Matchers/SequenceMatch.php
@@ -12,20 +12,32 @@ class SequenceMatch extends BaseMatch
public $pattern = 'sequence';
- /** @var string The name of the detected sequence. */
+ /**
+ * The name of the detected sequence.
+ *
+ * @var string
+ */
public $sequenceName;
- /** @var int The number of characters in the complete sequence space. */
+ /**
+ * The number of characters in the complete sequence space.
+ *
+ * @var int
+ */
public $sequenceSpace;
- /** @var bool True if the sequence is ascending, and false if it is descending. */
+ /**
+ * True if the sequence is ascending, and false if it is descending.
+ *
+ * @var bool
+ */
public $ascending;
/**
* Match sequences of three or more characters.
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @return SequenceMatch[]
*/
public static function match(string $password, array $userInputs = []): array
@@ -54,11 +66,20 @@ public static function match(string $password, array $userInputs = []): array
$lastDelta = $delta;
}
- static::findSequenceMatch($password, $begin, $passwordLength - 1, $lastDelta, $matches);
+ static::findSequenceMatch($password, $begin, $passwordLength - 1, (int)$lastDelta, $matches);
return $matches;
}
+ /**
+ * @param string $password
+ * @param int $begin
+ * @param int $end
+ * @param int $delta
+ * @param array $matches
+ *
+ * @return void
+ */
public static function findSequenceMatch(string $password, int $begin, int $end, int $delta, array &$matches)
{
if ($end - $begin > 1 || abs($delta) === 1) {
@@ -120,7 +141,7 @@ protected function getRawGuesses(): float
$firstCharacter = mb_substr($this->token, 0, 1);
$guesses = 0;
- if (in_array($firstCharacter, array('a', 'A', 'z', 'Z', '0', '1', '9'), true)) {
+ if (\in_array($firstCharacter, array('a', 'A', 'z', 'Z', '0', '1', '9'), true)) {
$guesses += 4; // lower guesses for obvious starting points
} elseif (ctype_digit($firstCharacter)) {
$guesses += 10; // digits
diff --git a/src/Matchers/SpatialMatch.php b/src/Matchers/SpatialMatch.php
index 6de328b..00b21ff 100644
--- a/src/Matchers/SpatialMatch.php
+++ b/src/Matchers/SpatialMatch.php
@@ -20,23 +20,39 @@ class SpatialMatch extends BaseMatch
public $pattern = 'spatial';
- /** @var int The number of characters the shift key was held for in the token. */
+ /**
+ * The number of characters the shift key was held for in the token.
+ *
+ * @var int
+ */
public $shiftedCount;
- /** @var int The number of turns on the keyboard required to complete the token. */
+ /**
+ * The number of turns on the keyboard required to complete the token.
+ *
+ * @var int
+ */
public $turns;
- /** @var string The keyboard layout that the token is a spatial match on. */
+ /**
+ * The keyboard layout that the token is a spatial match on.
+ *
+ * @var string
+ */
public $graph;
- /** @var array A cache of the adjacency_graphs json file */
+ /**
+ * A cache of the adjacency_graphs json file.
+ *
+ * @var array|array{qwerty: array>, keypad: array>, mac_keypad: array>}
+ */
protected static $adjacencyGraphs = [];
/**
* Match spatial patterns based on keyboard layouts (e.g. qwerty, dvorak, keypad).
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @param array $graphs
* @return SpatialMatch[]
*/
@@ -95,7 +111,7 @@ public function __construct(string $password, int $begin, int $end, string $toke
* @param string $password
* @param array $graph
* @param string $graphName
- * @return array
+ * @return array|array
*/
protected static function graphMatch(string $password, array $graph, string $graphName): array
{
@@ -201,12 +217,13 @@ protected static function indexOf(string $string, string $char): int
public static function getAdjacencyGraphs(): array
{
if (empty(self::$adjacencyGraphs)) {
- $json = file_get_contents(dirname(__FILE__) . '/adjacency_graphs.json');
+ $json = file_get_contents(__DIR__ . '/adjacency_graphs.json');
$data = json_decode($json, true);
// This seems pointless, but the data file is not guaranteed to be in any particular order.
// We want to be in the exact order below so as to match most closely with upstream, because when a match
// can be found in multiple graphs (such as 789), the one that's listed first is that one that will be picked.
+ /** @var array|array{qwerty: array>, keypad: array>, mac_keypad: array>} $data */
$data = [
'qwerty' => $data['qwerty'],
'dvorak' => $data['dvorak'],
diff --git a/src/Matchers/YearMatch.php b/src/Matchers/YearMatch.php
index 9deeea6..987697d 100644
--- a/src/Matchers/YearMatch.php
+++ b/src/Matchers/YearMatch.php
@@ -12,13 +12,17 @@ class YearMatch extends BaseMatch
public const NUM_YEARS = 119;
public $pattern = 'regex';
+
+ /**
+ * @var string
+ */
public $regexName = 'recent_year';
/**
* Match occurrences of years in a password
*
* @param string $password
- * @param array $userInputs
+ * @param array $userInputs
* @return YearMatch[]
*/
public static function match(string $password, array $userInputs = []): array
@@ -46,6 +50,7 @@ public function getFeedback(bool $isSoleMatch): array
protected function getRawGuesses(): float
{
+ \assert(is_numeric($this->token));
$yearSpace = abs($this->token - DateMatch::getReferenceYear());
return max($yearSpace, DateMatch::MIN_YEAR_SPACE);
}
diff --git a/src/Math/Binomial.php b/src/Math/Binomial.php
index 94e882f..e665b46 100644
--- a/src/Math/Binomial.php
+++ b/src/Math/Binomial.php
@@ -10,6 +10,9 @@
class Binomial
{
+ /**
+ * @var null|BinomialProvider
+ */
private static $provider = null;
private function __construct()
@@ -39,13 +42,13 @@ public static function getProvider(): BinomialProvider
}
/**
- * @return string[]
+ * @return array>
*/
public static function getUsableProviderClasses(): array
{
// In order of priority. The first provider with a value of true will be used.
$possibleProviderClasses = [
- BinomialProviderPhp73Gmp::class => function_exists('gmp_binomial'),
+ BinomialProviderPhp73Gmp::class => \function_exists('gmp_binomial'),
BinomialProviderInt64::class => PHP_INT_SIZE >= 8,
BinomialProviderFloat64::class => PHP_FLOAT_DIG >= 15,
];
diff --git a/src/Scorer.php b/src/Scorer.php
index 7cbd258..c9abff4 100644
--- a/src/Scorer.php
+++ b/src/Scorer.php
@@ -20,8 +20,19 @@ class Scorer
public const MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10;
public const MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50;
+ /**
+ * @var string
+ */
protected $password;
+
+ /**
+ * @var bool
+ */
protected $excludeAdditive;
+
+ /**
+ * @var array|array{g: array>, m: array>, pi: array>}
+ */
protected $optimal = [];
/**
@@ -57,9 +68,9 @@ class Scorer
* D^(l-1) approximates Sum(D^i for i in [1..l-1]
*
* @param string $password
- * @param MatchInterface[] $matches
+ * @param array $matches
* @param bool $excludeAdditive
- * @return array Returns an array with these keys: [password, guesses, guesses_log10, sequence]
+ * @return array{password: string, guesses: float, guesses_log10: float, sequence: array}
*/
public function getMostGuessableMatchSequence(string $password, array $matches, bool $excludeAdditive = false): array
{
@@ -76,13 +87,19 @@ public function getMostGuessableMatchSequence(string $password, array $matches,
}
// small detail: for deterministic output, sort each sublist by i.
- foreach ($matchesByEndIndex as &$matches) {
- usort($matches, function ($a, $b) {
- /** @var $a BaseMatch */
- /** @var $b BaseMatch */
- return $a->begin - $b->begin;
- });
+ foreach ($matchesByEndIndex as &$matchesInner) {
+ usort(
+ $matchesInner,
+ /**
+ * @param BaseMatch $a
+ * @param BaseMatch $b
+ */
+ function ($a, $b) {
+ return $a->begin - $b->begin;
+ }
+ );
}
+ unset($matchesInner);
$this->optimal = [
// optimal.m[k][l] holds final match in the best length-l match sequence covering the
@@ -114,13 +131,13 @@ public function getMostGuessableMatchSequence(string $password, array $matches,
$this->bruteforceUpdate($k);
}
-
if ($length === 0) {
$guesses = 1.0;
$optimalSequence = [];
} else {
$optimalSequence = $this->unwind($length);
$optimalSequenceLength = count($optimalSequence);
+ /** @psalm-suppress EmptyArrayAccess | $this->optimal was changed in other methods */
$guesses = $this->optimal['g'][$length - 1][$optimalSequenceLength];
}
@@ -146,6 +163,8 @@ protected function update(BaseMatch $match, int $length): void
// object-oriented approach we can just call getGuesses on the match directly.
$pi = $match->getGuesses();
+ \assert(empty($this->optimal['pi']) === false);
+
if ($length > 1) {
// we're considering a length-l sequence ending with match m:
// obtain the product term in the minimization function by multiplying m's guesses
@@ -159,6 +178,8 @@ protected function update(BaseMatch $match, int $length): void
$g += pow(self::MIN_GUESSES_BEFORE_GROWING_SEQUENCE, $length - 1);
}
+ \assert(empty($this->optimal['g']) === false);
+
// update state if new best.
// first see if any competing sequences covering this prefix, with l or fewer matches,
// fare better than this sequence. if so, skip it and return.
@@ -192,12 +213,15 @@ protected function bruteforceUpdate(int $end): void
$match = $this->makeBruteforceMatch(0, $end);
$this->update($match, 1);
+ \assert(empty($this->optimal['m']) === false);
+
// generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
// see if adding these new matches to any of the sequences in optimal[i-1]
// leads to new bests.
for ($i = 1; $i <= $end; $i++) {
$match = $this->makeBruteforceMatch($i, $end);
foreach ($this->optimal['m'][$i - 1] as $l => $lastM) {
+ /** @psalm-suppress RedundantCastGivenDocblockType | maybe it's needed here? */
$l = (int)$l;
// corner: an optimal sequence will never have two adjacent bruteforce matches.
@@ -227,7 +251,7 @@ protected function makeBruteforceMatch(int $begin, int $end): Bruteforce
/**
* helper: step backwards through optimal.m starting at the end, constructing the final optimal match sequence.
* @param int $n
- * @return MatchInterface[]
+ * @return array
*/
protected function unwind(int $n): array
{
@@ -238,6 +262,8 @@ protected function unwind(int $n): array
$l = null;
$g = INF;
+ \assert(empty($this->optimal['g']) === false);
+
foreach ($this->optimal['g'][$k] as $candidateL => $candidateG) {
if ($candidateG < $g) {
$l = $candidateL;
@@ -245,7 +271,13 @@ protected function unwind(int $n): array
}
}
+ \assert(empty($this->optimal['m']) === false);
+
while ($k >= 0) {
+ /**
+ * @psalm-suppress PossiblyNullArrayOffset | try to add phpdoc for $this->optimal :/
+ * @var BaseMatch $m
+ */
$m = $this->optimal['m'][$k][$l];
array_unshift($optimalSequence, $m);
$k = $m->begin - 1;
diff --git a/src/TimeEstimator.php b/src/TimeEstimator.php
index bf67de9..ac1f40b 100644
--- a/src/TimeEstimator.php
+++ b/src/TimeEstimator.php
@@ -13,8 +13,23 @@
class TimeEstimator
{
/**
- * @param int|float $guesses
- * @return array
+ * @param float $guesses
+ *
+ * @return array{
+ * crack_times_seconds: array{
+ * online_throttling_100_per_hour: float,
+ * online_no_throttling_10_per_second: float,
+ * offline_slow_hashing_1e4_per_second: float,
+ * offline_fast_hashing_1e10_per_second: float
+ * },
+ * crack_times_display: array{
+ * online_throttling_100_per_hour: string,
+ * online_no_throttling_10_per_second: string,
+ * offline_slow_hashing_1e4_per_second: string,
+ * offline_fast_hashing_1e10_per_second: string
+ * },
+ * score: int
+ * }
*/
public function estimateAttackTimes(float $guesses): array
{
diff --git a/src/Zxcvbn.php b/src/Zxcvbn.php
index 697dd76..f3e17bd 100644
--- a/src/Zxcvbn.php
+++ b/src/Zxcvbn.php
@@ -12,22 +12,22 @@
class Zxcvbn
{
/**
- * @var
+ * @var Matcher
*/
protected $matcher;
/**
- * @var
+ * @var Scorer
*/
protected $scorer;
/**
- * @var
+ * @var TimeEstimator
*/
protected $timeEstimator;
/**
- * @var
+ * @var Feedback
*/
protected $feedback;
@@ -39,6 +39,11 @@ public function __construct()
$this->feedback = new \ZxcvbnPhp\Feedback();
}
+ /**
+ * @param class-string<\ZxcvbnPhp\Matchers\BaseMatch> $className
+ *
+ * @return $this
+ */
public function addMatcher(string $className): self
{
$this->matcher->addMatcher($className);
@@ -49,14 +54,10 @@ public function addMatcher(string $className): self
/**
* Calculate password strength via non-overlapping minimum entropy patterns.
*
- * @param string $password Password to measure
- * @param array $userInputs Optional user inputs
+ * @param string $password Password to measure
+ * @param array $userInputs Optional user inputs
*
- * @return array Strength result array with keys:
- * password
- * entropy
- * match_sequence
- * score
+ * @return array{calc_time: float, feedback: array, guesses: float, guesses_log10: float, password: string, sequence: array}
*/
public function passwordStrength(string $password, array $userInputs = []): array
{
@@ -64,6 +65,7 @@ public function passwordStrength(string $password, array $userInputs = []): arra
$sanitizedInputs = array_map(
function ($input) {
+ /** @psalm-suppress RedundantCastGivenDocblockType | maybe it's needed here? */
return mb_strtolower((string) $input);
},
$userInputs
diff --git a/test/FeedbackTest.php b/test/FeedbackTest.php
index 4140c87..98f02e4 100644
--- a/test/FeedbackTest.php
+++ b/test/FeedbackTest.php
@@ -12,7 +12,9 @@
class FeedbackTest extends TestCase
{
- /** @var Feedback */
+ /**
+ * @var Feedback
+ */
private $feedback;
public function setUp(): void
diff --git a/test/Matchers/MockMatch.php b/test/Matchers/MockMatch.php
index cf5d10b..d2e032f 100644
--- a/test/Matchers/MockMatch.php
+++ b/test/Matchers/MockMatch.php
@@ -9,7 +9,9 @@
class MockMatch extends BaseMatch
{
- /** @var float */
+ /**
+ * @var float
+ */
protected $guesses;
public function __construct(int $begin, int $end, float $guesses)
@@ -44,7 +46,7 @@ public function getRawGuesses(): float
*
* @param string $password
* Password to check for match.
- * @param array $userInputs
+ * @param array $userInputs
* Array of values related to the user (optional).
* @code
* array('Alice Smith')
diff --git a/test/ScorerTest.php b/test/ScorerTest.php
index 594e711..d48fbec 100644
--- a/test/ScorerTest.php
+++ b/test/ScorerTest.php
@@ -15,7 +15,9 @@ class ScorerTest extends TestCase
{
public const PASSWORD = '0123456789';
- /** @var Scorer */
+ /**
+ * @var Scorer
+ */
private $scorer;
public function setUp(): void
diff --git a/test/TimeEstimatorTest.php b/test/TimeEstimatorTest.php
index 1efca32..081e850 100644
--- a/test/TimeEstimatorTest.php
+++ b/test/TimeEstimatorTest.php
@@ -9,7 +9,9 @@
class TimeEstimatorTest extends TestCase
{
- /** @var TimeEstimator */
+ /**
+ * @var TimeEstimator
+ */
private $timeEstimator;
public function setUp(): void
diff --git a/test/ZxcvbnTest.php b/test/ZxcvbnTest.php
index 962ed49..3e8f78e 100644
--- a/test/ZxcvbnTest.php
+++ b/test/ZxcvbnTest.php
@@ -12,7 +12,9 @@
class ZxcvbnTest extends TestCase
{
- /** @var Zxcvbn */
+ /**
+ * @var Zxcvbn
+ */
private $zxcvbn;
public function setUp(): void