diff --git a/.travis.yml b/.travis.yml index 3dc8c7b..95a4687 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,26 +8,37 @@ matrix: - php: 5.5 dist: trusty - php: 5.6 + env: dist: xenial - php: 7.0 dist: xenial - php: 7.1 + env: + - psalm=yes dist: bionic - php: 7.2 + env: + - psalm=yes dist: bionic - php: 7.3 dist: bionic + env: + - psalm=yes - php: 7.4 + env: + - psalm=yes dist: bionic install: - if [ "$deps" = "low" ]; then composer update --prefer-lowest; else composer install; fi + - if [ "$psalm" = "yes" ]; then composer require --dev vimeo/psalm; fi before_script: - mkdir -p build/logs script: - vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - if [ "$psalm" = "yes" ]; then vendor/bin/psalm; fi after_script: - php vendor/bin/coveralls diff --git a/EmailValidator/EmailLexer.php b/EmailValidator/EmailLexer.php index 4485728..cb47c01 100644 --- a/EmailValidator/EmailLexer.php +++ b/EmailValidator/EmailLexer.php @@ -73,10 +73,37 @@ class EmailLexer extends AbstractLexer '\0' => self::C_NUL, ); + /** + * @var bool + */ protected $hasInvalidTokens = false; - protected $previous; + /** + * @var array + * + * @psalm-var array{value:string, type:null|int, position:int}|array + */ + protected $previous = []; + + /** + * The last matched/seen token. + * + * @var array + * + * @psalm-var array{value:string, type:null|int, position:int} + */ + public $token; + /** + * The next token in the input. + * + * @var array|null + */ + public $lookahead; + + /** + * @psalm-var array{value:'', type:null, position:0} + */ private static $nullToken = [ 'value' => '', 'type' => null, @@ -86,6 +113,7 @@ class EmailLexer extends AbstractLexer public function __construct() { $this->previous = $this->token = self::$nullToken; + $this->lookahead = null; } /** @@ -98,15 +126,20 @@ public function reset() $this->previous = $this->token = self::$nullToken; } + /** + * @return bool + */ public function hasInvalidTokens() { return $this->hasInvalidTokens; } /** - * @param string $type + * @param int $type * @throws \UnexpectedValueException * @return boolean + * + * @psalm-suppress InvalidScalarArgument */ public function find($type) { @@ -122,7 +155,7 @@ public function find($type) /** * getPrevious * - * @return array token + * @return array */ public function getPrevious() { @@ -196,6 +229,11 @@ protected function getType(&$value) return self::GENERIC; } + /** + * @param string $value + * + * @return bool + */ protected function isValid($value) { if (isset($this->charValue[$value])) { diff --git a/EmailValidator/EmailParser.php b/EmailValidator/EmailParser.php index 5bf605a..6b7bad6 100644 --- a/EmailValidator/EmailParser.php +++ b/EmailValidator/EmailParser.php @@ -17,11 +17,33 @@ class EmailParser { const EMAIL_MAX_LENGTH = 254; - protected $warnings; + /** + * @var array + */ + protected $warnings = []; + + /** + * @var string + */ protected $domainPart = ''; + + /** + * @var string + */ protected $localPart = ''; + /** + * @var EmailLexer + */ protected $lexer; + + /** + * @var LocalPart + */ protected $localPartParser; + + /** + * @var DomainPart + */ protected $domainPartParser; public function __construct(EmailLexer $lexer) @@ -29,7 +51,6 @@ public function __construct(EmailLexer $lexer) $this->lexer = $lexer; $this->localPartParser = new LocalPart($this->lexer); $this->domainPartParser = new DomainPart($this->lexer); - $this->warnings = new \SplObjectStorage(); } /** @@ -57,6 +78,9 @@ public function parse($str) return array('local' => $this->localPart, 'domain' => $this->domainPart); } + /** + * @return Warning\Warning[] + */ public function getWarnings() { $localPartWarnings = $this->localPartParser->getWarnings(); @@ -68,11 +92,17 @@ public function getWarnings() return $this->warnings; } + /** + * @return string + */ public function getParsedDomainPart() { return $this->domainPart; } + /** + * @param string $email + */ protected function setParts($email) { $parts = explode('@', $email); @@ -80,6 +110,9 @@ protected function setParts($email) $this->localPart = $parts[0]; } + /** + * @return bool + */ protected function hasAtToken() { $this->lexer->moveNext(); diff --git a/EmailValidator/EmailValidator.php b/EmailValidator/EmailValidator.php index 1c27707..a30f21d 100644 --- a/EmailValidator/EmailValidator.php +++ b/EmailValidator/EmailValidator.php @@ -13,12 +13,12 @@ class EmailValidator private $lexer; /** - * @var array + * @var Warning\Warning[] */ - protected $warnings; + protected $warnings = []; /** - * @var InvalidEmail + * @var InvalidEmail|null */ protected $error; @@ -58,7 +58,7 @@ public function getWarnings() } /** - * @return InvalidEmail + * @return InvalidEmail|null */ public function getError() { diff --git a/EmailValidator/Parser/DomainPart.php b/EmailValidator/Parser/DomainPart.php index 8ed240b..0613e31 100644 --- a/EmailValidator/Parser/DomainPart.php +++ b/EmailValidator/Parser/DomainPart.php @@ -35,6 +35,10 @@ class DomainPart extends Parser { const DOMAIN_MAX_LENGTH = 254; + + /** + * @var string + */ protected $domainPart = ''; public function parse($domainPart) @@ -95,11 +99,18 @@ private function checkInvalidTokensAfterAT() } } + /** + * @return string + */ public function getDomainPart() { return $this->domainPart; } + /** + * @param string $addressLiteral + * @param int $maxGroups + */ public function checkIPV6Tag($addressLiteral, $maxGroups = 8) { $prev = $this->lexer->getPrevious(); @@ -143,6 +154,9 @@ public function checkIPV6Tag($addressLiteral, $maxGroups = 8) } } + /** + * @return string + */ protected function doParseDomainPart() { $domain = ''; @@ -189,7 +203,7 @@ protected function doParseDomainPart() return $domain; } - private function checkNotAllowedChars($token) + private function checkNotAllowedChars(array $token) { $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true]; if (isset($notAllowed[$token['type']])) { @@ -197,6 +211,9 @@ private function checkNotAllowedChars($token) } } + /** + * @return string|false + */ protected function parseDomainLiteral() { if ($this->lexer->isNextToken(EmailLexer::S_COLON)) { @@ -213,6 +230,9 @@ protected function parseDomainLiteral() return $this->doParseDomainLiteral(); } + /** + * @return string|false + */ protected function doParseDomainLiteral() { $IPv6TAG = false; @@ -280,6 +300,11 @@ protected function doParseDomainLiteral() return $addressLiteral; } + /** + * @param string $addressLiteral + * + * @return string|false + */ protected function checkIPV4Tag($addressLiteral) { $matchesIP = array(); @@ -297,13 +322,13 @@ protected function checkIPV4Tag($addressLiteral) return false; } // Convert IPv4 part to IPv6 format for further testing - $addressLiteral = substr($addressLiteral, 0, $index) . '0:0'; + $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0'; } return $addressLiteral; } - protected function checkDomainPartExceptions($prev) + protected function checkDomainPartExceptions(array $prev) { $invalidDomainTokens = array( EmailLexer::S_DQUOTE => true, @@ -338,6 +363,9 @@ protected function checkDomainPartExceptions($prev) } } + /** + * @return bool + */ protected function hasBrackets() { if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) { @@ -353,7 +381,7 @@ protected function hasBrackets() return true; } - protected function checkLabelLength($prev) + protected function checkLabelLength(array $prev) { if ($this->lexer->token['type'] === EmailLexer::S_DOT && $prev['type'] === EmailLexer::GENERIC && diff --git a/EmailValidator/Parser/LocalPart.php b/EmailValidator/Parser/LocalPart.php index fa1d17b..5f6b8c2 100644 --- a/EmailValidator/Parser/LocalPart.php +++ b/EmailValidator/Parser/LocalPart.php @@ -5,7 +5,6 @@ use Egulias\EmailValidator\Exception\DotAtEnd; use Egulias\EmailValidator\Exception\DotAtStart; use Egulias\EmailValidator\EmailLexer; -use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Exception\ExpectingAT; use Egulias\EmailValidator\Exception\ExpectingATEXT; use Egulias\EmailValidator\Exception\UnclosedQuotedString; @@ -67,6 +66,9 @@ public function parse($localPart) } } + /** + * @return bool + */ protected function parseDoubleQuote() { $parseAgain = true; @@ -118,7 +120,10 @@ protected function parseDoubleQuote() return $parseAgain; } - protected function isInvalidToken($token, $closingQuote) + /** + * @param bool $closingQuote + */ + protected function isInvalidToken(array $token, $closingQuote) { $forbidden = array( EmailLexer::S_COMMA, diff --git a/EmailValidator/Parser/Parser.php b/EmailValidator/Parser/Parser.php index fa7bd44..cc9e26b 100644 --- a/EmailValidator/Parser/Parser.php +++ b/EmailValidator/Parser/Parser.php @@ -21,8 +21,19 @@ abstract class Parser { + /** + * @var \Egulias\EmailValidator\Warning\Warning[] + */ protected $warnings = []; + + /** + * @var EmailLexer + */ protected $lexer; + + /** + * @var int + */ protected $openedParenthesis = 0; public function __construct(EmailLexer $lexer) @@ -30,11 +41,17 @@ public function __construct(EmailLexer $lexer) $this->lexer = $lexer; } + /** + * @return \Egulias\EmailValidator\Warning\Warning[] + */ public function getWarnings() { return $this->warnings; } + /** + * @param string $str + */ abstract public function parse($str); /** @return int */ @@ -80,6 +97,9 @@ protected function parseComments() } } + /** + * @return bool + */ protected function isUnclosedComment() { try { @@ -122,6 +142,9 @@ protected function checkConsecutiveDots() } } + /** + * @return bool + */ protected function isFWS() { if ($this->escaped()) { @@ -140,6 +163,9 @@ protected function isFWS() return false; } + /** + * @return bool + */ protected function escaped() { $previous = $this->lexer->getPrevious(); @@ -154,6 +180,9 @@ protected function escaped() return false; } + /** + * @return bool + */ protected function warnEscaping() { if ($this->lexer->token['type'] !== EmailLexer::S_BACKSLASH) { @@ -174,6 +203,11 @@ protected function warnEscaping() } + /** + * @param bool $hasClosingQuote + * + * @return bool + */ protected function checkDQUOTE($hasClosingQuote) { if ($this->lexer->token['type'] !== EmailLexer::S_DQUOTE) { diff --git a/EmailValidator/Validation/DNSCheckValidation.php b/EmailValidator/Validation/DNSCheckValidation.php index e5c3e5d..da13253 100644 --- a/EmailValidator/Validation/DNSCheckValidation.php +++ b/EmailValidator/Validation/DNSCheckValidation.php @@ -15,13 +15,13 @@ class DNSCheckValidation implements EmailValidation private $warnings = []; /** - * @var InvalidEmail + * @var InvalidEmail|null */ private $error; - + public function __construct() { - if (!extension_loaded('intl')) { + if (!function_exists('idn_to_ascii')) { throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__)); } } @@ -49,6 +49,11 @@ public function getWarnings() return $this->warnings; } + /** + * @param string $host + * + * @return bool + */ protected function checkDNS($host) { $variant = INTL_IDNA_VARIANT_2003; diff --git a/EmailValidator/Validation/Exception/EmptyValidationList.php b/EmailValidator/Validation/Exception/EmptyValidationList.php index 775ad16..ee7c41a 100644 --- a/EmailValidator/Validation/Exception/EmptyValidationList.php +++ b/EmailValidator/Validation/Exception/EmptyValidationList.php @@ -6,6 +6,9 @@ class EmptyValidationList extends \InvalidArgumentException { + /** + * @param int $code + */ public function __construct($code = 0, Exception $previous = null) { parent::__construct("Empty validation list is not allowed", $code, $previous); diff --git a/EmailValidator/Validation/MultipleErrors.php b/EmailValidator/Validation/MultipleErrors.php index d5e87d8..3be5973 100644 --- a/EmailValidator/Validation/MultipleErrors.php +++ b/EmailValidator/Validation/MultipleErrors.php @@ -9,16 +9,22 @@ class MultipleErrors extends InvalidEmail const CODE = 999; const REASON = "Accumulated errors for multiple validations"; /** - * @var array + * @var InvalidEmail[] */ private $errors = []; + /** + * @param InvalidEmail[] $errors + */ public function __construct(array $errors) { $this->errors = $errors; parent::__construct(); } + /** + * @return InvalidEmail[] + */ public function getErrors() { return $this->errors; diff --git a/EmailValidator/Validation/MultipleValidationWithAnd.php b/EmailValidator/Validation/MultipleValidationWithAnd.php index b823f7e..feb2240 100644 --- a/EmailValidator/Validation/MultipleValidationWithAnd.php +++ b/EmailValidator/Validation/MultipleValidationWithAnd.php @@ -30,12 +30,12 @@ class MultipleValidationWithAnd implements EmailValidation private $warnings = []; /** - * @var MultipleErrors + * @var MultipleErrors|null */ private $error; /** - * @var bool + * @var int */ private $mode; @@ -79,6 +79,12 @@ public function isValid($email, EmailLexer $emailLexer) return $result; } + /** + * @param \Egulias\EmailValidator\Exception\InvalidEmail|null $possibleError + * @param \Egulias\EmailValidator\Exception\InvalidEmail[] $errors + * + * @return \Egulias\EmailValidator\Exception\InvalidEmail[] + */ private function addNewError($possibleError, array $errors) { if (null !== $possibleError) { @@ -88,6 +94,11 @@ private function addNewError($possibleError, array $errors) return $errors; } + /** + * @param bool $result + * + * @return bool + */ private function shouldStop($result) { return !$result && $this->mode === self::STOP_ON_ERROR; diff --git a/EmailValidator/Validation/NoRFCWarningsValidation.php b/EmailValidator/Validation/NoRFCWarningsValidation.php index e4bf0cc..6b31e54 100644 --- a/EmailValidator/Validation/NoRFCWarningsValidation.php +++ b/EmailValidator/Validation/NoRFCWarningsValidation.php @@ -9,7 +9,7 @@ class NoRFCWarningsValidation extends RFCValidation { /** - * @var InvalidEmail + * @var InvalidEmail|null */ private $error; diff --git a/EmailValidator/Validation/RFCValidation.php b/EmailValidator/Validation/RFCValidation.php index c4ffe35..8781e0b 100644 --- a/EmailValidator/Validation/RFCValidation.php +++ b/EmailValidator/Validation/RFCValidation.php @@ -9,7 +9,7 @@ class RFCValidation implements EmailValidation { /** - * @var EmailParser + * @var EmailParser|null */ private $parser; @@ -19,7 +19,7 @@ class RFCValidation implements EmailValidation private $warnings = []; /** - * @var InvalidEmail + * @var InvalidEmail|null */ private $error; diff --git a/EmailValidator/Validation/SpoofCheckValidation.php b/EmailValidator/Validation/SpoofCheckValidation.php index 4721f0d..e10bfab 100644 --- a/EmailValidator/Validation/SpoofCheckValidation.php +++ b/EmailValidator/Validation/SpoofCheckValidation.php @@ -10,7 +10,7 @@ class SpoofCheckValidation implements EmailValidation { /** - * @var InvalidEmail + * @var InvalidEmail|null */ private $error; @@ -21,6 +21,9 @@ public function __construct() } } + /** + * @psalm-suppress InvalidArgument + */ public function isValid($email, EmailLexer $emailLexer) { $checker = new Spoofchecker(); @@ -33,6 +36,9 @@ public function isValid($email, EmailLexer $emailLexer) return $this->error === null; } + /** + * @return InvalidEmail|null + */ public function getError() { return $this->error; diff --git a/EmailValidator/Warning/QuotedPart.php b/EmailValidator/Warning/QuotedPart.php index 7be9e6a..36a4265 100644 --- a/EmailValidator/Warning/QuotedPart.php +++ b/EmailValidator/Warning/QuotedPart.php @@ -6,6 +6,10 @@ class QuotedPart extends Warning { const CODE = 36; + /** + * @param scalar $prevToken + * @param scalar $postToken + */ public function __construct($prevToken, $postToken) { $this->message = "Deprecated Quoted String found between $prevToken and $postToken"; diff --git a/EmailValidator/Warning/QuotedString.php b/EmailValidator/Warning/QuotedString.php index e9d56e1..817e4e8 100644 --- a/EmailValidator/Warning/QuotedString.php +++ b/EmailValidator/Warning/QuotedString.php @@ -6,6 +6,10 @@ class QuotedString extends Warning { const CODE = 11; + /** + * @param scalar $prevToken + * @param scalar $postToken + */ public function __construct($prevToken, $postToken) { $this->message = "Quoted String found between $prevToken and $postToken"; diff --git a/EmailValidator/Warning/Warning.php b/EmailValidator/Warning/Warning.php index ec6a365..bce7e7a 100644 --- a/EmailValidator/Warning/Warning.php +++ b/EmailValidator/Warning/Warning.php @@ -5,19 +5,36 @@ abstract class Warning { const CODE = 0; - protected $message; - protected $rfcNumber; + /** + * @var string + */ + protected $message = ''; + + /** + * @var int + */ + protected $rfcNumber = 0; + + /** + * @return string + */ public function message() { return $this->message; } + /** + * @return int + */ public function code() { return self::CODE; } + /** + * @return int + */ public function RFCNumber() { return $this->rfcNumber; diff --git a/Tests/EmailValidator/Validation/IsEmailFunctionTests.php b/Tests/EmailValidator/Validation/IsEmailFunctionTests.php index 4674fd4..565741a 100644 --- a/Tests/EmailValidator/Validation/IsEmailFunctionTests.php +++ b/Tests/EmailValidator/Validation/IsEmailFunctionTests.php @@ -6,7 +6,6 @@ use Egulias\EmailValidator\Validation\DNSCheckValidation; use Egulias\EmailValidator\Validation\MultipleValidationWithAnd; use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; -use Egulias\EmailValidator\Validation\RFCValidation; use PHPUnit\Framework\TestCase; class IsEmailFunctionTests extends TestCase diff --git a/Tests/EmailValidator/Validation/MultipleValidationWithAndTest.php b/Tests/EmailValidator/Validation/MultipleValidationWithAndTest.php index e6bba8e..9e3610d 100644 --- a/Tests/EmailValidator/Validation/MultipleValidationWithAndTest.php +++ b/Tests/EmailValidator/Validation/MultipleValidationWithAndTest.php @@ -3,13 +3,11 @@ namespace Egulias\Tests\EmailValidator\Validation; use Egulias\EmailValidator\EmailLexer; -use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Exception\CommaInDomain; use Egulias\EmailValidator\Exception\NoDomainPart; use Egulias\EmailValidator\Validation\EmailValidation; use Egulias\EmailValidator\Validation\MultipleErrors; use Egulias\EmailValidator\Validation\MultipleValidationWithAnd; -use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; use Egulias\EmailValidator\Validation\RFCValidation; use Egulias\EmailValidator\Warning\AddressLiteral; use Egulias\EmailValidator\Warning\DomainLiteral; diff --git a/composer.json b/composer.json index 37e87d5..595caff 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "require": { "php": ">=5.5", - "doctrine/lexer": "^1.0.1" + "doctrine/lexer": "^1.0.1", + "symfony/polyfill-intl-idn": "^1.10" }, "require-dev": { "satooshi/php-coveralls": "^1.0.1", diff --git a/psalm.baseline.xml b/psalm.baseline.xml new file mode 100644 index 0000000..f81df72 --- /dev/null +++ b/psalm.baseline.xml @@ -0,0 +1,19 @@ + + + + + self::$nullToken + + + + + parse + + + + + Spoofchecker + Spoofchecker + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..fb17dc8 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + +