diff --git a/dsymbol/src/dsymbol/conversion/first.d b/dsymbol/src/dsymbol/conversion/first.d index c9ac1b6..500b639 100644 --- a/dsymbol/src/dsymbol/conversion/first.d +++ b/dsymbol/src/dsymbol/conversion/first.d @@ -85,6 +85,11 @@ final class FirstPass : ASTVisitor override void visit(const Unittest u) { + if (previousSymbol && previousSymbol.acSymbol) + makeExampleDocumentation(u, previousSymbol.acSymbol.doc); + + auto associated = previousSymbol; + scope(exit) previousSymbol = associated; // Create a dummy symbol because we don't want unit test symbols leaking // into the symbol they're declared in. pushSymbol(UNITTEST_SYMBOL_NAME, @@ -130,7 +135,7 @@ final class FirstPass : ASTVisitor dec.name.index, dec.returnType); scope (exit) popSymbol(); currentSymbol.acSymbol.protection = protection.current; - currentSymbol.acSymbol.doc = makeDocumentation(dec.comment); + makeDocumentation(currentSymbol.acSymbol.doc, dec.comment); istring lastComment = this.lastComment; this.lastComment = istring.init; @@ -250,7 +255,7 @@ final class FirstPass : ASTVisitor addTypeToLookups(symbol.typeLookups, dec.type); symbol.parent = currentSymbol; symbol.acSymbol.protection = protection.current; - symbol.acSymbol.doc = makeDocumentation(declarator.comment); + makeDocumentation(symbol.acSymbol.doc, declarator.comment); currentSymbol.addChild(symbol, true); currentScope.addSymbol(symbol.acSymbol, false); @@ -272,7 +277,7 @@ final class FirstPass : ASTVisitor symbol.parent = currentSymbol; populateInitializer(symbol, part.initializer); symbol.acSymbol.protection = protection.current; - symbol.acSymbol.doc = makeDocumentation(dec.comment); + makeDocumentation(symbol.acSymbol.doc, dec.comment); currentSymbol.addChild(symbol, true); currentScope.addSymbol(symbol.acSymbol, false); @@ -301,7 +306,7 @@ final class FirstPass : ASTVisitor currentSymbol.addChild(symbol, true); currentScope.addSymbol(symbol.acSymbol, false); symbol.acSymbol.protection = protection.current; - symbol.acSymbol.doc = makeDocumentation(aliasDeclaration.comment); + makeDocumentation(symbol.acSymbol.doc, aliasDeclaration.comment); } } else @@ -317,7 +322,7 @@ final class FirstPass : ASTVisitor currentSymbol.addChild(symbol, true); currentScope.addSymbol(symbol.acSymbol, false); symbol.acSymbol.protection = protection.current; - symbol.acSymbol.doc = makeDocumentation(aliasDeclaration.comment); + makeDocumentation(symbol.acSymbol.doc, aliasDeclaration.comment); } } } @@ -399,7 +404,7 @@ final class FirstPass : ASTVisitor symbol.parent = currentSymbol; currentSymbol.addChild(symbol, true); currentScope.addSymbol(symbol.acSymbol, false); - symbol.acSymbol.doc = makeDocumentation(dec.comment); + makeDocumentation(symbol.acSymbol.doc, dec.comment); istring lastComment = this.lastComment; this.lastComment = istring.init; @@ -415,6 +420,7 @@ final class FirstPass : ASTVisitor } currentSymbol = currentSymbol.parent; + previousSymbol = symbol; } mixin visitEnumMember!EnumMember; @@ -847,11 +853,15 @@ private: currentSymbol.addChild(symbol, true); currentScope.addSymbol(symbol.acSymbol, false); currentSymbol = symbol; + pushedSymbolsStack.assumeSafeAppend ~= symbol; } void popSymbol() { + assert(pushedSymbolsStack.length, "called popSymbol without pushSymbol"); currentSymbol = currentSymbol.parent; + previousSymbol = pushedSymbolsStack[$ - 1]; + pushedSymbolsStack.length--; } template visitEnumMember(T) @@ -861,7 +871,7 @@ private: pushSymbol(member.name.text, CompletionKind.enumMember, symbolFile, member.name.index, member.type); scope(exit) popSymbol(); - currentSymbol.acSymbol.doc = makeDocumentation(member.comment); + makeDocumentation(currentSymbol.acSymbol.doc, member.comment); } } @@ -881,7 +891,7 @@ private: else currentSymbol.acSymbol.addChildren(aggregateSymbols[], false); currentSymbol.acSymbol.protection = protection.current; - currentSymbol.acSymbol.doc = makeDocumentation(dec.comment); + makeDocumentation(currentSymbol.acSymbol.doc, dec.comment); istring lastComment = this.lastComment; this.lastComment = istring.init; @@ -910,7 +920,7 @@ private: currentSymbol.addChild(symbol, true); processParameters(symbol, null, THIS_SYMBOL_NAME, parameters, templateParameters); symbol.acSymbol.protection = protection.current; - symbol.acSymbol.doc = makeDocumentation(doc); + makeDocumentation(symbol.acSymbol.doc, doc); istring lastComment = this.lastComment; this.lastComment = istring.init; @@ -923,6 +933,7 @@ private: currentSymbol = symbol; functionBody.accept(this); currentSymbol = currentSymbol.parent; + previousSymbol = symbol; } } @@ -934,7 +945,7 @@ private: currentSymbol.addChild(symbol, true); symbol.acSymbol.callTip = internString("~this()"); symbol.acSymbol.protection = protection.current; - symbol.acSymbol.doc = makeDocumentation(doc); + makeDocumentation(symbol.acSymbol.doc, doc); istring lastComment = this.lastComment; this.lastComment = istring.init; @@ -947,6 +958,7 @@ private: currentSymbol = symbol; functionBody.accept(this); currentSymbol = currentSymbol.parent; + previousSymbol = symbol; } } @@ -1189,14 +1201,85 @@ private: lookups.insert(lookup); } - DocString makeDocumentation(string documentation) + void makeDocumentation(ref DocString into, string documentation) { if (documentation.isDitto) - return DocString(lastComment, true); + { + into = DocString(lastComment, true); + into.dittoOf = lastDocStringInstance; + lastDocStringInstance = &into; + } else { lastComment = internString(documentation); - return DocString(lastComment, false); + into = DocString(lastComment, false); + lastDocStringInstance = &into; + } + } + + static makeExampleDocumentation(const Unittest block, ref DocString doc) + { + import dparse.trivia; + import std.algorithm; + import std.array; + import std.string; + + auto tokens = block.tokens; + if (tokens.length) + { + if (block.comment !is null) + { + auto data = appender!string; + data ~= "Examples:\n\n"; + if (block.comment.length) + { + data ~= block.comment; + data ~= "\n\n"; + } + data ~= "---\n"; + assert(tokens.length >= 3); + auto unittestTok = tokens.countUntil!(t => t.type == tok!"unittest"); + assert(unittestTok != -1); + auto openingTok = tokens[unittestTok .. $].countUntil!(t => t.type == tok!"{"); + assert(openingTok != -1); + openingTok += unittestTok; + assert(tokens[$ - 1].type == tok!"}"); + + auto codeData = appender!string; + foreach (trailingStart; tokens[openingTok].trailingTrivia) + codeData ~= trailingStart.text; + + size_t currentLine = size_t.max; + foreach (token; tokens[openingTok + 1 .. $ - 1]) + { + currentLine = token.line; + + foreach (leading; token.leadingTrivia) + codeData ~= leading.text; + + if (token.text.length) + codeData ~= token.text; + else + codeData ~= str(token.type); + + foreach (trailing; token.trailingTrivia) + codeData ~= trailing.text; + } + + foreach (leadingEnd; tokens[$ - 1].leadingTrivia) + codeData ~= leadingEnd.text; + + data ~= codeData.data.outdent.chompPrefix("\n").chomp("\n"); + + data ~= "\n---\n"; + + DocString* s = &doc; + while (s) + { + s.examples ~= istring(data.data); + s = s.dittoOf; + } + } } } @@ -1207,7 +1290,10 @@ private: Scope* currentScope; /// Current symbol - SemanticSymbol* currentSymbol; + SemanticSymbol* currentSymbol, previousSymbol; + + /// Stack of semantic symbols, for referencing the previousSymbol from popSymbol + SemanticSymbol*[] pushedSymbolsStack; /// Path to the file being converted istring symbolFile; @@ -1224,6 +1310,8 @@ private: /// Last comment for ditto-ing istring lastComment; + DocString* lastDocStringInstance; + const Module mod; Rebindable!(const ExpressionNode) feExpression; diff --git a/dsymbol/src/dsymbol/conversion/package.d b/dsymbol/src/dsymbol/conversion/package.d index 6b0df50..303c941 100644 --- a/dsymbol/src/dsymbol/conversion/package.d +++ b/dsymbol/src/dsymbol/conversion/package.d @@ -149,10 +149,15 @@ class SimpleParser : Parser { override Unittest parseUnittest() { + auto start = index; expect(tok!"unittest"); if (currentIs(tok!"{")) skipBraces(); - return allocator.make!Unittest; + auto ret = allocator.make!Unittest; + ret.tokens = tokens[start .. index]; + ret.comment = comment; + comment = null; + return ret; } override MissingFunctionBody parseMissingFunctionBody() diff --git a/dsymbol/src/dsymbol/symbol.d b/dsymbol/src/dsymbol/symbol.d index c70be9e..b4c009b 100644 --- a/dsymbol/src/dsymbol/symbol.d +++ b/dsymbol/src/dsymbol/symbol.d @@ -524,22 +524,40 @@ struct DocString /// Creates a non-ditto comment. this(istring content) { - this.content = content; + this.rawContent = content; } /// Creates a comment which may have been ditto, but has been resolved. this(istring content, bool ditto) { - this.content = content; + this.rawContent = content; this.ditto = ditto; } - alias content this; + alias toString this; + + deprecated("use toString to get a full formatted doc string or rawContent for just what is applied directly (or ditto'd) on the function") alias content = rawContent; /// Contains the documentation string associated with this symbol, resolves ditto to the previous comment with correct scope. - istring content; + istring rawContent; /// `true` if the documentation was just a "ditto" comment copying from the previous comment. bool ditto; + /// Contains the source code + docstring for each documented unittest example associated with this symbol. + istring[] examples; + + // package-private because we don't want to overcomplicate the lifetime in + // the public API. (This might point to a broken address later) + package(dsymbol) DocString* dittoOf; + + string toString() const @safe pure + { + import std.algorithm; + import std.array; + + return examples.length + ? (rawContent ~ "\n\n" ~ examples.map!"a.data".join("\n")) + : rawContent; + } } struct UpdatePair diff --git a/tests/tc_unittest_docs/expected1.txt b/tests/tc_unittest_docs/expected1.txt new file mode 100644 index 0000000..1f36ded --- /dev/null +++ b/tests/tc_unittest_docs/expected1.txt @@ -0,0 +1 @@ +Does foo stuff.\n\nExamples:\n\n---\n// usable with ints\nfoo(1);\n// and with strings!\nif (auto line = readln())\n foo(line);\n\n// or here\nfoo( 1+2 );\n---\n\nExamples:\n\nsecond usage works too\n\n---\nfoo();\n---\n diff --git a/tests/tc_unittest_docs/file.d b/tests/tc_unittest_docs/file.d new file mode 100644 index 0000000..500cf19 --- /dev/null +++ b/tests/tc_unittest_docs/file.d @@ -0,0 +1,30 @@ +void main() +{ + foo(1); +} + +/// Does foo stuff. +template foo() +{ + void foo(int a) {} + void foo(string b) {} +} + +/// +unittest +{ + // usable with ints + foo(1); + // and with strings! + if (auto line = readln()) + foo(line); + + // or here + foo( 1+2 ); +} + +/// second usage works too +unittest +{ + foo(); +} diff --git a/tests/tc_unittest_docs/run.sh b/tests/tc_unittest_docs/run.sh new file mode 100755 index 0000000..bf9cefc --- /dev/null +++ b/tests/tc_unittest_docs/run.sh @@ -0,0 +1,5 @@ +set -e +set -u + +../../bin/dcd-client $1 file.d -d -c20 > actual1.txt +diff actual1.txt expected1.txt