Completed
Pull Request — master (#266)
by Eduardo Gulias
03:35 queued 01:50
created

DomainPart   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 383
Duplicated Lines 3.66 %

Coupling/Cohesion

Components 1
Dependencies 30

Test Coverage

Coverage 89.47%

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 30
dl 14
loc 383
ccs 204
cts 228
cp 0.8947
rs 2.08
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 25 5
A performDomainStartChecks() 0 10 2
A checkEmptyDomain() 0 10 4
A checkInvalidTokensAfterAT() 0 9 3
A getDomainPart() 0 4 1
B checkIPV6Tag() 0 42 10
B doParseDomainPart() 7 48 9
A checkNotAllowedChars() 0 7 2
A parseDomainLiteral() 0 15 4
C doParseDomainLiteral() 0 66 14
A checkIPV4Tag() 0 22 3
B checkDomainPartExceptions() 7 34 10
A hasBrackets() 0 14 3
A checkLabelLength() 0 9 4
A parseDomainComments() 0 13 3
A addTLDWarnings() 0 6 2

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\Exception\CharNotAllowed;
7
use Egulias\EmailValidator\Exception\CommaInDomain;
8
use Egulias\EmailValidator\Exception\ConsecutiveAt;
9
use Egulias\EmailValidator\Exception\CRLFAtTheEnd;
10
use Egulias\EmailValidator\Exception\CRNoLF;
11
use Egulias\EmailValidator\Exception\DomainHyphened;
12
use Egulias\EmailValidator\Exception\DotAtEnd;
13
use Egulias\EmailValidator\Exception\DotAtStart;
14
use Egulias\EmailValidator\Exception\ExpectingATEXT;
15
use Egulias\EmailValidator\Exception\ExpectingDomainLiteralClose;
16
use Egulias\EmailValidator\Exception\ExpectingDTEXT;
17
use Egulias\EmailValidator\Exception\NoDomainPart;
18
use Egulias\EmailValidator\Exception\UnopenedComment;
19
use Egulias\EmailValidator\Warning\AddressLiteral;
20
use Egulias\EmailValidator\Warning\CFWSWithFWS;
21
use Egulias\EmailValidator\Warning\DeprecatedComment;
22
use Egulias\EmailValidator\Warning\DomainLiteral;
23
use Egulias\EmailValidator\Warning\DomainTooLong;
24
use Egulias\EmailValidator\Warning\IPV6BadChar;
25
use Egulias\EmailValidator\Warning\IPV6ColonEnd;
26
use Egulias\EmailValidator\Warning\IPV6ColonStart;
27
use Egulias\EmailValidator\Warning\IPV6Deprecated;
28
use Egulias\EmailValidator\Warning\IPV6DoubleColon;
29
use Egulias\EmailValidator\Warning\IPV6GroupCount;
30
use Egulias\EmailValidator\Warning\IPV6MaxGroups;
31
use Egulias\EmailValidator\Warning\LabelTooLong;
32
use Egulias\EmailValidator\Warning\ObsoleteDTEXT;
33
use Egulias\EmailValidator\Warning\TLD;
34
35
class DomainPart extends Parser
36
{
37
    const DOMAIN_MAX_LENGTH = 254;
38
39
    /**
40
     * @var string
41
     */
42
    protected $domainPart = '';
43
44 94
    public function parse($domainPart)
45
    {
46 94
        $this->lexer->moveNext();
47
48 94
        $this->performDomainStartChecks();
49
50 84
        $domain = $this->doParseDomainPart();
51
52 56
        $prev = $this->lexer->getPrevious();
53 56
        $length = strlen($domain);
54
55 56
        if ($prev['type'] === EmailLexer::S_DOT) {
56 2
            throw new DotAtEnd();
57
        }
58 54
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
59 1
            throw new DomainHyphened();
60
        }
61 53
        if ($length > self::DOMAIN_MAX_LENGTH) {
62 2
            $this->warnings[DomainTooLong::CODE] = new DomainTooLong();
63 2
        }
64 53
        if ($prev['type'] === EmailLexer::S_CR) {
65
            throw new CRLFAtTheEnd();
66
        }
67 53
        $this->domainPart = $domain;
68 53
    }
69
70 94
    private function performDomainStartChecks()
71
    {
72 94
        $this->checkInvalidTokensAfterAT();
73 93
        $this->checkEmptyDomain();
74
75 86
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
76 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
77 3
            $this->parseDomainComments();
78 1
        }
79 84
    }
80
81 93
    private function checkEmptyDomain()
82
    {
83 93
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
84 87
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
85 93
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
86
87 93
        if ($thereIsNoDomain) {
88 7
            throw new NoDomainPart();
89
        }
90 86
    }
91
92 94
    private function checkInvalidTokensAfterAT()
