Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ jobs:
- run: npm ci
working-directory: editors/code

- name: Publish Extension
working-directory: editors/code
run: npx @vscode/vsce publish --pat ${{ secrets.MARKETPLACE_TOKEN }} --target ${{ join(matrix.code-target, ' ') }}
# - name: Publish Extension
# working-directory: editors/code
# run: npx @vscode/vsce publish --pat ${{ secrets.MARKETPLACE_TOKEN }} --target ${{ join(matrix.code-target, ' ') }}

- name: Publish OpenVSX Extension
working-directory: editors/code
run: npx ovsx publish --pat ${{ secrets.OVSX_TOKEN }} --target ${{ join(matrix.code-target, ' ') }}
# - name: Publish OpenVSX Extension
# working-directory: editors/code
# run: npx ovsx publish --pat ${{ secrets.OVSX_TOKEN }} --target ${{ join(matrix.code-target, ' ') }}
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ option(LUAU_ENABLE_TIME_TRACE "Build with Luau TimeTrace" OFF)
option(LSP_BUILD_ASAN "Build with ASAN" OFF)
option(LSP_STATIC_CRT "Link with the static CRT (/MT)" OFF)
option(LSP_BUILD_WITH_SENTRY "Build with Sentry (crash reporting)" OFF)
option(NEVERMORE_STRING_REQUIRE "Build with Nevermore String Requires" ON)

if (LSP_STATIC_CRT)
cmake_policy(SET CMP0091 NEW)
Expand All @@ -20,6 +21,10 @@ if (LSP_BUILD_WITH_SENTRY)
add_definitions(-DLSP_BUILD_WITH_SENTRY)
endif ()

if (NEVERMORE_STRING_REQUIRE)
add_definitions(-DNEVERMORE_STRING_REQUIRE)
endif ()

project(Luau.LanguageServer LANGUAGES CXX)

