| // | |
| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| // | |
| using System.IO; | |
| using System.Linq; | |
| using Antlr4.Runtime; | |
| using org.apache.plc4net.tools.codegen.grammar; | |
| namespace org.apache.plc4net.tools.codegen | |
| { | |
| /// <summary> | |
| /// Parses an mspec protocol description file into an ANTLR parse tree. | |
| /// This is the first stage of the pure-.NET mspec toolchain: the grammar | |
| /// layer that turns text into a parse tree. The type model layer (the | |
| /// next stage) walks the tree with a listener. | |
| /// </summary> | |
| public static class MspecReader | |
| { | |
| /// <summary> | |
| /// Parses the content of an mspec file and returns the parse tree root. | |
| /// </summary> | |
| /// <summary> | |
| /// The empty-string literal <c>''</c> - which the shared expression | |
| /// grammar's <c>TICK innerExpression TICK</c> rule cannot match (the | |
| /// inner is not optional) - rewritten to a sentinel the model builder | |
| /// maps back to <c>""</c>. It appears only in knx-master-data.mspec's | |
| /// <c>KnxDatapointType</c> table, for the IEC-type rows with no DPST id. | |
| /// </summary> | |
| public const string EmptyStringSentinel = "__plc4net_empty_string__"; | |
| public static MSpecParser.FileContext Read(string mspecContent, bool strict = true) | |
| { | |
| mspecContent = mspecContent.Replace("''", "'" + EmptyStringSentinel + "'"); | |
| var inputStream = new AntlrInputStream(mspecContent); | |
| var lexer = new MSpecLexer(inputStream); | |
| var lexerErrors = new LexerErrorListener(); | |
| lexer.RemoveErrorListeners(); | |
| lexer.AddErrorListener(lexerErrors); | |
| var tokenStream = new CommonTokenStream(lexer); | |
| var parser = new MSpecParser(tokenStream); | |
| // Collect errors rather than printing to stderr. | |
| var errorListener = new CollectingErrorListener(); | |
| parser.RemoveErrorListeners(); | |
| parser.AddErrorListener(errorListener); | |
| var tree = parser.file(); | |
| var errors = lexerErrors.Errors.Concat(errorListener.Errors).ToArray(); | |
| if (errors.Length != 0 && strict) | |
| { | |
| throw new MspecParseException( | |
| $"Failed to parse mspec: {string.Join("; ", errors)}"); | |
| } | |
| // Non-strict: ANTLR's error recovery has already skipped the bad | |
| // tokens and produced a best-effort tree. The Java MessageFormatParser | |
| // behaves the same on knx-master-data.mspec (it logs the same | |
| // "mismatched input" lines to stderr and carries on). | |
| LastErrors = errors; | |
| return tree; | |
| } | |
| /// <summary>Syntax errors from the most recent non-strict | |
| /// <see cref="Read(string, bool)"/>, for a caller that wants to report them.</summary> | |
| public static System.Collections.Generic.IReadOnlyList<string> LastErrors { get; private set; } | |
| = System.Array.Empty<string>(); | |
| /// <summary> | |
| /// Parses an mspec file at the given path. | |
| /// </summary> | |
| public static MSpecParser.FileContext ReadFile(string filePath, bool strict = true) | |
| { | |
| return Read(File.ReadAllText(filePath), strict); | |
| } | |
| } | |
| /// <summary> | |
| /// Collects syntax errors rather than printing them. | |
| /// </summary> | |
| internal class CollectingErrorListener : BaseErrorListener | |
| { | |
| private readonly System.Collections.Generic.List<string> _errors | |
| = new System.Collections.Generic.List<string>(); | |
| public System.Collections.Generic.IReadOnlyList<string> Errors => _errors; | |
| public bool HasErrors => _errors.Count > 0; | |
| public override void SyntaxError(TextWriter output, IRecognizer recognizer, | |
| IToken offendingSymbol, int line, int charPositionInLine, | |
| string msg, RecognitionException e) | |
| { | |
| _errors.Add($"line {line}:{charPositionInLine} {msg}"); | |
| } | |
| } | |
| internal sealed class LexerErrorListener : IAntlrErrorListener<int> | |
| { | |
| public System.Collections.Generic.List<string> Errors { get; } = new System.Collections.Generic.List<string>(); | |
| public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, | |
| int line, int charPositionInLine, string msg, RecognitionException e) | |
| { | |
| Errors.Add($"line {line}:{charPositionInLine} {msg}"); | |
| } | |
| } | |
| } |