Skip to content

Commit

Permalink
NatSpec: Implement @inheritdoc
Browse files Browse the repository at this point in the history
  • Loading branch information
Marenz committed Jul 13, 2020
1 parent 1d66633 commit d164c71
Show file tree
Hide file tree
Showing 18 changed files with 1,006 additions and 216 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Language Features:
Compiler Features:
* NatSpec: Add fields ``kind`` and ``version`` to the JSON output.
* NatSpec: Inherit tags from unique base functions if derived function does not provide any.
* NatSpec: Implement tag ``@inheritdoc`` to copy documentation from a specific contract.
* Commandline Interface: Prevent some incompatible commandline options from being used together.
* NatSpec: Support NatSpec comments on events.
* Yul Optimizer: Store knowledge about storage / memory after ``a := sload(x)`` / ``a := mload(x)``.
Expand Down
50 changes: 38 additions & 12 deletions docs/natspec-format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ The following example shows a contract and a function using all available tags.
.. code:: solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;
pragma solidity >0.6.10 <0.7.0;
/// @title A simulator for trees
/// @author Larry A. Gardner
Expand All @@ -60,9 +60,33 @@ The following example shows a contract and a function using all available tags.
/// @dev The Alexandr N. Tetearing algorithm could increase precision
/// @param rings The number of rings from dendrochronological sample
/// @return age in years, rounded up for partial years
function age(uint256 rings) external pure returns (uint256) {
function age(uint256 rings) external virtual pure returns (uint256) {
return rings + 1;
}
/// @notice Returns the amount of leaves the tree has.
/// @dev Returns only a fixed number.
function leaves() external virtual pure returns(uint256) {
return 2;
}
}
contract Plant {
function leaves() external virtual pure returns(uint256) {
return 3;
}
}
contract KumquatTree is Tree, Plant {
function age(uint256 rings) external override pure returns (uint256) {
return rings + 2;
}
/// Return the amount of leaves that this specific kind of tree has
/// @inheritdoc Tree
function leaves() external override(Tree, Plant) pure returns(uint256) {
return 3;
}
}
.. _header-tags:
Expand All @@ -75,16 +99,17 @@ NatSpec tag and where it may be used. As a special case, if no tags are
used then the Solidity compiler will interpret a ``///`` or ``/**`` comment
in the same way as if it were tagged with ``@notice``.

=========== =============================================================================== =============================
Tag Context
=========== =============================================================================== =============================
``@title`` A title that should describe the contract/interface contract, interface
``@author`` The name of the author contract, interface
``@notice`` Explain to an end user what this does contract, interface, function, public state variable, event
``@dev`` Explain to a developer any extra details contract, interface, function, state variable, event
``@param`` Documents a parameter just like in doxygen (must be followed by parameter name) function, event
``@return`` Documents the return variables of a contract's function function, public state variable
=========== =============================================================================== =============================
=============== ====================================================================================== =============================
Tag Context
=============== ====================================================================================== =============================
``@title`` A title that should describe the contract/interface contract, interface
``@author`` The name of the author contract, interface
``@notice`` Explain to an end user what this does contract, interface, function, public state variable, event
``@dev`` Explain to a developer any extra details contract, interface, function, state variable, event
``@param`` Documents a parameter just like in doxygen (must be followed by parameter name) function, event
``@return`` Documents the return variables of a contract's function function, public state variable
``@inheritdoc`` Copies all missing tags from the base function (must be followed by the contract name) function, public state variable
=============== ====================================================================================== =============================

If your function returns multiple values, like ``(int quotient, int remainder)``
then use multiple ``@return`` statements in the same format as the
Expand Down Expand Up @@ -127,6 +152,7 @@ base function. Exceptions to this are:

* When the parameter names are different.
* When there is more than one base function.
* When there is an explicit ``@inheritdoc`` tag which specifies which contract should be used to inherit.

.. _header-output:

