Skip to content

Commit

Permalink
Merge pull request #11072 from NixOS/doc-comments
Browse files Browse the repository at this point in the history
Doc comments
  • Loading branch information
roberth authored Jul 15, 2024
2 parents 9d7397c + 61a4d3d commit c6b5503
Show file tree
Hide file tree
Showing 42 changed files with 1,045 additions and 44 deletions.
53 changes: 53 additions & 0 deletions doc/manual/rl-next/repl-doc-renders-doc-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
synopsis: "`nix-repl`'s `:doc` shows documentation comments"
significance: significant
issues:
- 3904
- 10771
prs:
- 1652
- 9054
- 11072
---

`nix repl` has a `:doc` command that previously only rendered documentation for internally defined functions.
This feature has been extended to also render function documentation comments, in accordance with [RFC 145].

Example:

```
nix-repl> :doc lib.toFunction
Function toFunction
… defined at /home/user/h/nixpkgs/lib/trivial.nix:1072:5
Turns any non-callable values into constant functions. Returns
callable values as is.
Inputs
v
: Any value
Examples
:::{.example}
## lib.trivial.toFunction usage example
| nix-repl> lib.toFunction 1 2
| 1
|
| nix-repl> lib.toFunction (x: x + 1) 2
| 3
:::
```

Known limitations:
- 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].

[RFC 145]: https://github.com/NixOS/rfcs/pull/145
2 changes: 1 addition & 1 deletion doc/manual/src/contributing/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ A environment variables that Google Test accepts are also worth knowing:
Putting the two together, one might run
```bash
GTEST_BREIF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v
GTEST_BRIEF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v
```
for short but comprensive output.
Expand Down
2 changes: 1 addition & 1 deletion src/libcmd/repl-interacter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ static int listPossibleCallback(char * s, char *** avp)
{
auto possible = curRepl->completePrefix(s);

if (possible.size() > (INT_MAX / sizeof(char *)))
if (possible.size() > (std::numeric_limits<int>::max() / sizeof(char *)))
throw Error("too many completions");

int ac = 0;
Expand Down
46 changes: 46 additions & 0 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -616,6 +617,38 @@ 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) {
// When missing, trigger the normal exception
// e.g. :doc builtins.foo
// behaves like
// nix-repl> builtins.foo
// error: attribute 'foo' missing
evalString(arg, v);
assert(false);
}
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;
Expand All @@ -633,6 +666,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");
}
Expand Down
90 changes: 89 additions & 1 deletion src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,54 @@ std::optional<EvalState::Doc> EvalState::getDoc(Value & v)
.doc = doc,
};
}
if (v.isLambda()) {
auto exprLambda = v.payload.lambda.fun;

std::stringstream s(std::ios_base::out);
std::string name;
auto pos = positions[exprLambda->getPos()];
std::string docStr;

if (exprLambda->name) {
name = symbols[exprLambda->name];
}

if (exprLambda->docComment) {
docStr = exprLambda->docComment.getInnerText(positions);
}

if (name.empty()) {
s << "Function ";
}
else {
s << "Function `" << name << "`";
if (pos)
s << "\\\n" ;
else
s << "\\\n";
}
if (pos) {
s << "defined at " << pos;
}
if (!docStr.empty()) {
s << "\n\n";
}

s << docStr;

s << '\0'; // for making a c string below
std::string ss = s.str();

return Doc {
.pos = pos,
.name = name,
.arity = 0, // FIXME: figure out how deep by syntax only? It's not semantically useful though...
.args = {},
.doc =
// FIXME: this leaks; make the field std::string?
strdup(ss.data()),
};
}
return {};
}

Expand Down Expand Up @@ -1367,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)
{
Expand Down Expand Up @@ -2828,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
{
Expand Down
10 changes: 10 additions & 0 deletions src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ struct Constant
typedef std::map<std::string, Value *> ValMap;
#endif

typedef std::map<PosIdx, DocComment> DocCommentMap;

struct Env
{
Env * up;
Expand Down Expand 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;
Expand Down Expand Up @@ -771,6 +779,8 @@ public:
std::string_view pathArg,
PosIdx pos);

DocComment getDocCommentForPos(PosIdx pos);

private:

/**
Expand Down
30 changes: 30 additions & 0 deletions src/libexpr/lexer-helpers.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include "lexer-tab.hh"
#include "lexer-helpers.hh"
#include "parser-tab.hh"

void nix::lexer::internal::initLoc(YYLTYPE * loc)
{
loc->beginOffset = loc->endOffset = 0;
}

void nix::lexer::internal::adjustLoc(yyscan_t yyscanner, YYLTYPE * loc, const char * s, size_t len)
{
loc->stash();

LexerState & lexerState = *yyget_extra(yyscanner);

if (lexerState.docCommentDistance == 1) {
// Preceding token was a doc comment.
ParserLocation doc;
doc.beginOffset = lexerState.lastDocCommentLoc.beginOffset;
ParserLocation docEnd;
docEnd.beginOffset = lexerState.lastDocCommentLoc.endOffset;
DocComment docComment{lexerState.at(doc), lexerState.at(docEnd)};
PosIdx locPos = lexerState.at(*loc);
lexerState.positionToDocComment.emplace(locPos, docComment);
}
lexerState.docCommentDistance++;

loc->beginOffset = loc->endOffset;
loc->endOffset += len;
}
9 changes: 9 additions & 0 deletions src/libexpr/lexer-helpers.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

namespace nix::lexer::internal {

void initLoc(YYLTYPE * loc);

void adjustLoc(yyscan_t yyscanner, YYLTYPE * loc, const char * s, size_t len);

} // namespace nix::lexer
Loading

0 comments on commit c6b5503

Please sign in to comment.