Passed
Push — master ( f3670f...6349c7 )
by Christoffer
02:45
created

Lexer::readToken()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

194
        throw new SyntaxErrorException(/** @scrutinizer ignore-type */ $this->source, $pos, $this->unexpectedCharacterMessage($code));
Loading history...
195
    }
196
197
    /**
198
     * @param Token $prev
199
     * @return Token
200
     * @throws SyntaxErrorException
201
     */
202
    protected function readToken(Token $prev): Token
203
    {
204
        $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

204
        /** @scrutinizer ignore-call */ 
205
        $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...
205
        $bodyLength = mb_strlen($body);
206
207
        $pos  = $this->positionAfterWhitespace($body, $prev->getEnd());
208
        $line = $this->line;
209
        $col  = 1 + $pos - $this->lineStart;
210
211
        if ($pos >= $bodyLength) {
212
            return new Token(TokenKindEnum::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
213
        }
214
215
        $code = charCodeAt($body, $pos);
216
217
        if (isSourceCharacter($code)) {
218
            throw new SyntaxErrorException(
219
                $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

219
                /** @scrutinizer ignore-type */ $this->source,
Loading history...
220
                $pos,
221
                sprintf('Cannot contain the invalid character %s', printCharCode($code))
222
            );
223
        }
224
225
        return $this->read($code, $pos, $line, $col, $prev);
226
    }
227
228
    /**
229
     * @param int $code
230
     * @return string
231
     */
232
    protected function unexpectedCharacterMessage(int $code): string
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 string $body
244
     * @param int    $startPosition
245
     * @return int
246
     */
247
    protected function positionAfterWhitespace(string $body, int $startPosition): int
248
    {
249
        $bodyLength = mb_strlen($body);
250
        $pos        = $startPosition;
251
252
        while ($pos < $bodyLength) {
253
            $code = charCodeAt($body, $pos);
254
255
            if ($code === 9 || $code === 32 || $code === 44 || $code === 0xfeff) {
256
                // tab | space | comma | BOM
257
                ++$pos;
258
            } elseif ($code === 10) {
259
                // new line
260
                ++$pos;
261
                $this->advanceLine($pos);
262
            } elseif ($code === 13) {
263
                // carriage return
264
                if (charCodeAt($body, $pos + 1) === 10) {
265
                    $pos += 2;
266
                } else {
267
                    ++$pos;
268
                }
269
                $this->advanceLine($pos);
270
            } else {
271
                break;
272
            }
273
        }
274
275
        return $pos;
276
    }
277
278
    /**
279
     * @param int $pos
280
     */
281
    protected function advanceLine(int $pos)
282
    {
283
        ++$this->line;
284
        $this->lineStart = $pos;
285
    }
286
}
287