# Sentry setup
Expand Down Expand Up @@ -52,6 +57,7 @@ target_sources(Luau.LanguageServer PRIVATE
src/CliConfigurationParser.cpp
src/platform/AutoImports.cpp
src/platform/LSPPlatform.cpp
src/platform/roblox/NevermoreStringRequire.cpp
src/platform/StringRequireAutoImporter.cpp
src/platform/StringRequireSuggester.cpp
src/platform/roblox/RobloxCodeAction.cpp
Expand Down
2 changes: 1 addition & 1 deletion luau
Submodule luau updated from 92cce5 to 2767d0
15 changes: 15 additions & 0 deletions src/LuauExt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ std::optional<Luau::AstExpr*> matchRequire(const Luau::AstExprCall& call)
if (call.args.size != 1)
return std::nullopt;

#ifdef NEVERMORE_STRING_REQUIRE
const Luau::AstExprLocal* local = call.func->as<Luau::AstExprLocal>();
if (local && local->local->name == require)
{
return call.args.data[0];
}
#endif

const Luau::AstExprGlobal* funcAsGlobal = call.func->as<Luau::AstExprGlobal>();
if (!funcAsGlobal || funcAsGlobal->name != require)
return std::nullopt;
Expand Down Expand Up @@ -727,6 +735,13 @@ bool isRequire(const Luau::AstExpr* expr)
{
if (auto funcAsGlobal = call->func->as<Luau::AstExprGlobal>(); funcAsGlobal && funcAsGlobal->name == "require")
return true;

#ifdef NEVERMORE_STRING_REQUIRE
if (const Luau::AstExprLocal* local = call->as<Luau::AstExprLocal>(); local && local->local->name == "require")
{
return true;
}
#endif
}
else if (auto assertion = expr->as<Luau::AstExprTypeAssertion>())
{
Expand Down
13 changes: 13 additions & 0 deletions src/WorkspaceFileResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ std::optional<Luau::SourceCode> WorkspaceFileResolver::readSource(const Luau::Mo
if (platform->isVirtualPath(name))
{
auto filePath = platform->resolveToRealPath(name);

#ifdef NEVERMORE_STRING_REQUIRE
// If we are missing a file path, then try to resolve into a virtual node that we generate
if (!filePath.has_value())
{
std::optional<Luau::SourceCode> sourceCode = platform->resolveToVirtualSourceCode(name);
if (sourceCode.has_value())
{
return sourceCode;
}
}
#endif

if (!filePath)
return std::nullopt;

Expand Down
7 changes: 7 additions & 0 deletions src/include/Platform/LSPPlatform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ class LSPPlatform
return name;
}

#ifdef NEVERMORE_STRING_REQUIRE
[[nodiscard]] virtual std::optional<Luau::SourceCode> resolveToVirtualSourceCode(const Luau::ModuleName& name) const
{
return std::nullopt;
}
#endif

[[nodiscard]] virtual Luau::SourceCode::Type sourceCodeTypeFromPath(const std::filesystem::path& path) const
{
return Luau::SourceCode::Type::Module;
Expand Down
17 changes: 17 additions & 0 deletions src/include/Platform/RobloxPlatform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ struct SourceNode
// A different TypeId is created for each type checker (frontend.typeChecker and frontend.typeCheckerForAutocomplete)
std::unordered_map<Luau::GlobalTypes const*, Luau::TypeId> tys{}; // NB: NOT POPULATED BY SOURCEMAP, created manually. Can be null!

#ifdef NEVERMORE_STRING_REQUIRE
bool isVirtualNevermoreLoader = false;
// The corresponding TypeId for this sourcemap node
// A different TypeId is created for each type checker (frontend.typeChecker and frontend.typeCheckerForAutocomplete)
std::unordered_map<Luau::GlobalTypes const*, Luau::TypeId> stringRequireTypes{}; // NB: NOT POPULATED BY SOURCEMAP, created manually. Can be null!
#endif

bool isScript();
std::optional<std::filesystem::path> getScriptFilePath();
Luau::SourceCode::Type sourceCodeType() const;
Expand Down Expand Up @@ -136,6 +143,10 @@ class RobloxPlatform : public LSPPlatform
mutable std::unordered_map<std::string, SourceNodePtr> realPathsToSourceNodes{};
mutable std::unordered_map<Luau::ModuleName, SourceNodePtr> virtualPathsToSourceNodes{};

#ifdef NEVERMORE_STRING_REQUIRE
mutable std::unordered_map<std::string, SourceNodePtr> moduleNameToSourceNode{};
#endif

std::optional<SourceNodePtr> getSourceNodeFromVirtualPath(const Luau::ModuleName& name) const;
std::optional<SourceNodePtr> getSourceNodeFromRealPath(const std::string& name) const;

Expand Down Expand Up @@ -170,6 +181,12 @@ class RobloxPlatform : public LSPPlatform

std::optional<std::filesystem::path> resolveToRealPath(const Luau::ModuleName& name) const override;

#ifdef NEVERMORE_STRING_REQUIRE
std::optional<Luau::SourceCode> resolveToVirtualSourceCode(const Luau::ModuleName& name) const override;
Luau::TypeId getStringRequireType(const Luau::GlobalTypes& globals, Luau::TypeArena& arena, const SourceNodePtr& node) const;
std::optional<SourceNodePtr> findStringModule(const std::string& moduleName) const;
#endif

Luau::SourceCode::Type sourceCodeTypeFromPath(const std::filesystem::path& path) const override;

std::optional<std::string> readSourceCode(const Luau::ModuleName& name, const std::filesystem::path& path) const override;
Expand Down
191 changes: 191 additions & 0 deletions src/platform/roblox/NevermoreStringRequire.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "Platform/RobloxPlatform.hpp"
#include "LSP/JsonTomlSyntaxParser.hpp"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/ConstraintSolver.h"
#include "Luau/TypeInfer.h"

#include <filesystem>
#include <queue>

#ifdef NEVERMORE_STRING_REQUIRE

struct MagicStringRequireLookup final : Luau::MagicFunction
{
const Luau::GlobalTypes& globals;
const RobloxPlatform& platform;
Luau::TypeArena& arena;
SourceNodePtr node;

MagicStringRequireLookup(const Luau::GlobalTypes& globals, const RobloxPlatform& platform, Luau::TypeArena& arena, SourceNodePtr node)
: globals(globals)
, platform(platform)
, arena(arena)
, node(std::move(node))
{
}

std::optional<Luau::WithPredicate<Luau::TypePackId>> handleOldSolver(Luau::TypeChecker& typeChecker, const Luau::ScopePtr& scope,
const Luau::AstExprCall& expr, Luau::WithPredicate<Luau::TypePackId> withPredicate) override;
bool infer(const Luau::MagicFunctionCallContext& context) override;
};

std::optional<Luau::WithPredicate<Luau::TypePackId>> MagicStringRequireLookup::handleOldSolver(
Luau::TypeChecker& typeChecker, const Luau::ScopePtr& scope, const Luau::AstExprCall& expr, Luau::WithPredicate<Luau::TypePackId>)
{
if (expr.args.size < 1)
{
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{}});
return std::nullopt;
}

auto str = expr.args.data[0]->as<Luau::AstExprConstantString>();
if (!str)
{
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{}});
return std::nullopt;
}

auto moduleName = std::string(str->value.data, str->value.size);

if (node->name == moduleName)
{
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{ moduleName }});
return std::nullopt;
}

auto module = platform.findStringModule(moduleName);
if (!module.has_value())
{
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{ moduleName }});
return std::nullopt;
}

Luau::ModuleInfo moduleInfo;
moduleInfo.name = module.value()->virtualPath;

return Luau::WithPredicate<Luau::TypePackId>{arena.addTypePack({typeChecker.checkRequire(scope, moduleInfo, expr.args.data[0]->location)})};
}

bool MagicStringRequireLookup::infer(const Luau::MagicFunctionCallContext& context)
{
// TODO: Actually like, do something here
if (context.callSite->args.size < 1)
return false;

auto str = context.callSite->args.data[0]->as<Luau::AstExprConstantString>();
if (!str)
return false;

auto moduleName = std::string(str->value.data, str->value.size);
auto module = platform.findStringModule(moduleName);
if (!module.has_value())
{
context.solver->reportError(Luau::UnknownRequire{ moduleName }, context.callSite->args.data[0]->location);
return false;
}


Luau::ModuleInfo moduleInfo;
moduleInfo.name = module.value()->virtualPath;

asMutable(context.result)->ty.emplace<Luau::BoundTypePack>(context.solver->arena->addTypePack({
context.solver->resolveModule(moduleInfo, context.callSite->args.data[0]->location)
}));

return true;
}