93
    {
94 94
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
95 1
            throw new DotAtStart();
96
        }
97 93
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
98
            throw new DomainHyphened();
99
        }
100 93
    }
101
102
    /**
103
     * @return string
104
     */
105 53
    public function getDomainPart()
106
    {
107 53
        return $this->domainPart;
108
    }
109
110
    /**
111
     * @param string $addressLiteral
112
     * @param int $maxGroups
113
     */
114 7
    public function checkIPV6Tag($addressLiteral, $maxGroups = 8)
115
    {
116 7
        $prev = $this->lexer->getPrevious();
117 7
        if ($prev['type'] === EmailLexer::S_COLON) {
118 1
            $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd();
119 1
        }
120
121 7
        $IPv6       = substr($addressLiteral, 5);
122
        //Daniel Marschall's new IPv6 testing strategy
123 7
        $matchesIP  = explode(':', $IPv6);
124 7
        $groupCount = count($matchesIP);
125 7
        $colons     = strpos($IPv6, '::');
126
127 7
        if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) {
128 1
            $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar();
129 1
        }
130
131 7
        if ($colons === false) {
132
            // We need exactly the right number of groups
133 4
            if ($groupCount !== $maxGroups) {
134 1
                $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount();
135 1
            }
136 4
            return;
137
        }
138
139 3
        if ($colons !== strrpos($IPv6, '::')) {
140 1
            $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon();
141 1
            return;
142
        }
143
144 2
        if ($colons === 0 || $colons === (strlen($IPv6) - 2)) {
145
            // RFC 4291 allows :: at the start or end of an address
146
            //with 7 other groups in addition
147 2
            ++$maxGroups;
148 2
        }
149
150 2
        if ($groupCount > $maxGroups) {
151 1
            $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups();
152 2
        } elseif ($groupCount === $maxGroups) {
153 1
            $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated();
154 1
        }
155 2
    }
156
157
    /**
158
     * @return string
159
     */
160 84
    protected function doParseDomainPart()
161
    {
162 84
        $domain = '';
163 84
        $openedParenthesis = 0;
164
        do {
165 84
            $prev = $this->lexer->getPrevious();
166
167 84
            $this->checkNotAllowedChars($this->lexer->token);
168
169 84
            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
170 3
                $this->parseComments();
171 3
                $openedParenthesis += $this->getOpenedParenthesis();
172 3
                $this->lexer->moveNext();
173 3
                $tmpPrev = $this->lexer->getPrevious();
174 3
                if ($tmpPrev['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
175 3
                    $openedParenthesis--;
176 3
                }
177 3
            }
178 84 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
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...
179 3
                if ($openedParenthesis === 0) {
180 3
                    throw new UnopenedComment();
181
                } else {
182
                    $openedParenthesis--;
183
                }
184
            }
185
186 83
            $this->checkConsecutiveDots();
187 83
            $this->checkDomainPartExceptions($prev);
188
189 82
            if ($this->hasBrackets()) {
190 13
                $this->parseDomainLiteral();
191 11
            }
192
193 80
            $this->checkLabelLength($prev);
194
195 80
            if ($this->isFWS()) {
196 3
                $this->parseFWS();
197 2
            }
198
199 80
            $domain .= $this->lexer->token['value'];
200 80
            $this->lexer->moveNext();
201 80
            if ($this->lexer->token['type'] === EmailLexer::S_SP) {
202 8
                throw new CharNotAllowed();
203
            }
204 80
        } while (null !== $this->lexer->token['type']);
205
206 56
        return $domain;
207
    }
208
209 84
    private function checkNotAllowedChars(array $token)
210
    {
211 84
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
212 84
        if (isset($notAllowed[$token['type']])) {
213 3
            throw new CharNotAllowed();
214
        }
215 84
    }
216
217
    /**
218
     * @return string|false
219
     */
220 13
    protected function parseDomainLiteral()
221
    {
222 13
        if ($this->lexer->isNextToken(EmailLexer::S_COLON)) {
223
            $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
224
        }
225 13
        if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) {
226 7
            $lexer = clone $this->lexer;
227 7
            $lexer->moveNext();
228 7
            if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) {
229 1
                $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
230 1
            }
231 7
        }
232
233 13
        return $this->doParseDomainLiteral();
234
    }
235
236
    /**
237
     * @return string|false
238
     */
239 13
    protected function doParseDomainLiteral()
