Passed
Push — master ( c4b8d1...e834ee )
by Eduardo Gulias
02:09
created

DomainPart   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 380
Duplicated Lines 3.68 %

Coupling/Cohesion

Components 1
Dependencies 30

Test Coverage

Coverage 89.38%

Importance

Changes 0
Metric Value
wmc 78
lcom 1
cbo 30
dl 14
loc 380
ccs 202
cts 226
cp 0.8938
rs 2.16
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 45 8
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 92
    public function parse($domainPart)
45
    {
46 92
        $this->lexer->moveNext();
47
48 92
        $this->performDomainStartChecks();
49
50 82
        $domain = $this->doParseDomainPart();
51
52 55
        $prev = $this->lexer->getPrevious();
53 55
        $length = strlen($domain);
54
55 55
        if ($prev['type'] === EmailLexer::S_DOT) {
56 2
            throw new DotAtEnd();
57
        }
58 53
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
59 1
            throw new DomainHyphened();
60
        }
61 52
        if ($length > self::DOMAIN_MAX_LENGTH) {
62 2
            $this->warnings[DomainTooLong::CODE] = new DomainTooLong();
63 2
        }
64 52
        if ($prev['type'] === EmailLexer::S_CR) {
65
            throw new CRLFAtTheEnd();
66
        }
67 52
        $this->domainPart = $domain;
68 52
    }
69
70 92
    private function performDomainStartChecks()
71
    {
72 92
        $this->checkInvalidTokensAfterAT();
73 91
        $this->checkEmptyDomain();
74
75 84
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
76 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
77 3
            $this->parseDomainComments();
78 1
        }
79 82
    }
80
81 91
    private function checkEmptyDomain()
82
    {
83 91
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
84 85
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
85 91
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
86
87 91
        if ($thereIsNoDomain) {
88 7
            throw new NoDomainPart();
89
        }
90 84
    }
91
92 92
    private function checkInvalidTokensAfterAT()
93
    {
94 92
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
95 1
            throw new DotAtStart();
96
        }
97 91
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
98
            throw new DomainHyphened();
99
        }
100 91
    }
101
102
    /**
103
     * @return string
104
     */
105 52
    public function getDomainPart()
106
    {
107 52
        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 82
    protected function doParseDomainPart()
161
    {
162 82
        $domain = '';
163 82
        $openedParenthesis = 0;
164
        do {
165 82
            $prev = $this->lexer->getPrevious();
166
167 82
            $this->checkNotAllowedChars($this->lexer->token);
168
169 82
            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 82 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 81
            $this->checkConsecutiveDots();
187 81
            $this->checkDomainPartExceptions($prev);
188
189 80
            if ($this->hasBrackets()) {
190 13
                $this->parseDomainLiteral();
191 11
            }
192
193 78
            $this->checkLabelLength($prev);
194
195 78
            if ($this->isFWS()) {
196 9
                $this->parseFWS();
197 7
            }
198
199 78
            $domain .= $this->lexer->token['value'];
200 78
            $this->lexer->moveNext();
201 78
        } while (null !== $this->lexer->token['type']);
202
203 55
        return $domain;
204
    }
205
206 82
    private function checkNotAllowedChars(array $token)
207
    {
208 82
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
209 82
        if (isset($notAllowed[$token['type']])) {
210 8
            throw new CharNotAllowed();
211
        }
212 82
    }
213
214
    /**
215
     * @return string|false
216
     */
217 13
    protected function parseDomainLiteral()
218
    {
219 13
        if ($this->lexer->isNextToken(EmailLexer::S_COLON)) {
220
            $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
221
        }
222 13
        if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) {
223 7
            $lexer = clone $this->lexer;
224 7
            $lexer->moveNext();
225 7
            if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) {
226 1
                $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
227 1
            }
228 7
        }
229
230 13
        return $this->doParseDomainLiteral();
231
    }
232
233
    /**
234
     * @return string|false
235
     */
236 13
    protected function doParseDomainLiteral()
237
    {
238 13
        $IPv6TAG = false;
239 13
        $addressLiteral = '';
240
        do {
241 13
            if ($this->lexer->token['type'] === EmailLexer::C_NUL) {
242
                throw new ExpectingDTEXT();
243
            }
244
245 13
            if ($this->lexer->token['type'] === EmailLexer::INVALID ||
246 13
                $this->lexer->token['type'] === EmailLexer::C_DEL   ||
247 13
                $this->lexer->token['type'] === EmailLexer::S_LF
248 13
            ) {
249 1
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
250 1
            }
251
252 13
            if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENQBRACKET, EmailLexer::S_OPENBRACKET))) {
253 1
                throw new ExpectingDTEXT();
254
            }
