Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slow-bees-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mermaid-js/parser': patch
---

fix: enhanced parser error messages to include line and column numbers for better debugging experience
4 changes: 2 additions & 2 deletions packages/mermaid/src/diagrams/info/info.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ describe('info', () => {
it('should throw because of unsupported info grammar', async () => {
const str = `info unsupported`;
await expect(parser.parse(str)).rejects.toThrow(
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
'Parsing failed: Lexer error on line 1, column 6: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
);
});

it('should throw because of unsupported info grammar', async () => {
const str = `info unsupported`;
await expect(parser.parse(str)).rejects.toThrow(
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
'Parsing failed: Lexer error on line 1, column 6: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
);
});
});
22 changes: 20 additions & 2 deletions packages/parser/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,26 @@ export async function parse<T extends DiagramAST>(

export class MermaidParseError extends Error {
constructor(public result: ParseResult<DiagramAST>) {
const lexerErrors: string = result.lexerErrors.map((err) => err.message).join('\n');
const parserErrors: string = result.parserErrors.map((err) => err.message).join('\n');
const lexerErrors: string = result.lexerErrors
.map((err) => {
const line = err.line !== undefined && !isNaN(err.line) ? err.line : '?';
const column = err.column !== undefined && !isNaN(err.column) ? err.column : '?';
return `Lexer error on line ${line}, column ${column}: ${err.message}`;
})
.join('\n');
const parserErrors: string = result.parserErrors
.map((err) => {
const line =
err.token.startLine !== undefined && !isNaN(err.token.startLine)
? err.token.startLine
: '?';
const column =
err.token.startColumn !== undefined && !isNaN(err.token.startColumn)
? err.token.startColumn
: '?';
return `Parse error on line ${line}, column ${column}: ${err.message}`;
})
.join('\n');
super(`Parsing failed: ${lexerErrors} ${parserErrors}`);
}
}
82 changes: 82 additions & 0 deletions packages/parser/tests/radar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';

import { Radar } from '../src/language/index.js';
import { expectNoErrorsOrAlternatives, radarParse as parse } from './test-util.js';
import { parse as parseAsync, MermaidParseError } from '../src/parse.js';

const mutateGlobalSpacing = (context: string) => {
return [
Expand Down Expand Up @@ -340,4 +341,85 @@ describe('radar', () => {
}
);
});

describe('error messages with line and column numbers', () => {
it('should include line and column numbers in parser errors for radar diagrams', async () => {
const invalidRadar = `radar-beta
title Restaurant Comparison
axis food["Food Quality"], service["Service"], price["Price"]
axis ambiance["Ambiance"],

curve a["Restaurant A"]{4, 3, 2, 4}`;

try {
await parseAsync('radar', invalidRadar);
expect.fail('Should have thrown MermaidParseError');
} catch (error: any) {
expect(error).toBeInstanceOf(MermaidParseError);
expect(error.message).toMatch(/line \d+/);
expect(error.message).toMatch(/column \d+/);
}
});

it('should include line and column numbers for missing curve entries', async () => {
const invalidRadar = `radar-beta
axis my-axis
curve my-curve`;

try {
await parseAsync('radar', invalidRadar);
expect.fail('Should have thrown MermaidParseError');
} catch (error: any) {
expect(error).toBeInstanceOf(MermaidParseError);
// Line and column may be ? if not available
expect(error.message).toMatch(/line (\d+|\?)/);
expect(error.message).toMatch(/column (\d+|\?)/);
}
});

it('should include line and column numbers for invalid axis syntax', async () => {
const invalidRadar = `radar-beta
axis my-axis my-axis2`;

try {
await parseAsync('radar', invalidRadar);
expect.fail('Should have thrown MermaidParseError');
} catch (error: any) {
expect(error).toBeInstanceOf(MermaidParseError);
expect(error.message).toMatch(/line \d+/);
expect(error.message).toMatch(/column \d+/);
}
});

it('should handle lexer errors with line and column numbers', async () => {
const invalidRadar = `radar-beta
axis A
curve B{1}
invalid@symbol`;

try {
await parseAsync('radar', invalidRadar);
expect.fail('Should have thrown MermaidParseError');
} catch (error: any) {
expect(error).toBeInstanceOf(MermaidParseError);
// Should have line and column in the error message
expect(error.message).toMatch(/line (\d+|\?)/);
expect(error.message).toMatch(/column (\d+|\?)/);
}
});

it('should format error message with "Parse error on line X, column Y" prefix', async () => {
const invalidRadar = `radar-beta
axis`;

try {
await parseAsync('radar', invalidRadar);
expect.fail('Should have thrown MermaidParseError');
} catch (error: any) {
expect(error).toBeInstanceOf(MermaidParseError);
// Line and column may be ? if not available
expect(error.message).toMatch(/Parse error on line (\d+|\?), column (\d+|\?):/);
}
});
});
});
Loading