240
    {
241 13
        $IPv6TAG = false;
242 13
        $addressLiteral = '';
243
        do {
244 13
            if ($this->lexer->token['type'] === EmailLexer::C_NUL) {
245
                throw new ExpectingDTEXT();
246
            }
247
248 13
            if ($this->lexer->token['type'] === EmailLexer::INVALID ||
249 13
                $this->lexer->token['type'] === EmailLexer::C_DEL   ||
250 13
                $this->lexer->token['type'] === EmailLexer::S_LF
251 13
            ) {
252 1
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
253 1
            }
254
255 13
            if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENQBRACKET, EmailLexer::S_OPENBRACKET))) {
256 1
                throw new ExpectingDTEXT();
257
            }
258
259 12
            if ($this->lexer->isNextTokenAny(
260 12
                array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF)
261 12
            )) {
262
                $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
263
                $this->parseFWS();
264
            }
265
266 12
            if ($this->lexer->isNextToken(EmailLexer::S_CR)) {
267 1
                throw new CRNoLF();
268
            }
269
270 11
            if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) {
271
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
272
                $addressLiteral .= $this->lexer->token['value'];
273
                $this->lexer->moveNext();
274
                $this->validateQuotedPair();
275
            }
276 11
            if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) {
277 7
                $IPv6TAG = true;
278 7
            }
279 11
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEQBRACKET) {
280
                break;
281
            }
282
283 11
            $addressLiteral .= $this->lexer->token['value'];
284
285 11
        } while ($this->lexer->moveNext());
286
287 11
        $addressLiteral = str_replace('[', '', $addressLiteral);
288 11
        $addressLiteral = $this->checkIPV4Tag($addressLiteral);
289
290 11
        if (false === $addressLiteral) {
291 1
            return $addressLiteral;
292
        }
293
294 10
        if (!$IPv6TAG) {
295 3
            $this->warnings[DomainLiteral::CODE] = new DomainLiteral();
296 3
            return $addressLiteral;
297
        }
298
299 7
        $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
300
301 7
        $this->checkIPV6Tag($addressLiteral);
302
303 7
        return $addressLiteral;
304
    }
305
306
    /**
307
     * @param string $addressLiteral
308
     *
309
     * @return string|false
310
     */
311 11
    protected function checkIPV4Tag($addressLiteral)
312
    {
313 11
        $matchesIP  = array();
314
315
        // Extract IPv4 part from the end of the address-literal (if there is one)
316 11
        if (preg_match(
317 11
            '/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/',
318 11
            $addressLiteral,
319
            $matchesIP
320 11
        ) > 0
321 11
        ) {
322 2
            $index = strrpos($addressLiteral, $matchesIP[0]);
323 2
            if ($index === 0) {
324 1
                $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
325 1
                return false;
326
            }
327
            // Convert IPv4 part to IPv6 format for further testing
328 1
            $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0';
329 1
        }
330
331 10
        return $addressLiteral;
332
    }
333
334 83
    protected function checkDomainPartExceptions(array $prev)
335
    {
336
        $invalidDomainTokens = array(
337 83
            EmailLexer::S_DQUOTE => true,
338 83
            EmailLexer::S_SEMICOLON => true,
339 83
            EmailLexer::S_GREATERTHAN => true,
340 83
            EmailLexer::S_LOWERTHAN => true,
341 83
        );
342
343 83
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
344 4
            throw new ExpectingATEXT();
345
        }
346
347 83
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
348 1
            throw new CommaInDomain();
349
        }
350
351 83
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
352 2
            throw new ConsecutiveAt();
353
        }
354
355 82
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
356 1
            throw new ExpectingATEXT();
357
        }
358
359 82 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...
360 1
            throw new DomainHyphened();
361
        }
362
363 82 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...
364 82
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
365
            throw new ExpectingATEXT();
366
        }
367 82
    }
368
369
    /**
370
     * @return bool
371
     */
372 82
    protected function hasBrackets()
373
    {
374 82
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
375 69
            return false;
376
        }
377
378
        try {
379 13
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
380 13
        } catch (\RuntimeException $e) {
381
            throw new ExpectingDomainLiteralClose();
382
        }
383
384 13
        return true;
385
    }
386
387 80
    protected function checkLabelLength(array $prev)
388
    {
389 80
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
390 80
            $prev['type'] === EmailLexer::GENERIC &&
391 43
            strlen($prev['value']) > 63
392 80
        ) {
393 1
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
394 1
        }
395 80
    }
396
397 3
    protected function parseDomainComments()
398
    {
399 3
        $this->isUnclosedComment();
400 1
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
401 1
            $this->warnEscaping();
402 1
            $this->lexer->moveNext();
403 1
        }
404
405 1
        $this->lexer->moveNext();
406 1
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
407
            throw new ExpectingATEXT();
408
        }
409 1
    }
410
411
    protected function addTLDWarnings()
412
    {
413
        if ($this->warnings[DomainLiteral::CODE]) {
414
            $this->warnings[TLD::CODE] = new TLD();
415
        }
416
    }
417
}
418