// SPDX-License-Identifier: MIT
#include "IncludeResolver.hh"
#include <algorithm>
#include <filesystem>
#include "Array.hh"
#include "Document.hh"
#include "FsonParser.hh"
#include "Object.hh"
#include "Value.hh"
using namespace std;
namespace fedem
{
namespace fson
{
IncludeResolver::IncludeResolver( ) = default;
vector< parser::ParseError > const& IncludeResolver::errors( ) const noexcept
{
return collectedErrors;
}
vector< string > const& IncludeResolver::dependencies( ) const noexcept
{
return dependencyPaths;
}
set< string > const& IncludeResolver::dependencySet( ) const noexcept
{
return allFiles;
}
void IncludeResolver::recordDependency( string const& canonical )
{
if( find( dependencyPaths.begin( ), dependencyPaths.end( ), canonical )
== dependencyPaths.end( ) )
dependencyPaths.push_back( canonical );
allFiles.insert( canonical );
}
void IncludeResolver::resolve( Document& document, string const& basePath )
{
error_code ec;
filesystem::path const base = filesystem::path( basePath );
filesystem::path const baseCanonical = filesystem::weakly_canonical( base, ec );
filesystem::path const anchor = ec ? base : baseCanonical;
string const ownerDirectory = anchor.parent_path( ).string( );
string const ownerCanonical = anchor.string( );
// The document root's own file is the cycle-detection anchor and is
// itself part of the file set needed to resolve the document.
activeStack.push_back( ownerCanonical );
allFiles.insert( ownerCanonical );
if( Object* const root = document.getRootObject( ) )
resolveObject( *root, ownerDirectory );
activeStack.pop_back( );
}
unique_ptr< Document > IncludeResolver::loadTarget( string const& target,
string const& ownerDirectory,
string& canonical )
{
error_code ec;
filesystem::path targetPath( target );
if( targetPath.is_relative( ) )
targetPath = filesystem::path( ownerDirectory ) / targetPath;
filesystem::path const resolved = filesystem::weakly_canonical( targetPath, ec );
canonical = ( ec ? targetPath : resolved ).string( );
// Cycle: the target is already being resolved further up the stack.
if( find( activeStack.begin( ), activeStack.end( ), canonical )
!= activeStack.end( ) )
{
collectedErrors.push_back( parser::ParseError{
parser::ParseError::Kind::Faulty,
"include cycle detected at '" + target + "'",
canonical, 0UL, 0UL } );
return nullptr;
}
error_code existsEc;
if( !filesystem::exists( canonical, existsEc ) || existsEc )
{
collectedErrors.push_back( parser::ParseError{
parser::ParseError::Kind::Syntax,
"included file not found: '" + target + "'",
canonical, 0UL, 0UL } );
return nullptr;
}
FsonParser parser;
unique_ptr< Document > targetDocument = parser.parseFile( canonical );
for( auto const& error : parser.errors( ) )
collectedErrors.push_back( error );
if( !targetDocument )
{
collectedErrors.push_back( parser::ParseError{
parser::ParseError::Kind::Syntax,
"included file could not be read: '" + target + "'",
canonical, 0UL, 0UL } );
return nullptr;
}
recordDependency( canonical );
// Resolve the target's own includes before its content is used, so
// transitive defaults flow through.
filesystem::path const targetDir =
filesystem::path( canonical ).parent_path( );
activeStack.push_back( canonical );
if( Object* const targetRoot = targetDocument->getRootObject( ) )
resolveObject( *targetRoot, targetDir.string( ) );
activeStack.pop_back( );
return targetDocument;
}
void IncludeResolver::resolveObject( Object& object,
string const& ownerDirectory )
{
// Recurse into nested (non-merged) member objects first so their own
// directives resolve.
for( size_t index = 0U; index < object.memberCount( ); ++index )
{
Member& member = object.getMember( index );
if( member.isMerged( ) )
continue;
if( Object* const nested = member.getValue( ).asObject( ) )
resolveObject( *nested, ownerDirectory );
}
// Snapshot the current include set: resolution appends members, which
// must not be re-scanned.
size_t const directiveCount = object.includeCount( );
for( size_t index = 0U; index < directiveCount; ++index )
{
// Re-fetch by index each turn; appendMember never touches includes[].
bool const disabled = object.getInclude( index ).isDisabled( );
if( disabled )
continue; // "--%include": preserved, not resolved
bool const hasAlias = object.getInclude( index ).hasAlias( );
string const path = object.getInclude( index ).getPath( );
string const alias = hasAlias ? *object.getInclude( index ).getAlias( )
: string( );
string canonical;
unique_ptr< Document > targetDocument =
loadTarget( path, ownerDirectory, canonical );
if( !targetDocument )
continue; // error already recorded
if( hasAlias )
{
if( object.find( alias ) )
continue; // host wins
unique_ptr< Value > rootClone =
targetDocument->getRootValue( ).clone( );
Key key = Key::isValidName( alias ) ? Key::bare( alias )
: Key::quoted( alias );
Member merged( std::move( key ), std::move( rootClone ) );
merged.setOrigin( canonical, string( ) ); // target root value
object.appendMember( std::move( merged ) );
}
else
{
Object const* const targetRoot = targetDocument->getRootObject( );
if( !targetRoot )
{
collectedErrors.push_back( parser::ParseError{
parser::ParseError::Kind::Type,
"merge %include target root is not an object: '" + path + "'",
canonical, 0UL, 0UL } );
continue;
}
for( size_t m = 0U; m < targetRoot->memberCount( ); ++m )
{
Member const& source = targetRoot->getMember( m );
if( source.isDisabled( ) )
continue;
string const& name = source.getKey( ).getName( );
if( object.find( name ) )
continue; // host wins
Member merged( source.getKey( ), source.getValue( ).clone( ) );
// Propagate the ultimate origin for a transitively merged source.
if( source.isMerged( ) )
merged.setOrigin( source.getOriginFile( ),
source.getOriginLocalPath( ) );
else
merged.setOrigin( canonical, name );
object.appendMember( std::move( merged ) );
}
}
}
}
} // end namespace fson
} // end namespace fedem