Skip to content

Commit ae748fc

Browse files
committed
Improving error documentation, test coverage, and adding a type
- Updates readme with an errors section. - Adds a `GlslSyntaxError` type alais for peggy errors, and exports it. - Adds tests for errors and an API function test
1 parent 2c5d60a commit ae748fc

File tree

8 files changed

+128
-12
lines changed

8 files changed

+128
-12
lines changed

README.md

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ type ParserOptions = {
4747
// like undefined functions and variables. If `failOnWarn` is set to true,
4848
// warnings will still cause the parser to raise an error. Defaults to false.
4949
quiet: boolean;
50-
// The origin of the GLSL, for debugging. For example, "main.js", If the
51-
// parser raises an error (specifically a GrammarError), and you call
52-
// error.format([]) on it, the error shows { source: 'main.js', ... }.
53-
// Defaults to null.
50+
// An optional string reprsenting the origin of the GLSL, for debugging and
51+
// error messages. For example, "main.js". If the parser raises an error, the
52+
// grammarSource shows up in the error.source field. If you format the error
53+
// (see the errors section), the grammarSource shows up in the formatted error
54+
// string. Defaults to undefined.
5455
grammarSource: string;
5556
// If true, sets location information on each AST node, in the form of
5657
// { column: number, line: number, offset: number }. Defaults to false.
@@ -207,6 +208,65 @@ matches based on the name and arity of the functions.
207208

208209
See also [#Utility-Functions] for renaming scope references.
209210

211+
## Errors
212+
213+
If you have invalid GLSL, the parser throws a `GlslSyntaxError`, which is a type
214+
alias for `peggy.SyntaxError`.
215+
216+
```ts
217+
import { parser, GlslSyntaxError } from '@shaderfrog/glsl-parser';
218+
219+
let error: GlslSyntaxError | undefined;
220+
try {
221+
// Line without a semicolon
222+
c.parse(`float a`);
223+
} catch (e) {
224+
error = e as GlslSyntaxError;
225+
}
226+
```
227+
228+
The error class lives on the parser object itself:
229+
```ts
230+
console.log(error instanceof parser.SyntaxError)
231+
// true
232+
```
233+
234+
The error message is automatically generated by Peggy:
235+
```ts
236+
console.log(error.message)
237+
// 'Expected ",", ";", "=", or array specifier but end of input found'
238+
```
239+
240+
It includes the location of the error. Note `source` is the `grammarSource`
241+
string provided to the parser options, which is `undefined` by default.
242+
```ts
243+
console.log(error.location)
244+
/*
245+
{
246+
source: undefined,
247+
start: { offset: 7, line: 1, column: 8 },
248+
end: { offset: 7, line: 1, column: 8 }
249+
}
250+
*/
251+
```
252+
253+
The standard Peggy error object also has a fairly confusing `format()` method,
254+
which produces an ASCII formatted string with arrows and underlines. The
255+
`source` option passed to `.format()` **must** match your `grammarSource` in
256+
parser options (which is `undefined` by default). This API is awkward and I
257+
might override it in future versions of the parser.
258+
259+
```ts
260+
console.log(error.format([{ text, source: undefined }])
261+
/*
262+
Error: Expected ",", ";", "=", or array specifier but "f" found.
263+
--> undefined:2:1
264+
|
265+
2 | float b
266+
| ^
267+
*/
268+
```
269+
210270
## Manipulating and Searching ASTs
211271
212272
### Visitors

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"engines": {
44
"node": ">=16"
55
},
6-
"version": "2.0.1",
6+
"version": "2.1.0",
77
"description": "A GLSL ES 1.0 and 3.0 parser and preprocessor that can preserve whitespace and comments",
88
"scripts": {
99
"prepare": "npm run build && ./prepublish.sh",

src/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { parser } from 'peggy';
2+
3+
export type GlslSyntaxError = parser.SyntaxError;

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import generate from './parser/generator';
22
import parser from './parser/parser';
33

4-
export = { generate, parser };
4+
export type * from './error';
5+
6+
export { generate, parser };

src/parser/parse.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1-
import { AstNode, FunctionCallNode, TypeSpecifierNode, visit } from '../ast';
2-
import { buildParser, inspect } from './test-helpers';
1+
import { FunctionCallNode, visit } from '../ast';
2+
import { GlslSyntaxError } from '../error';
3+
import { buildParser } from './test-helpers';
34

45
let c!: ReturnType<typeof buildParser>;
56
beforeAll(() => (c = buildParser()));
67

8+
test('parse error', () => {
9+
let error: GlslSyntaxError | undefined;
10+
// Missing a semicolon
11+
const text = `float a
12+
float b`;
13+
try {
14+
c.parse(text);
15+
} catch (e) {
16+
error = e as GlslSyntaxError;
17+
}
18+
19+
expect(error).toBeInstanceOf(c.parser.SyntaxError);
20+
expect(error!.location.start.line).toBe(2);
21+
expect(error!.location.end.line).toBe(2);
22+
});
23+
724
test('declarations', () => {
825
c.expectParsedProgram(`
926
float a, b = 1.0, c = a;

src/parser/test-helpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ export const buildParser = () => {
2626
execSync(
2727
'npx peggy --cache -o src/parser/parser.js src/parser/glsl-grammar.pegjs'
2828
);
29-
const parse = require('./parser').parse as Parse;
29+
const parser = require('./parser');
30+
const parse = parser.parse as Parse;
3031
const ps = parseSrc(parse);
3132
const ctx: Context = {
3233
parse,
3334
parseSrc: ps,
3435
};
3536
return {
3637
parse,
38+
parser,
3739
parseSrc: ps,
3840
debugSrc: debugSrc(ctx),
3941
debugStatement: debugStatement(ctx),

src/preprocessor/preprocessor.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import fs from 'fs';
2-
import path from 'path';
32
import peggy from 'peggy';
43
import util from 'util';
54
import {
@@ -8,6 +7,7 @@ import {
87
PreprocessorProgram,
98
} from './preprocessor';
109
import generate from './generator';
10+
import { GlslSyntaxError } from '../error';
1111

1212
const fileContents = (filePath: string): string =>
1313
fs.readFileSync(filePath).toString();
@@ -37,6 +37,38 @@ const expectParsedProgram = (sourceGlsl: string) => {
3737
// expectParsedProgram(fileContents('./preprocess-test-grammar.glsl'));
3838
// });
3939

40+
test('#preprocessComments', () => {
41+
// Should strip comments and replace single-line comments with a single space
42+
expect(
43+
preprocessComments(`// ccc
44+
/* cc */aaa/* cc */
45+
/**
46+
* cccc
47+
*/
48+
bbb
49+
`)
50+
).toBe(`
51+
aaa
52+
53+
54+
55+
bbb
56+
`);
57+
});
58+
59+
test('preprocessor error', () => {
60+
let error: GlslSyntaxError | undefined;
61+
try {
62+
parse(`#if defined(#)`);
63+
} catch (e) {
64+
error = e as GlslSyntaxError;
65+
}
66+
67+
expect(error).toBeInstanceOf(parser.SyntaxError);
68+
expect(error!.location.start.line).toBe(1);
69+
expect(error!.location.end.line).toBe(1);
70+
});
71+
4072
test('preprocessor ast', () => {
4173
expectParsedProgram(`
4274
#line 0

src/preprocessor/preprocessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ const preprocessComments = (src: string): string => {
7777
let in_multi = 0;
7878

7979
for (i = 0; i < src.length; i++) {
80-
chr = src.substr(i, 1);
81-
la = src.substr(i + 1, 1);
80+
chr = src.substring(i, i + 1);
81+
la = src.substring(i + 1, i + 2);
8282

8383
// Enter single line comment
8484
if (chr == '/' && la == '/' && !in_single && !in_multi) {

0 commit comments

Comments
 (0)