From d4f576b0b281265b58bb497108f8d063e2a0f06a Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 9 Jul 2024 19:25:45 +0200 Subject: [PATCH] nix repl: Render docs for attributes --- .../rl-next/repl-doc-renders-doc-comments.md | 2 +- src/libcmd/repl.cc | 41 +++++++ src/libexpr/eval.cc | 42 +++++++- src/libexpr/eval.hh | 10 ++ src/libexpr/nixexpr.hh | 11 ++ src/libexpr/parser-state.hh | 2 +- src/libexpr/parser.y | 7 ++ src/libutil/position.hh | 8 ++ tests/functional/repl/doc-constant.expected | 102 ++++++++++++++++++ tests/functional/repl/doc-constant.in | 15 +++ 10 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 tests/functional/repl/doc-constant.expected create mode 100644 tests/functional/repl/doc-constant.in diff --git a/doc/manual/rl-next/repl-doc-renders-doc-comments.md b/doc/manual/rl-next/repl-doc-renders-doc-comments.md index c9213a88c..05023697c 100644 --- a/doc/manual/rl-next/repl-doc-renders-doc-comments.md +++ b/doc/manual/rl-next/repl-doc-renders-doc-comments.md @@ -45,7 +45,7 @@ Examples ``` Known limitations: -- It currently only works for functions. We plan to extend this to attributes, which may contain arbitrary values. +- It does not render documentation for "formals", such as `{ /** the value to return */ x, ... }: x`. - Some extensions to markdown are not yet supported, as you can see in the example above. We'd like to acknowledge Yingchi Long for proposing a proof of concept for this functionality in [#9054](https://github.com/NixOS/nix/pull/9054), as well as @sternenseemann and Johannes Kirschbauer for their contributions, proposals, and their work on [RFC 145]. diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 37a34e3de..fb8106c46 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -27,6 +27,7 @@ #include "local-fs-store.hh" #include "print.hh" #include "ref.hh" +#include "value.hh" #if HAVE_BOEHMGC #define GC_INCLUDE_NEW @@ -616,6 +617,33 @@ ProcessLineResult NixRepl::processLine(std::string line) else if (command == ":doc") { Value v; + + auto expr = parseString(arg); + std::string fallbackName; + PosIdx fallbackPos; + DocComment fallbackDoc; + if (auto select = dynamic_cast(expr)) { + Value vAttrs; + auto name = select->evalExceptFinalSelect(*state, *env, vAttrs); + fallbackName = state->symbols[name]; + + state->forceAttrs(vAttrs, noPos, "while evaluating an attribute set to look for documentation"); + auto attrs = vAttrs.attrs(); + assert(attrs); + auto attr = attrs->get(name); + if (!attr) { + // Trigger the normal error + evalString(arg, v); + } + if (attr->pos) { + fallbackPos = attr->pos; + fallbackDoc = state->getDocCommentForPos(fallbackPos); + } + + } else { + evalString(arg, v); + } + evalString(arg, v); if (auto doc = state->getDoc(v)) { std::string markdown; @@ -633,6 +661,19 @@ ProcessLineResult NixRepl::processLine(std::string line) markdown += stripIndentation(doc->doc); logger->cout(trim(renderMarkdownToTerminal(markdown))); + } else if (fallbackPos) { + std::stringstream ss; + ss << "Attribute `" << fallbackName << "`\n\n"; + ss << " … defined at " << state->positions[fallbackPos] << "\n\n"; + if (fallbackDoc) { + ss << fallbackDoc.getInnerText(state->positions); + } else { + ss << "No documentation found.\n\n"; + } + + auto markdown = ss.str(); + logger->cout(trim(renderMarkdownToTerminal(markdown))); + } else throw Error("value does not have documentation"); } diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index f8282c400..81c64ba71 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1415,6 +1415,22 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v) v = *vAttrs; } +Symbol ExprSelect::evalExceptFinalSelect(EvalState & state, Env & env, Value & attrs) +{ + Value vTmp; + Symbol name = getName(attrPath[attrPath.size() - 1], state, env); + + if (attrPath.size() == 1) { + e->eval(state, env, vTmp); + } else { + ExprSelect init(*this); + init.attrPath.pop_back(); + init.eval(state, env, vTmp); + } + attrs = vTmp; + return name; +} + void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v) { @@ -2876,13 +2892,37 @@ Expr * EvalState::parse( const SourcePath & basePath, std::shared_ptr & staticEnv) { - auto result = parseExprFromBuf(text, length, origin, basePath, symbols, settings, positions, rootFS, exprSymbols); + DocCommentMap tmpDocComments; // Only used when not origin is not a SourcePath + DocCommentMap *docComments = &tmpDocComments; + + if (auto sourcePath = std::get_if(&origin)) { + auto [it, _] = positionToDocComment.try_emplace(*sourcePath); + docComments = &it->second; + } + + auto result = parseExprFromBuf(text, length, origin, basePath, symbols, settings, positions, *docComments, rootFS, exprSymbols); result->bindVars(*this, staticEnv); return result; } +DocComment EvalState::getDocCommentForPos(PosIdx pos) +{ + auto pos2 = positions[pos]; + auto path = pos2.getSourcePath(); + if (!path) + return {}; + + auto table = positionToDocComment.find(*path); + if (table == positionToDocComment.end()) + return {}; + + auto it = table->second.find(pos); + if (it == table->second.end()) + return {}; + return it->second; +} std::string ExternalValueBase::coerceToString(EvalState & state, const PosIdx & pos, NixStringContext & context, bool copyMore, bool copyToStore) const { diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 7dbf61c5d..3918fb092 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -130,6 +130,8 @@ struct Constant typedef std::map ValMap; #endif +typedef std::map DocCommentMap; + struct Env { Env * up; @@ -329,6 +331,12 @@ private: #endif FileEvalCache fileEvalCache; + /** + * Associate source positions of certain AST nodes with their preceding doc comment, if they have one. + * Grouped by file. + */ + std::map positionToDocComment; + LookupPath lookupPath; std::map> lookupPathResolved; @@ -771,6 +779,8 @@ public: std::string_view pathArg, PosIdx pos); + DocComment getDocCommentForPos(PosIdx pos); + private: /** diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index 803b5ed8f..4c4c8af19 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -202,6 +202,17 @@ struct ExprSelect : Expr ExprSelect(const PosIdx & pos, Expr * e, AttrPath attrPath, Expr * def) : pos(pos), e(e), def(def), attrPath(std::move(attrPath)) { }; ExprSelect(const PosIdx & pos, Expr * e, Symbol name) : pos(pos), e(e), def(0) { attrPath.push_back(AttrName(name)); }; PosIdx getPos() const override { return pos; } + + /** + * Evaluate the `a.b.c` part of `a.b.c.d`. This exists mostly for the purpose of :doc in the repl. + * + * @param[out] v The attribute set that should contain the last attribute name (if it exists). + * @return The last attribute name in `attrPath` + * + * @note This does *not* evaluate the final attribute, and does not fail if that's the only attribute that does not exist. + */ + Symbol evalExceptFinalSelect(EvalState & state, Env & env, Value & attrs); + COMMON_METHODS }; diff --git a/src/libexpr/parser-state.hh b/src/libexpr/parser-state.hh index 8dc910468..1df64e73d 100644 --- a/src/libexpr/parser-state.hh +++ b/src/libexpr/parser-state.hh @@ -64,7 +64,7 @@ struct LexerState /** * @brief Maps some positions to a DocComment, where the comment is relevant to the location. */ - std::map positionToDocComment; + std::map & positionToDocComment; PosTable & positions; PosTable::Origin origin; diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 3c1bf95c8..a25c6dd87 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -33,6 +33,8 @@ namespace nix { +typedef std::map DocCommentMap; + Expr * parseExprFromBuf( char * text, size_t length, @@ -41,6 +43,7 @@ Expr * parseExprFromBuf( SymbolTable & symbols, const EvalSettings & settings, PosTable & positions, + DocCommentMap & docComments, const ref rootFS, const Expr::AstSymbols & astSymbols); @@ -335,10 +338,12 @@ binds $$ = $1; auto pos = state->at(@2); + auto exprPos = state->at(@4); { auto it = state->lexerState.positionToDocComment.find(pos); if (it != state->lexerState.positionToDocComment.end()) { $4->setDocComment(it->second); + state->lexerState.positionToDocComment.emplace(exprPos, it->second); } } @@ -463,11 +468,13 @@ Expr * parseExprFromBuf( SymbolTable & symbols, const EvalSettings & settings, PosTable & positions, + DocCommentMap & docComments, const ref rootFS, const Expr::AstSymbols & astSymbols) { yyscan_t scanner; LexerState lexerState { + .positionToDocComment = docComments, .positions = positions, .origin = positions.addOrigin(origin, length), }; diff --git a/src/libutil/position.hh b/src/libutil/position.hh index 729f2a523..aba263fdf 100644 --- a/src/libutil/position.hh +++ b/src/libutil/position.hh @@ -7,6 +7,7 @@ #include #include +#include #include "source-path.hh" @@ -65,6 +66,13 @@ struct Pos std::string getSnippetUpTo(const Pos & end) const; + /** + * Get the SourcePath, if the source was loaded from a file. + */ + std::optional getSourcePath() const { + return *std::get_if(&origin); + } + struct LinesIterator { using difference_type = size_t; using value_type = std::string_view; diff --git a/tests/functional/repl/doc-constant.expected b/tests/functional/repl/doc-constant.expected new file mode 100644 index 000000000..9aca06178 --- /dev/null +++ b/tests/functional/repl/doc-constant.expected @@ -0,0 +1,102 @@ +Nix +Type :? for help. +Added variables. + +error: value does not have documentation + +Attribute version + + … defined at + /path/to/tests/functional/repl/doc-comments.nix:29:3 + + Immovably fixed. + +Attribute empty + + … defined at + /path/to/tests/functional/repl/doc-comments.nix:32:3 + + Unchangeably constant. + +error: + … while evaluating the attribute 'attr.undocument' + at /path/to/tests/functional/repl/doc-comments.nix:32:3: + 31| /** Unchangeably constant. */ + 32| lib.attr.empty = { }; + | ^ + 33| + + error: attribute 'undocument' missing + at «string»:1:1: + 1| lib.attr.undocument + | ^ + Did you mean undocumented? + +Attribute constant + + … defined at + /path/to/tests/functional/repl/doc-comments.nix:26:3 + + Firmly rigid. + +Attribute version + + … defined at + /path/to/tests/functional/repl/doc-comments.nix:29:3 + + Immovably fixed. + +Attribute empty + + … defined at + /path/to/tests/functional/repl/doc-comments.nix:32:3 + + Unchangeably constant. + +Attribute undocumented + + … defined at + /path/to/tests/functional/repl/doc-comments.nix:34:3 + + No documentation found. + +error: undefined variable 'missing' + at «string»:1:1: + 1| missing + | ^ + +error: undefined variable 'constanz' + at «string»:1:1: + 1| constanz + | ^ + +error: undefined variable 'missing' + at «string»:1:1: + 1| missing.attr + | ^ + +error: attribute 'missing' missing + at «string»:1:1: + 1| lib.missing + | ^ + +error: attribute 'missing' missing + at «string»:1:1: + 1| lib.missing.attr + | ^ + +error: + … while evaluating the attribute 'attr.undocumental' + at /path/to/tests/functional/repl/doc-comments.nix:32:3: + 31| /** Unchangeably constant. */ + 32| lib.attr.empty = { }; + | ^ + 33| + + error: attribute 'undocumental' missing + at «string»:1:1: + 1| lib.attr.undocumental + | ^ + Did you mean undocumented? + + diff --git a/tests/functional/repl/doc-constant.in b/tests/functional/repl/doc-constant.in new file mode 100644 index 000000000..9c0dde5e1 --- /dev/null +++ b/tests/functional/repl/doc-constant.in @@ -0,0 +1,15 @@ +:l doc-comments.nix +:doc constant +:doc lib.version +:doc lib.attr.empty +:doc lib.attr.undocument +:doc (import ./doc-comments.nix).constant +:doc (import ./doc-comments.nix).lib.version +:doc (import ./doc-comments.nix).lib.attr.empty +:doc (import ./doc-comments.nix).lib.attr.undocumented +:doc missing +:doc constanz +:doc missing.attr +:doc lib.missing +:doc lib.missing.attr +:doc lib.attr.undocumental