From 18cc77d6b21370ed4f238033153c1fd5843e31a4 Mon Sep 17 00:00:00 2001 From: Mike Lischke Date: Sat, 4 Jan 2025 13:52:51 +0100 Subject: [PATCH] Make sure escape chars in a edge labels in the DOT generator are all replaced correctly Fixes #33. --- src/tool/DOTGenerator.ts | 21 +++--- tests/TestATNInterpreter.spec.ts | 2 - tests/bugs/DotGenerator.spec.ts | 38 +++++++++++ tests/bugs/data/abbLexer.g4 | 107 +++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 tests/bugs/DotGenerator.spec.ts create mode 100644 tests/bugs/data/abbLexer.g4 diff --git a/src/tool/DOTGenerator.ts b/src/tool/DOTGenerator.ts index bfd67dc..337d2c7 100644 --- a/src/tool/DOTGenerator.ts +++ b/src/tool/DOTGenerator.ts @@ -12,9 +12,8 @@ import { RangeTransition, RuleStartState, RuleStopState, RuleTransition, SetTransition, StarBlockStartState, StarLoopEntryState, StarLoopbackState, } from "antlr4ng"; -import { STGroupFile } from "stringtemplate4ts"; +import { STGroupFile, IST } from "stringtemplate4ts"; -import type { IST } from "stringtemplate4ts/dist/compiler/common.js"; import { Utils } from "../misc/Utils.js"; import { Grammar } from "./Grammar.js"; @@ -113,14 +112,14 @@ export class DOTGenerator { throw new Error("no such template: edge"); } - edgeST.add("label", this.getEdgeLabel(String(edge))); + edgeST.add("label", this.getEdgeLabel(edge.toString())); } else if (edge.isEpsilon) { edgeST = DOTGenerator.stLib.getInstanceOf("epsilon-edge"); if (!edgeST) { throw new Error("no such template: epsilon-edge"); } - edgeST.add("label", this.getEdgeLabel(String(edge))); + edgeST.add("label", this.getEdgeLabel(edge.toString())); let loopback = false; if (edge.target instanceof PlusBlockStartState) { loopback = s.equals((edge.target).loopBackState); @@ -183,7 +182,7 @@ export class DOTGenerator { throw new Error("no such template: edge"); } - edgeST.add("label", this.getEdgeLabel(String(edge))); + edgeST.add("label", this.getEdgeLabel(edge.toString())); } edgeST.add("src", `s${s.stateNumber}`); @@ -316,7 +315,7 @@ export class DOTGenerator { return Utils.sortLinesInString(output); } - protected getStateLabel(s: DFAState | ATNState): string { + private getStateLabel(s: DFAState | ATNState): string { if (s instanceof DFAState) { let buf = `s${s.stateNumber}`; @@ -401,11 +400,11 @@ export class DOTGenerator { /** * Fix edge strings so they print out in DOT properly. */ - protected getEdgeLabel(label: string): string { - label = label.replace("\\", "\\\\"); - label = label.replace("\"", "\\\""); - label = label.replace("\n", "\\\\n"); - label = label.replace("\r", ""); + private getEdgeLabel(label: string): string { + label = label.replaceAll("\\", "\\\\"); + label = label.replaceAll("\"", "\\\""); + label = label.replaceAll("\n", "\\\\n"); + label = label.replaceAll("\r", ""); return label; } diff --git a/tests/TestATNInterpreter.spec.ts b/tests/TestATNInterpreter.spec.ts index c2e1d63..60a4ae4 100644 --- a/tests/TestATNInterpreter.spec.ts +++ b/tests/TestATNInterpreter.spec.ts @@ -24,8 +24,6 @@ describe("TestATNInterpreter", () => { g.importVocab(lg); - //const f = new ParserATNFactory(g); - //const atn = f.createATN(); const atn = g.atn!; const input = new MockIntTokenStream(types); diff --git a/tests/bugs/DotGenerator.spec.ts b/tests/bugs/DotGenerator.spec.ts new file mode 100644 index 0000000..a77a8ac --- /dev/null +++ b/tests/bugs/DotGenerator.spec.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) Mike Lischke. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; + +import { DOTGenerator } from "../../src/tool/DOTGenerator.js"; +import { Grammar, LexerGrammar } from "../../src/tool/index.js"; + +describe("DOTGenerator", () => { + let lexerGrammar: Grammar; + let dotGenerator: DOTGenerator; + + beforeAll(async () => { + const sourcePath = join(dirname(import.meta.url), "data/abbLexer.g4").substring("file:".length); + const lexerGrammarText = await readFile(sourcePath, "utf8"); + lexerGrammar = new LexerGrammar(lexerGrammarText); + lexerGrammar.tool.process(lexerGrammar, false); + + }); + + beforeEach(() => { + dotGenerator = new DOTGenerator(lexerGrammar); + }); + + it("Bug #33", () => { + const rule = lexerGrammar.getRule("EscapeSequence")!; + const startState = lexerGrammar.atn!.ruleToStartState[rule.index]!; + const result = dotGenerator.getDOTFromState(startState, true); + expect(result.indexOf(`s327 -> s335 [fontsize=11, fontname="Courier", arrowsize=.7, ` + + String.raw`label = "'\\\\'", arrowhead = normal];`)).toBeGreaterThan(-1); + }); + +}); diff --git a/tests/bugs/data/abbLexer.g4 b/tests/bugs/data/abbLexer.g4 new file mode 100644 index 0000000..7885c06 --- /dev/null +++ b/tests/bugs/data/abbLexer.g4 @@ -0,0 +1,107 @@ +// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine +// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true + +lexer grammar abbLexer; + +options { + caseInsensitive = true; +} + +MODULE : 'module'; +ENDMODULE : 'endmodule'; +PROC : 'PROC'; +ENDPROC : 'ENDPROC'; +LOCAL : 'LOCAL'; +CONST : 'CONST'; +PERS : 'PERS'; +VAR : 'VAR'; +TOOLDATA : 'TOOLDATA'; +WOBJDATA : 'WOBJDATA'; +SPEEDDATA : 'SPEEDDATA'; +ZONEDATA : 'ZONEDATA'; +CLOCK : 'CLOCK'; +BOOL : 'BOOL'; +ON_CALL : '\\ON'; +OFF_CALL : '\\OFF'; + +SLASH : '/'; +EQUALS : ':='; +COMMA : ','; +CURLY_OPEN : '{'; +CURLY_CLOSE : '}'; +COLON : ':'; +SEMICOLON : ';'; +BRACKET_OPEN : '('; +BRACKET_CLOSE : ')'; +SQUARE_OPEN : '['; +SQUARE_CLOSE : ']'; +DOT : '.'; +DOUBLEDOT : '..'; +REL_BIGGER : '>'; +REL_BIGGER_OR_EQUAL : '>='; +REL_SMALLER : '<'; +REL_SMALLER_OR_EQUAL : '<='; +REL_EQUAL : '=='; +REL_NOTEQUAL : '<>'; +PLUS : '+'; +MINUS : '-'; +MULTIPLY : '*'; +PERCENT : '%'; +HASH : '#'; + +WS: (' ' | '\t' | '\u000C') -> skip; + +NEWLINE: '\r'? '\n'; + +LINE_COMMENT: '!' ~ ('\n' | '\r')* -> skip; + +BOOLLITERAL: 'FALSE' | 'TRUE'; + +CHARLITERAL: '\'' (EscapeSequence | ~ ('\'' | '\\' | '\r' | '\n')) '\''; + +STRINGLITERAL: '"' (EscapeSequence | ~ ('\\' | '"' | '\r' | '\n'))* '"'; + +fragment EscapeSequence: + '\\' ( + 'b' + | 't' + | 'n' + | 'f' + | 'r' + | '"' + | '\'' + | '\\' + | '0' .. '3' '0' .. '7' '0' .. '7' + | '0' .. '7' '0' .. '7' + | '0' .. '7' + ) +; + +FLOATLITERAL: + ('0' .. '9')+ '.' ('0' .. '9')* Exponent? + | '.' ('0' .. '9')+ Exponent? + | ('0' .. '9')+ Exponent +; + +fragment Exponent: 'E' ('+' | '-')? ('0' .. '9')+; + +INTLITERAL: ('0' .. '9')+ | HexPrefix HexDigit+ HexSuffix | BinPrefix BinDigit+ BinSuffix; + +fragment HexPrefix: '\'' 'H'; + +fragment HexDigit: '0' .. '9' | 'A' .. 'F'; + +fragment HexSuffix: '\''; + +fragment BinPrefix: '\'' 'B'; + +fragment BinDigit: '0' | '1'; + +fragment BinSuffix: '\''; + +IDENTIFIER: IdentifierStart IdentifierPart*; + +fragment IdentifierStart: 'A' .. 'Z' | '_'; + +fragment IdentifierPart: IdentifierStart | '0' .. '9'; \ No newline at end of file