Passed
Push — message-id-validator ( 1cb5b2...07464f )
by Eduardo Gulias
02:11
created

DomainPart   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 299
Duplicated Lines 6.69 %

Coupling/Cohesion

Components 1
Dependencies 21

Test Coverage

Coverage 94.63%

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 21
dl 20
loc 299
ccs 141
cts 149
cp 0.9463
rs 3.6
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
B parse() 0 36 6
A checkEndOfDomain() 0 16 4
A performDomainStartChecks() 0 17 4
A checkEmptyDomain() 0 12 4
A checkInvalidTokensAfterAT() 0 10 3
A getDomainPart() 0 4 1
A parseComments() 0 8 1
C doParseDomainPart() 13 69 14
A checkNotAllowedChars() 0 8 2
A parseDomainLiteral() 0 13 2
B checkDomainPartExceptions() 7 17 7
A validateTokens() 0 19 3
A checkLabelLength() 0 11 4
A isLabelTooLong() 0 8 2
A addTLDWarnings() 0 6 2
A domainPart() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DomainPart often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DomainPart, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Egulias\EmailValidator\Parser;
4
5
use Egulias\EmailValidator\EmailLexer;
6
use Egulias\EmailValidator\Warning\TLD;
7
use Egulias\EmailValidator\Result\Result;
8
use Egulias\EmailValidator\Result\ValidEmail;
9
use Egulias\EmailValidator\Result\InvalidEmail;
10
use Egulias\EmailValidator\Result\Reason\DotAtEnd;
11
use Egulias\EmailValidator\Result\Reason\DotAtStart;
12
use Egulias\EmailValidator\Warning\DeprecatedComment;
13
use Egulias\EmailValidator\Result\Reason\CRLFAtTheEnd;
14
use Egulias\EmailValidator\Result\Reason\LabelTooLong;
15
use Egulias\EmailValidator\Result\Reason\NoDomainPart;
16
use Egulias\EmailValidator\Result\Reason\ConsecutiveAt;
17
use Egulias\EmailValidator\Result\Reason\DomainTooLong;
18
use Egulias\EmailValidator\Result\Reason\CharNotAllowed;
19
use Egulias\EmailValidator\Result\Reason\DomainHyphened;
20
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
21
use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment;
22
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose;
23
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser;
24
25
class DomainPart extends Parser
26
{
27
    const DOMAIN_MAX_LENGTH = 253;
28
    const LABEL_MAX_LENGTH = 63;
29
30
    /**
31
     * @var string
32
     */
33
    protected $domainPart = '';
34
35
    /**
36
     * @var string
37
     */
38
    protected $label = '';
39
40 144
    public function parse() : Result
41
    {
42 144
        $this->lexer->clearRecorded();
43 144
        $this->lexer->startRecording();
44
45 144
        $this->lexer->moveNext();
46
47 144
        $domainChecks = $this->performDomainStartChecks();
48 144
        if ($domainChecks->isInvalid()) {
49 12
            return $domainChecks;
50
        }
51
52 132
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
53 1
            return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']);
54
        }
55
56 131
        $result = $this->doParseDomainPart();
57 131
        if ($result->isInvalid()) {
58 65
            return $result;
59
        }
60
61 66
        $end = $this->checkEndOfDomain();
62 66
        if ($end->isInvalid()) {
63 3
            return $end;
64
        }
65
66 63
        $this->lexer->stopRecording();
67 63
        $this->domainPart = $this->lexer->getAccumulatedValues();
68
69 63
        $length = strlen($this->domainPart);
70 63
        if ($length > self::DOMAIN_MAX_LENGTH) {
71
            return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']);
72
        }
73
74 63
        return new ValidEmail();
75
    }
76
77 66
    private function checkEndOfDomain() : Result
78
    {
79 66
        $prev = $this->lexer->getPrevious();
80 66
        if ($prev['type'] === EmailLexer::S_DOT) {
81 2
            return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']);
82
        }
83 64
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
84 1
            return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev['value']);
85
        }
86
87 63
        if ($this->lexer->token['type'] === EmailLexer::S_SP) {
88
            return new InvalidEmail(new CRLFAtTheEnd(), $prev['value']);
89
        }
90 63
        return new ValidEmail();
91
92
    }
93
94 144
    private function performDomainStartChecks() : Result
95
    {
96 144
        $invalidTokens = $this->checkInvalidTokensAfterAT();
97 144
        if ($invalidTokens->isInvalid()) {
98 2
            return $invalidTokens;
99
        }
100
        
101 142
        $missingDomain = $this->checkEmptyDomain();
102 142
        if ($missingDomain->isInvalid()) {
103 10
            return $missingDomain;
104
        }
105
106 132
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
107 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
108
        }
109 132
        return new ValidEmail();
110
    }
111
112 142
    private function checkEmptyDomain() : Result
113
    {
114 142
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
115 133
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
116 142
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
117
118 142
        if ($thereIsNoDomain) {
119 10
            return new InvalidEmail(new NoDomainPart(), $this->lexer->token['value']);
120
        }
121
122 132
        return new ValidEmail();
123
    }
124
125 144
    private function checkInvalidTokensAfterAT() : Result
126
    {
127 144
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
128 1
            return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']);
129
        }
130 143
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
131 1
            return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->token['value']);
132
        }
133 142
        return new ValidEmail();
134
    }
135
136
    /**
137
     * @return string
138
     */
139
    public function getDomainPart()
140
    {
141
        return $this->domainPart;
142
    }
143
144 7
    protected function parseComments(): Result
