Skip to content
Open
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
9 changes: 6 additions & 3 deletions nixd/include/nixd/Controller/Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,12 @@ class Controller : public lspserver::LSPServer {

/// Determine whether or not this diagnostic is suppressed.
bool isSuppressed(nixf::Diagnostic::DiagnosticKind Kind);
void publishDiagnostics(lspserver::PathRef File,
std::optional<int64_t> Version, std::string_view Src,
const std::vector<nixf::Diagnostic> &Diagnostics);
std::vector<NixdDiagnostic> collectOptionDiagnostics(const NixTU &TU);
void
publishDiagnostics(lspserver::PathRef File, std::optional<int64_t> Version,
std::string_view Src,
const std::vector<nixf::Diagnostic> &Diagnostics,
const std::vector<NixdDiagnostic> &NixdDiagnostics = {});

void onRename(const lspserver::RenameParams &Params,
lspserver::Callback<lspserver::WorkspaceEdit> Reply);
Expand Down
25 changes: 25 additions & 0 deletions nixd/include/nixd/Controller/NixTU.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,32 @@

#include <memory>
#include <optional>
#include <string>
#include <vector>

namespace nixd {

enum class NixdDiagnosticSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4,
};

struct NixdDiagnostic {
nixf::LexerCursorRange Range;
NixdDiagnosticSeverity Severity = NixdDiagnosticSeverity::Error;
std::string Code;
std::string Source;
std::string Message;
};

/// \brief Holds analyzed information about a document.
///
/// TU stands for "Translation Unit".
class NixTU {
std::vector<nixf::Diagnostic> Diagnostics;
std::vector<NixdDiagnostic> NixdDiagnostics;
std::shared_ptr<nixf::Node> AST;
std::optional<util::OwnedRegion> ASTByteCode;
std::unique_ptr<nixf::VariableLookupAnalysis> VLA;
Expand All @@ -36,6 +53,14 @@ class NixTU {
return Diagnostics;
}

[[nodiscard]] const std::vector<NixdDiagnostic> &nixdDiagnostics() const {
return NixdDiagnostics;
}

void setNixdDiagnostics(std::vector<NixdDiagnostic> Diagnostics) {
NixdDiagnostics = std::move(Diagnostics);
}

[[nodiscard]] const std::shared_ptr<nixf::Node> &ast() const { return AST; }

[[nodiscard]] const nixf::ParentMapAnalysis *parentMap() const {
Expand Down
1 change: 1 addition & 0 deletions nixd/include/nixd/Protocol/AttrSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ using AttrPathCompleteResponse = std::vector<std::string>;

struct OptionType {
std::optional<std::string> Description;
std::optional<std::vector<std::string>> EnumValues;
std::optional<std::string> Name;
};

Expand Down
3 changes: 2 additions & 1 deletion nixd/lib/Controller/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ void Controller::updateConfig(Configuration NewConfig) {
// After all, notify all AST modules the diagnostic set has been updated.
std::lock_guard TUsGuard(TUsLock);
for (const auto &[File, TU] : TUs) {
publishDiagnostics(File, std::nullopt, TU->src(), TU->diagnostics());
publishDiagnostics(File, std::nullopt, TU->src(), TU->diagnostics(),
TU->nixdDiagnostics());
}
}

Expand Down
14 changes: 12 additions & 2 deletions nixd/lib/Controller/Diagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ bool Controller::isSuppressed(nixf::Diagnostic::DiagnosticKind Kind) {

void Controller::publishDiagnostics(
PathRef File, std::optional<int64_t> Version, std::string_view Src,
const std::vector<nixf::Diagnostic> &Diagnostics) {
const std::vector<nixf::Diagnostic> &Diagnostics,
const std::vector<NixdDiagnostic> &NixdDiagnostics) {
std::vector<Diagnostic> LSPDiags;
LSPDiags.reserve(Diagnostics.size());
LSPDiags.reserve(Diagnostics.size() + NixdDiagnostics.size());
for (const nixf::Diagnostic &D : Diagnostics) {
// Before actually doing anything,
// let's check if the diagnostic is suppressed.
Expand Down Expand Up @@ -112,6 +113,15 @@ void Controller::publishDiagnostics(
});
}
}
for (const NixdDiagnostic &D : NixdDiagnostics) {
LSPDiags.emplace_back(Diagnostic{
.range = toLSPRange(Src, D.Range),
.severity = static_cast<int>(D.Severity),
.code = D.Code,
.source = D.Source,
.message = D.Message,
});
}
PublishDiagnostic({
.uri = URIForFile::canonicalize(File, File),
.diagnostics = std::move(LSPDiags),
Expand Down
165 changes: 165 additions & 0 deletions nixd/lib/Controller/OptionDiagnostics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#include "AST.h"

#include "nixd/Controller/Controller.h"
#include "nixd/Protocol/AttrSet.h"

#include "lspserver/Logger.h"

#include <nixf/Basic/Nodes/Attrs.h>
#include <nixf/Basic/Nodes/Basic.h>
#include <nixf/Basic/Nodes/Simple.h>

#include <llvm/ADT/STLExtras.h>
#include <llvm/ADT/StringRef.h>
#include <llvm/Support/Error.h>

#include <optional>
#include <semaphore>
#include <sstream>

using namespace lspserver;
using namespace nixd;
using namespace nixf;

namespace {

constexpr llvm::StringLiteral OptionValueTypeCode = "option-value-type";

std::optional<OptionDescription>
resolveOptionInfo(AttrSetClient &Client,
const std::vector<std::string> &Scope) {
std::binary_semaphore Ready(0);
std::optional<OptionDescription> Desc;
auto OnReply = [&Ready, &Desc](llvm::Expected<OptionInfoResponse> Resp) {
if (Resp)
Desc = *Resp;
else
llvm::consumeError(Resp.takeError());
Ready.release();
};

Client.optionInfo(Scope, std::move(OnReply));
Ready.acquire();
return Desc;
}

std::string joinOptionPath(const std::vector<std::string> &Scope) {
std::ostringstream OS;
for (std::size_t I = 0; I < Scope.size(); ++I) {
if (I != 0)
OS << ".";
OS << Scope[I];
}
return OS.str();
}

bool enumContains(const OptionType &Type, llvm::StringRef Value) {
assert(Type.EnumValues && "must have enum values");
return llvm::is_contained(*Type.EnumValues, Value);
}

std::string sourceText(llvm::StringRef Src, LexerCursorRange Range) {
return Src
.substr(Range.lCur().offset(),
Range.rCur().offset() - Range.lCur().offset())
.str();
}

std::optional<std::string> getLiteralString(const ExprString &String) {
if (String.parts().fragments().empty())
return "";

if (!String.isLiteral())
return std::nullopt;

return String.literal();
}

std::optional<NixdDiagnostic>
validateOptionBinding(const Binding &Binding, const ParentMapAnalysis &PM,
llvm::StringRef Src, Controller::OptionMapTy &Options) {
const auto &Value = Binding.value();
if (!Value || Value->kind() != Node::NK_ExprString)
return std::nullopt;

const auto &String = static_cast<const ExprString &>(*Value);
std::optional<std::string> LiteralString = getLiteralString(String);
if (!LiteralString)
return std::nullopt;

const auto &Names = Binding.path().names();
if (Names.empty())
return std::nullopt;

std::vector<std::string> Scope;
if (findAttrPathForOptions(*Names.back(), PM, Scope) !=
FindAttrPathResult::OK ||
Scope.empty())
return std::nullopt;

for (const auto &[Name, Provider] : Options) {
AttrSetClient *Client = Provider->client();
if (!Client) {
elog("skipped option client {0} as it is dead", Name);
continue;
}

std::optional<OptionDescription> Desc = resolveOptionInfo(*Client, Scope);
if (!Desc || !Desc->Type)
continue;

const OptionType &Type = *Desc->Type;
if (Type.Name != "enum")
continue;

if (!Type.EnumValues)
return std::nullopt;

if (enumContains(Type, *LiteralString))
return std::nullopt;

std::string TypeDescription =
Type.Description.value_or(Type.Name.value_or("expected option type"));
return NixdDiagnostic{
.Range = Value->range(),
.Severity = NixdDiagnosticSeverity::Error,
.Code = std::string(OptionValueTypeCode),
.Source = "nixd",
.Message = "definition `" + sourceText(Src, Value->range()) +
"` for option `" + joinOptionPath(Scope) +
"` is not of type `" + TypeDescription + "`.",
};
}

return std::nullopt;
}

void collectOptionDiagnosticsFromNode(
const Node &N, const ParentMapAnalysis &PM, llvm::StringRef Src,
Controller::OptionMapTy &Options,
std::vector<NixdDiagnostic> &Diagnostics) {
if (N.kind() == Node::NK_Binding) {
if (std::optional<NixdDiagnostic> Diag = validateOptionBinding(
static_cast<const Binding &>(N), PM, Src, Options))
Diagnostics.emplace_back(std::move(*Diag));
}

for (const Node *Child : N.children()) {
if (Child)
collectOptionDiagnosticsFromNode(*Child, PM, Src, Options, Diagnostics);
}
}

} // namespace

std::vector<NixdDiagnostic>
Controller::collectOptionDiagnostics(const NixTU &TU) {
std::vector<NixdDiagnostic> Diagnostics;
if (!TU.ast() || !TU.parentMap())
return Diagnostics;

std::lock_guard _(OptionsLock);
collectOptionDiagnosticsFromNode(*TU.ast(), *TU.parentMap(), TU.src(),
Options, Diagnostics);
return Diagnostics;
}
20 changes: 11 additions & 9 deletions nixd/lib/Controller/Support.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,27 @@ void Controller::actOnDocumentAdd(PathRef File,
nixf::parse(*Draft->Contents, Diagnostics);

if (!AST) {
std::lock_guard G(TUsLock);
publishDiagnostics(File, Version, *Src, Diagnostics);
TUs.insert_or_assign(File,
std::make_shared<NixTU>(std::move(Diagnostics),
std::move(AST), std::nullopt,
/*VLA=*/nullptr, Src));
auto TU = std::make_shared<NixTU>(std::move(Diagnostics), std::move(AST),
std::nullopt, /*VLA=*/nullptr, Src);
std::lock_guard G(TUsLock);
TUs.insert_or_assign(File, std::move(TU));
return;
}

auto VLA = std::make_unique<nixf::VariableLookupAnalysis>(Diagnostics);
VLA->runOnAST(*AST);

publishDiagnostics(File, Version, *Src, Diagnostics);
auto TU = std::make_shared<NixTU>(std::move(Diagnostics), std::move(AST),
std::nullopt, std::move(VLA), Src);
TU->setNixdDiagnostics(collectOptionDiagnostics(*TU));

publishDiagnostics(File, Version, *Src, TU->diagnostics(),
TU->nixdDiagnostics());

{
std::lock_guard G(TUsLock);
TUs.insert_or_assign(
File, std::make_shared<NixTU>(std::move(Diagnostics), std::move(AST),
std::nullopt, std::move(VLA), Src));
TUs.insert_or_assign(File, std::move(TU));
return;
}
};
Expand Down
25 changes: 25 additions & 0 deletions nixd/lib/Eval/AttrSetProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,34 @@ void fillOptionDeclarations(nix::EvalState &State, nix::Value &V,
}
}

std::optional<std::vector<std::string>>
getEnumValuesFromFunctor(nix::EvalState &State, nix::Value &VType) {
try {
nix::Value &Values =
nixt::selectStringViews(State, VType, {"functor", "payload", "values"});
State.forceValue(Values, nix::noPos);
if (Values.type() != nix::ValueType::nList)
return std::nullopt;

std::vector<std::string> EnumValues;
for (nix::Value *Item : Values.listView()) {
State.forceValue(*Item, nix::noPos);
if (Item->type() != nix::ValueType::nString)
return std::nullopt;
EnumValues.emplace_back(Item->string_view());
}
return EnumValues;
} catch (std::exception &E) {
return std::nullopt;
}
}

void fillOptionType(nix::EvalState &State, nix::Value &VType, OptionType &R) {
fillString(State, VType, {"description"}, R.Description);
fillString(State, VType, {"name"}, R.Name);

if (R.Name && *R.Name == "enum")
R.EnumValues = getEnumValuesFromFunctor(State, VType);
}

void fillOptionDescription(nix::EvalState &State, nix::Value &V,
Expand Down
6 changes: 5 additions & 1 deletion nixd/lib/Protocol/AttrSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ using namespace nixd;
using namespace llvm::json;

Value nixd::toJSON(const OptionType &Params) {
return Object{
Object Result{
{"Description", Params.Description},
{"Name", Params.Name},
};
if (Params.EnumValues)
Result["EnumValues"] = *Params.EnumValues;
return Result;
}

bool nixd::fromJSON(const Value &Params, OptionType &R, Path P) {
ObjectMapper O(Params, P);
return O //
&& O.mapOptional("Description", R.Description) //
&& O.mapOptional("EnumValues", R.EnumValues) //
&& O.mapOptional("Name", R.Name);
}

Expand Down
1 change: 1 addition & 0 deletions nixd/lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ libnixd_lib = library(
'Controller/InlayHints.cpp',
'Controller/LifeTime.cpp',
'Controller/NixTU.cpp',
'Controller/OptionDiagnostics.cpp',
'Controller/Rename.cpp',
'Controller/SemanticTokens.cpp',
'Controller/Support.cpp',
Expand Down
Loading
Loading