static void attachMagicStringRequireLookupFunction(const Luau::GlobalTypes& globals, const RobloxPlatform& platform, Luau::TypeArena& arena, const SourceNodePtr& node, Luau::TypeId lookupFuncTy)
{

Luau::attachMagicFunction(
lookupFuncTy, std::make_shared<MagicStringRequireLookup>(globals, platform, arena, node));
Luau::attachTag(lookupFuncTy, kSourcemapGeneratedTag);
Luau::attachTag(lookupFuncTy, "StringRequires");
Luau::attachTag(lookupFuncTy, "require"); // Magic tag
}

Luau::TypeId RobloxPlatform::getStringRequireType(const Luau::GlobalTypes& globals, Luau::TypeArena& arena, const SourceNodePtr& node) const
{
// Gets the type corresponding to the sourcemap node if it exists
// Make sure to use the correct ty version (base typeChecker vs autocomplete typeChecker)
if (node->stringRequireTypes.find(&globals) != node->stringRequireTypes.end())
return node->stringRequireTypes.at(&globals);

// TODO: Memory safety for RobloxPlatform this

Luau::LazyType lazyTypeValue(
[&globals, this, &arena, node](Luau::LazyType& lazyTypeValue) -> void
{
// Check if the lazy type value already has an unwrapped type
if (lazyTypeValue.unwrapped.load())
return;

// Handle if the node is no longer valid
if (!node)
{
lazyTypeValue.unwrapped = globals.builtinTypes->anyType;
return;
}

// TODO: Resolve name to lazy instance
// Or type union
Luau::TypePackId argTypes = arena.addTypePack({ globals.builtinTypes->stringType });
Luau::TypePackId retTypes = arena.addTypePack({ globals.builtinTypes->anyType }); // This should be overriden by the type checker
Luau::FunctionType functionCtv(argTypes, retTypes);

auto typeId = arena.addType(std::move(functionCtv));
attachMagicStringRequireLookupFunction(globals, *this, arena, node, typeId);

lazyTypeValue.unwrapped = typeId;
return;
});

auto ty = arena.addType(std::move(lazyTypeValue));
node->stringRequireTypes.insert_or_assign(&globals, ty);

return ty;
}

std::optional<SourceNodePtr> RobloxPlatform::findStringModule(const std::string& moduleName) const
{
// TODO: Use "node_modules" as a project scope and handle duplications
auto result = this->moduleNameToSourceNode.find(moduleName);
if (result != this->moduleNameToSourceNode.end())
return result->second;

return std::nullopt;
}

std::optional<Luau::SourceCode> RobloxPlatform::resolveToVirtualSourceCode(const Luau::ModuleName& name) const
{
if (!isVirtualPath(name))
{
return std::nullopt;
}

auto sourceNode = getSourceNodeFromVirtualPath(name);
if (!sourceNode || !sourceNode.value()->isVirtualNevermoreLoader)
{
return std::nullopt;
}

std::string source = R"lua(
--!strict

local loader = {}

function loader.load(thisScript: ModuleScript): typeof(StringRequire)
return nil :: never
end

return loader
)lua";

return Luau::SourceCode {
source,
Luau::SourceCode::Type::Module,
};
}

#endif
12 changes: 12 additions & 0 deletions src/platform/roblox/RobloxCompletion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ std::optional<Luau::AutocompleteEntryMap> RobloxPlatform::completionCallback(
}
}
}
#ifdef NEVERMORE_STRING_REQUIRE
else if (tag == "StringRequires")
{
Luau::AutocompleteEntryMap result;
for (const auto& pair : this->moduleNameToSourceNode)
result.insert_or_assign(
pair.first, Luau::AutocompleteEntry{Luau::AutocompleteEntryKind::String, workspaceFolder->frontend.builtinTypes->stringType, false,
false, Luau::TypeCorrectKind::Correct});

return result;
}
#endif
else if (tag == "Properties")
{
if (ctx && ctx.value())
Expand Down
13 changes: 13 additions & 0 deletions src/platform/roblox/RobloxFileResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ static std::string mapContext(const std::string& context)

std::optional<Luau::ModuleInfo> RobloxPlatform::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) {

#ifdef NEVERMORE_STRING_REQUIRE
// Resolve Nevermore string require before the platform tries to resolve the require path
if (auto* str = node->as<Luau::AstExprConstantString>())
{
auto module = this->findStringModule(std::string(str->value.data, str->value.size));
if (module.has_value())
{
Luau::ModuleName virtualPath = getVirtualPathFromSourceNode(module.value());
return Luau::ModuleInfo{virtualPath};
}
}
#endif

if (auto parentResult = LSPPlatform::resolveModule(context, node))
return parentResult;

Expand Down
Loading