// SPDX-License-Identifier: MIT
#include "MStringParser.hh"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <system_error>
#include "MUtil.hh"
using fedem::parser::ParseError;
namespace fs = std::filesystem;
namespace
{
/// cparse's safeGet() / safePeek() end-of-input sentinel (ASCII SUB).
constexpr char EOF_CHAR = '\032';
bool IsBlank( char c )
{
return c == ' ' || c == '\t' || c == '\r';
}
bool IsIdentifierStart( char c )
{
return std::isalpha( static_cast<unsigned char>( c ) ) || c == '_';
}
bool IsIdentifierChar( char c )
{
return std::isalnum( static_cast<unsigned char>( c ) ) || c == '_';
}
/// `tr`, `tr_TR`, `tr_TR.UTF-8`, `sr_RS@latin`, `C`, `C.utf8`
bool IsLocaleChar( char c )
{
return IsIdentifierChar( c ) || c == '.' || c == '-' || c == '@';
}
/// `UTF-8`, `ISO-8859-9`, `KOI8-R`
bool IsCharsetChar( char c )
{
return IsIdentifierChar( c ) || c == '.' || c == '-' || c == ':';
}
/// `messages`, `error-texts`
bool IsModuleChar( char c )
{
return IsIdentifierChar( c ) || c == '-';
}
/// `hh`, `hpp`, `cc`, `c++`
bool IsExtensionChar( char c )
{
return IsIdentifierChar( c ) || c == '+';
}
std::string Upper( std::string word )
{
std::transform( word.begin( ), word.end( ), word.begin( ),
[]( unsigned char c ) { return static_cast<char>( std::toupper( c ) ); } );
return word;
}
std::string Describe( char c )
{
if ( c == EOF_CHAR )
{
return "end of file";
}
if ( c == '\n' )
{
return "end of line";
}
if ( std::isprint( static_cast<unsigned char>( c ) ) )
{
return std::string( "'" ) + c + "'";
}
std::ostringstream stream;
stream << "character \\x" << std::hex << static_cast<unsigned>( static_cast<unsigned char>( c ) );
return stream.str( );
}
struct NamedValue
{
const char *name;
const char *value;
};
constexpr NamedValue Facilities[] =
{
{ "AUTH", "LOG_AUTH" }, { "AUTHPRIV", "LOG_AUTHPRIV" }, { "CRON", "LOG_CRON" },
{ "DAEMON", "LOG_DAEMON" }, { "FTP", "LOG_FTP" }, { "KERN", "LOG_KERN" },
{ "LOCAL0", "LOG_LOCAL0" }, { "LOCAL1", "LOG_LOCAL1" }, { "LOCAL2", "LOG_LOCAL2" },
{ "LOCAL3", "LOG_LOCAL3" }, { "LOCAL4", "LOG_LOCAL4" }, { "LOCAL5", "LOG_LOCAL5" },
{ "LOCAL6", "LOG_LOCAL6" }, { "LOCAL7", "LOG_LOCAL7" }, { "LPR", "LOG_LPR" },
{ "MAIL", "LOG_MAIL" }, { "NEWS", "LOG_NEWS" }, { "USER", "LOG_USER" },
{ "UUCP", "LOG_UUCP" },
};
constexpr NamedValue Levels[] =
{
{ "EMERG", "LOG_EMERG" }, { "EMERGENCY", "LOG_EMERG" }, { "ALERT", "LOG_ALERT" },
{ "CRIT", "LOG_CRIT" }, { "CRITICAL", "LOG_CRIT" }, { "ERR", "LOG_ERR" },
{ "ERROR", "LOG_ERR" }, { "WARNING", "LOG_WARNING" }, { "NOTICE", "LOG_NOTICE" },
{ "INFO", "LOG_INFO" }, { "DEBUG", "LOG_DEBUG" },
};
template <std::size_t N>
const char *Lookup( const NamedValue ( &table )[N], const std::string &name )
{
for ( const NamedValue &entry : table )
{
if ( name == entry.name )
{
return entry.value;
}
}
return nullptr;
}
}
//
// ParseContext
//
ParseContext::ParseContext( Module &m )
: module( m )
{
// blank
}
bool ParseContext::failed( ) const
{
return std::any_of( diagnostics.begin( ), diagnostics.end( ),
[]( const ParseError &e ) { return e.kind != ParseError::Kind::Warning; } );
}
//
// MStringParser — entry points
//
MStringParser::MStringParser( ParseContext &context )
: _Context( context )
{
// blank
}
std::string MStringParser::getGrammarName( ) const noexcept
{
return "mstring";
}
bool MStringParser::parseFile( const std::string &path )
{
_DisplayName = path;
std::error_code ec;
fs::path const canonical = fs::canonical( path, ec );
if ( ec || ! fs::is_regular_file( canonical, ec ) )
{
ParseError e;
e.kind = ParseError::Kind::Syntax;
e.message = "cannot open '" + path + "' for reading";
e.filename = path;
_Context.diagnostics.push_back( e );
return false;
}
std::string const key = canonical.string( );
if ( std::find( _Context.importStack.begin( ), _Context.importStack.end( ), key )
!= _Context.importStack.end( ) )
{
ParseError e;
e.kind = ParseError::Kind::Syntax;
e.message = "recursive $IMPORT of '" + path + "'";
e.filename = path;
_Context.diagnostics.push_back( e );
return false;
}
_Context.importStack.push_back( key );
bool const parsed = parse( key );
_Context.importStack.pop_back( );
if ( ! parsed && ! _Context.failed( ) )
{
// cparse could not run the grammar at all (e.g. the file vanished).
ParseError e;
e.kind = ParseError::Kind::Internal;
e.message = errors( ).empty( ) ? "cannot parse '" + path + "'" : errors( ).front( ).message;
e.filename = path;
_Context.diagnostics.push_back( e );
}
return parsed && ! _Context.failed( );
}
bool MStringParser::parseStream( const std::string &name, std::istream &input )
{
_DisplayName = name;
// The cursor seeks back when it backtracks, which a pipe cannot do: read
// the whole input first.
auto buffer = std::make_shared<std::stringstream>( );
*buffer << input.rdbuf( );
buffer->clear( ); // an empty input sets failbit, which the cursor would throw on
buffer->seekg( 0 );
pushCursor( name, buffer );
return start( ) && ! _Context.failed( );
}
bool MStringParser::start( )
{
try
{
while ( true )
{
skipEmptyLines( );
if ( atEnd( ) )
{
closeMessage( );
break;
}
if ( peek( ) == '$' )
{
directive( );
}
else
{
messageLine( );
}
}
return true;
}
catch ( Abort const & )
{
return false;
}
}
//
// lexical helpers
//
char MStringParser::peek( )
{
return getCursor( ).safePeek( );
}
char MStringParser::next( )
{
char const c = getCursor( ).safeGet( );
_Last = c;
return c;
}
bool MStringParser::atEnd( )
{
return peek( ) == EOF_CHAR;
}
bool MStringParser::atEndOfLine( )
{
char const c = peek( );
return c == '\n' || c == '#' || c == EOF_CHAR;
}
MStringParser::Location MStringParser::here( ) const
{
auto const &cursor = getCursor( );
if ( cursor.getLineNumber( ) == 0 || _Last == '\n' )
{
return { cursor.getLineNumber( ) + 1, 1 };
}
return { cursor.getLineNumber( ), cursor.getColumnNumber( ) + 1 };
}
void MStringParser::skipBlanks( )
{
while ( IsBlank( peek( ) ) )
{
next( );
}
}
bool MStringParser::skipComments( )
{
if ( peek( ) != '#' )
{
return false;
}
while ( ! atEnd( ) && peek( ) != '\n' )
{
next( );
}
return true;
}
void MStringParser::skipEmptyLines( )
{
while ( true )
{
skipBlanks( );
skipComments( );
if ( peek( ) != '\n' )
{
return;
}
next( );
}
}
void MStringParser::expectEndOfLine( )
{
skipBlanks( );
skipComments( );
if ( peek( ) == '\n' )
{
next( );
}
else if ( ! atEnd( ) )
{
error( "unexpected " + Describe( peek( ) ) + ", expected end of line" );
}
}
std::string MStringParser::readWhile( bool ( *accept )( char ) )
{
std::string word;
while ( accept( peek( ) ) )
{
word += next( );
}
return word;
}
std::string MStringParser::readIdentifier( )
{
if ( ! IsIdentifierStart( peek( ) ) )
{
return "";
}
return readWhile( IsIdentifierChar );
}
std::string MStringParser::requireIdentifier( const std::string &what )
{
skipBlanks( );
Location const location = here( );
std::string const name = readIdentifier( );
if ( name.empty( ) )
{
error( "expected " + what + ", found " + Describe( peek( ) ), location );
}
return name;
}
std::string MStringParser::requireWord( bool ( *accept )( char ), const std::string &what )
{
skipBlanks( );
Location const location = here( );
std::string const word = readWhile( accept );
if ( word.empty( ) )
{
error( "expected " + what + ", found " + Describe( peek( ) ), location );
}
return word;
}
std::string MStringParser::readQualifiedName( const std::string &what )
{
std::string name = requireIdentifier( what );
while ( true )
{
skipBlanks( );
if ( peek( ) != ':' )
{
return name;
}
Location const location = here( );
next( );
if ( peek( ) != ':' )
{
error( "expected '::'", location );
}
next( );
name += "::" + requireIdentifier( "a name after '::'" );
}
}
std::string MStringParser::readRestOfLine( )
{
std::string text;
while ( ! atEndOfLine( ) )
{
text += next( );
}
return Trim( text );
}
std::string MStringParser::readKeyword( )
{
skipBlanks( );
return Upper( readIdentifier( ) );
}
void MStringParser::error( const std::string &message, Location location )
{
ParseError e;
e.kind = ParseError::Kind::Syntax;
e.message = message;
e.filename = _DisplayName;
e.line = location.line;
e.col = location.col;
_Context.diagnostics.push_back( e );
throw Abort( );
}
void MStringParser::error( const std::string &message )
{
error( message, here( ) );
}
void MStringParser::warning( const std::string &message, Location location )
{
ParseError e;
e.kind = ParseError::Kind::Warning;
e.message = message;
e.filename = _DisplayName;
e.line = location.line;
e.col = location.col;
_Context.diagnostics.push_back( e );
}
//
// directives
//
void MStringParser::directive( )
{
Location const location = here( );
next( ); // '$'
// Any directive closes the message being defined.
closeMessage( );
std::string const name = readKeyword( );
if ( name.empty( ) )
{
error( "expected a directive name after '$'", location );
}
if ( name == "LANGUAGE" )
languageDirective( );
else if ( name == "CHARSET" )
charsetDirective( );
else if ( name == "STREAMABLE" )
classDirective( _Context.settings._Streamable, "$STREAMABLE", false );
else if ( name == "EXCEPTION" )
classDirective( _Context.settings._Exception, "$EXCEPTION", true );
else if ( name == "STRING" )
functionDirective( _Context.settings._String, "$STRING" );
else if ( name == "THROW" )
functionDirective( _Context.settings._Throw, "$THROW" );
else if ( name == "ERROR" )
errorDirective( );
else if ( name == "SYSLOG" )
syslogDirective( );
else if ( name == "INCLUDE" )
includeDirective( );
else if ( name == "IMPORT" )
importDirective( );
else if ( name == "USING" )
usingDirective( );
else if ( name == "NAMESPACE" )
namespaceDirective( );
else if ( name == "MODULE" )
moduleDirective( );
else if ( name == "MESSAGE" )
messageDirective( );
else if ( name == "EXPORT" )
{
// `$EXPORT MACRO` — or `$EXPORT` alone: no macro.
skipBlanks( );
_Context.module.Export = atEndOfLine( ) ? "" : requireIdentifier( "a macro name" );
expectEndOfLine( );
}
else
error( "unknown directive '$" + name + "'", location );
}
void MStringParser::languageDirective( )
{
_Context.language = requireWord( IsLocaleChar, "a language name" );
expectEndOfLine( );
}
void MStringParser::charsetDirective( )
{
skipBlanks( );
Location const location = here( );
_Context.charset = requireWord( IsCharsetChar, "a charset name" );
expectEndOfLine( );
warning( "$CHARSET is deprecated: it only labels the texts with a comment "
"(the text bytes are written as they are)", location );
}
bool MStringParser::commonOption( Concept &concept, const std::string &option )
{
if ( option == "NOPREFIX" )
{
concept._Prefix.clear( );
concept._Customized = true;
}
else if ( option == "NOPOSTFIX" )
{
concept._Postfix.clear( );
concept._Customized = true;
}
else if ( option == "PREFIX" || option == "POSTFIX" )
{
std::string const word = requireIdentifier( "a " + Upper( option ) + " word" );
// The first customisation replaces both defaults: `PREFIX get` turns
// `HelloStr` into `getHello`, not `getHelloStr`.
if ( ! concept._Customized )
{
concept._Prefix.clear( );
concept._Postfix.clear( );
}
( option == "PREFIX" ? concept._Prefix : concept._Postfix ) = word;
concept._Customized = true;
}
else if ( option == "ENABLE" )
{
concept._Active = true;
}
else if ( option == "DISABLE" )
{
concept._Active = false;
}
else if ( option == "LOCALE" )
{
Location const location = here( );
if ( readKeyword( ) != "EXTRA" )
{
error( "expected EXTRA after LOCALE", location );
}
Location const valueLocation = here( );
std::string const value = readKeyword( );
if ( value == "ENABLE" )
concept._LocaleExtra = true;
else if ( value == "DISABLE" )
concept._LocaleExtra = false;
else
error( "expected ENABLE or DISABLE after LOCALE EXTRA", valueLocation );
}
else
{
return false;
}
return true;
}
bool MStringParser::classOption( ClassConcept &concept, const std::string &option )
{
if ( option != "INHERITED" && option != "PARENT" )
{
return false;
}
skipBlanks( );
Location const location = here( );
std::string word = readIdentifier( );
std::string const access = Upper( word );
inheritance type = PUBLIC_INHERITANCE; // `catch ( std::exception & )` must see it
bool const isAccess = access == "PUBLIC" || access == "PROTECTED" || access == "PRIVATE";
skipBlanks( );
if ( isAccess && ! IsIdentifierStart( peek( ) ) && peek( ) != ':' )
{
error( "expected a base class name after " + access );
}
if ( isAccess && IsIdentifierStart( peek( ) ) )
{
type = access == "PUBLIC" ? PUBLIC_INHERITANCE
: access == "PROTECTED" ? PROTECTED_INHERITANCE
: PRIVATE_INHERITANCE;
word.clear( );
}
std::string parent;
if ( word.empty( ) )
{
parent = readQualifiedName( "a base class name" );
}
else
{
// `word` already is the first component of the base class name.
parent = word;
while ( peek( ) == ':' )
{
Location const colons = here( );
next( );
if ( peek( ) != ':' )
{
error( "expected '::'", colons );
}
next( );
parent += "::" + requireIdentifier( "a name after '::'" );
skipBlanks( );
}
}
if ( parent.empty( ) )
{
error( "expected a base class name", location );
}
concept._InheritanceType = type;
concept._Parent = parent;
return true;
}
bool MStringParser::functionOption( FunctionConcept &concept, const std::string &option, Location location )
{
if ( option == "NONMEMBER" )
{
concept._Type = NONMEMBER;
concept._Naming.clear( );
return true;
}
bool constness = true;
std::string member = option;
if ( option == "CONST" || option == "NONCONST" )
{
constness = option == "CONST";
location = here( );
member = readKeyword( );
if ( member != "MEMBER" )
{
error( "expected MEMBER after " + option, location );
}
}
else if ( option != "MEMBER" )
{
return false;
}
Location const kindLocation = here( );
std::string const kind = readKeyword( );
if ( kind == "OF" )
{
concept._Type = MEMBER_OF;
concept._Naming = readQualifiedName( "a class name after MEMBER OF" );
}
else if ( kind == "AS" )
{
concept._Type = MEMBER_AS;
concept._Naming = requireIdentifier( "a function name after MEMBER AS" );
}
else
{
error( "expected OF or AS after MEMBER", kindLocation );
}
concept._Constness = constness;
return true;
}
void MStringParser::unknownOption( const std::string &option, const std::string &directive, Location location )
{
if ( option.empty( ) )
{
error( "expected an option for " + directive + ", found " + Describe( peek( ) ), location );
}
error( "unknown " + directive + " option '" + option + "'", location );
}
void MStringParser::classDirective( ClassConcept &concept, const std::string &directive, bool exception )
{
do
{
skipBlanks( );
Location const location = here( );
std::string const option = readKeyword( );
if ( exception && option == "OVERRIDE" )
{
// `OVERRIDE name` — or `OVERRIDE NONE`: back to What() / what().
std::string const method = requireIdentifier( "the name of the base's text method" );
concept._Override = Upper( method ) == "NONE" ? "" : method;
}
else if ( option.empty( ) || ( ! commonOption( concept, option ) && ! classOption( concept, option ) ) )
{
unknownOption( option, directive, location );
}
skipBlanks( );
}
while ( ! atEndOfLine( ) );
expectEndOfLine( );
}
void MStringParser::functionDirective( FunctionConcept &concept, const std::string &directive )
{
do
{
skipBlanks( );
Location const location = here( );
std::string const option = readKeyword( );
if ( option.empty( ) ||
( ! commonOption( concept, option ) && ! functionOption( concept, option, location ) ) )
{
unknownOption( option, directive, location );
}
skipBlanks( );
}
while ( ! atEndOfLine( ) );
expectEndOfLine( );
}
void MStringParser::errorDirective( )
{
ErrorConcept &concept = _Context.settings._Error;
do
{
skipBlanks( );
Location const location = here( );
std::string const option = readKeyword( );
if ( option == "EXIT" )
{
// EXIT takes the rest of the line: it is the last option.
skipBlanks( );
Location const expression = here( );
std::string call = readRestOfLine( );
while ( ! call.empty( ) && call.back( ) == ';' )
{
call = Trim( call.substr( 0, call.size( ) - 1 ) );
}
if ( call.empty( ) )
{
error( "expected a statement after EXIT", expression );
}
concept._ExitCall = call;
break;
}
if ( option.empty( ) ||
( ! commonOption( concept, option ) && ! functionOption( concept, option, location ) ) )
{
unknownOption( option, "$ERROR", location );
}
skipBlanks( );
}
while ( ! atEndOfLine( ) );
expectEndOfLine( );
}
void MStringParser::syslogDirective( )
{
LogConcept &concept = _Context.settings._Log;
do
{
skipBlanks( );
Location const location = here( );
std::string const option = readKeyword( );
if ( option == "FACILITY" || option == "LEVEL" )
{
skipBlanks( );
Location const valueLocation = here( );
std::string const value = Upper( readWhile( IsIdentifierChar ) );
bool const facility = option == "FACILITY";
const char *constant = facility ? Lookup( Facilities, value ) : Lookup( Levels, value );
if ( constant == nullptr )
{
error( "unknown syslog " + std::string( facility ? "facility" : "level" ) + " '" + value + "'",
valueLocation );
}
( facility ? concept._Facility : concept._Level ) = constant;
}
else if ( option.empty( ) ||
( ! commonOption( concept, option ) && ! functionOption( concept, option, location ) ) )
{
unknownOption( option, "$SYSLOG", location );
}
skipBlanks( );
}
while ( ! atEndOfLine( ) );
expectEndOfLine( );
}
void MStringParser::includeDirective( )
{
skipBlanks( );
Location const location = here( );
std::string include;
char const open = peek( );
if ( open == '"' || open == '<' )
{
char const close = open == '"' ? '"' : '>';
next( );
std::string name;
while ( peek( ) != close )
{
if ( peek( ) == '\n' || atEnd( ) )
{
error( std::string( "missing closing " ) + close + " in $INCLUDE", location );
}
name += next( );
}
next( );
if ( Trim( name ).empty( ) )
{
error( "empty file name in $INCLUDE", location );
}
include = open + name + close;
}
else
{
include = readIdentifier( );
if ( include.empty( ) )
{
error( "expected \"file\", <file> or a macro name after $INCLUDE", location );
}
}
expectEndOfLine( );
auto &includes = _Context.module.Includes;
if ( std::find( includes.begin( ), includes.end( ), include ) == includes.end( ) )
{
includes.push_back( include );
}
}
std::string MStringParser::resolveImport( const std::string &filename ) const
{
fs::path const name( filename );
std::error_code ec;
if ( name.is_absolute( ) )
{
return fs::is_regular_file( name, ec ) ? filename : "";
}
// Like #include "...": next to the importing file first, then the working
// folder, then the -I folders in the order given.
std::vector<fs::path> folders;
fs::path const current( _DisplayName );
if ( current.has_parent_path( ) && fs::is_regular_file( current, ec ) )
{
folders.push_back( current.parent_path( ) );
}
folders.emplace_back( "." );
for ( const std::string &folder : _Context.importFolders )
{
folders.emplace_back( folder );
}
for ( const fs::path &folder : folders )
{
fs::path const candidate = folder / name;
if ( fs::is_regular_file( candidate, ec ) )
{
return candidate.lexically_normal( ).string( );
}
}
return "";
}
void MStringParser::importDirective( )
{
skipBlanks( );
Location const location = here( );
std::string filename;
if ( peek( ) == '"' )
{
next( );
while ( peek( ) != '"' )
{
if ( peek( ) == '\n' || atEnd( ) )
{
error( "missing closing \" in $IMPORT", location );
}
filename += next( );
}
next( );
}
else
{
filename = readRestOfLine( );
}
if ( filename.empty( ) )
{
error( "expected a file name after $IMPORT", location );
}
expectEndOfLine( );
std::string const path = resolveImport( filename );
if ( path.empty( ) )
{
error( "import file '" + filename + "' does not exist", location );
}
// A separate parser with its own cursor, sharing the context: the
// imported file behaves as if its lines were written here.
MStringParser imported( _Context );
if ( ! imported.parseFile( path ) )
{
error( "error in imported file '" + filename + "'", location );
}
}
void MStringParser::usingDirective( )
{
skipBlanks( );
Location const location = here( );
std::string entity = readRestOfLine( );
while ( ! entity.empty( ) && entity.back( ) == ';' )
{
entity = Trim( entity.substr( 0, entity.size( ) - 1 ) );
}
if ( entity.empty( ) )
{
error( "expected a name after $USING", location );
}
expectEndOfLine( );
_Context.module.Usings.push_back( entity );
}
void MStringParser::namespaceDirective( )
{
skipBlanks( );
std::vector<std::string> names;
if ( ! atEndOfLine( ) )
{
std::string const qualified = readQualifiedName( "a namespace name" );
std::string::size_type begin = 0;
while ( true )
{
std::string::size_type const end = qualified.find( "::", begin );
names.push_back( qualified.substr( begin, end - begin ) );
if ( end == std::string::npos )
{
break;
}
begin = end + 2;
}
}
expectEndOfLine( );
_Context.module.Namespaces = names;
}
void MStringParser::moduleDirective( )
{
Module &module = _Context.module;
module.Name = requireWord( IsModuleChar, "a module name" );
skipBlanks( );
std::optional<Location> noSource;
while ( ! atEndOfLine( ) )
{
Location const location = here( );
std::string const option = readKeyword( );
if ( option == "SOURCE" )
module.SourceExtension = requireWord( IsExtensionChar, "a file extension after SOURCE" );
else if ( option == "HEADER" )
module.HeaderExtension = requireWord( IsExtensionChar, "a file extension after HEADER" );
else if ( option == "NOSOURCE" )
{
module.SourceExtension.reset( );
noSource = location;
}
else if ( option == "INLINE" )
{
module.Inline = true;
module.SourceExtension.reset( );
}
else if ( option == "NOHEADER" )
module.HeaderExtension.reset( );
else
unknownOption( option, "$MODULE", location );
skipBlanks( );
}
expectEndOfLine( );
if ( noSource && ! module.Inline )
{
warning( "NOSOURCE writes declarations without definitions; INLINE puts the "
"definitions into the header", *noSource );
}
}
void MStringParser::messageDirective( )
{
skipBlanks( );
Location const location = here( );
std::string const name = requireIdentifier( "a message name" );
// `$MESSAGE Name : STRING THROW` — exactly these artefacts, for this
// message only; their naming comes from the current settings.
MessageSettings settings = _Context.settings;
skipBlanks( );
if ( peek( ) == ':' )
{
next( );
Concept *const concepts[] =
{
&settings._Streamable, &settings._String, &settings._Error,
&settings._Exception, &settings._Throw, &settings._Log
};
for ( Concept *concept : concepts )
{
concept->_Active = false;
}
skipBlanks( );
if ( atEndOfLine( ) )
{
error( "expected STREAMABLE, STRING, ERROR, EXCEPTION, THROW or SYSLOG after ':'" );
}
while ( ! atEndOfLine( ) )
{
Location const optionLocation = here( );
std::string const option = readKeyword( );
if ( option == "STREAMABLE" )
settings._Streamable._Active = true;
else if ( option == "STRING" )
settings._String._Active = true;
else if ( option == "ERROR" )
settings._Error._Active = true;
else if ( option == "EXCEPTION" )
settings._Exception._Active = true;
else if ( option == "THROW" )
settings._Throw._Active = true;
else if ( option == "SYSLOG" )
settings._Log._Active = true;
else if ( option.empty( ) )
error( "unexpected " + Describe( peek( ) ) +
", expected STREAMABLE, STRING, ERROR, EXCEPTION, THROW or SYSLOG", optionLocation );
else
error( "'" + option + "' is not an artefact: expected STREAMABLE, STRING, ERROR, "
"EXCEPTION, THROW or SYSLOG", optionLocation );
skipBlanks( );
}
}
expectEndOfLine( );
Module &module = _Context.module;
if ( module.FindMessage( name ) != nullptr )
{
error( "message '" + name + "' is already defined", location );
}
if ( _Context.trace )
{
std::cerr << "$MESSAGE " << name << std::endl;
}
module.Messages.emplace_back( name, _Context.language, settings );
module.Messages.back( ).File = _DisplayName;
module.Messages.back( ).Line = location.line;
module.Messages.back( ).Col = location.col;
_InMessage = true;
_MessageLocation = location;
_MessageIndex = module.Messages.size( ) - 1;
_TextStarted = false;
_MessageLanguage = _Context.language;
_MessageCharset = _Context.charset;
}
void MStringParser::closeMessage( )
{
if ( _InMessage && currentMessage( ).Locales.empty( ) )
{
warning( "message '" + currentMessage( ).Name + "' has no text", _MessageLocation );
}
_InMessage = false;
}
//
// message body
//
Message &MStringParser::currentMessage( )
{
return _Context.module.Messages[_MessageIndex];
}
LocaleMessage &MStringParser::currentLocale( )
{
return currentMessage( ).Locale( _MessageLanguage, _MessageCharset );
}
void MStringParser::messageLine( )
{
char const c = peek( );
if ( ! _InMessage )
{
if ( c == '@' || c == '[' || c == '\'' || c == '"' || c == '|' )
{
error( "message text outside of a $MESSAGE block" );
}
error( "unexpected " + Describe( c ) + ", expected a $DIRECTIVE" );
}
switch ( c )
{
case '@':
parameterLine( );
break;
case '[':
setHeaderLine( );
break;
case '\'':
case '"':
quotedTextLine( c );
break;
case '|':
barTextLine( );
break;
default:
error( "unexpected " + Describe( c ) +
", expected a message text ('...', \"...\" or |...), a [language] header, "
"a @parameter or a $DIRECTIVE" );
}
}
void MStringParser::parameterLine( )
{
Location const location = here( );
if ( _TextStarted )
{
error( "parameters must be declared before the message texts", location );
}
next( ); // '@'
skipBlanks( );
Location const nameLocation = here( );
std::string const name = requireIdentifier( "a parameter name after '@'" );
skipBlanks( );
std::string const type = readRestOfLine( );
if ( type.empty( ) )
{
error( "expected the type of parameter '" + name + "'" );
}
expectEndOfLine( );
Message &message = currentMessage( );
// Names the generated functions use for their own parameters and locals.
static const char *const reserved[] =
{
"stream", "loc", "option_parameter", "streamLocale", "originalFormat", "internal_locale_name"
};
for ( const char *word : reserved )
{
if ( name == word )
{
error( "parameter name '" + name + "' is reserved for the generated code", nameLocation );
}
}
if ( name.compare( 0, 7, "member_" ) == 0 || name.compare( 0, 9, "internal_" ) == 0 )
{
error( "parameter names starting with 'member_' or 'internal_' are reserved for the generated code",
nameLocation );
}
if ( message.FindParameter( name ) != nullptr )
{
error( "parameter '" + name + "' is already declared for message '" + message.Name + "'", nameLocation );
}
message.Parameters.emplace_back( name, type );
}
void MStringParser::setHeaderLine( )
{
next( ); // '['
std::string const language = requireWord( IsLocaleChar, "a language name after '['" );
skipBlanks( );
std::string charset = _Context.charset;
if ( peek( ) != ']' )
{
Location const charsetLocation = here( );
charset = requireWord( IsCharsetChar, "a charset name or ']'" );
skipBlanks( );
warning( "the charset of a [language charset] header is deprecated: it only "
"labels the text with a comment", charsetLocation );
}
if ( peek( ) != ']' )
{
error( "expected ']' to close the [" + language + "] header, found " + Describe( peek( ) ) );
}
next( );
expectEndOfLine( );
_TextStarted = true;
_MessageLanguage = language;
_MessageCharset = charset;
}
void MStringParser::escapeSequence( std::string &text )
{
// The backslash has been consumed.
char const c = peek( );
switch ( c )
{
case 'a': text += '\a'; break;
case 'b': text += '\b'; break;
case 'f': text += '\f'; break;
case 'n': text += '\n'; break;
case 'r': text += '\r'; break;
case 't': text += '\t'; break;
case 'v': text += '\v'; break;
case '\\': text += '\\'; break;
case '\'': text += '\''; break;
case '"': text += '"'; break;
case '{': text += '{'; break;
case 'x':
{
// \xH or \xHH: one byte
next( );
int value = 0;
int digits = 0;
while ( digits < 2 && std::isxdigit( static_cast<unsigned char>( peek( ) ) ) )
{
char const h = next( );
value = value * 16 + ( std::isdigit( static_cast<unsigned char>( h ) )
? h - '0' : std::tolower( static_cast<unsigned char>( h ) ) - 'a' + 10 );
++digits;
}
if ( digits == 0 )
{
text += "\\x"; // not an escape after all
}
else
{
text += static_cast<char>( value );
}
return;
}
default:
if ( c >= '0' && c <= '7' )
{
// \o, \oo or \ooo: one byte (UTF-8 bytes of another charset, ...)
int value = 0;
for ( int digits = 0; digits < 3 && peek( ) >= '0' && peek( ) <= '7'; ++digits )
{
value = value * 8 + ( next( ) - '0' );
}
text += static_cast<char>( value & 0xff );
return;
}
// Not an escape: keep the backslash, `C:\temp` stays `C:\temp`.
text += '\\';
return;
}
next( );
}
void MStringParser::quotedTextLine( char quote )
{
Location const open = here( );
next( ); // the opening quote
bool const macros = quote == '"';
LocaleMessage &locale = currentLocale( );
std::string text;
_TextStarted = true;
while ( true )
{
char const c = peek( );
if ( c == '\n' || c == EOF_CHAR )
{
error( std::string( "unterminated string, missing closing " ) + quote, open );
}
Location const location = here( );
next( );
if ( c == quote )
{
break;
}
if ( c == '\\' )
{
if ( peek( ) == '\n' || atEnd( ) )
{
error( std::string( "unterminated string, missing closing " ) + quote, open );
}
escapeSequence( text );
}
else if ( c == '{' && macros && peek( ) == '$' )
{
next( );
locale.AppendText( text );
text.clear( );
macro( locale, location );
}
else
{
text += c;
}
}
locale.AppendText( text );
expectEndOfLine( );
}
void MStringParser::barTextLine( )
{
next( ); // '|'
LocaleMessage &locale = currentLocale( );
std::string text;
bool newline = true;
_TextStarted = true;
while ( true )
{
char const c = peek( );
if ( c == EOF_CHAR )
{
break;
}
Location const location = here( );
next( );
if ( c == '\n' )
{
break;
}
if ( c == '\r' && peek( ) == '\n' )
{
continue; // CRLF line end
}
if ( c == '\\' )
{
if ( peek( ) == '\r' )
{
next( );
if ( peek( ) != '\n' )
{
text += "\\\r";
continue;
}
}
if ( peek( ) == '\n' || atEnd( ) )
{
// A trailing backslash joins the next line without a newline.
if ( ! atEnd( ) )
{
next( );
}
newline = false;
break;
}
escapeSequence( text );
}
else if ( c == '{' && peek( ) == '$' )
{
next( );
locale.AppendText( text );
text.clear( );
macro( locale, location );
}
else
{
text += c;
}
}
if ( newline )
{
text += '\n';
}
locale.AppendText( text );
}
void MStringParser::macro( LocaleMessage &locale, Location open )
{
// `{$` has been consumed: {$ expression [; format]... }
TextElement element( true );
std::string part;
bool expression = true;
int depth = 0;
while ( true )
{
char const c = peek( );
if ( c == '\n' || c == EOF_CHAR )
{
error( "unterminated macro, missing '}'", open );
}
next( );
if ( c == '\\' && peek( ) != '\n' && ! atEnd( ) )
{
part += next( ); // \; and \} are literal
continue;
}
if ( c == '{' )
{
++depth;
}
else if ( c == '}' && depth > 0 )
{
--depth;
}
else if ( ( c == ';' && depth == 0 ) || c == '}' )
{
std::string const value = Trim( part );
part.clear( );
if ( expression )
{
if ( value.empty( ) )
{
error( "empty macro expression", open );
}
element.Text = value;
expression = false;
}
else if ( ! value.empty( ) )
{
element.Formats.push_back( value );
}
if ( c == '}' )
{
break;
}
continue;
}
part += c;
}
locale.TextElements.push_back( element );
}
//
// whole-module checks
//
namespace
{
/// Does the macro expression \p expression mention the identifier \p name?
bool Mentions( const std::string &expression, const std::string &name )
{
for ( std::string::size_type pos = expression.find( name ); pos != std::string::npos;
pos = expression.find( name, pos + 1 ) )
{
bool const before = pos == 0 || ! IsIdentifierChar( expression[pos - 1] );
std::string::size_type const end = pos + name.size( );
bool const after = end == expression.size( ) || ! IsIdentifierChar( expression[end] );
if ( before && after )
{
return true;
}
}
return false;
}
bool Uses( const LocaleMessage &locale, const std::string &name )
{
return std::any_of( locale.TextElements.begin( ), locale.TextElements.end( ),
[&name]( const TextElement &element )
{ return element.Macro && Mentions( element.Text, name ); } );
}
}
void CheckTranslations( ParseContext &context )
{
auto warn = [&context]( const Message &message, const std::string &text )
{
ParseError e;
e.kind = ParseError::Kind::Warning;
e.message = text;
e.filename = message.File;
e.line = message.Line;
e.col = message.Col;
context.diagnostics.push_back( e );
};
std::vector<std::string> languages;
for ( const Message &message : context.module.Messages )
{
for ( const LocaleMessage &locale : message.Locales )
{
if ( std::find( languages.begin( ), languages.end( ), locale.Language ) == languages.end( ) )
{
languages.push_back( locale.Language );
}
}
}
for ( const Message &message : context.module.Messages )
{
if ( message.Locales.empty( ) )
{
continue; // already warned: "has no text"
}
if ( languages.size( ) > 1 )
{
for ( const std::string &language : languages )
{
bool const present = std::any_of( message.Locales.begin( ), message.Locales.end( ),
[&language]( const LocaleMessage &locale )
{ return locale.Language == language; } );
if ( ! present )
{
warn( message, "message '" + message.Name + "' has no [" + language +
"] text; its default text is used" );
}
}
}
// A parameter used by one language and not by another is almost always
// a translation mistake. One no language uses is data the class carries.
for ( const Parameter ¶meter : message.Parameters )
{
const LocaleMessage *user = nullptr;
for ( const LocaleMessage &locale : message.Locales )
{
if ( Uses( locale, parameter.Name ) )
{
user = &locale;
break;
}
}
if ( user == nullptr )
{
continue;
}
for ( const LocaleMessage &locale : message.Locales )
{
if ( ! Uses( locale, parameter.Name ) )
{
warn( message, "parameter '" + parameter.Name + "' of message '" + message.Name +
"' is used in the [" + user->Language + "] text but not in the [" +
locale.Language + "] text" );
}
}
}
}
}