Code View

fson / source / fson-1.1.0.0 / libs / internal / sdk / fson / IncludeResolver.cpp
// 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