145
    {
146 7
        $commentParser = new Comment($this->lexer, new DomainComment());
147 7
        $result = $commentParser->parse();
148 7
        $this->warnings = array_merge($this->warnings, $commentParser->getWarnings());
149
150 7
        return $result;
151
    }
152
153 131
    protected function doParseDomainPart() : Result
154
    {
155 131
        $tldMissing = true;
156 131
        $hasComments = false;
157 131
        $domain = '';
158
        do {
159 131
            $prev = $this->lexer->getPrevious();
160
161 131
            $notAllowedChars = $this->checkNotAllowedChars($this->lexer->token);
162 131
            if ($notAllowedChars->isInvalid()) {
163 3
                return $notAllowedChars;
164
            }
165
166 131 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS || 
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
167 131
                $this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS ) {
168 7
                $hasComments = true;
169 7
                $commentsResult = $this->parseComments();
170
171
                //Invalid comment parsing
172 7
                if($commentsResult->isInvalid()) {
173 5
                    return $commentsResult;
174
                }
175
            }
176
177 128
            $dotsResult = $this->checkConsecutiveDots();
178 128
            if ($dotsResult->isInvalid()) {
179 2
                return $dotsResult;
180
            }
181
182 128
            if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET) {
183 18
                $literalResult = $this->parseDomainLiteral();
184
185 18
                $this->addTLDWarnings($tldMissing);
186 18
                return $literalResult;
187
            }
188
189 110
                $labelCheck = $this->checkLabelLength();
190 110
                if ($labelCheck->isInvalid()) {
191 5
                    return $labelCheck;
192
                }
193
194 110
            $FwsResult = $this->parseFWS();
195 110
            if($FwsResult->isInvalid()) {
196 4
                return $FwsResult;
197
            }
198
199 110
            $domain .= $this->lexer->token['value'];
200
201 110 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202 55
                $tldMissing = false;
203
            }
204
205 110
            $exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments);
206 110
            if ($exceptionsResult->isInvalid()) {
207 41
                return $exceptionsResult;
208
            }
209 108
            $this->lexer->moveNext();
210
211 108
        } while (null !== $this->lexer->token['type']);
212
213 53
        $labelCheck = $this->checkLabelLength(true);
214 53
        if ($labelCheck->isInvalid()) {
215 2
            return $labelCheck;
216
        }
217 51
        $this->addTLDWarnings($tldMissing);
218
219 51
        $this->domainPart = $domain;
220 51
        return new ValidEmail();
221
    }
222
223 131
    private function checkNotAllowedChars(array $token) : Result
224
    {
225 131
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
226 131
        if (isset($notAllowed[$token['type']])) {
227 3
            return new InvalidEmail(new CharNotAllowed(), $token['value']);
228
        }
229 131
        return new ValidEmail();
230
    }
231
232
    /**
233
     * @return Result
234
     */
235 18
    protected function parseDomainLiteral() : Result
236
    {
237
        try {
238 18
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
239
        } catch (\RuntimeException $e) {
240
            return new InvalidEmail(new ExpectingDomainLiteralClose(), $this->lexer->token['value']);
241
        }
242
243 18
        $domainLiteralParser = new DomainLiteralParser($this->lexer);
244 18
        $result = $domainLiteralParser->parse();
245 18
        $this->warnings = array_merge($this->warnings, $domainLiteralParser->getWarnings());
246 18
        return $result;
247
    }
248
249
    /**
250
     * @return InvalidEmail|ValidEmail
251
     */
252 110
    protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result
253
    {
254 110
        if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) {
255
            return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->token['value']);
256
        }
257
258 110 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259 1
            return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->token['value']);
260
        }
261
262 110 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263 110
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
264
            return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']);
265
        }
266
267 110
        return $this->validateTokens($hasComments);
268
    }
269
270 107
    protected function validateTokens(bool $hasComments) : Result
271
    {
272
        $validDomainTokens = array(
273 107
            EmailLexer::GENERIC => true,
274
            EmailLexer::S_HYPHEN => true,
275
            EmailLexer::S_DOT => true,
276
        );
277
278 107
        if ($hasComments) {
279 2
            $validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
280 2
            $validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
281
        }
282
283 107
        if (!isset($validDomainTokens[$this->lexer->token['type']])) {
284 40
            return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
285
        }
286
287 105
        return new ValidEmail();
288
    }
289
290 110
    private function checkLabelLength(bool $isEndOfDomain = false) : Result
291
    {
292 110
        if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
293 70
            if ($this->isLabelTooLong($this->label)) {
294 7
                $this->label = '';
295 7
                return new InvalidEmail(new LabelTooLong(), $this->lexer->token['value']);
296
            }
297
        }
298 110
        $this->label .= $this->lexer->token['value'];
299 110
        return new ValidEmail();
300
    }
301
302
303 70
    private function isLabelTooLong(string $label) : bool
304
    {
305 70
        if (preg_match('/[^\x00-\x7F]/', $label)) {
306 10
            idn_to_ascii(utf8_decode($label), IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
307 10
            return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
308
        }
309 61
        return strlen($label) > self::LABEL_MAX_LENGTH;
310
    }
311
312 69
    private function addTLDWarnings(bool $isTLDMissing) : void
313
    {
314 69
        if ($isTLDMissing) {
315 25
            $this->warnings[TLD::CODE] = new TLD();
316
        }
317 69
    }
318
319 144
    public function domainPart() : string
320
    {
321 144
        return $this->domainPart;
322
    }
323
}