luau/tests/RequireByString.test.cpp
Andy Friesen c51743268b
Sync to upstream/release/671 (#1787)
# General

* Internally rename `ClassType` to `ExternType`. In definition files,
the syntax to define these types has changed to `declare extern type Foo
with prop: type end`
* Add `luarequire_registermodule` to Luau.Require
* Support yieldable Luau C functions calling other functions
* Store return types as `AstTypePack*` on Ast nodes

## New Solver

* Improve the logic that determines constraint dispatch ordering
* Fix a crash in the type solver that arose when using multi-return
functions with `string.format`
* Fix https://github.com/luau-lang/luau/issues/1736
* Initial steps toward rethinking function generalization:
* Instead of generalizing every type in a function all at once, we will
instead generalize individual type variables once their bounds have been
fully resolved. This will make it possible to properly interleave type
function reduction and generalization.
* Magic functions are no longer considered magical in cases where they
are not explicitly called by the code.
* The most prominent example of this is in `for..in` loops where the
function call is part of the desugaring process.
* Almost all magic functions work by directly inspecting the AST, so
they can't work without an AST fragment anyway.
* Further, none of the magic functions we have are usefully used in this
way.

Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Sora Kanosue <skanosue@roblox.com>
Co-authored-by: Talha Pathan <tpathan@roblox.com>
Co-authored-by: Varun Saini <vsaini@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2025-04-25 14:19:27 -07:00

618 lines
20 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Common.h"
#include "Luau/Config.h"
#include "ScopedFlags.h"
#include "lua.h"
#include "lualib.h"
#include "Luau/Repl.h"
#include "Luau/Require.h"
#include "Luau/FileUtils.h"
#include "doctest.h"
#include <algorithm>
#include <cstring>
#include <initializer_list>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#if __APPLE__
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#include <CoreFoundation/CoreFoundation.h>
std::optional<std::string> getResourcePath0()
{
CFBundleRef mainBundle = CFBundleGetMainBundle();
if (mainBundle == NULL)
{
return std::nullopt;
}
CFURLRef mainBundleURL = CFBundleCopyBundleURL(mainBundle);
if (mainBundleURL == NULL)
{
CFRelease(mainBundle);
return std::nullopt;
}
char pathBuffer[PATH_MAX];
if (!CFURLGetFileSystemRepresentation(mainBundleURL, true, (UInt8*)pathBuffer, PATH_MAX))
{
CFRelease(mainBundleURL);
CFRelease(mainBundle);
return std::nullopt;
}
CFRelease(mainBundleURL);
CFRelease(mainBundle);
return std::string(pathBuffer);
}
std::optional<std::string> getResourcePath()
{
static std::optional<std::string> path0 = getResourcePath0();
return path0;
}
#endif
#endif
class ReplWithPathFixture
{
public:
ReplWithPathFixture()
: luaState(luaL_newstate(), lua_close)
{
L = luaState.get();
setupState(L);
luaL_sandboxthread(L);
runCode(L, prettyPrintSource);
}
// Returns all of the output captured from the pretty printer
std::string getCapturedOutput()
{
lua_getglobal(L, "capturedoutput");
const char* str = lua_tolstring(L, -1, nullptr);
std::string result(str);
lua_pop(L, 1);
return result;
}
enum class PathType
{
Absolute,
Relative
};
std::string getLuauDirectory(PathType type)
{
std::string luauDirRel = ".";
std::string luauDirAbs;
#if TARGET_OS_IPHONE
std::optional<std::string> cwd0 = getCurrentWorkingDirectory();
std::optional<std::string> cwd = getResourcePath();
if (cwd && cwd0)
{
// when running in xcode cwd0 is "/", however that is not always the case
const auto& _res = *cwd;
const auto& _cwd = *cwd0;
if (_res.find(_cwd) == 0)
{
// we need relative path so we subtract cwd0 from cwd
luauDirRel = "./" + _res.substr(_cwd.length());
}
}
#else
std::optional<std::string> cwd = getCurrentWorkingDirectory();
#endif
REQUIRE_MESSAGE(cwd, "Error getting Luau path");
std::replace((*cwd).begin(), (*cwd).end(), '\\', '/');
luauDirAbs = *cwd;
for (int i = 0; i < 20; ++i)
{
bool engineTestDir = isDirectory(luauDirAbs + "/Client/Luau/tests");
bool luauTestDir = isDirectory(luauDirAbs + "/tests/require");
if (engineTestDir || luauTestDir)
{
if (engineTestDir)
{
luauDirRel += "/Client/Luau";
luauDirAbs += "/Client/Luau";
}
if (type == PathType::Relative)
return luauDirRel;
if (type == PathType::Absolute)
return luauDirAbs;
}
if (luauDirRel == ".")
luauDirRel = "..";
else
luauDirRel += "/..";
std::optional<std::string> parentPath = getParentPath(luauDirAbs);
REQUIRE_MESSAGE(parentPath, "Error getting Luau path");
luauDirAbs = *parentPath;
}
// Could not find the directory
REQUIRE_MESSAGE(false, "Error getting Luau path");
return {};
}
void runProtectedRequire(const std::string& path)
{
runCode(L, "return pcall(function() return require(\"" + path + "\") end)");
}
void assertOutputContainsAll(const std::initializer_list<std::string>& list)
{
const std::string capturedOutput = getCapturedOutput();
for (const std::string& elem : list)
{
CHECK_MESSAGE(capturedOutput.find(elem) != std::string::npos, "Captured output: ", capturedOutput);
}
}
lua_State* L;
private:
std::unique_ptr<lua_State, void (*)(lua_State*)> luaState;
// This is a simplistic and incomplete pretty printer.
// It is included here to test that the pretty printer hook is being called.
// More elaborate tests to ensure correct output can be added if we introduce
// a more feature rich pretty printer.
std::string prettyPrintSource = R"(
-- Accumulate pretty printer output in `capturedoutput`
capturedoutput = ""
function arraytostring(arr)
local strings = {}
table.foreachi(arr, function(k,v) table.insert(strings, pptostring(v)) end )
return "{" .. table.concat(strings, ", ") .. "}"
end
function pptostring(x)
if type(x) == "table" then
-- Just assume array-like tables for now.
return arraytostring(x)
elseif type(x) == "string" then
return '"' .. x .. '"'
else
return tostring(x)
end
end
-- Note: Instead of calling print, the pretty printer just stores the output
-- in `capturedoutput` so we can check for the correct results.
function _PRETTYPRINT(...)
local args = table.pack(...)
local strings = {}
for i=1, args.n do
local item = args[i]
local str = pptostring(item, customoptions)
if i == 1 then
capturedoutput = capturedoutput .. str
else
capturedoutput = capturedoutput .. "\t" .. str
end
end
end
)";
};
TEST_SUITE_BEGIN("RequireByStringTests");
TEST_CASE("PathResolution")
{
#ifdef _WIN32
std::string prefix = "C:/";
#else
std::string prefix = "/";
#endif
// tuple format: {inputPath, inputBaseFilePath, expected}
std::vector<std::tuple<std::string, std::string, std::string>> tests = {
// 1. Basic path resolution
// a. Relative to a relative path that begins with './'
{"./dep", "./src/modules/module.luau", "./src/modules/dep"},
{"../dep", "./src/modules/module.luau", "./src/dep"},
{"../../dep", "./src/modules/module.luau", "./dep"},
{"../../", "./src/modules/module.luau", "./"},
// b. Relative to a relative path that begins with '../'
{"./dep", "../src/modules/module.luau", "../src/modules/dep"},
{"../dep", "../src/modules/module.luau", "../src/dep"},
{"../../dep", "../src/modules/module.luau", "../dep"},
{"../../", "../src/modules/module.luau", "../"},
// c. Relative to an absolute path
{"./dep", prefix + "src/modules/module.luau", prefix + "src/modules/dep"},
{"../dep", prefix + "src/modules/module.luau", prefix + "src/dep"},
{"../../dep", prefix + "src/modules/module.luau", prefix + "dep"},
{"../../", prefix + "src/modules/module.luau", prefix},
// 2. Check behavior for extraneous ".."
// a. Relative paths retain '..' and append if needed
{"../../../", "./src/modules/module.luau", "../"},
{"../../../", "../src/modules/module.luau", "../../"},
// b. Absolute paths ignore '..' if already at root
{"../../../", prefix + "src/modules/module.luau", prefix},
};
for (const auto& [inputPath, inputBaseFilePath, expected] : tests)
{
std::optional<std::string> resolved = resolvePath(inputPath, inputBaseFilePath);
CHECK(resolved);
CHECK_EQ(resolved, expected);
}
}
TEST_CASE("PathNormalization")
{
#ifdef _WIN32
std::string prefix = "C:/";
#else
std::string prefix = "/";
#endif
// pair format: {input, expected}
std::vector<std::pair<std::string, std::string>> tests = {
// 1. Basic formatting checks
{"", "./"},
{".", "./"},
{"..", "../"},
{"a/relative/path", "./a/relative/path"},
// 2. Paths containing extraneous '.' and '/' symbols
{"./remove/extraneous/symbols/", "./remove/extraneous/symbols"},
{"./remove/extraneous//symbols", "./remove/extraneous/symbols"},
{"./remove/extraneous/symbols/.", "./remove/extraneous/symbols"},
{"./remove/extraneous/./symbols", "./remove/extraneous/symbols"},
{"../remove/extraneous/symbols/", "../remove/extraneous/symbols"},
{"../remove/extraneous//symbols", "../remove/extraneous/symbols"},
{"../remove/extraneous/symbols/.", "../remove/extraneous/symbols"},
{"../remove/extraneous/./symbols", "../remove/extraneous/symbols"},
{prefix + "remove/extraneous/symbols/", prefix + "remove/extraneous/symbols"},
{prefix + "remove/extraneous//symbols", prefix + "remove/extraneous/symbols"},
{prefix + "remove/extraneous/symbols/.", prefix + "remove/extraneous/symbols"},
{prefix + "remove/extraneous/./symbols", prefix + "remove/extraneous/symbols"},
// 3. Paths containing '..'
// a. '..' removes the erasable component before it
{"./remove/me/..", "./remove"},
{"./remove/me/../", "./remove"},
{"../remove/me/..", "../remove"},
{"../remove/me/../", "../remove"},
{prefix + "remove/me/..", prefix + "remove"},
{prefix + "remove/me/../", prefix + "remove"},
// b. '..' stays if path is relative and component is non-erasable
{"./..", "../"},
{"./../", "../"},
{"../..", "../../"},
{"../../", "../../"},
// c. '..' disappears if path is absolute and component is non-erasable
{prefix + "..", prefix},
};
for (const auto& [input, expected] : tests)
{
CHECK_EQ(normalizePath(input), expected);
}
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireSimpleRelativePath")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/dependency";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from dependency"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireRelativeToRequiringFile")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/module";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from dependency", "required into module"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireLua")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/lua_dependency";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from lua_dependency"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireInitLuau")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/luau";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from init.luau"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireInitLua")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/lua";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from init.lua"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireSubmoduleUsingSelf")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/nested_module_requirer";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from submodule"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireWithFileAmbiguity")
{
std::string ambiguousPath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/ambiguous_file_requirer";
runProtectedRequire(ambiguousPath);
assertOutputContainsAll({"false", "require path could not be resolved to a unique file"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireWithDirectoryAmbiguity")
{
std::string ambiguousPath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/ambiguous_directory_requirer";
runProtectedRequire(ambiguousPath);
assertOutputContainsAll({"false", "require path could not be resolved to a unique file"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCacheAfterRequireLuau")
{
std::string relativePath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/module";
std::string absolutePath = getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/module";
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + ".luau").c_str());
REQUIRE_MESSAGE(lua_isnil(L, -1), "Cache already contained module result");
runProtectedRequire(relativePath);
assertOutputContainsAll({"true", "result from dependency", "required into module"});
// Check cache for the absolute path as a cache key
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + ".luau").c_str());
REQUIRE_FALSE_MESSAGE(lua_isnil(L, -1), "Cache did not contain module result");
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCacheAfterRequireLua")
{
std::string relativePath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/lua_dependency";
std::string absolutePath = getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/lua_dependency";
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + ".luau").c_str());
REQUIRE_MESSAGE(lua_isnil(L, -1), "Cache already contained module result");
runProtectedRequire(relativePath);
assertOutputContainsAll({"true", "result from lua_dependency"});
// Check cache for the absolute path as a cache key
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + ".lua").c_str());
REQUIRE_FALSE_MESSAGE(lua_isnil(L, -1), "Cache did not contain module result");
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCacheAfterRequireInitLuau")
{
std::string relativePath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/luau";
std::string absolutePath = getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/luau";
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + "/init.luau").c_str());
REQUIRE_MESSAGE(lua_isnil(L, -1), "Cache already contained module result");
runProtectedRequire(relativePath);
assertOutputContainsAll({"true", "result from init.luau"});
// Check cache for the absolute path as a cache key
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + "/init.luau").c_str());
REQUIRE_FALSE_MESSAGE(lua_isnil(L, -1), "Cache did not contain module result");
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCacheAfterRequireInitLua")
{
std::string relativePath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/lua";
std::string absolutePath = getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/lua";
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + "/init.lua").c_str());
REQUIRE_MESSAGE(lua_isnil(L, -1), "Cache already contained module result");
runProtectedRequire(relativePath);
assertOutputContainsAll({"true", "result from init.lua"});
// Check cache for the absolute path as a cache key
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
lua_getfield(L, -1, (absolutePath + "/init.lua").c_str());
REQUIRE_FALSE_MESSAGE(lua_isnil(L, -1), "Cache did not contain module result");
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCachedResult")
{
std::string relativePath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/validate_cache";
runProtectedRequire(relativePath);
assertOutputContainsAll({"true"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RegisterRuntimeModule")
{
lua_pushcfunction(L, luarequire_registermodule, nullptr);
lua_pushstring(L, "@test/helloworld");
lua_newtable(L);
lua_pushstring(L, "hello");
lua_pushstring(L, "world");
lua_settable(L, -3);
lua_call(L, 2, 0);
runCode(L, "return require('@test/helloworld').hello == 'world'");
assertOutputContainsAll({"true"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "LoadStringRelative")
{
runCode(L, "return pcall(function() return loadstring(\"require('a/relative/path')\")() end)");
assertOutputContainsAll({"false", "require is not supported in this context"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireAbsolutePath")
{
std::string absolutePath = "/an/absolute/path";
runProtectedRequire(absolutePath);
assertOutputContainsAll({"false", "require path must start with a valid prefix: ./, ../, or @"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireUnprefixedPath")
{
std::string path = "an/unprefixed/path";
runProtectedRequire(path);
assertOutputContainsAll({"false", "require path must start with a valid prefix: ./, ../, or @"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequirePathWithAlias")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/alias_requirer";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from dependency"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequirePathWithParentAlias")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/parent_alias_requirer";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from other_dependency"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequirePathWithAliasPointingToDirectory")
{
std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/directory_alias_requirer";
runProtectedRequire(path);
assertOutputContainsAll({"true", "result from subdirectory_dependency"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireAliasThatDoesNotExist")
{
std::string nonExistentAlias = "@this.alias.does.not.exist";
runProtectedRequire(nonExistentAlias);
assertOutputContainsAll({"false", "@this.alias.does.not.exist is not a valid alias"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "AliasHasIllegalFormat")
{
std::string illegalCharacter = "@@";
runProtectedRequire(illegalCharacter);
assertOutputContainsAll({"false", "@@ is not a valid alias"});
std::string pathAlias1 = "@.";
runProtectedRequire(pathAlias1);
assertOutputContainsAll({"false", ". is not a valid alias"});
std::string pathAlias2 = "@..";
runProtectedRequire(pathAlias2);
assertOutputContainsAll({"false", ".. is not a valid alias"});
std::string emptyAlias = "@";
runProtectedRequire(emptyAlias);
assertOutputContainsAll({"false", " is not a valid alias"});
}
TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireFromLuauBinary")
{
char executable[] = "luau";
std::vector<std::string> paths = {
getLuauDirectory(PathType::Relative) + "/tests/require/without_config/dependency.luau",
getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/dependency.luau",
getLuauDirectory(PathType::Relative) + "/tests/require/without_config/module.luau",
getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/module.luau",
};
for (const std::string& path : paths)
{
std::vector<char> pathStr(path.size() + 1);
strncpy(pathStr.data(), path.c_str(), path.size());
pathStr[path.size()] = '\0';
char* argv[2] = {executable, pathStr.data()};
CHECK_EQ(replMain(2, argv), 0);
}
}
TEST_CASE("ParseAliases")
{
std::string configJson = R"({
"aliases": {
"MyAlias": "/my/alias/path",
}
})";
Luau::Config config;
Luau::ConfigOptions::AliasOptions aliasOptions;
aliasOptions.configLocation = "/default/location";
aliasOptions.overwriteAliases = true;
Luau::ConfigOptions options{false, aliasOptions};
std::optional<std::string> error = Luau::parseConfig(configJson, config, options);
REQUIRE(!error);
auto checkContents = [](Luau::Config& config) -> void
{
CHECK(config.aliases.size() == 1);
REQUIRE(config.aliases.contains("myalias"));
Luau::Config::AliasInfo& aliasInfo = config.aliases["myalias"];
CHECK(aliasInfo.value == "/my/alias/path");
CHECK(aliasInfo.originalCase == "MyAlias");
};
checkContents(config);
// Ensure that copied Configs retain the same information
Luau::Config copyConstructedConfig = config;
checkContents(copyConstructedConfig);
Luau::Config copyAssignedConfig;
copyAssignedConfig = config;
checkContents(copyAssignedConfig);
}
TEST_SUITE_END();