diff --git a/docs/api/enum/nw__script__DiagnosticLevel.rst b/docs/api/enum/nw__script__DiagnosticLevel.rst new file mode 100644 index 000000000..5618c7a1a --- /dev/null +++ b/docs/api/enum/nw__script__DiagnosticLevel.rst @@ -0,0 +1,4 @@ +nw::script::DiagnosticLevel +=========================== + +.. doxygenenum:: nw::script::DiagnosticLevel diff --git a/docs/api/enum/nw__script__DiagnosticType.rst b/docs/api/enum/nw__script__DiagnosticType.rst new file mode 100644 index 000000000..cb60f3314 --- /dev/null +++ b/docs/api/enum/nw__script__DiagnosticType.rst @@ -0,0 +1,4 @@ +nw::script::DiagnosticType +========================== + +.. doxygenenum:: nw::script::DiagnosticType diff --git a/docs/api/struct/nw__script__Diagnostic.rst b/docs/api/struct/nw__script__Diagnostic.rst new file mode 100644 index 000000000..3d3482b1e --- /dev/null +++ b/docs/api/struct/nw__script__Diagnostic.rst @@ -0,0 +1,6 @@ +nw::script::Diagnostic +====================== + +.. doxygenstruct:: nw::script::Diagnostic + :members: + :undoc-members: diff --git a/docs/api/struct/nw__script__LspContext.rst b/docs/api/struct/nw__script__LspContext.rst new file mode 100644 index 000000000..fc18a9e72 --- /dev/null +++ b/docs/api/struct/nw__script__LspContext.rst @@ -0,0 +1,7 @@ + +nw::script::LspContext +====================== + +.. doxygenstruct:: nw::script::LspContext + :members: + :undoc-members: diff --git a/docs/fake/rollnw/script/__init__.py b/docs/fake/rollnw/script/__init__.py index 8066abde7..d4744d00e 100644 --- a/docs/fake/rollnw/script/__init__.py +++ b/docs/fake/rollnw/script/__init__.py @@ -104,6 +104,38 @@ def __init__(self, command_script: str = "nwscript"): pass +class DiagnosticType(enum.IntEnum): + lexical = auto() + parse = auto() + semantic = auto() + + +class DiagnosticLevel(enum.IntEnum): + warning = auto() + error = auto() + + +class Diagnostic: + """Parsed script Diagnostic + + Attributes: + type (DiagnosticType) + level (DiagnosticLevel) + script (str) + message (str) + location (SourceLocation)""" + + +class LspContext: + """Provides a context built around providing diagnostics to a language server""" + + def __init__(self, command_script: str = "nwscript"): + pass + + def diagnostics(self) -> List[Diagnostic]: + return [] + + class Nss: """Implementation of nwscript""" diff --git a/lib/nw/CMakeLists.txt b/lib/nw/CMakeLists.txt index 13535346d..af0540b1e 100644 --- a/lib/nw/CMakeLists.txt +++ b/lib/nw/CMakeLists.txt @@ -110,6 +110,7 @@ add_library(nw STATIC script/Ast.cpp script/Context.cpp + script/LspContext.cpp script/Nss.cpp script/NssLexer.cpp script/NssParser.cpp diff --git a/lib/nw/script/LspContext.cpp b/lib/nw/script/LspContext.cpp new file mode 100644 index 000000000..88de11b15 --- /dev/null +++ b/lib/nw/script/LspContext.cpp @@ -0,0 +1,50 @@ +#include "LspContext.hpp" + +#include "Nss.hpp" + +namespace nw::script { + +LspContext::LspContext(std::string command_script) + : Context(std::move(command_script)) +{ +} + +const std::vector& LspContext::diagnostics() const +{ + return diagnostics_; +} + +void LspContext::lexical_diagnostic(Nss* script, std::string_view msg, bool is_warning, SourceLocation loc) +{ + Diagnostic result; + result.type = DiagnosticType::lexical; + result.level = is_warning ? DiagnosticLevel::warning : DiagnosticLevel::error; + result.script = script ? script->name() : ""; + result.message = std::string(msg); + result.location = loc; + diagnostics_.push_back(std::move(result)); +} + +void LspContext::parse_diagnostic(Nss* script, std::string_view msg, bool is_warning, SourceLocation loc) +{ + Diagnostic result; + result.type = DiagnosticType::parse; + result.level = is_warning ? DiagnosticLevel::warning : DiagnosticLevel::error; + result.script = script ? script->name() : ""; + result.message = std::string(msg); + result.location = loc; + diagnostics_.push_back(std::move(result)); +} + +void LspContext::semantic_diagnostic(Nss* script, std::string_view msg, bool is_warning, SourceLocation loc) +{ + Diagnostic result; + result.type = DiagnosticType::semantic; + result.level = is_warning ? DiagnosticLevel::warning : DiagnosticLevel::error; + result.script = script ? script->name() : ""; + result.message = std::string(msg); + result.location = loc; + diagnostics_.push_back(std::move(result)); +} + +} // namespace nw::script diff --git a/lib/nw/script/LspContext.hpp b/lib/nw/script/LspContext.hpp new file mode 100644 index 000000000..08dcf3996 --- /dev/null +++ b/lib/nw/script/LspContext.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "Context.hpp" + +namespace nw::script { + +enum struct DiagnosticType { + lexical, + parse, + semantic, +}; + +enum struct DiagnosticLevel { + warning, + error, +}; + +struct Diagnostic { + DiagnosticType type; + DiagnosticLevel level; + std::string script; + std::string message; + SourceLocation location; +}; + +struct LspContext : public Context { + LspContext(std::string command_script = "nwscript"); + virtual ~LspContext() = default; + + std::vector diagnostics_; + + const std::vector& diagnostics() const; + + virtual void lexical_diagnostic(Nss* script, std::string_view msg, bool is_warning, SourceLocation loc) override; + virtual void parse_diagnostic(Nss* script, std::string_view msg, bool is_warning, SourceLocation loc) override; + virtual void semantic_diagnostic(Nss* script, std::string_view msg, bool is_warning, SourceLocation loc) override; +}; + +} // namespace nw::script diff --git a/rollnw-py/wrapper_script.cpp b/rollnw-py/wrapper_script.cpp index dc9341cdf..f6b05fe33 100644 --- a/rollnw-py/wrapper_script.cpp +++ b/rollnw-py/wrapper_script.cpp @@ -1,6 +1,7 @@ #include "opaque_types.hpp" #include +#include #include #include #include @@ -23,6 +24,27 @@ void init_script(py::module& nw) .def(py::init<>()) .def(py::init()); + py::enum_(nw, "DiagnosticType") + .value("lexical", nws::DiagnosticType::lexical) + .value("parse", nws::DiagnosticType::parse) + .value("semantic", nws::DiagnosticType::semantic); + + py::enum_(nw, "DiagnosticLevel") + .value("warning", nws::DiagnosticLevel::warning) + .value("error", nws::DiagnosticLevel::error); + + py::class_(nw, "Diagnostic") + .def_readonly("type", &nws::Diagnostic::type) + .def_readonly("level", &nws::Diagnostic::level) + .def_readonly("script", &nws::Diagnostic::script) + .def_readonly("message", &nws::Diagnostic::message) + .def_readonly("location", &nws::Diagnostic::location); + + py::class_(nw, "LspContext") + .def(py::init<>()) + .def(py::init()) + .def("diagnostics", &nws::LspContext::diagnostics); + py::enum_(nw, "NssTokenType") .value("INVALID", nws::NssTokenType::INVALID) .value("END", nws::NssTokenType::END) diff --git a/rollnw-stubs/script.pyi b/rollnw-stubs/script.pyi index 4f4c2f845..0c8783b5e 100644 --- a/rollnw-stubs/script.pyi +++ b/rollnw-stubs/script.pyi @@ -7,6 +7,32 @@ class Context: def __init__(self, command_script: str = "nwscript") -> None: ... +class DiagnosticType: + __members__: ClassVar[dict] = ... # read-only + lexical: ClassVar[NssTokenType] = ... + parse: ClassVar[NssTokenType] = ... + semantic: ClassVar[NssTokenType] = ... + + +class DiagnosticLevel: + __members__: ClassVar[dict] = ... # read-only + warning: ClassVar[NssTokenType] = ... + error: ClassVar[NssTokenType] = ... + + +class Diagnostic: + type: DiagnosticType + level: DiagnosticLevel + script: str + message: str + location: SourceLocation + + +class LspContext(Context): + def __init__(self, command_script: str = "nwscript") -> None: ... + def diagnostics(self) -> List[Diagnostic]: ... + + class AssignExpression(Expression): def __init__(self, *args, **kwargs) -> None: ... @property diff --git a/tests/script_nss.cpp b/tests/script_nss.cpp index df3c25ed8..2edca55d9 100644 --- a/tests/script_nss.cpp +++ b/tests/script_nss.cpp @@ -1,3 +1,4 @@ +#include "nw/script/LspContext.hpp" #include #include @@ -1000,3 +1001,24 @@ TEST(Nss, While) EXPECT_NO_THROW(nss1.resolve()); EXPECT_EQ(nss1.errors(), 1); } + +TEST(Nss, LspContext) +{ + auto ctx = std::make_unique(); + + script::Nss nss3(R"( + struct a { int test1; }; + + void main() { + struct a s; + s.test2; + } + )"sv, + ctx.get()); + + EXPECT_NO_THROW(nss3.parse()); + EXPECT_NO_THROW(nss3.resolve()); + EXPECT_EQ(ctx->diagnostics().size(), 1); + EXPECT_EQ(ctx->diagnostics()[0].type, nw::script::DiagnosticType::semantic); + EXPECT_EQ(ctx->diagnostics()[0].level, nw::script::DiagnosticLevel::error); +} diff --git a/tests/test_script.py b/tests/test_script.py index 608677295..aa86f0ab7 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -27,6 +27,17 @@ def test_function_decl(): assert isinstance(decl[0], VarDecl) +def test_function_decl2(): + ctx = LspContext("nwscript") + nss = Nss.from_string("void test_function(string s, int b)", ctx) + try: + nss.parse() + except: + pass + nss.resolve() + assert len(ctx.diagnostics()) == 1 + + def test_var_decl(): ctx = Context("nwscript") nss = Nss.from_string("int TRUE = 1; const int MY_GLOBAL = 1;", ctx)