Merge pull request #11072 from NixOS/doc-comments

Doc comments
This commit is contained in:
Robert Hensing 2024-07-15 21:00:42 +02:00 committed by GitHub
commit c6b5503190
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1045 additions and 44 deletions

View file

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

View file

@ -91,7 +91,7 @@ A environment variables that Google Test accepts are also worth knowing:
Putting the two together, one might run Putting the two together, one might run
```bash ```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. for short but comprensive output.

View file

@ -73,7 +73,7 @@ static int listPossibleCallback(char * s, char *** avp)
{ {
auto possible = curRepl->completePrefix(s); 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"); throw Error("too many completions");
int ac = 0; int ac = 0;

View file

@ -27,6 +27,7 @@
#include "local-fs-store.hh" #include "local-fs-store.hh"
#include "print.hh" #include "print.hh"
#include "ref.hh" #include "ref.hh"
#include "value.hh"
#if HAVE_BOEHMGC #if HAVE_BOEHMGC
#define GC_INCLUDE_NEW #define GC_INCLUDE_NEW
@ -616,6 +617,38 @@ ProcessLineResult NixRepl::processLine(std::string line)
else if (command == ":doc") { else if (command == ":doc") {
Value v; 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); evalString(arg, v);
if (auto doc = state->getDoc(v)) { if (auto doc = state->getDoc(v)) {
std::string markdown; std::string markdown;
@ -633,6 +666,19 @@ ProcessLineResult NixRepl::processLine(std::string line)
markdown += stripIndentation(doc->doc); markdown += stripIndentation(doc->doc);
logger->cout(trim(renderMarkdownToTerminal(markdown))); 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 } else
throw Error("value does not have documentation"); throw Error("value does not have documentation");
} }

View file

@ -559,6 +559,54 @@ std::optional<EvalState::Doc> EvalState::getDoc(Value & v)
.doc = doc, .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 {}; return {};
} }
@ -1367,6 +1415,22 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v)
v = *vAttrs; 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) void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v)
{ {
@ -2828,13 +2892,37 @@ Expr * EvalState::parse(
const SourcePath & basePath, const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv) 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); result->bindVars(*this, staticEnv);
return result; 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 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; typedef std::map<std::string, Value *> ValMap;
#endif #endif
typedef std::map<PosIdx, DocComment> DocCommentMap;
struct Env struct Env
{ {
Env * up; Env * up;
@ -329,6 +331,12 @@ private:
#endif #endif
FileEvalCache fileEvalCache; 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; LookupPath lookupPath;
std::map<std::string, std::optional<std::string>> lookupPathResolved; std::map<std::string, std::optional<std::string>> lookupPathResolved;
@ -771,6 +779,8 @@ public:
std::string_view pathArg, std::string_view pathArg,
PosIdx pos); PosIdx pos);
DocComment getDocCommentForPos(PosIdx pos);
private: private:
/** /**

View file

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

View file

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

View file

@ -5,7 +5,7 @@
%option stack %option stack
%option nodefault %option nodefault
%option nounput noyy_top_state %option nounput noyy_top_state
%option extra-type="::nix::LexerState *"
%s DEFAULT %s DEFAULT
%x STRING %x STRING
@ -14,6 +14,10 @@
%x INPATH_SLASH %x INPATH_SLASH
%x PATH_START %x PATH_START
%top {
#include "parser-tab.hh" // YYSTYPE
#include "parser-state.hh"
}
%{ %{
#ifdef __clang__ #ifdef __clang__
@ -22,28 +26,19 @@
#include "nixexpr.hh" #include "nixexpr.hh"
#include "parser-tab.hh" #include "parser-tab.hh"
#include "lexer-helpers.hh"
namespace nix {
struct LexerState;
}
using namespace nix; using namespace nix;
using namespace nix::lexer::internal;
namespace nix { namespace nix {
#define CUR_POS state->at(*yylloc) #define CUR_POS state->at(*yylloc)
static void initLoc(YYLTYPE * loc)
{
loc->first_line = loc->last_line = 0;
loc->first_column = loc->last_column = 0;
}
static void adjustLoc(YYLTYPE * loc, const char * s, size_t len)
{
loc->stash();
loc->first_column = loc->last_column;
loc->last_column += len;
}
// we make use of the fact that the parser receives a private copy of the input // we make use of the fact that the parser receives a private copy of the input
// string and can munge around in it. // string and can munge around in it.
static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length) static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length)
@ -79,7 +74,7 @@ static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length)
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" #pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
#define YY_USER_INIT initLoc(yylloc) #define YY_USER_INIT initLoc(yylloc)
#define YY_USER_ACTION adjustLoc(yylloc, yytext, yyleng); #define YY_USER_ACTION adjustLoc(yyscanner, yylloc, yytext, yyleng);
#define PUSH_STATE(state) yy_push_state(state, yyscanner) #define PUSH_STATE(state) yy_push_state(state, yyscanner)
#define POP_STATE() yy_pop_state(yyscanner) #define POP_STATE() yy_pop_state(yyscanner)
@ -279,9 +274,32 @@ or { return OR_KW; }
{SPATH} { yylval->path = {yytext, (size_t) yyleng}; return SPATH; } {SPATH} { yylval->path = {yytext, (size_t) yyleng}; return SPATH; }
{URI} { yylval->uri = {yytext, (size_t) yyleng}; return URI; } {URI} { yylval->uri = {yytext, (size_t) yyleng}; return URI; }
[ \t\r\n]+ /* eat up whitespace */ %{
\#[^\r\n]* /* single-line comments */ // Doc comment rule
\/\*([^*]|\*+[^*/])*\*+\/ /* long comments */ //
// \/\*\* /**
// [^/*] reject /**/ (empty comment) and /***
// ([^*]|\*+[^*/])*\*+\/ same as the long comment rule
// ( )* zero or more non-ending sequences
// \* end(1)
// \/ end(2)
%}
\/\*\*[^/*]([^*]|\*+[^*/])*\*+\/ /* doc comments */ {
LexerState & lexerState = *yyget_extra(yyscanner);
lexerState.docCommentDistance = 0;
lexerState.lastDocCommentLoc.beginOffset = yylloc->beginOffset;
lexerState.lastDocCommentLoc.endOffset = yylloc->endOffset;
}
%{
// The following rules have docCommentDistance--
// This compensates for the docCommentDistance++ which happens by default to
// make all the other rules invalidate the doc comment.
%}
[ \t\r\n]+ /* eat up whitespace */ { yyget_extra(yyscanner)->docCommentDistance--; }
\#[^\r\n]* /* single-line comments */ { yyget_extra(yyscanner)->docCommentDistance--; }
\/\*([^*]|\*+[^*/])*\*+\/ /* long comments */ { yyget_extra(yyscanner)->docCommentDistance--; }
{ANY} { {ANY} {
/* Don't return a negative number, as this will cause /* Don't return a negative number, as this will cause

View file

@ -139,6 +139,7 @@ sources = files(
'function-trace.cc', 'function-trace.cc',
'get-drvs.cc', 'get-drvs.cc',
'json-to-value.cc', 'json-to-value.cc',
'lexer-helpers.cc',
'nixexpr.cc', 'nixexpr.cc',
'paths.cc', 'paths.cc',
'primops.cc', 'primops.cc',
@ -165,6 +166,7 @@ headers = [config_h] + files(
'gc-small-vector.hh', 'gc-small-vector.hh',
'get-drvs.hh', 'get-drvs.hh',
'json-to-value.hh', 'json-to-value.hh',
# internal: 'lexer-helpers.hh',
'nixexpr.hh', 'nixexpr.hh',
'parser-state.hh', 'parser-state.hh',
'pos-idx.hh', 'pos-idx.hh',

View file

@ -583,6 +583,22 @@ std::string ExprLambda::showNamePos(const EvalState & state) const
return fmt("%1% at %2%", id, state.positions[pos]); return fmt("%1% at %2%", id, state.positions[pos]);
} }
void ExprLambda::setDocComment(DocComment docComment) {
// RFC 145 specifies that the innermost doc comment wins.
// See https://github.com/NixOS/rfcs/blob/master/rfcs/0145-doc-strings.md#ambiguous-placement
if (!this->docComment) {
this->docComment = docComment;
// Curried functions are defined by putting a function directly
// in the body of another function. To render docs for those, we
// need to propagate the doc comment to the innermost function.
//
// If we have our own comment, we've already propagated it, so this
// belongs in the same conditional.
body->setDocComment(docComment);
}
};
/* Position table. */ /* Position table. */
@ -627,4 +643,22 @@ size_t SymbolTable::totalSize() const
return n; return n;
} }
std::string DocComment::getInnerText(const PosTable & positions) const {
auto beginPos = positions[begin];
auto endPos = positions[end];
auto docCommentStr = beginPos.getSnippetUpTo(endPos).value_or("");
// Strip "/**" and "*/"
constexpr size_t prefixLen = 3;
constexpr size_t suffixLen = 2;
std::string docStr = docCommentStr.substr(prefixLen, docCommentStr.size() - prefixLen - suffixLen);
if (docStr.empty())
return {};
// Turn the now missing "/**" into indentation
docStr = " " + docStr;
// Strip indentation (for the whole, potentially multi-line string)
docStr = stripIndentation(docStr);
return docStr;
}
} }

