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
39 changes: 39 additions & 0 deletions nixd/lib/Controller/AST.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ struct AttrPathHasDynamicError : std::exception {
}
};

/// \brief Is \p Call a call to `<x>.attrValues <arg>` (e.g. `lib.attrValues`,
/// `builtins.attrValues`) with a single argument?
///
/// Purely syntactic: matches an ExprSelect callee whose last AttrPath name
/// is `attrValues`. Doesn't resolve through identifier rebinding. False
/// positives are bounded by the options-tree lookup downstream — a
/// misrecognized call only causes the analyzer to construct a path that
/// won't match any real option, producing no hover (same as today).
bool isAttrValuesCall(const nixf::ExprCall &Call) {
if (Call.args().size() != 1)
return false;
const nixf::Expr &Fn = Call.fn();
if (Fn.kind() != Node::NK_ExprSelect)
return false;
const auto &Sel = static_cast<const nixf::ExprSelect &>(Fn);
const nixf::AttrPath *SelPath = Sel.path();
if (!SelPath || SelPath->names().empty())
return false;
const auto &LastName = SelPath->names().back();
return LastName && LastName->isStatic() &&
LastName->staticName() == "attrValues";
}

/// \brief Find nested attrpath.
/// e.g. a.b.c.d
/// ^<- a.b.c.d
Expand Down Expand Up @@ -62,6 +85,22 @@ void getValueAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM,
if (Up->kind() != Node::NK_ExprAttrs)
return;

// Transparency for `lib.attrValues <attrset>` / `builtins.attrValues
// <attrset>`. When the enclosing attrset is itself the sole argument of an
// attrValues call, the call discards the attribute names — the keys of
// `Up` are not option-path components. Recurse past the call as if
// `N` were a list-literal element of the surrounding context, without
// appending `N`'s key in `Up`.
if (const nixf::Node *UpUp = PM.query(*Up)) {
if (UpUp->kind() == Node::NK_ExprCall) {
const auto &Call = static_cast<const nixf::ExprCall &>(*UpUp);
if (isAttrValuesCall(Call) && Call.args()[0].get() == Up) {
getValueAttrPath(Call, PM, Path);
return;
}
}
}

std::vector<std::string_view> Basic;
// Recursively search up all nested entries
if (!PM.isRoot(*Up))
Expand Down
60 changes: 60 additions & 0 deletions nixd/tools/nixd/test/hover/attrvalues-transparency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# RUN: nixd --lit-test \
# RUN: --nixos-options-expr="{ services.openssh.enable = { _type = \"option\"; description = \"openssh option\"; type.description = \"bool\"; }; }" \
# RUN: < %s | FileCheck %s

# Cursor on `enable` inside `lib.mkMerge (lib.attrValues { ssh = MODULE; })`.
# Without the attrValues transparency in AST.cpp::getValueAttrPath, the
# attribute key `ssh` would be incorrectly prepended to the option path,
# producing the unresolvable `ssh.services.openssh.enable` and no hover.
# With it, the key is treated as transparent (since attrValues discards
# names) and the option resolves normally.

<-- initialize(0)

```json
{
"jsonrpc":"2.0",
"id":0,
"method":"initialize",
"params":{"processId":123,"rootPath":"","capabilities":{},"trace":"off"}
}
```


<-- textDocument/didOpen

```nix file:///test.nix
{ lib, ... }: lib.mkMerge (lib.attrValues {
ssh = {
services.openssh.enable = true;
};
})
```


<-- textDocument/hover(2)

```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/hover",
"params":{
"textDocument":{"uri":"file:///test.nix"},
"position":{"line":2,"character":23}
}
}
```

```
CHECK: "id": 2,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "contents": {
CHECK-NEXT: "kind": "markdown",
CHECK-NEXT: "value": " (bool)\n\nopenssh option"
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
Loading