Skip to content

Commit a1e06b2

Browse files
committed
Account for query offset in files for errors
1 parent 6050af4 commit a1e06b2

File tree

3 files changed

+110
-13
lines changed

3 files changed

+110
-13
lines changed

src/Error/SyntaxError.php

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ class SyntaxError extends Error
1414
public function __construct(Source $source, $position, $description)
1515
{
1616
$location = $source->getLocation($position);
17+
$line = $location->line + $source->locationOffset->line - 1;
18+
$columnOffset = self::getColumnOffset($source, $location);
19+
$column = $location->column + $columnOffset;
20+
1721
$syntaxError =
18-
"Syntax Error {$source->name} ({$location->line}:{$location->column}) $description\n\n" .
22+
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
23+
"\n".
1924
self::highlightSourceAtLocation($source, $location);
2025

2126
parent::__construct($syntaxError, null, $source, [$position]);
@@ -29,22 +34,38 @@ public function __construct(Source $source, $position, $description)
2934
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
3035
{
3136
$line = $location->line;
32-
$prevLineNum = (string) ($line - 1);
33-
$lineNum = (string) $line;
34-
$nextLineNum = (string) ($line + 1);
37+
$lineOffset = $source->locationOffset->line - 1;
38+
$columnOffset = self::getColumnOffset($source, $location);
39+
40+
$contextLine = $line + $lineOffset;
41+
$prevLineNum = (string) ($contextLine - 1);
42+
$lineNum = (string) $contextLine;
43+
$nextLineNum = (string) ($contextLine + 1);
3544
$padLen = mb_strlen($nextLineNum, 'UTF-8');
3645

3746
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
3847
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
3948

40-
$lpad = function($len, $str) {
49+
$whitespace = function ($len) {
50+
return str_repeat(' ', $len);
51+
};
52+
53+
$lpad = function ($len, $str) {
4154
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
4255
};
4356

57+
$lines[0] = $whitespace($source->locationOffset->column - 1) . $lines[0];
58+
4459
return
4560
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
4661
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
47-
(str_repeat(' ', 1 + $padLen + $location->column) . "^\n") .
62+
($whitespace(2 + $padLen + $location->column - 1 + $columnOffset) . "^\n") .
4863
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
4964
}
65+
66+
public static function getColumnOffset(Source $source, SourceLocation $location)
67+
{
68+
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
69+
}
70+
5071
}

src/Language/Source.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
use GraphQL\Utils\Utils;
55

6+
/**
7+
* Class Source
8+
* @package GraphQL\Language
9+
*/
610
class Source
711
{
812
/**
@@ -20,7 +24,26 @@ class Source
2024
*/
2125
public $name;
2226

23-
public function __construct($body, $name = null)
27+
/**
28+
* @var SourceLocation
29+
*/
30+
public $locationOffset;
31+
32+
/**
33+
* Source constructor.
34+
*
35+
* A representation of source input to GraphQL.
36+
* `name` and `locationOffset` are optional. They are useful for clients who
37+
* store GraphQL documents in source files; for example, if the GraphQL input
38+
* starts at line 40 in a file named Foo.graphql, it might be useful for name to
39+
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
40+
* line and column in locationOffset are 1-indexed
41+
*
42+
* @param $body
43+
* @param null $name
44+
* @param SourceLocation|null $location
45+
*/
46+
public function __construct($body, $name = null, SourceLocation $location = null)
2447
{
2548
Utils::invariant(
2649
is_string($body),
@@ -30,6 +53,16 @@ public function __construct($body, $name = null)
3053
$this->body = $body;
3154
$this->length = mb_strlen($body, 'UTF-8');
3255
$this->name = $name ?: 'GraphQL';
56+
$this->locationOffset = $location ?: new SourceLocation(1, 1);
57+
58+
Utils::invariant(
59+
$this->locationOffset->line > 0,
60+
'line in locationOffset is 1-indexed and must be positive'
61+
);
62+
Utils::invariant(
63+
$this->locationOffset->column > 0,
64+
'column in locationOffset is 1-indexed and must be positive'
65+
);
3366
}
3467

3568
/**

tests/Language/LexerTest.php

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use GraphQL\Language\Lexer;
55
use GraphQL\Language\Source;
6+
use GraphQL\Language\SourceLocation;
67
use GraphQL\Language\Token;
78
use GraphQL\Error\SyntaxError;
89
use GraphQL\Utils\Utils;
@@ -107,14 +108,14 @@ public function testSkipsWhitespacesAndComments()
107108
*/
108109
public function testErrorsRespectWhitespace()
109110
{
110-
$example = "
111+
$str = '' .
112+
"\n" .
113+
"\n" .
114+
" ?\n" .
115+
"\n";
111116

112-
?
113-
114-
115-
";
116117
try {
117-
$this->lexOne($example);
118+
$this->lexOne($str);
118119
$this->fail('Expected exception not thrown');
119120
} catch (SyntaxError $e) {
120121
$this->assertEquals(
@@ -129,6 +130,48 @@ public function testErrorsRespectWhitespace()
129130
}
130131
}
131132

133+
/**
134+
* @it updates line numbers in error for file context
135+
*/
136+
public function testUpdatesLineNumbersInErrorForFileContext()
137+
{
138+
$str = '' .
139+
"\n" .
140+
"\n" .
141+
" ?\n" .
142+
"\n";
143+
$source = new Source($str, 'foo.js', new SourceLocation(11, 12));
144+
145+
$this->setExpectedException(
146+
SyntaxError::class,
147+
'Syntax Error foo.js (13:6) ' .
148+
'Cannot parse the unexpected character "?".' . "\n" .
149+
"\n" .
150+
'12: ' . "\n" .
151+
'13: ?' . "\n" .
152+
' ^' . "\n" .
153+
'14: ' . "\n"
154+
);
155+
$lexer = new Lexer($source);
156+
$lexer->advance();
157+
}
158+
159+
public function testUpdatesColumnNumbersInErrorForFileContext()
160+
{
161+
$source = new Source('?', 'foo.js', new SourceLocation(1, 5));
162+
163+
$this->setExpectedException(
164+
SyntaxError::class,
165+
'Syntax Error foo.js (1:5) ' .
166+
'Cannot parse the unexpected character "?".' . "\n" .
167+
"\n" .
168+
'1: ?' . "\n" .
169+
' ^' . "\n"
170+
);
171+
$lexer = new Lexer($source);
172+
$lexer->advance();
173+
}
174+
132175
/**
133176
* @it lexes strings
134177
*/

0 commit comments

Comments
 (0)