Expand Down
2 changes: 2 additions & 0 deletions libsolidity/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ set(sources
analysis/DeclarationTypeChecker.h
analysis/DocStringAnalyser.cpp
analysis/DocStringAnalyser.h
analysis/DocStringTagParser.cpp
analysis/DocStringTagParser.h
analysis/ImmutableValidator.cpp
analysis/ImmutableValidator.h
analysis/GlobalContext.cpp
Expand Down
184 changes: 38 additions & 146 deletions libsolidity/analysis/DocStringAnalyser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ void copyMissingTags(StructurallyDocumentedAnnotation& _target, set<CallableDecl
_target.docTags.emplace(tag, content);
}

CallableDeclaration const* findBaseCallable(set<CallableDeclaration const*> const& _baseFunctions, int64_t _contractId)
{
for (CallableDeclaration const* baseFuncCandidate: _baseFunctions)
if (baseFuncCandidate->annotation().contract->id() == _contractId)
return baseFuncCandidate;
else if (auto callable = findBaseCallable(baseFuncCandidate->annotation().baseFunctions, _contractId))
return callable;

return nullptr;
}

bool parameterNamesEqual(CallableDeclaration const& _a, CallableDeclaration const& _b)
{
return boost::range::equal(_a.parameters(), _b.parameters(), [](auto const& pa, auto const& pb) { return pa->name() == pb->name(); });
Expand All @@ -66,50 +77,23 @@ bool DocStringAnalyser::analyseDocStrings(SourceUnit const& _sourceUnit)
return errorWatcher.ok();
}

bool DocStringAnalyser::visit(ContractDefinition const& _contract)
{
static set<string> const validTags = set<string>{"author", "title", "dev", "notice"};
parseDocStrings(_contract, _contract.annotation(), validTags, "contracts");

return true;
}

bool DocStringAnalyser::visit(FunctionDefinition const& _function)
{
if (_function.isConstructor())
handleConstructor(_function, _function, _function.annotation());
else
if (!_function.isConstructor())
handleCallable(_function, _function, _function.annotation());
return true;
}

bool DocStringAnalyser::visit(VariableDeclaration const& _variable)
{
if (_variable.isStateVariable())
{
static set<string> const validPublicTags = set<string>{"dev", "notice", "return", "title", "author"};
if (_variable.isPublic())
parseDocStrings(_variable, _variable.annotation(), validPublicTags, "public state variables");
else
{
parseDocStrings(_variable, _variable.annotation(), validPublicTags, "non-public state variables");
if (_variable.annotation().docTags.count("notice") > 0)
m_errorReporter.warning(
7816_error, _variable.documentation()->location(),
"Documentation tag on non-public state variables will be disallowed in 0.7.0. "
"You will need to use the @dev tag explicitly."
);
}
if (_variable.annotation().docTags.count("title") > 0 || _variable.annotation().docTags.count("author") > 0)
m_errorReporter.warning(
8532_error, _variable.documentation()->location(),
"Documentation tag @title and @author is only allowed on contract definitions. "
"It will be disallowed in 0.7.0."
);
if (!_variable.isStateVariable())
return false;

if (CallableDeclaration const* baseFunction = resolveInheritDoc(_variable.annotation().baseFunctions, _variable, _variable.annotation()))
copyMissingTags(_variable.annotation(), {baseFunction});
else if (_variable.annotation().docTags.empty())
copyMissingTags(_variable.annotation(), _variable.annotation().baseFunctions);

if (_variable.annotation().docTags.empty())
copyMissingTags(_variable.annotation(), _variable.annotation().baseFunctions);
}
return false;
}

Expand All @@ -127,133 +111,41 @@ bool DocStringAnalyser::visit(EventDefinition const& _event)
return true;
}

void DocStringAnalyser::checkParameters(
CallableDeclaration const& _callable,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation
)
{
set<string> validParams;
for (auto const& p: _callable.parameters())
validParams.insert(p->name());
if (_callable.returnParameterList())
for (auto const& p: _callable.returnParameterList()->parameters())
validParams.insert(p->name());
auto paramRange = _annotation.docTags.equal_range("param");
for (auto i = paramRange.first; i != paramRange.second; ++i)
if (!validParams.count(i->second.paramName))
m_errorReporter.docstringParsingError(
3881_error,
_node.documentation()->location(),
"Documented parameter \"" +
i->second.paramName +
"\" not found in the parameter list of the function."
);

}

void DocStringAnalyser::handleConstructor(
CallableDeclaration const& _callable,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation
)
{
static set<string> const validTags = set<string>{"author", "dev", "notice", "param"};
parseDocStrings(_node, _annotation, validTags, "constructor");
checkParameters(_callable, _node, _annotation);
}

