Passed
Pull Request — master (#196)
by Christoffer
02:33
created

Lexer::skipWhitespace()   D

Complexity

Conditions 9
Paths 6

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 4.909
c 0
b 0
f 0
cc 9
eloc 17
nc 6
nop 0
1
<?php
2
3
namespace Digia\GraphQL\Language;
4
5
use Digia\GraphQL\Error\LanguageException;
6
use Digia\GraphQL\Error\SyntaxErrorException;
7
8
class Lexer implements LexerInterface
9
{
10
11
    /**
12
     * TODO: Document this
13
     * 
14
     * @var Source|null
15
     */
16
    protected $source;
17
18
    /**
19
     * TODO: Document this
20
     * 
21
     * @var string|null
22
     */
23
    protected $body;
24
25
    /**
26
     * TODO: Document this
27
     * 
28
     * @var int
29
     */
30
    protected $bodyLength;
31
32
    /**
33
     * @var array
34
     */
35
    protected $options = [];
36
37
    /**
38
     * The previously focused non-ignored token.
39
     *
40
     * @var Token
41
     */
42
    protected $lastToken;
43
44
    /**
45
     * The currently focused non-ignored token.
46
     *
47
     * @var Token
48
     */
49
    protected $token;
50
51
    /**
52
     * TODO: Document this
53
     * 
54
     * @var int
55
     */
56
    protected $pos;
57
58
    /**
59
     * The (1-indexed) line containing the current token.
60
     *
61
     * @var int
62
     */
63
    protected $line;
64
65
    /**
66
     * The character offset at which the current line begins.
67
     *
68
     * @var int
69
     */
70
    protected $lineStart;
71
72
    /**
73
     * The token reader.
74
     *
75
     * @var TokenReaderInterface
76
     */
77
    protected $reader;
78
79
    /**
80
     * Lexer constructor.
81
     *
82
     * @param TokenReaderInterface $reader
83
     */
84
    public function __construct(TokenReaderInterface $reader)
85
    {
86
        $startOfFileToken = new Token(TokenKindEnum::SOF);
87
88
        $reader->setLexer($this);
89
90
        $this->reader    = $reader;
91
        $this->lastToken = $startOfFileToken;
92
        $this->token     = $startOfFileToken;
93
        $this->line      = 1;
94
        $this->lineStart = 0;
95
    }
96
97
    /**
98
     * @inheritdoc
99
     * @throws SyntaxErrorException
100
     */
101
    public function advance(): Token
102
    {
103
        $this->lastToken = $this->token;
104
105
        return $this->token = $this->lookahead();
106
    }
107
108
    /**
109
     * @inheritdoc
110
     * @throws SyntaxErrorException
111
     */
112
    public function lookahead(): Token
113
    {
114
        $token = $this->token;
115
116
        if (TokenKindEnum::EOF !== $token->getKind()) {
117
            do {
118
                $next = $this->readToken($token);
119
                $token->setNext($next);
120
                $token = $next;
121
            } while (TokenKindEnum::COMMENT === $token->getKind());
122
        }
123
124
        return $token;
125
    }
126
127
    /**
128
     * @inheritdoc
129
     */
130
    public function getOption(string $name, $default = null)
131
    {
132
        return $this->options[$name] ?? $default;
133
    }
134
135
    /**
136
     * @inheritdoc
137
     * @throws LanguageException
138
     */
139
    public function getBody(): string
140
    {
141
        return $this->getSource()->getBody();
142
    }
143
144
    /**
145
     * @inheritdoc
146
     */
147
    public function getTokenKind(): string
148
    {
149
        return $this->token->getKind();
150
    }
151
152
    /**
153
     * @inheritdoc
154
     */
155
    public function getTokenValue(): ?string
156
    {
157
        return $this->token->getValue();
158
    }
159
160
    /**
161
     * @inheritdoc
162
     */
163
    public function getToken(): Token
164
    {
165
        return $this->token;
166
    }
167
168
    /**
169
     * @inheritdoc
170
     * @throws LanguageException
171
     */
172
    public function getSource(): Source
173
    {
174
        if ($this->source instanceof Source) {
175
            return $this->source;
176
        }
177
178
        throw new LanguageException('No source has been set.');
179
    }
180
181
    /**
182
     * @inheritdoc
183
     */
184
    public function getLastToken(): Token
185
    {
186
        return $this->lastToken;
187
    }
188
189
    /**
190
     * @inheritdoc
191
     */
192
    public function setSource(Source $source)
193
    {
194
        $this->body       = $source->getBody();
195
        $this->bodyLength = \strlen($this->body);
196
        $this->source     = $source;
197
        return $this;
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203
    public function setOptions(array $options)
204
    {
205
        $this->options = $options;
206
        return $this;
207
    }
208
209
    /**
210
     * @param Token $prev
211
     * @return Token
212
     * @throws SyntaxErrorException
213
     */
214
    protected function readToken(Token $prev): Token
215
    {
216
        $this->pos = $prev->getEnd();
217
218
        $this->skipWhitespace();
219
220
        $line = $this->line;
221
        $col  = 1 + $this->pos - $this->lineStart;
222
223
        if ($this->pos >= $this->bodyLength) {
224
            return new Token(TokenKindEnum::EOF, $this->bodyLength, $this->bodyLength, $line, $col, $prev);
225
        }
226
227
        $code = charCodeAt($this->body, $this->pos);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $string of Digia\GraphQL\Language\charCodeAt() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
        $code = charCodeAt(/** @scrutinizer ignore-type */ $this->body, $this->pos);
Loading history...
228
229
        $token = $this->reader->read($this->body, $this->bodyLength, $code, $this->pos, $line, $col, $prev);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $body of Digia\GraphQL\Language\T...ReaderInterface::read() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
        $token = $this->reader->read(/** @scrutinizer ignore-type */ $this->body, $this->bodyLength, $code, $this->pos, $line, $col, $prev);
Loading history...
230
231
        if (null !== $token) {
232
            return $token;
233
        }
234
235
        throw new SyntaxErrorException($this->source, $this->pos, $this->unexpectedCharacterMessage($code));
0 ignored issues
show
Bug introduced by
It seems like $this->source can also be of type null; however, parameter $source of Digia\GraphQL\Error\Synt...xception::__construct() does only seem to accept Digia\GraphQL\Language\Source, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
        throw new SyntaxErrorException(/** @scrutinizer ignore-type */ $this->source, $this->pos, $this->unexpectedCharacterMessage($code));
Loading history...
236
    }
237
238
    /**
239
     * Report a message that an unexpected character was encountered.
240
     *
241
     * @param int $code
242
     * @return string
243
     */
244
    protected function unexpectedCharacterMessage(int $code): string
245
    {
246
        if (isSourceCharacter($code) && !isLineTerminator($code)) {
247
            return \sprintf('Cannot contain the invalid character %s.', printCharCode($code));
248
        }
249
250
        if ($code === 39) {
251
            // '
252
            return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
253
        }
254
255
        return \sprintf('Cannot parse the unexpected character %s.', printCharCode($code));
256
    }
257
258
    /**
259
     *
260
     */
261
    protected function skipWhitespace(): void
262
    {
263
        while ($this->pos < $this->bodyLength) {
264
            $code = charCodeAt($this->body, $this->pos);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $string of Digia\GraphQL\Language\charCodeAt() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

264
            $code = charCodeAt(/** @scrutinizer ignore-type */ $this->body, $this->pos);
Loading history...
265
266
            if ($code === 9 || $code === 32 || $code === 44 || $code === 0xfeff) {
267
                // tab | space | comma | BOM
268
                ++$this->pos;
269
            } elseif ($code === 10) {
270
                // new line (\n)
271
                ++$this->pos;
272
                ++$this->line;
273
                $this->lineStart = $this->pos;
274
            } elseif ($code === 13) {
275
                // carriage return (\r)
276
                if (charCodeAt($this->body, $this->pos + 1) === 10) {
277
                    // carriage return and new line (\r\n)
278
                    $this->pos += 2;
279
                } else {
280
                    ++$this->pos;
281
                }
282
                ++$this->line;
283
                $this->lineStart = $this->pos;
284
            } else {
285
                break;
286
            }
287
        }
288
    }
289
}
290