Passed
Pull Request — master (#188)
by Sebastian
02:49
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
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
        $this->reader    = $reader;
66
        $this->reader->setLexer($this);
67
68
        $this->lastToken = $startOfFileToken;
69
        $this->token     = $startOfFileToken;
70
        $this->line      = 1;
71
        $this->lineStart = 0;
72
    }
73
74
    /**
75
     * @inheritdoc
76
     */
77
    public function advance(): Token
78
    {
79
        $this->lastToken = $this->token;
80
81
        return $this->token = $this->lookahead();
82
    }
83
84
    /**
85
     * @inheritdoc
86
     */
87
    public function lookahead(): Token
88
    {
89
        $token = $this->token;
90
91
        if (TokenKindEnum::EOF !== $token->getKind()) {
92
            do {
93
                $next = $this->readToken($token);
94
                $token->setNext($next);
95
                $token = $next;
96
            } while (TokenKindEnum::COMMENT === $token->getKind());
97
        }
98
99
        return $token;
100
    }
101
102
    /**
103
     * @inheritdoc
104
     */
105
    public function getOption(string $name, $default = null)
106
    {
107
        return $this->options[$name] ?? $default;
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function getBody(): string
114
    {
115
        return $this->getSource()->getBody();
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function getTokenKind(): string
122
    {
123
        return $this->token->getKind();
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129
    public function getTokenValue(): ?string
130
    {
131
        return $this->token->getValue();
132
    }
133
134
    /**
135
     * @inheritdoc
136
     */
137
    public function getToken(): Token
138
    {
139
        return $this->token;
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function getSource(): Source
146
    {
147
        if ($this->source instanceof Source) {
148
            return $this->source;
149
        }
150
151
        throw new LanguageException('No source has been set.');
152
    }
153
154
    /**
155
     * @inheritdoc
156
     */
157
    public function getLastToken(): Token
158
    {
159
        return $this->lastToken;
160
    }
161
162
    /**
163
     * @inheritdoc
164
     */
165
    public function setSource(Source $source)
166
    {
167
        $this->source = $source;
168
        return $this;
169
    }
170
171
    /**
172
     * @inheritdoc
173
     */
174
    public function setOptions(array $options)
175
    {
176
        $this->options = $options;
177
        return $this;
178
    }
179
180
    /**
181
     * @param int   $code
182
     * @param int   $pos
183
     * @param int   $line
184
     * @param int   $col
185
     * @param Token $prev
186
     * @return Token
187
     * @throws SyntaxErrorException
188
     */
189
    public function read(int $code, int $pos, int $line, int $col, Token $prev): Token
190
    {
191
        if ($token = $this->reader->read($code, $pos, $line, $col, $prev)) {
192
            return $token;
193
        }
194
195
        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

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

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

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