255
256 12
            if ($this->lexer->isNextTokenAny(
257 12
                array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF)
258 12
            )) {
259
                $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
260
                $this->parseFWS();
261
            }
262
263 12
            if ($this->lexer->isNextToken(EmailLexer::S_CR)) {
264 1
                throw new CRNoLF();
265
            }
266
267 11
            if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) {
268
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
269
                $addressLiteral .= $this->lexer->token['value'];
270
                $this->lexer->moveNext();
271
                $this->validateQuotedPair();
272
            }
273 11
            if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) {
274 7
                $IPv6TAG = true;
275 7
            }
276 11
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEQBRACKET) {
277
                break;
278
            }
279
280 11
            $addressLiteral .= $this->lexer->token['value'];
281
282 11
        } while ($this->lexer->moveNext());
283
284 11
        $addressLiteral = str_replace('[', '', $addressLiteral);
285 11
        $addressLiteral = $this->checkIPV4Tag($addressLiteral);
286
287 11
        if (false === $addressLiteral) {
288 1
            return $addressLiteral;
289
        }
290
291 10
        if (!$IPv6TAG) {
292 3
            $this->warnings[DomainLiteral::CODE] = new DomainLiteral();
293 3
            return $addressLiteral;
294
        }
295
296 7
        $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
297
298 7
        $this->checkIPV6Tag($addressLiteral);
299
300 7
        return $addressLiteral;
301
    }
302
303
    /**
304
     * @param string $addressLiteral
305
     *
306
     * @return string|false
307
     */
308 11
    protected function checkIPV4Tag($addressLiteral)
309
    {
310 11
        $matchesIP  = array();
311
312
        // Extract IPv4 part from the end of the address-literal (if there is one)
313 11
        if (preg_match(
314 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]?)$/',
315 11
            $addressLiteral,
316
            $matchesIP
317 11
        ) > 0
318 11
        ) {
319 2
            $index = strrpos($addressLiteral, $matchesIP[0]);
320 2
            if ($index === 0) {
321 1
                $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
322 1
                return false;
323
            }
324
            // Convert IPv4 part to IPv6 format for further testing
325 1
            $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0';
326 1
        }
327
328 10
        return $addressLiteral;
329
    }
330
331 81
    protected function checkDomainPartExceptions(array $prev)
332
    {
333
        $invalidDomainTokens = array(
334 81
            EmailLexer::S_DQUOTE => true,
335 81
            EmailLexer::S_SEMICOLON => true,
336 81
            EmailLexer::S_GREATERTHAN => true,
337 81
            EmailLexer::S_LOWERTHAN => true,
338 81
        );
339
340 81
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
341 4
            throw new ExpectingATEXT();
342
        }
343
344 81
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
345 1
            throw new CommaInDomain();
346
        }
347
348 81
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
349 2
            throw new ConsecutiveAt();
350
        }
351
352 80
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
353 1
            throw new ExpectingATEXT();
354
        }
355
356 80 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...
357 1
            throw new DomainHyphened();
358
        }
359
360 80 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...
361 80
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
362
            throw new ExpectingATEXT();
363
        }
364 80
    }
365
366
    /**
367
     * @return bool
368
     */
369 80
    protected function hasBrackets()
370
    {
371 80
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
372 67
            return false;
373
        }
374
375
        try {
376 13
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
377 13
        } catch (\RuntimeException $e) {
378
            throw new ExpectingDomainLiteralClose();
379
        }
380
381 13
        return true;
382
    }
383
384 78
    protected function checkLabelLength(array $prev)
385
    {
386 78
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
387 78
            $prev['type'] === EmailLexer::GENERIC &&
388 41
            strlen($prev['value']) > 63
389 78
        ) {
390 1
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
391 1
        }
392 78
    }
393
394 3
    protected function parseDomainComments()
395
    {
396 3
        $this->isUnclosedComment();
397 1
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
398 1
            $this->warnEscaping();
399 1
            $this->lexer->moveNext();
400 1
        }
401
402 1
        $this->lexer->moveNext();
403 1
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
404
            throw new ExpectingATEXT();
405
        }
406 1
    }
407
408
    protected function addTLDWarnings()
409
    {
410
        if ($this->warnings[DomainLiteral::CODE]) {
411
            $this->warnings[TLD::CODE] = new TLD();
412
        }
413
    }
414
}
415