View file

@ -11,13 +11,53 @@
namespace nix { namespace nix {
struct Env;
struct Value;
class EvalState; class EvalState;
class PosTable;
struct Env;
struct ExprWith; struct ExprWith;
struct StaticEnv; struct StaticEnv;
struct Value;
/**
* A documentation comment, in the sense of [RFC 145](https://github.com/NixOS/rfcs/blob/master/rfcs/0145-doc-strings.md)
*
* Note that this does not implement the following:
* - argument attribute names ("formals"): TBD
* - argument names: these are internal to the function and their names may not be optimal for documentation
* - function arity (degree of currying or number of ':'s):
* - Functions returning partially applied functions have a higher arity
* than can be determined locally and without evaluation.
* We do not want to present false data.
* - Some functions should be thought of as transformations of other
* functions. For instance `overlay -> overlay -> overlay` is the simplest
* way to understand `composeExtensions`, but its implementation looks like
* `f: g: final: prev: <...>`. The parameters `final` and `prev` are part
* of the overlay concept, while distracting from the function's purpose.
*/
struct DocComment {
/**
* Start of the comment, including the opening, ie `/` and `**`.
*/
PosIdx begin;
/**
* Position right after the final asterisk and `/` that terminate the comment.
*/
PosIdx end;
/**
* Whether the comment is set.
*
* A `DocComment` is small enough that it makes sense to pass by value, and
* therefore baking optionality into it is also useful, to avoiding the memory
* overhead of `std::optional`.
*/
operator bool() const { return static_cast<bool>(begin); }
std::string getInnerText(const PosTable & positions) const;
};
/** /**
* An attribute path is a sequence of attribute names. * An attribute path is a sequence of attribute names.
@ -54,6 +94,7 @@ struct Expr
virtual void eval(EvalState & state, Env & env, Value & v); virtual void eval(EvalState & state, Env & env, Value & v);
virtual Value * maybeThunk(EvalState & state, Env & env); virtual Value * maybeThunk(EvalState & state, Env & env);
virtual void setName(Symbol name); virtual void setName(Symbol name);
virtual void setDocComment(DocComment docComment) { };
virtual PosIdx getPos() const { return noPos; } virtual PosIdx getPos() const { return noPos; }
}; };
@ -156,6 +197,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, 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)); }; 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; } 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 COMMON_METHODS
}; };
@ -278,6 +330,8 @@ struct ExprLambda : Expr
Symbol arg; Symbol arg;
Formals * formals; Formals * formals;
Expr * body; Expr * body;
DocComment docComment;
ExprLambda(PosIdx pos, Symbol arg, Formals * formals, Expr * body) ExprLambda(PosIdx pos, Symbol arg, Formals * formals, Expr * body)
: pos(pos), arg(arg), formals(formals), body(body) : pos(pos), arg(arg), formals(formals), body(body)
{ {
@ -290,6 +344,7 @@ struct ExprLambda : Expr
std::string showNamePos(const EvalState & state) const; std::string showNamePos(const EvalState & state) const;
inline bool hasFormals() const { return formals != nullptr; } inline bool hasFormals() const { return formals != nullptr; }
PosIdx getPos() const override { return pos; } PosIdx getPos() const override { return pos; }
virtual void setDocComment(DocComment docComment) override;
COMMON_METHODS COMMON_METHODS
}; };

View file

@ -1,6 +1,8 @@
#pragma once #pragma once
///@file ///@file
#include <limits>
#include "eval.hh" #include "eval.hh"
namespace nix { namespace nix {
@ -20,25 +22,59 @@ struct StringToken
struct ParserLocation struct ParserLocation
{ {
int first_line, first_column; int beginOffset;
int last_line, last_column; int endOffset;
// backup to recover from yyless(0) // backup to recover from yyless(0)
int stashed_first_column, stashed_last_column; int stashedBeginOffset, stashedEndOffset;
void stash() { void stash() {
stashed_first_column = first_column; stashedBeginOffset = beginOffset;
stashed_last_column = last_column; stashedEndOffset = endOffset;
} }
void unstash() { void unstash() {
first_column = stashed_first_column; beginOffset = stashedBeginOffset;
last_column = stashed_last_column; endOffset = stashedEndOffset;
} }
/** Latest doc comment position, or 0. */
int doc_comment_first_column, doc_comment_last_column;
};
struct LexerState
{
/**
* Tracks the distance to the last doc comment, in terms of lexer tokens.
*
* The lexer sets this to 0 when reading a doc comment, and increments it
* for every matched rule; see `lexer-helpers.cc`.
* Whitespace and comment rules decrement the distance, so that they result
* in a net 0 change in distance.
*/
int docCommentDistance = std::numeric_limits<int>::max();
/**
* The location of the last doc comment.
*
* (stashing fields are not used)
*/
ParserLocation lastDocCommentLoc;
/**
* @brief Maps some positions to a DocComment, where the comment is relevant to the location.
*/
std::map<PosIdx, DocComment> & positionToDocComment;
PosTable & positions;
PosTable::Origin origin;
PosIdx at(const ParserLocation & loc);
}; };
struct ParserState struct ParserState
{ {
const LexerState & lexerState;
SymbolTable & symbols; SymbolTable & symbols;
PosTable & positions; PosTable & positions;
Expr * result; Expr * result;
@ -270,9 +306,14 @@ inline Expr * ParserState::stripIndentation(const PosIdx pos,
return new ExprConcatStrings(pos, true, es2); return new ExprConcatStrings(pos, true, es2);
} }
inline PosIdx LexerState::at(const ParserLocation & loc)
{
return positions.add(origin, loc.beginOffset);
}
inline PosIdx ParserState::at(const ParserLocation & loc) inline PosIdx ParserState::at(const ParserLocation & loc)
{ {
return positions.add(origin, loc.first_column); return positions.add(origin, loc.beginOffset);
} }
} }

