nix repl: Render docs for attributes

This commit is contained in:
Robert Hensing 2024-07-09 19:25:45 +02:00
parent 491b9cf415
commit d4f576b0b2
10 changed files with 237 additions and 3 deletions

View file

@ -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].

View file

@ -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<ExprSelect *>(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");
}

View file

@ -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> & 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<SourcePath>(&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
{

View file

@ -130,6 +130,8 @@ struct Constant
typedef std::map<std::string, Value *> ValMap;
#endif
typedef std::map<PosIdx, DocComment> 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<SourcePath, DocCommentMap> positionToDocComment;
LookupPath lookupPath;
std::map<std::string, std::optional<std::string>> lookupPathResolved;
@ -771,6 +779,8 @@ public:
std::string_view pathArg,
PosIdx pos);
DocComment getDocCommentForPos(PosIdx pos);
private:
/**

View file

@ -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
};

View file

@ -64,7 +64,7 @@ struct LexerState
/**
* @brief Maps some positions to a DocComment, where the comment is relevant to the location.
*/
std::map<PosIdx, DocComment> positionToDocComment;
std::map<PosIdx, DocComment> & positionToDocComment;
PosTable & positions;
PosTable::Origin origin;

View file

@ -33,6 +33,8 @@
namespace nix {
typedef std::map<PosIdx, DocComment> 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<SourceAccessor> 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<SourceAccessor> rootFS,
const Expr::AstSymbols & astSymbols)
{
yyscan_t scanner;
LexerState lexerState {
.positionToDocComment = docComments,
.positions = positions,
.origin = positions.addOrigin(origin, length),
};

View file

@ -7,6 +7,7 @@
#include <cstdint>
#include <string>
#include <variant>
#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<SourcePath> getSourcePath() const {
return *std::get_if<SourcePath>(&origin);
}
struct LinesIterator {
using difference_type = size_t;
using value_type = std::string_view;

View file

@ -0,0 +1,102 @@
Nix <nix version>
Type :? for help.
Added <number omitted> 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?

View file

@ -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