diff --git a/nixd/lib/Controller/AST.cpp b/nixd/lib/Controller/AST.cpp index baf09371b..b9b84709b 100644 --- a/nixd/lib/Controller/AST.cpp +++ b/nixd/lib/Controller/AST.cpp @@ -15,6 +15,29 @@ struct AttrPathHasDynamicError : std::exception { } }; +/// \brief Is \p Call a call to `.attrValues ` (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(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 @@ -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 ` / `builtins.attrValues + // `. 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(*UpUp); + if (isAttrValuesCall(Call) && Call.args()[0].get() == Up) { + getValueAttrPath(Call, PM, Path); + return; + } + } + } + std::vector Basic; // Recursively search up all nested entries if (!PM.isRoot(*Up)) diff --git a/nixd/tools/nixd/test/hover/attrvalues-transparency.md b/nixd/tools/nixd/test/hover/attrvalues-transparency.md new file mode 100644 index 000000000..b957ed421 --- /dev/null +++ b/nixd/tools/nixd/test/hover/attrvalues-transparency.md @@ -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"} +```