View file

@ -31,8 +31,25 @@
#define YY_DECL int yylex \ #define YY_DECL int yylex \
(YYSTYPE * yylval_param, YYLTYPE * yylloc_param, yyscan_t yyscanner, nix::ParserState * state) (YYSTYPE * yylval_param, YYLTYPE * yylloc_param, yyscan_t yyscanner, nix::ParserState * state)
// For efficiency, we only track offsets; not line,column coordinates
# define YYLLOC_DEFAULT(Current, Rhs, N) \
do \
if (N) \
{ \
(Current).beginOffset = YYRHSLOC (Rhs, 1).beginOffset; \
(Current).endOffset = YYRHSLOC (Rhs, N).endOffset; \
} \
else \
{ \
(Current).beginOffset = (Current).endOffset = \
YYRHSLOC (Rhs, 0).endOffset; \
} \
while (0)
namespace nix { namespace nix {
typedef std::map<PosIdx, DocComment> DocCommentMap;
Expr * parseExprFromBuf( Expr * parseExprFromBuf(
char * text, char * text,
size_t length, size_t length,
@ -41,6 +58,7 @@ Expr * parseExprFromBuf(
SymbolTable & symbols, SymbolTable & symbols,
const EvalSettings & settings, const EvalSettings & settings,
PosTable & positions, PosTable & positions,
DocCommentMap & docComments,
const ref<SourceAccessor> rootFS, const ref<SourceAccessor> rootFS,
const Expr::AstSymbols & astSymbols); const Expr::AstSymbols & astSymbols);
@ -65,8 +83,7 @@ using namespace nix;
void yyerror(YYLTYPE * loc, yyscan_t scanner, ParserState * state, const char * error) void yyerror(YYLTYPE * loc, yyscan_t scanner, ParserState * state, const char * error)
{ {
if (std::string_view(error).starts_with("syntax error, unexpected end of file")) { if (std::string_view(error).starts_with("syntax error, unexpected end of file")) {
loc->first_column = loc->last_column; loc->beginOffset = loc->endOffset;
loc->first_line = loc->last_line;
} }
throw ParseError({ throw ParseError({
.msg = HintFmt(error), .msg = HintFmt(error),
@ -74,6 +91,14 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParserState * state, const char *
}); });
} }
#define SET_DOC_POS(lambda, pos) setDocPosition(state->lexerState, lambda, state->at(pos))
static void setDocPosition(const LexerState & lexerState, ExprLambda * lambda, PosIdx start) {
auto it = lexerState.positionToDocComment.find(start);
if (it != lexerState.positionToDocComment.end()) {
lambda->setDocComment(it->second);
}
}
%} %}
@ -119,6 +144,7 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParserState * state, const char *
%token IND_STRING_OPEN IND_STRING_CLOSE %token IND_STRING_OPEN IND_STRING_CLOSE
%token ELLIPSIS %token ELLIPSIS
%right IMPL %right IMPL
%left OR %left OR
%left AND %left AND
@ -140,18 +166,28 @@ expr: expr_function;
expr_function expr_function
: ID ':' expr_function : ID ':' expr_function
{ $$ = new ExprLambda(CUR_POS, state->symbols.create($1), 0, $3); } { auto me = new ExprLambda(CUR_POS, state->symbols.create($1), 0, $3);
$$ = me;
SET_DOC_POS(me, @1);
}
| '{' formals '}' ':' expr_function | '{' formals '}' ':' expr_function
{ $$ = new ExprLambda(CUR_POS, state->validateFormals($2), $5); } { auto me = new ExprLambda(CUR_POS, state->validateFormals($2), $5);
$$ = me;
SET_DOC_POS(me, @1);
}
| '{' formals '}' '@' ID ':' expr_function | '{' formals '}' '@' ID ':' expr_function
{ {
auto arg = state->symbols.create($5); auto arg = state->symbols.create($5);
$$ = new ExprLambda(CUR_POS, arg, state->validateFormals($2, CUR_POS, arg), $7); auto me = new ExprLambda(CUR_POS, arg, state->validateFormals($2, CUR_POS, arg), $7);
$$ = me;
SET_DOC_POS(me, @1);
} }
| ID '@' '{' formals '}' ':' expr_function | ID '@' '{' formals '}' ':' expr_function
{ {
auto arg = state->symbols.create($1); auto arg = state->symbols.create($1);
$$ = new ExprLambda(CUR_POS, arg, state->validateFormals($4, CUR_POS, arg), $7); auto me = new ExprLambda(CUR_POS, arg, state->validateFormals($4, CUR_POS, arg), $7);
$$ = me;
SET_DOC_POS(me, @1);
} }
| ASSERT expr ';' expr_function | ASSERT expr ';' expr_function
{ $$ = new ExprAssert(CUR_POS, $2, $4); } { $$ = new ExprAssert(CUR_POS, $2, $4); }
@ -312,7 +348,22 @@ ind_string_parts
; ;
binds binds
: binds attrpath '=' expr ';' { $$ = $1; state->addAttr($$, std::move(*$2), $4, state->at(@2)); delete $2; } : binds attrpath '=' expr ';' {
$$ = $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);
}
}
state->addAttr($$, std::move(*$2), $4, pos);
delete $2;
}
| binds INHERIT attrs ';' | binds INHERIT attrs ';'
{ $$ = $1; { $$ = $1;
for (auto & [i, iPos] : *$3) { for (auto & [i, iPos] : *$3) {
@ -431,21 +482,28 @@ Expr * parseExprFromBuf(
SymbolTable & symbols, SymbolTable & symbols,
const EvalSettings & settings, const EvalSettings & settings,
PosTable & positions, PosTable & positions,
DocCommentMap & docComments,
const ref<SourceAccessor> rootFS, const ref<SourceAccessor> rootFS,
const Expr::AstSymbols & astSymbols) const Expr::AstSymbols & astSymbols)
{ {
yyscan_t scanner; yyscan_t scanner;
LexerState lexerState {
.positionToDocComment = docComments,
.positions = positions,
.origin = positions.addOrigin(origin, length),
};
ParserState state { ParserState state {
.lexerState = lexerState,
.symbols = symbols, .symbols = symbols,
.positions = positions, .positions = positions,
.basePath = basePath, .basePath = basePath,
.origin = positions.addOrigin(origin, length), .origin = lexerState.origin,
.rootFS = rootFS, .rootFS = rootFS,
.s = astSymbols, .s = astSymbols,
.settings = settings, .settings = settings,
}; };
yylex_init(&scanner); yylex_init_extra(&lexerState, &scanner);
Finally _destroy([&] { yylex_destroy(scanner); }); Finally _destroy([&] { yylex_destroy(scanner); });
yy_scan_buffer(text, length, scanner); yy_scan_buffer(text, length, scanner);

View file

@ -110,4 +110,50 @@ void Pos::LinesIterator::bump(bool atFirst)
input.remove_prefix(eol); input.remove_prefix(eol);
} }
std::optional<std::string> Pos::getSnippetUpTo(const Pos & end) const {
assert(this->origin == end.origin);
if (end.line < this->line)
return std::nullopt;
if (auto source = getSource()) {
auto firstLine = LinesIterator(*source);
for (auto i = 1; i < this->line; ++i) {
++firstLine;
}
auto lastLine = LinesIterator(*source);
for (auto i = 1; i < end.line; ++i) {
++lastLine;
}
LinesIterator linesEnd;
std::string result;
for (auto i = firstLine; i != linesEnd; ++i) {
auto firstColumn = i == firstLine ? (this->column ? this->column - 1 : 0) : 0;
if (firstColumn > i->size())
firstColumn = i->size();
auto lastColumn = i == lastLine ? (end.column ? end.column - 1 : 0) : std::numeric_limits<int>::max();
if (lastColumn < firstColumn)
lastColumn = firstColumn;
if (lastColumn > i->size())
lastColumn = i->size();
result += i->substr(firstColumn, lastColumn - firstColumn);
if (i == lastLine) {
break;
} else {
result += '\n';
}
}
return result;
}
return std::nullopt;
}
} }

View file

@ -7,6 +7,7 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <variant>
#include "source-path.hh" #include "source-path.hh"
@ -63,6 +64,15 @@ struct Pos
bool operator==(const Pos & rhs) const = default; bool operator==(const Pos & rhs) const = default;
auto operator<=>(const Pos & rhs) const = default; auto operator<=>(const Pos & rhs) const = default;
std::optional<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 { struct LinesIterator {
using difference_type = size_t; using difference_type = size_t;
using value_type = std::string_view; using value_type = std::string_view;

View file

@ -63,7 +63,7 @@ function characterisationTestExit() {
echo '' echo ''
echo 'You can rerun this test with:' echo 'You can rerun this test with:'
echo '' echo ''
echo " _NIX_TEST_ACCEPT=1 make tests/functional/${TEST_NAME}.test" echo " _NIX_TEST_ACCEPT=1 make tests/functional/${TEST_NAME}.sh.test"
echo '' echo ''
echo 'to regenerate the files containing the expected output,' echo 'to regenerate the files containing the expected output,'
echo 'and then view the git diff to decide whether a change is' echo 'and then view the git diff to decide whether a change is'

View file

@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
source common.sh source common.sh
source characterisation/framework.sh
testDir="$PWD" testDir="$PWD"
cd "$TEST_ROOT" cd "$TEST_ROOT"
@ -244,3 +245,42 @@ testReplResponseNoRegex '
y = { a = 1 }; y = { a = 1 };
} }
' '
# TODO: move init to characterisation/framework.sh
badDiff=0
badExitCode=0
nixVersion="$(nix eval --impure --raw --expr 'builtins.nixVersion' --extra-experimental-features nix-command)"
runRepl () {
# That is right, we are also filtering out the testdir _without underscores_.
# This is crazy, but without it, GHA will fail to run the tests, showing paths
# _with_ underscores in the set -x log, but _without_ underscores in the
# supposed nix repl output. I have looked in a number of places, but I cannot
# find a mechanism that could cause this to happen.
local testDirNoUnderscores
testDirNoUnderscores="${testDir//_/}"
# TODO: pass arguments to nix repl; see lang.sh
nix repl 2>&1 \
| stripColors \
| sed \
-e "s@$testDir@/path/to/tests/functional@g" \
-e "s@$testDirNoUnderscores@/path/to/tests/functional@g" \
-e "s@$nixVersion@<nix version>@g" \
-e "s@Added [0-9]* variables@Added <number omitted> variables@g" \
| grep -vF $'warning: you don\'t have Internet access; disabling some network-dependent features' \
;
}
for test in $(cd "$testDir/repl"; echo *.in); do
test="$(basename "$test" .in)"
in="$testDir/repl/$test.in"
actual="$testDir/repl/$test.actual"
expected="$testDir/repl/$test.expected"
(cd "$testDir/repl"; set +x; runRepl 2>&1) < "$in" > "$actual"
diffAndAcceptInner "$test" "$actual" "$expected"
done
characterisationTestExit

View file

@ -0,0 +1,24 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function curriedArgs
… defined at
/path/to/tests/functional/repl/doc-comments.nix:48:5
A documented function.
"Note that users may not expect this to behave as it currently does"
Function curriedArgs
… defined at
/path/to/tests/functional/repl/doc-comments.nix:50:5
The function returned by applying once
"This won't produce documentation, because we can't actually document arbitrary values"
error: value does not have documentation

View file

@ -0,0 +1,7 @@
:l doc-comments.nix
:doc curriedArgs
x = curriedArgs 1
"Note that users may not expect this to behave as it currently does"
:doc x
"This won't produce documentation, because we can't actually document arbitrary values"
:doc x 2

View file

@ -0,0 +1,13 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
"Note that this is not yet complete"
Function documentedFormals
… defined at
/path/to/tests/functional/repl/doc-comments.nix:57:5
Finds x

View file

@ -0,0 +1,3 @@
:l doc-comments.nix
"Note that this is not yet complete"
:doc documentedFormals

View file

@ -0,0 +1,8 @@
Nix <nix version>
Type :? for help.
Function defined at
/path/to/tests/functional/repl/doc-comment-function.nix:2:1
A doc comment for a file that only contains a function

View file

@ -0,0 +1 @@
:doc import ./doc-comment-function.nix

View file

@ -0,0 +1,3 @@
/** A doc comment for a file that only contains a function */
{ ... }:
{ }

View file

@ -0,0 +1,60 @@
{
/**
Perform *arithmetic* multiplication. It's kind of like repeated **addition**, very neat.
```nix
multiply 2 3
=> 6
```
*/
multiply = x: y: x * y;
/**👈 precisely this wide 👉*/
measurement = x: x;
floatedIn = /** This also works. */
x: y: x;
compact=/**boom*/x: x;
# https://github.com/NixOS/rfcs/blob/master/rfcs/0145-doc-strings.md#ambiguous-placement
/** Ignore!!! */
unambiguous =
/** Very close */
x: x;
/** Firmly rigid. */
constant = true;
/** Immovably fixed. */
lib.version = "9000";
/** Unchangeably constant. */
lib.attr.empty = { };
lib.attr.undocumented = { };
nonStrict = /** My syntax is not strict, but I'm strict anyway. */ x: x;
strict = /** I don't have to be strict, but I am anyway. */ { ... }: null;
# Note that pre and post are the same here. I just had to name them somehow.
strictPre = /** Here's one way to do this */ a@{ ... }: a;
strictPost = /** Here's another way to do this */ { ... }@a: a;
# TODO
/** You won't see this. */
curriedArgs =
/** A documented function. */
x:
/** The function returned by applying once */
y:
/** A function body performing summation of two items */
x + y;
/** Documented formals (but you won't see this comment) */
documentedFormals =
/** Finds x */
{ /** The x attribute */
x
}: x;
}

View file

@ -0,0 +1,11 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function compact
… defined at
/path/to/tests/functional/repl/doc-comments.nix:18:20
boom

View file

@ -0,0 +1,2 @@
:l doc-comments.nix
:doc compact

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:30:3
Immovably fixed.
Attribute empty
… defined at
/path/to/tests/functional/repl/doc-comments.nix:33:3
Unchangeably constant.
error:
… while evaluating the attribute 'attr.undocument'
at /path/to/tests/functional/repl/doc-comments.nix:33:3:
32| /** Unchangeably constant. */
33| lib.attr.empty = { };
| ^
34|
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:27:3
Firmly rigid.
Attribute version
… defined at
/path/to/tests/functional/repl/doc-comments.nix:30:3
Immovably fixed.
Attribute empty
… defined at
/path/to/tests/functional/repl/doc-comments.nix:33:3
Unchangeably constant.
Attribute undocumented
… defined at
/path/to/tests/functional/repl/doc-comments.nix:35: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:33:3:
32| /** Unchangeably constant. */
33| lib.attr.empty = { };
| ^
34|
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

View file

@ -0,0 +1,11 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function floatedIn
… defined at
/path/to/tests/functional/repl/doc-comments.nix:16:5
This also works.

View file

@ -0,0 +1,2 @@
:l doc-comments.nix
:doc floatedIn

View file

@ -0,0 +1,29 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function nonStrict
… defined at
/path/to/tests/functional/repl/doc-comments.nix:37:70
My syntax is not strict, but I'm strict anyway.
Function strict
… defined at
/path/to/tests/functional/repl/doc-comments.nix:38:63
I don't have to be strict, but I am anyway.
Function strictPre
… defined at
/path/to/tests/functional/repl/doc-comments.nix:40:48
Here's one way to do this
Function strictPost
… defined at
/path/to/tests/functional/repl/doc-comments.nix:41:53
Here's another way to do this

View file

@ -0,0 +1,5 @@
:l doc-comments.nix
:doc nonStrict
:doc strict
:doc strictPre
:doc strictPost

View file

@ -0,0 +1,11 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function measurement
… defined at
/path/to/tests/functional/repl/doc-comments.nix:13:17
👈 precisely this wide 👉

View file

@ -0,0 +1,2 @@
:l doc-comments.nix
:doc measurement

View file

@ -0,0 +1,15 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function multiply
… defined at
/path/to/tests/functional/repl/doc-comments.nix:10:14
Perform arithmetic multiplication. It's kind of like
repeated addition, very neat.
| multiply 2 3
| => 6

View file

@ -0,0 +1,2 @@
:l doc-comments.nix
:doc multiply

View file

@ -0,0 +1,11 @@
Nix <nix version>
Type :? for help.
Added <number omitted> variables.
Function unambiguous
… defined at
/path/to/tests/functional/repl/doc-comments.nix:24:5
Very close

View file

@ -0,0 +1,2 @@
:l doc-comments.nix
:doc unambiguous

View file

@ -0,0 +1,122 @@
#include <gtest/gtest.h>
#include "position.hh"
namespace nix {
inline Pos::Origin makeStdin(std::string s)
{
return Pos::Stdin{make_ref<std::string>(s)};
}
TEST(Position, getSnippetUpTo_0)
{
Pos::Origin o = makeStdin("");
Pos p(1, 1, o);
ASSERT_EQ(p.getSnippetUpTo(p), "");
}
TEST(Position, getSnippetUpTo_1)
{
Pos::Origin o = makeStdin("x");
{
// NOTE: line and column are actually 1-based indexes
Pos start(0, 0, o);
Pos end(99, 99, o);
ASSERT_EQ(start.getSnippetUpTo(start), "");
ASSERT_EQ(start.getSnippetUpTo(end), "x");
ASSERT_EQ(end.getSnippetUpTo(end), "");
ASSERT_EQ(end.getSnippetUpTo(start), std::nullopt);
}
{
// NOTE: line and column are actually 1-based indexes
Pos start(0, 99, o);
Pos end(99, 0, o);
ASSERT_EQ(start.getSnippetUpTo(start), "");
// "x" might be preferable, but we only care about not crashing for invalid inputs
ASSERT_EQ(start.getSnippetUpTo(end), "");
ASSERT_EQ(end.getSnippetUpTo(end), "");
ASSERT_EQ(end.getSnippetUpTo(start), std::nullopt);
}
{
Pos start(1, 1, o);
Pos end(1, 99, o);
ASSERT_EQ(start.getSnippetUpTo(start), "");
ASSERT_EQ(start.getSnippetUpTo(end), "x");
ASSERT_EQ(end.getSnippetUpTo(end), "");
ASSERT_EQ(end.getSnippetUpTo(start), "");
}
{
Pos start(1, 1, o);
Pos end(99, 99, o);
ASSERT_EQ(start.getSnippetUpTo(start), "");
ASSERT_EQ(start.getSnippetUpTo(end), "x");
ASSERT_EQ(end.getSnippetUpTo(end), "");
ASSERT_EQ(end.getSnippetUpTo(start), std::nullopt);
}
}
TEST(Position, getSnippetUpTo_2)
{
Pos::Origin o = makeStdin("asdf\njkl\nqwer");
{
Pos start(1, 1, o);
Pos end(1, 2, o);
ASSERT_EQ(start.getSnippetUpTo(start), "");
ASSERT_EQ(start.getSnippetUpTo(end), "a");
ASSERT_EQ(end.getSnippetUpTo(end), "");
// nullopt? I feel like changing the column handling would just make it more fragile
ASSERT_EQ(end.getSnippetUpTo(start), "");
}
{
Pos start(1, 2, o);
Pos end(1, 3, o);
ASSERT_EQ(start.getSnippetUpTo(end), "s");
}
{
Pos start(1, 2, o);
Pos end(2, 2, o);
ASSERT_EQ(start.getSnippetUpTo(end), "sdf\nj");
}
{
Pos start(1, 2, o);
Pos end(3, 2, o);
ASSERT_EQ(start.getSnippetUpTo(end), "sdf\njkl\nq");
}
{
Pos start(1, 2, o);
Pos end(2, 99, o);
ASSERT_EQ(start.getSnippetUpTo(end), "sdf\njkl");
}
{
Pos start(1, 4, o);
Pos end(2, 99, o);
ASSERT_EQ(start.getSnippetUpTo(end), "f\njkl");
}
{
Pos start(1, 5, o);
Pos end(2, 99, o);
ASSERT_EQ(start.getSnippetUpTo(end), "\njkl");
}
{
Pos start(1, 6, o); // invalid: starting column past last "line character", ie at the newline
Pos end(2, 99, o);
ASSERT_EQ(start.getSnippetUpTo(end), "\njkl"); // jkl might be acceptable for this invalid start position
}
{
Pos start(1, 1, o);
Pos end(2, 0, o); // invalid
ASSERT_EQ(start.getSnippetUpTo(end), "asdf\n");
}
}
TEST(Position, example_1)
{
Pos::Origin o = makeStdin(" unambiguous = \n /** Very close */\n x: x;\n# ok\n");
Pos start(2, 5, o);
Pos end(2, 22, o);
ASSERT_EQ(start.getSnippetUpTo(end), "/** Very close */");
}
} // namespace nix