Completed
Pull Request — master (#195)
by Christoffer
17:48
created

Lexer::readToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

198
        /** @scrutinizer ignore-call */ 
199
        $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...
199
        $bodyLength = \mb_strlen($body);
200
201
        $pos  = $this->positionAfterWhitespace($body, $prev->getEnd());
202
        $line = $this->line;
203
        $col  = 1 + $pos - $this->lineStart;
204
205
        if ($pos >= $bodyLength) {
206
            return new Token(TokenKindEnum::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
207
        }
208
209
        $token = $this->reader->read($body, $pos, $line, $col, $prev);
210
211
        if (null !== $token) {
212
            return $token;
213
        }
214
215
        throw new SyntaxErrorException($this->source, $pos, $this->unexpectedCharacterMessage($code));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $code seems to be never defined.
Loading history...
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

215
        throw new SyntaxErrorException(/** @scrutinizer ignore-type */ $this->source, $pos, $this->unexpectedCharacterMessage($code));
Loading history...
216
    }
217
218
    /**
219
     * Report a message that an unexpected character was encountered.
220
     *
221
     * @param int $code
222
     * @return string
223
     */
224
    protected function unexpectedCharacterMessage(int $code): string
225
    {
226
        if (isSourceCharacter($code) && !isLineTerminator($code)) {
227
            return \sprintf('Cannot contain the invalid character %s.', printCharCode($code));
228
        }
229
230
        if ($code === 39) {
231
            // '
232
            return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
233
        }
234
235
        return \sprintf('Cannot parse the unexpected character %s.', printCharCode($code));
236
    }
237
238
    /**
239
     * @param string $body
240
     * @param int    $startPosition
241
     * @return int
242
     */
243
    protected function positionAfterWhitespace(string $body, int $startPosition): int
244
    {
245
        $bodyLength = \mb_strlen($body);
246
        $pos        = $startPosition;
247
248
        while ($pos < $bodyLength) {
249
            $code = charCodeAt($body, $pos);
250
251
            if ($code === 9 || $code === 32 || $code === 44 || $code === 0xfeff) {
252
                // tab | space | comma | BOM
253
                ++$pos;
254
            } elseif ($code === 10) {
255
                // new line (\n)
256
                ++$pos;
257
                $this->advanceLine($pos);
258
            } elseif ($code === 13) {
259
                // carriage return (\r)
260
                if (charCodeAt($body, $pos + 1) === 10) {
261
                    $pos += 2;
262
                } else {
263
                    ++$pos;
264
                }
265
                $this->advanceLine($pos);
266
            } else {
267
                break;
268
            }
269
        }
270
271
        return $pos;
272
    }
273
274
    /**
275
     * @param int $pos
276
     */
277
    protected function advanceLine(int $pos): void
278
    {
279
        ++$this->line;
280
        $this->lineStart = $pos;
281
    }
282
}
283