Skip to content

Commit

Permalink
add psalm (level 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
voku committed Dec 26, 2021
1 parent f6a82ae commit ea22b81
Show file tree
Hide file tree
Showing 26 changed files with 352 additions and 115 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/.travis.yml export-ignore
/composer.lock export-ignore
/phpunit.xml.dist export-ignore
/psalm.xml export-ignore
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"ext-json": "*"
},
"require-dev": {
"vimeo/psalm": "4.*",
"phpunit/phpunit": "^8.5",
"php-coveralls/php-coveralls": "*",
"squizlabs/php_codesniffer": "3.*"
Expand Down
20 changes: 20 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<psalm
errorLevel="2"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<issueHandlers>
<PropertyNotSetInConstructor errorLevel="suppress" />
<UnsafeInstantiation errorLevel="suppress" />
</issueHandlers>

<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>
6 changes: 3 additions & 3 deletions src/Feedback.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class Feedback
{
/**
* @param int $score
* @param MatchInterface[] $sequence
* @return array
* @param array<int, \ZxcvbnPhp\Matchers\BaseMatch> $sequence
* @return array{warning: string, suggestions: array<int, string>}
*/
public function getFeedback(int $score, array $sequence): array
{
Expand All @@ -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;
}
Expand Down
39 changes: 29 additions & 10 deletions src/Matcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ class Matcher
Matchers\YearMatch::class,
];

/**
* @var array<class-string<BaseMatch>, class-string<BaseMatch>>
*/
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<int, string> $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
*/
Expand All @@ -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;
}
}
Expand All @@ -50,6 +53,11 @@ public function getMatches(string $password, array $userInputs = []): array
return $matches;
}

/**
* @param class-string<BaseMatch> $className
*
* @return $this
*/
public function addMatcher(string $className): self
{
if (!is_a($className, MatchInterface::class, true)) {
Expand All @@ -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<TSort> $array
* @param callable(TSort,TSort): int $value_compare_func
* @return bool
*/
public static function usortStable(array &$array, callable $value_compare_func): bool
Expand All @@ -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;
}

Expand All @@ -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<class-string<MatchInterface>>
*/
protected function getMatchers(): array
{
Expand Down
22 changes: 11 additions & 11 deletions src/Matchers/BaseMatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<int, string>}
* Associative array with warning (string) and suggestions (array of strings)
*/
#[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])]
Expand All @@ -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<int, array<array{begin: int, end: int, token: string}>>
* Array of capture groups. Captures in a group have named indexes: 'begin', 'end', 'token'.
* e.g. fishfish /(fish)/
* array(
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/Matchers/Bruteforce.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Bruteforce extends BaseMatch

/**
* @param string $password
* @param array $userInputs
* @param array<int, string> $userInputs
* @return Bruteforce[]
*/
public static function match(string $password, array $userInputs = []): array
Expand Down
43 changes: 33 additions & 10 deletions src/Matchers/DateMatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class DateMatch extends BaseMatch

public $pattern = 'date';

/**
* @var array<int, array<array<int, int>>>
*/
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)
Expand Down Expand Up @@ -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<int, string> $userInputs
* @return DateMatch[]
*/
public static function match(string $password, array $userInputs = []): array
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'],
Expand All @@ -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'],
Expand All @@ -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)
Expand Down
Loading

0 comments on commit ea22b81

Please sign in to comment.