blob: 344f6a7aaec54f72c2c73ef884f83137dfa6b78d [file]
//
// 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}");
}
}
}