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

Lexer::positionAfterWhitespace()   D

Complexity

Conditions 9
Paths 4

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

211
        $code = charCodeAt(/** @scrutinizer ignore-type */ $this->body, $pos);
Loading history...
212
213
        $token = $this->reader->read($this->body, $this->bodyLength, $code, $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

213
        $token = $this->reader->read(/** @scrutinizer ignore-type */ $this->body, $this->bodyLength, $code, $pos, $line, $col, $prev);
Loading history...
214
215
        if (null !== $token) {
216
            return $token;
217
        }
218
219
        throw new SyntaxErrorException($this->source, $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

219
        throw new SyntaxErrorException(/** @scrutinizer ignore-type */ $this->source, $pos, $this->unexpectedCharacterMessage($code));
Loading history...
220
    }
221
222
    /**
223
     * Report a message that an unexpected character was encountered.
224
     *
225
     * @param int $code
226
     * @return string
227
     */
228
    protected function unexpectedCharacterMessage(int $code): string
229
    {
230
        if (isSourceCharacter($code) && !isLineTerminator($code)) {
231
            return \sprintf('Cannot contain the invalid character %s.', printCharCode($code));
232
        }
233
234
        if ($code === 39) {
235
            // '
236
            return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
237
        }
238
239
        return \sprintf('Cannot parse the unexpected character %s.', printCharCode($code));
240
    }
241
242
    /**
243
     * @param int $startPosition
244
     * @return int
245
     */
246
    protected function positionAfterWhitespace(int $startPosition): int
247
    {
248
        $pos = $startPosition;
249
250
        while ($pos < $this->bodyLength) {
251
            $code = charCodeAt($this->body, $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

251
            $code = charCodeAt(/** @scrutinizer ignore-type */ $this->body, $pos);
Loading history...
252
253
            if ($code === 9 || $code === 32 || $code === 44 || $code === 0xfeff) {
254
                // tab | space | comma | BOM
255
                ++$pos;
256
            } elseif ($code === 10) {
257
                // new line (\n)
258
                ++$pos;
259
                ++$this->line;
260
                $this->lineStart = $pos;
261
            } elseif ($code === 13) {
262
                // carriage return (\r)
263
                if (charCodeAt($this->body, $pos + 1) === 10) {
264
                    // carriage return and new line (\r\n)
265
                    $pos += 2;
266
                } else {
267
                    ++$pos;
268
                }
269
                ++$this->line;
270
                $this->lineStart = $pos;
271
            } else {
272
                break;
273
            }
274
        }
275
276
        return $pos;
277
    }
278
}
279