#include "Luau/RequireNavigator.h"
#include "AliasCycleTracker.h"
#include "PathUtilities.h"
#include "Luau/Common.h"
#include "Luau/Config.h"
#include "Luau/LuauConfig.h"
#include <algorithm>
#include <optional>
#include <utility>
LUAU_FASTFLAGVARIABLE(LuauRequireAliasOverrideOrderFix)
namespace Luau::Require
{
using Error = std::optional<std::string>;
static std::string extractAlias(std::string_view path)
{
const size_t aliasStartPos = 1;
size_t aliasLen = path.find_first_of('/');
if (aliasLen != std::string::npos)
aliasLen -= aliasStartPos;
return std::string{path.substr(aliasStartPos, aliasLen)};
}
Navigator::Navigator(NavigationContext& navigationContext, ErrorHandler& errorHandler)
: navigationContext(navigationContext)
, errorHandler(errorHandler)
{
}
Navigator::Status Navigator::navigate(std::string path)
{
std::replace(path.begin(), path.end(), '\\', '/');
if (Error error = navigateImpl(path))
{
errorHandler.reportError(*error);
return Status::ErrorReported;
}
return Status::Success;
}
Error Navigator::navigateImpl(std::string_view path)
{
PathType pathType = getPathType(path);
if (pathType == PathType::Unsupported)
return "require path must start with a valid prefix: ./, ../, or @";
if (pathType == PathType::Aliased)
{
std::string alias = extractAlias(path);
std::transform(
alias.begin(),
alias.end(),
alias.begin(),
[](unsigned char c)
{
return ('A' <= c && c <= 'Z') ? (c + ('a' - 'A')) : c;
}
);
if (FFlag::LuauRequireAliasOverrideOrderFix)
{
if (Error error = resetToRequirer())
return error;
}
if (auto [error, wasOverridden] = toAliasOverride(alias); error)
{
return error;
}
else if (wasOverridden)
{
if (Error error = navigateThroughPath(path))
return error;
return std::nullopt;
}
if (!FFlag::LuauRequireAliasOverrideOrderFix)
{
if (Error error = resetToRequirer())
return error;
}
Config config;
if (Error error = navigateToAndPopulateConfig(alias, config))
return error;
if (config.aliases.contains(alias))
{
if (Error error = navigateToAlias(alias, config, {}))
return error;
if (Error error = navigateThroughPath(path))
return error;
return std::nullopt;
}
else
{
if (alias == "self")
{
if (Error error = resetToRequirer())
return error;
if (Error error = navigateThroughPath(path))
return error;
return std::nullopt;
}
if (Error error = toAliasFallback(alias))
return error;
if (Error error = navigateThroughPath(path))
return error;
return std::nullopt;
}
}
if (pathType == PathType::RelativeToCurrent || pathType == PathType::RelativeToParent)
{
if (Error error = resetToRequirer())
return error;
if (Error error = navigateToParent(std::nullopt))
return error;
if (Error error = navigateThroughPath(path))
return error;
}
return std::nullopt;
}
Error Navigator::navigateThroughPath(std::string_view path)
{
std::pair<std::string_view, std::string_view> components = splitPath(path);
if (path.size() >= 1 && path[0] == '@')
{
components = splitPath(components.second);
}
std::optional<std::string> previousComponent;
while (!(components.first.empty() && components.second.empty()))
{
if (components.first == "." || components.first.empty())
{
components = splitPath(components.second);
continue;
}
else if (components.first == "..")
{
if (Error error = navigateToParent(previousComponent))
return error;
}
else
{
if (Error error = navigateToChild(std::string{components.first}))
return error;
}
previousComponent = components.first;
components = splitPath(components.second);
}
return std::nullopt;
}
Error Navigator::navigateToAlias(const std::string& alias, const Config& config, AliasCycleTracker cycleTracker)
{
LUAU_ASSERT(config.aliases.contains(alias));
std::string value = config.aliases.find(alias)->value;
PathType pathType = getPathType(value);
if (pathType == PathType::RelativeToCurrent || pathType == PathType::RelativeToParent)
{
if (Error error = navigateThroughPath(value))
return error;
}
else if (pathType == PathType::Aliased)
{
if (Error error = cycleTracker.add(alias))
return error;
std::string nextAlias = extractAlias(value);
if (auto [error, wasOverridden] = toAliasOverride(nextAlias); error)
{
return error;
}
else if (wasOverridden)
{
if (Error error = navigateThroughPath(value))
return error;
return std::nullopt;
}
if (config.aliases.contains(nextAlias))
{
if (Error error = navigateToAlias(nextAlias, config, std::move(cycleTracker)))
return error;
}
else
{
Config parentConfig;
if (Error error = navigateToAndPopulateConfig(nextAlias, parentConfig))
return error;
if (parentConfig.aliases.contains(nextAlias))
{
if (Error error = navigateToAlias(nextAlias, parentConfig, {}))
return error;
}
else
{
if (Error error = toAliasFallback(nextAlias))
return error;
}
}
if (Error error = navigateThroughPath(value))
return error;
}
else
{
if (Error error = jumpToAlias(value))
return error;
}
return std::nullopt;
}
Error Navigator::navigateToAndPopulateConfig(const std::string& desiredAlias, Config& config)
{
while (!config.aliases.contains(desiredAlias))
{
config = {};
NavigationContext::NavigateResult result = navigationContext.toParent();
if (result == NavigationContext::NavigateResult::Ambiguous)
return "could not navigate up the ancestry chain during search for alias \"" + desiredAlias + "\" (ambiguous)";
if (result == NavigationContext::NavigateResult::NotFound)
break;
NavigationContext::ConfigStatus status = navigationContext.getConfigStatus();
if (status == NavigationContext::ConfigStatus::Absent)
{
continue;
}
else if (status == NavigationContext::ConfigStatus::Ambiguous)
{
return "could not resolve alias \"" + desiredAlias + "\" (ambiguous configuration file)";
}
else
{
if (navigationContext.getConfigBehavior() == NavigationContext::ConfigBehavior::GetAlias)
{
config.setAlias(desiredAlias, *navigationContext.getAlias(desiredAlias), "unused");
break;
}
std::optional<std::string> configContents = navigationContext.getConfig();
if (!configContents)
return "could not get configuration file contents to resolve alias \"" + desiredAlias + "\"";
Luau::ConfigOptions opts;
Luau::ConfigOptions::AliasOptions aliasOpts;
aliasOpts.configLocation = "unused";
aliasOpts.overwriteAliases = false;
opts.aliasOptions = std::move(aliasOpts);
if (status == NavigationContext::ConfigStatus::PresentJson)
{
if (Error error = Luau::parseConfig(*configContents, config, opts))
return error;
}
else if (status == NavigationContext::ConfigStatus::PresentLuau)
{
InterruptCallbacks callbacks;
callbacks.initCallback = navigationContext.luauConfigInit;
callbacks.interruptCallback = navigationContext.luauConfigInterrupt;
if (Error error = Luau::extractLuauConfig(*configContents, config, std::move(opts.aliasOptions), std::move(callbacks)))
return error;
}
}
};
return std::nullopt;
}
Error Navigator::resetToRequirer()
{
NavigationContext::NavigateResult result = navigationContext.resetToRequirer();
if (result == NavigationContext::NavigateResult::Success)
return std::nullopt;
std::string errorMessage = "could not reset to requiring context";
if (result == NavigationContext::NavigateResult::Ambiguous)
errorMessage += " (ambiguous)";
return errorMessage;
}
Error Navigator::jumpToAlias(const std::string& aliasPath)
{
NavigationContext::NavigateResult result = navigationContext.jumpToAlias(aliasPath);
if (result == NavigationContext::NavigateResult::Success)
return std::nullopt;
std::string errorMessage = "could not jump to alias \"" + aliasPath + "\"";
if (result == NavigationContext::NavigateResult::Ambiguous)
errorMessage += " (ambiguous)";
return errorMessage;
}
Error Navigator::navigateToParent(std::optional<std::string> previousComponent)
{
NavigationContext::NavigateResult result = navigationContext.toParent();
if (result == NavigationContext::NavigateResult::Success)
return std::nullopt;
std::string errorMessage;
if (previousComponent)
errorMessage = "could not get parent of component \"" + *previousComponent + "\"";
else
errorMessage = "could not get parent of requiring context";
if (result == NavigationContext::NavigateResult::Ambiguous)
errorMessage += " (ambiguous)";
return errorMessage;
}
Error Navigator::navigateToChild(const std::string& component)
{
NavigationContext::NavigateResult result = navigationContext.toChild(component);
if (result == NavigationContext::NavigateResult::Success)
return std::nullopt;
std::string errorMessage = "could not resolve child component \"" + component + "\"";
if (result == NavigationContext::NavigateResult::Ambiguous)
errorMessage += " (ambiguous)";
return errorMessage;
}
std::pair<Error, bool> Navigator::toAliasOverride(const std::string& aliasUnprefixed)
{
std::pair<Error, bool> result;
switch (navigationContext.toAliasOverride(aliasUnprefixed))
{
case NavigationContext::NavigateResult::Success:
result = {std::nullopt, true};
break;
case NavigationContext::NavigateResult::NotFound:
result = {std::nullopt, false};
break;
case NavigationContext::NavigateResult::Ambiguous:
result = {"@" + aliasUnprefixed + " is not a valid alias (ambiguous)", false};
break;
}
return result;
}
Error Navigator::toAliasFallback(const std::string& aliasUnprefixed)
{
NavigationContext::NavigateResult result = navigationContext.toAliasFallback(aliasUnprefixed);
if (result == NavigationContext::NavigateResult::Success)
return std::nullopt;
std::string errorMessage = "@" + aliasUnprefixed + " is not a valid alias";
if (result == NavigationContext::NavigateResult::Ambiguous)
errorMessage += " (ambiguous)";
return errorMessage;
}
}