Completed
Pull Request — master (#80)
by Christoffer
02:18
created

Lexer::positionAfterWhitespace()   D

Complexity

Conditions 9
Paths 4

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

196
        throw new SyntaxErrorException(/** @scrutinizer ignore-type */ $this->source, $pos, $this->unexpectedCharacterMessage($code));
Loading history...
197
    }
198
199
    /**
200
     * @param Token $prev
201
     * @return Token
202
     * @throws SyntaxErrorException
203
     */
204
    protected function readToken(Token $prev): Token
205
    {
206
        $body       = $this->source->getBody();
0 ignored issues
show
Bug introduced by
The method getBody() does not exist on null. ( Ignorable by Annotation )

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

206
        /** @scrutinizer ignore-call */ 
207
        $body       = $this->source->getBody();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
207
        $bodyLength = mb_strlen($body);
208
209
        $pos  = $this->positionAfterWhitespace($body, $prev->getEnd());
210
        $line = $this->line;
211
        $col  = 1 + $pos - $this->lineStart;
212
213
        if ($pos >= $bodyLength) {
214
            return new Token(TokenKindEnum::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
215
        }
216
217
        $code = charCodeAt($body, $pos);
218
219
        if (isSourceCharacter($code)) {
220
            throw new SyntaxErrorException(
221
                $this->source,
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

221
                /** @scrutinizer ignore-type */ $this->source,
Loading history...
222
                $pos,
223
                sprintf('Cannot contain the invalid character %s', printCharCode($code))
224
            );
225
        }
226
227
        return $this->read($code, $pos, $line, $col, $prev);
228
    }
229
230
    /**
231
     * @param int $code
232
     * @return string
233
     */
234
    protected function unexpectedCharacterMessage(int $code): string
235
    {
236
        if ($code === 39) {
237
            // '
238
            return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
239
        }
240
241
        return sprintf('Cannot parse the unexpected character %s', printCharCode($code));
242
    }
243
244
    /**
245
     * @param int $code
246
     * @param int $pos
247
     * @return ReaderInterface|null
248
     */
249
    protected function getReader(int $code, int $pos): ?ReaderInterface
250
    {
251
        foreach ($this->readers as $reader) {
252
            if ($reader instanceof ReaderInterface && $reader->supportsReader($code, $pos)) {
253
                return $reader;
254
            }
255
        }
256
257
        return null;
258
    }
259
260
    /**
261
     * @param string $body
262
     * @param int    $startPosition
263
     * @return int
264
     */
265
    protected function positionAfterWhitespace(string $body, int $startPosition): int
266
    {
267
        $bodyLength = mb_strlen($body);
268
        $pos        = $startPosition;
269
270
        while ($pos < $bodyLength) {
271
            $code = charCodeAt($body, $pos);
272
273
            if ($code === 9 || $code === 32 || $code === 44 || $code === 0xfeff) {
274
                // tab | space | comma | BOM
275
                ++$pos;
276
            } elseif ($code === 10) {
277
                // new line
278
                ++$pos;
279
                $this->advanceLine($pos);
280
            } elseif ($code === 13) {
281
                // carriage return
282
                if (charCodeAt($body, $pos + 1) === 10) {
283
                    $pos += 2;
284
                } else {
285
                    ++$pos;
286
                }
287
                $this->advanceLine($pos);
288
            } else {
289
                break;
290
            }
291
        }
292
293
        return $pos;
294
    }
295
296
    /**
297
     * @param int $pos
298
     */
299
    protected function advanceLine(int $pos)
300
    {
301
        ++$this->line;
302
        $this->lineStart = $pos;
303
    }
304
}
305