void DocStringAnalyser::handleCallable(
CallableDeclaration const& _callable,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation
)
{
static set<string> const validTags = set<string>{"author", "dev", "notice", "return", "param"};
parseDocStrings(_node, _annotation, validTags, "functions");
checkParameters(_callable, _node, _annotation);

if (
if (CallableDeclaration const* baseFunction = resolveInheritDoc(_callable.annotation().baseFunctions, _node, _annotation))
copyMissingTags(_annotation, {baseFunction});
else if (
_annotation.docTags.empty() &&
_callable.annotation().baseFunctions.size() == 1 &&
parameterNamesEqual(_callable, **_callable.annotation().baseFunctions.begin())
)
copyMissingTags(_annotation, _callable.annotation().baseFunctions);

if (_node.documentation() && _annotation.docTags.count("author") > 0)
m_errorReporter.warning(
9843_error, _node.documentation()->location(),
"Documentation tag @author is only allowed on contract definitions. "
"It will be disallowed in 0.7.0."
);
}

void DocStringAnalyser::parseDocStrings(
CallableDeclaration const* DocStringAnalyser::resolveInheritDoc(
std::set<CallableDeclaration const*>& _baseFuncs,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation,
set<string> const& _validTags,
string const& _nodeName
StructurallyDocumentedAnnotation& _annotation
)
{
DocStringParser parser;
if (_node.documentation() && !_node.documentation()->text()->empty())
{
parser.parse(*_node.documentation()->text(), m_errorReporter);
_annotation.docTags = parser.tags();
}
if (_annotation.inheritdocReference == nullptr)
return nullptr;

if (auto const callable = findBaseCallable(_baseFuncs, _annotation.inheritdocReference->id()))
return callable;

size_t returnTagsVisited = 0;
for (auto const& docTag: _annotation.docTags)
{
if (!_validTags.count(docTag.first))
m_errorReporter.docstringParsingError(
6546_error,
_node.documentation()->location(),
"Documentation tag @" + docTag.first + " not valid for " + _nodeName + "."
);
else
if (docTag.first == "return")
{
returnTagsVisited++;
if (auto* varDecl = dynamic_cast<VariableDeclaration const*>(&_node))
{
if (!varDecl->isPublic())
m_errorReporter.docstringParsingError(
9440_error,
_node.documentation()->location(),
"Documentation tag \"@" + docTag.first + "\" is only allowed on public state-variables."
);
if (returnTagsVisited > 1)
m_errorReporter.docstringParsingError(
5256_error,
_node.documentation()->location(),
"Documentation tag \"@" + docTag.first + "\" is only allowed once on state-variables."
);
}
else if (auto* function = dynamic_cast<FunctionDefinition const*>(&_node))
{
string content = docTag.second.content;
string firstWord = content.substr(0, content.find_first_of(" \t"));
m_errorReporter.docstringParsingError(
4682_error,
_node.documentation()->location(),
"Documentation tag @inheritdoc references contract \"" +
_annotation.inheritdocReference->name() +
"\", but the contract does not contain a function that is overridden by this function."
);

if (returnTagsVisited > function->returnParameters().size())
m_errorReporter.docstringParsingError(
2604_error,
_node.documentation()->location(),
"Documentation tag \"@" + docTag.first + " " + docTag.second.content + "\"" +
" exceeds the number of return parameters."
);
else
{
auto parameter = function->returnParameters().at(returnTagsVisited - 1);
if (!parameter->name().empty() && parameter->name() != firstWord)
m_errorReporter.docstringParsingError(
5856_error,
_node.documentation()->location(),
"Documentation tag \"@" + docTag.first + " " + docTag.second.content + "\"" +
" does not contain the name of its return parameter."
);
}
}
}
}
return nullptr;
}
32 changes: 3 additions & 29 deletions libsolidity/analysis/DocStringAnalyser.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @author Christian <[email protected]>
* @date 2015
* Parses and analyses the doc strings.
* Stores the parsing results in the AST annotations and reports errors.
*/

#pragma once

Expand All @@ -34,7 +28,7 @@ namespace solidity::frontend
{

/**
* Parses and analyses the doc strings.
* Analyses and validates the doc strings.
* Stores the parsing results in the AST annotations and reports errors.
*/
class DocStringAnalyser: private ASTConstVisitor
Expand All @@ -44,20 +38,13 @@ class DocStringAnalyser: private ASTConstVisitor
bool analyseDocStrings(SourceUnit const& _sourceUnit);

private:
bool visit(ContractDefinition const& _contract) override;
bool visit(FunctionDefinition const& _function) override;
bool visit(VariableDeclaration const& _variable) override;
bool visit(ModifierDefinition const& _modifier) override;
bool visit(EventDefinition const& _event) override;

void checkParameters(
CallableDeclaration const& _callable,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation
);

void handleConstructor(
CallableDeclaration const& _callable,
CallableDeclaration const* resolveInheritDoc(
std::set<CallableDeclaration const*>& _baseFunctions,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation
);
Expand All @@ -68,19 +55,6 @@ class DocStringAnalyser: private ASTConstVisitor
StructurallyDocumentedAnnotation& _annotation
);

void handleDeclaration(
Declaration const& _declaration,
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation
);

void parseDocStrings(
StructurallyDocumented const& _node,
StructurallyDocumentedAnnotation& _annotation,
std::set<std::string> const& _validTags,
std::string const& _nodeName
);

langutil::ErrorReporter& m_errorReporter;
};

Expand Down
Loading

0 comments on commit d164c71

Please sign in to comment.