diff --git a/Changelog.md b/Changelog.md index e0987a9195b9..abfdb3a79cdb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ Compiler Features: * Code Generator: Evaluate ``keccak256`` of string literals at compile-time. * Peephole Optimizer: Remove unnecessary masking of tags. * Yul EVM Code Transform: Free stack slots directly after visiting the right-hand-side of variable declarations instead of at the end of the statement only. + * NatSpec: Implement tag ``@inheritdoc`` to copy documentation from a specific contract. Bugfixes: * SMTChecker: Fix internal error when using bitwise operators on fixed bytes type. diff --git a/docs/natspec-format.rst b/docs/natspec-format.rst index ec774014bb6b..c566d54078b3 100644 --- a/docs/natspec-format.rst +++ b/docs/natspec-format.rst @@ -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 @@ -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: @@ -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 @@ -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: diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 8e930b6ab9b5..74f4a9a21e02 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -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 diff --git a/libsolidity/analysis/DocStringAnalyser.cpp b/libsolidity/analysis/DocStringAnalyser.cpp index 96fde8550292..94a841e00e81 100644 --- a/libsolidity/analysis/DocStringAnalyser.cpp +++ b/libsolidity/analysis/DocStringAnalyser.cpp @@ -53,6 +53,17 @@ void copyMissingTags(StructurallyDocumentedAnnotation& _target, set 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(); }); @@ -67,50 +78,23 @@ bool DocStringAnalyser::analyseDocStrings(SourceUnit const& _sourceUnit) return errorWatcher.ok(); } -bool DocStringAnalyser::visit(ContractDefinition const& _contract) -{ - static set const validTags = set{"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 const validPublicTags = set{"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; } @@ -128,133 +112,41 @@ bool DocStringAnalyser::visit(EventDefinition const& _event) return true; } -void DocStringAnalyser::checkParameters( - CallableDeclaration const& _callable, - StructurallyDocumented const& _node, - StructurallyDocumentedAnnotation& _annotation -) -{ - set 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 const validTags = set{"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 const validTags = set{"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( + set const& _baseFuncs, StructurallyDocumented const& _node, - StructurallyDocumentedAnnotation& _annotation, - set 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(&_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(&_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; } diff --git a/libsolidity/analysis/DocStringAnalyser.h b/libsolidity/analysis/DocStringAnalyser.h index ee79cd9f7719..e13cab8985a4 100644 --- a/libsolidity/analysis/DocStringAnalyser.h +++ b/libsolidity/analysis/DocStringAnalyser.h @@ -15,12 +15,6 @@ along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 -/** - * @author Christian - * @date 2015 - * Parses and analyses the doc strings. - * Stores the parsing results in the AST annotations and reports errors. - */ #pragma once @@ -35,7 +29,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 @@ -45,20 +39,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 const& _baseFunctions, StructurallyDocumented const& _node, StructurallyDocumentedAnnotation& _annotation ); @@ -69,19 +56,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 const& _validTags, - std::string const& _nodeName - ); - langutil::ErrorReporter& m_errorReporter; }; diff --git a/libsolidity/analysis/DocStringTagParser.cpp b/libsolidity/analysis/DocStringTagParser.cpp new file mode 100644 index 000000000000..36fec2f2cabc --- /dev/null +++ b/libsolidity/analysis/DocStringTagParser.cpp @@ -0,0 +1,227 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +/** + * @author Christian + * @date 2015 + * Parses and analyses the doc strings. + * Stores the parsing results in the AST annotations and reports errors. + */ + +#include + +#include +#include +#include +#include + +using namespace std; +using namespace solidity; +using namespace solidity::langutil; +using namespace solidity::frontend; + +bool DocStringTagParser::parseDocStrings(SourceUnit const& _sourceUnit) +{ + auto errorWatcher = m_errorReporter.errorWatcher(); + _sourceUnit.accept(*this); + return errorWatcher.ok(); +} + +bool DocStringTagParser::visit(ContractDefinition const& _contract) +{ + static set const validTags = set{"author", "title", "dev", "notice"}; + parseDocStrings(_contract, _contract.annotation(), validTags, "contracts"); + + return true; +} + +bool DocStringTagParser::visit(FunctionDefinition const& _function) +{ + if (_function.isConstructor()) + handleConstructor(_function, _function, _function.annotation()); + else + handleCallable(_function, _function, _function.annotation()); + return true; +} + +bool DocStringTagParser::visit(VariableDeclaration const& _variable) +{ + if (_variable.isStateVariable()) + { + static set const validPublicTags = set{"dev", "notice", "return", "title", "author", "inheritdoc"}; + 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." + ); + } + return false; +} + +bool DocStringTagParser::visit(ModifierDefinition const& _modifier) +{ + handleCallable(_modifier, _modifier, _modifier.annotation()); + + return true; +} + +bool DocStringTagParser::visit(EventDefinition const& _event) +{ + handleCallable(_event, _event, _event.annotation()); + + return true; +} + +void DocStringTagParser::checkParameters( + CallableDeclaration const& _callable, + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation +) +{ + set 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 DocStringTagParser::handleConstructor( + CallableDeclaration const& _callable, + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation +) +{ + static set const validTags = set{"author", "dev", "notice", "param"}; + parseDocStrings(_node, _annotation, validTags, "constructor"); + checkParameters(_callable, _node, _annotation); +} + +void DocStringTagParser::handleCallable( + CallableDeclaration const& _callable, + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation +) +{ + static set const validEventTags = set{"author", "dev", "notice", "return", "param"}; + static set const validTags = set{"author", "dev", "notice", "return", "param", "inheritdoc"}; + + if (dynamic_cast(&_callable)) + parseDocStrings(_node, _annotation, validEventTags, "events"); + else + parseDocStrings(_node, _annotation, validTags, "functions"); + + checkParameters(_callable, _node, _annotation); + + 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 DocStringTagParser::parseDocStrings( + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation, + set const& _validTags, + string const& _nodeName +) +{ + DocStringParser parser; + if (_node.documentation() && !_node.documentation()->text()->empty()) + { + parser.parse(*_node.documentation()->text(), m_errorReporter); + _annotation.docTags = parser.tags(); + } + + 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 const* varDecl = dynamic_cast(&_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 const* function = dynamic_cast(&_node)) + { + string content = docTag.second.content; + string firstWord = content.substr(0, content.find_first_of(" \t")); + + 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." + ); + } + } + } + } +} diff --git a/libsolidity/analysis/DocStringTagParser.h b/libsolidity/analysis/DocStringTagParser.h new file mode 100644 index 000000000000..41dcca5dfcd1 --- /dev/null +++ b/libsolidity/analysis/DocStringTagParser.h @@ -0,0 +1,76 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +namespace solidity::langutil +{ +class ErrorReporter; +} + +namespace solidity::frontend +{ + +/** + * Parses the doc tags and does basic validity checks. + * Stores the parsing results in the AST annotations and reports errors. + */ +class DocStringTagParser: private ASTConstVisitor +{ +public: + explicit DocStringTagParser(langutil::ErrorReporter& _errorReporter): m_errorReporter(_errorReporter) {} + bool parseDocStrings(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, + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation + ); + + void handleCallable( + CallableDeclaration const& _callable, + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation + ); + + void parseDocStrings( + StructurallyDocumented const& _node, + StructurallyDocumentedAnnotation& _annotation, + std::set const& _validTags, + std::string const& _nodeName + ); + + langutil::ErrorReporter& m_errorReporter; +}; + +} diff --git a/libsolidity/analysis/ReferencesResolver.cpp b/libsolidity/analysis/ReferencesResolver.cpp index 10ad415c114d..4d26f1888326 100644 --- a/libsolidity/analysis/ReferencesResolver.cpp +++ b/libsolidity/analysis/ReferencesResolver.cpp @@ -36,6 +36,7 @@ #include #include +#include using namespace std; using namespace solidity::langutil; @@ -106,6 +107,14 @@ void ReferencesResolver::endVisit(VariableDeclarationStatement const& _varDeclSt m_resolver.activateVariable(var->name()); } +bool ReferencesResolver::visit(VariableDeclaration const& _varDecl) +{ + if (_varDecl.documentation()) + resolveInheritDoc(*_varDecl.documentation(), _varDecl.annotation()); + + return true; +} + bool ReferencesResolver::visit(Identifier const& _identifier) { auto declarations = m_resolver.nameFromCurrentScope(_identifier.name()); @@ -132,6 +141,10 @@ bool ReferencesResolver::visit(Identifier const& _identifier) bool ReferencesResolver::visit(FunctionDefinition const& _functionDefinition) { m_returnParameters.push_back(_functionDefinition.returnParameterList().get()); + + if (_functionDefinition.documentation()) + resolveInheritDoc(*_functionDefinition.documentation(), _functionDefinition.annotation()); + return true; } @@ -141,9 +154,13 @@ void ReferencesResolver::endVisit(FunctionDefinition const&) m_returnParameters.pop_back(); } -bool ReferencesResolver::visit(ModifierDefinition const&) +bool ReferencesResolver::visit(ModifierDefinition const& _modifierDefinition) { m_returnParameters.push_back(nullptr); + + if (_modifierDefinition.documentation()) + resolveInheritDoc(*_modifierDefinition.documentation(), _modifierDefinition.annotation()); + return true; } @@ -284,4 +301,53 @@ void ReferencesResolver::operator()(yul::VariableDeclaration const& _varDecl) visit(*_varDecl.value); } +void ReferencesResolver::resolveInheritDoc(StructuredDocumentation const& _documentation, StructurallyDocumentedAnnotation& _annotation) +{ + switch (_annotation.docTags.count("inheritdoc")) + { + case 0: + break; + case 1: + { + string const& name = _annotation.docTags.find("inheritdoc")->second.content; + vector path; + boost::split(path, name, boost::is_any_of(".")); + Declaration const* result = m_resolver.pathFromCurrentScope(path); + + if (result == nullptr) + { + m_errorReporter.docstringParsingError( + 9397_error, + _documentation.location(), + "Documentation tag @inheritdoc references inexistent contract \"" + + name + + "\"." + ); + return; + } + else + { + _annotation.inheritdocReference = dynamic_cast(result); + + if (!_annotation.inheritdocReference) + m_errorReporter.docstringParsingError( + 1430_error, + _documentation.location(), + "Documentation tag @inheritdoc reference \"" + + name + + "\" is not a contract." + ); + } + break; + } + default: + m_errorReporter.docstringParsingError( + 5142_error, + _documentation.location(), + "Documentation tag @inheritdoc can only be given once." + ); + break; + } +} + } diff --git a/libsolidity/analysis/ReferencesResolver.h b/libsolidity/analysis/ReferencesResolver.h index 710f388cbf17..fc71b4d4edac 100644 --- a/libsolidity/analysis/ReferencesResolver.h +++ b/libsolidity/analysis/ReferencesResolver.h @@ -76,6 +76,7 @@ class ReferencesResolver: private ASTConstVisitor, private yul::ASTWalker bool visit(ForStatement const& _for) override; void endVisit(ForStatement const& _for) override; void endVisit(VariableDeclarationStatement const& _varDeclStatement) override; + bool visit(VariableDeclaration const& _varDecl) override; bool visit(Identifier const& _identifier) override; bool visit(FunctionDefinition const& _functionDefinition) override; void endVisit(FunctionDefinition const& _functionDefinition) override; @@ -89,6 +90,8 @@ class ReferencesResolver: private ASTConstVisitor, private yul::ASTWalker void operator()(yul::Identifier const& _identifier) override; void operator()(yul::VariableDeclaration const& _varDecl) override; + void resolveInheritDoc(StructuredDocumentation const& _documentation, StructurallyDocumentedAnnotation& _annotation); + langutil::ErrorReporter& m_errorReporter; NameAndTypeResolver& m_resolver; langutil::EVMVersion m_evmVersion; diff --git a/libsolidity/ast/ASTAnnotations.h b/libsolidity/ast/ASTAnnotations.h index d4df9d0cf91c..272bf6440e15 100644 --- a/libsolidity/ast/ASTAnnotations.h +++ b/libsolidity/ast/ASTAnnotations.h @@ -79,6 +79,8 @@ struct StructurallyDocumentedAnnotation /// Mapping docstring tag name -> content. std::multimap docTags; + /// contract that @inheritdoc references if it exists + ContractDefinition const* inheritdocReference = nullptr; }; struct SourceUnitAnnotation: ASTAnnotation diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index f8164bc9f3d3..27a51552e3bd 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -308,6 +309,11 @@ bool CompilerStack::analyze() if (source->ast && !syntaxChecker.checkSyntax(*source->ast)) noErrors = false; + DocStringTagParser DocStringTagParser(m_errorReporter); + for (Source const* source: m_sourceOrder) + if (source->ast && !DocStringTagParser.parseDocStrings(*source->ast)) + noErrors = false; + m_globalContext = make_shared(); // We need to keep the same resolver during the whole process. NameAndTypeResolver resolver(*m_globalContext, m_evmVersion, m_errorReporter); @@ -363,6 +369,7 @@ bool CompilerStack::analyze() if (!contractLevelChecker.check(*contract)) noErrors = false; + // Requires ContractLevelChecker DocStringAnalyser docStringAnalyser(m_errorReporter); for (Source const* source: m_sourceOrder) if (source->ast && !docStringAnalyser.analyseDocStrings(*source->ast)) diff --git a/libsolidity/interface/Natspec.cpp b/libsolidity/interface/Natspec.cpp index 1f5c4807af4d..67ef6f17f2f9 100644 --- a/libsolidity/interface/Natspec.cpp +++ b/libsolidity/interface/Natspec.cpp @@ -58,28 +58,22 @@ Json::Value Natspec::userDocumentation(ContractDefinition const& _contractDef) for (auto const& it: _contractDef.interfaceFunctions()) if (it.second->hasDeclaration()) { + string value; + if (auto const* f = dynamic_cast(&it.second->declaration())) - { - string value = extractDoc(f->annotation().docTags, "notice"); - if (!value.empty()) - { - Json::Value user; - // since @notice is the only user tag if missing function should not appear - user["notice"] = Json::Value(value); - methods[it.second->externalSignature()] = user; - } - } + value = extractDoc(f->annotation().docTags, "notice"); else if (auto var = dynamic_cast(&it.second->declaration())) { solAssert(var->isStateVariable() && var->isPublic(), ""); - string value = extractDoc(var->annotation().docTags, "notice"); - if (!value.empty()) - { - Json::Value user; - // since @notice is the only user tag if missing function should not appear - user["notice"] = Json::Value(value); - methods[it.second->externalSignature()] = user; - } + value = extractDoc(var->annotation().docTags, "notice"); + } + + if (!value.empty()) + { + Json::Value user; + // since @notice is the only user tag if missing function should not appear + user["notice"] = Json::Value(value); + methods[it.second->externalSignature()] = user; } } diff --git a/test/libsolidity/SolidityNatspecJSON.cpp b/test/libsolidity/SolidityNatspecJSON.cpp index 8edf470f406c..26e293b68b7b 100644 --- a/test/libsolidity/SolidityNatspecJSON.cpp +++ b/test/libsolidity/SolidityNatspecJSON.cpp @@ -77,7 +77,7 @@ class DocumentationChecker BOOST_REQUIRE(Error::containsErrorOfType(m_compilerStack.errors(), Error::Type::DocstringParsingError)); } -private: +protected: CompilerStack m_compilerStack; }; @@ -1277,13 +1277,7 @@ BOOST_AUTO_TEST_CASE(dev_default_inherit_variable) )"; char const *natspec = R"ABCDEF({ - "methods": - { - "x()": - { - "details": "test" - } - } + "methods": { "x()": { "details": "test" } } })ABCDEF"; char const *natspec1 = R"ABCDEF({ @@ -1318,15 +1312,82 @@ BOOST_AUTO_TEST_CASE(user_default_inherit_variable) )"; char const *natspec = R"ABCDEF({ - "methods": + "methods": { "x()": { "notice": "Hello world" } } + })ABCDEF"; + + checkNatspec(sourceCode, "C", natspec, true); + checkNatspec(sourceCode, "D", natspec, true); +} + +BOOST_AUTO_TEST_CASE(dev_explicit_inherit_variable) +{ + char const *sourceCode = R"( + contract B { + function x() virtual external returns (uint) { + return 1; + } + } + + contract C { + /// @notice Hello world + /// @dev test + function x() virtual external returns (uint) { + return 1; + } + } + + contract D is C, B { + /// @inheritdoc C + uint public override(C, B) x; + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": { "x()": { "details": "test" } } + })ABCDEF"; + + char const *natspec1 = R"ABCDEF({ + "methods" : {}, + "stateVariables" : { - "x()": + "x" : { - "notice": "Hello world" + "details" : "test" } } })ABCDEF"; + checkNatspec(sourceCode, "C", natspec, false); + checkNatspec(sourceCode, "D", natspec1, false); +} + +BOOST_AUTO_TEST_CASE(user_explicit_inherit_variable) +{ + char const *sourceCode = R"( + contract B { + function x() virtual external returns (uint) { + return 1; + } + } + + contract C { + /// @notice Hello world + /// @dev test + function x() virtual external returns (uint) { + return 1; + } + } + + contract D is C, B { + /// @inheritdoc C + uint public override(C, B) x; + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": { "x()": { "notice": "Hello world" } } + })ABCDEF"; + checkNatspec(sourceCode, "C", natspec, true); checkNatspec(sourceCode, "D", natspec, true); } @@ -1423,6 +1484,411 @@ BOOST_AUTO_TEST_CASE(user_default_inherit) checkNatspec(sourceCode, "Token", natspec, true); } +BOOST_AUTO_TEST_CASE(dev_explicit_inherit) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 { + function transfer(address to, uint amount) virtual external returns (bool) { + return false; + } + } + + contract Token is ERC21, ERC20 { + /// @inheritdoc ERC20 + function transfer(address to, uint amount) override(ERC21, ERC20) external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "test", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, false); + checkNatspec(sourceCode, "Token", natspec, false); +} + +BOOST_AUTO_TEST_CASE(user_explicit_inherit) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 { + function transfer(address to, uint amount) virtual external returns (bool) { + return false; + } + } + + contract Token is ERC21, ERC20 { + /// @inheritdoc ERC20 + function transfer(address to, uint amount) override(ERC21, ERC20) external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "notice": "Transfer ``amount`` from ``msg.sender`` to ``to``." + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, true); + checkNatspec(sourceCode, "Token", natspec, true); +} + +BOOST_AUTO_TEST_CASE(dev_explicit_inherit2) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 is ERC20 { + function transfer(address to, uint amount) virtual override external returns (bool) { + return false; + } + } + + contract Token is ERC20 { + /// @inheritdoc ERC20 + function transfer(address to, uint amount) override external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "test", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, false); + checkNatspec(sourceCode, "ERC21", natspec, false); + checkNatspec(sourceCode, "Token", natspec, false); +} + +BOOST_AUTO_TEST_CASE(user_explicit_inherit2) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 is ERC20 { + function transfer(address to, uint amount) virtual override external returns (bool) { + return false; + } + } + + contract Token is ERC20 { + /// @inheritdoc ERC20 + function transfer(address to, uint amount) override external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "notice": "Transfer ``amount`` from ``msg.sender`` to ``to``." + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, true); + checkNatspec(sourceCode, "ERC21", natspec, true); + checkNatspec(sourceCode, "Token", natspec, true); +} + +BOOST_AUTO_TEST_CASE(dev_explicit_inherit_partial2) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 is ERC20 { + /// @inheritdoc ERC20 + /// @dev override dev comment + /// @notice override notice + function transfer(address to, uint amount) virtual override external returns (bool) { + return false; + } + } + + contract Token is ERC21 { + function transfer(address to, uint amount) override external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "test", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + char const *natspec2 = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "override dev comment", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, false); + checkNatspec(sourceCode, "Token", natspec2, false); +} + +BOOST_AUTO_TEST_CASE(user_explicit_inherit_partial2) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 is ERC20 { + /// @inheritdoc ERC20 + /// @dev override dev comment + /// @notice override notice + function transfer(address to, uint amount) virtual override external returns (bool) { + return false; + } + } + + contract Token is ERC21 { + function transfer(address to, uint amount) override external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "notice": "Transfer ``amount`` from ``msg.sender`` to ``to``." + } + } + })ABCDEF"; + + char const *natspec2 = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "notice": "override notice" + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, true); + checkNatspec(sourceCode, "Token", natspec2, true); +} +BOOST_AUTO_TEST_CASE(dev_explicit_inherit_partial) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 { + function transfer(address to, uint amount) virtual external returns (bool) { + return false; + } + } + + contract Token is ERC21, ERC20 { + /// @inheritdoc ERC20 + /// @dev override dev comment + /// @notice override notice + function transfer(address to, uint amount) override(ERC21, ERC20) external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "test", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + char const *natspec2 = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "override dev comment", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, false); + checkNatspec(sourceCode, "Token", natspec2, false); +} + +BOOST_AUTO_TEST_CASE(user_explicit_inherit_partial) +{ + char const *sourceCode = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + contract ERC21 { + function transfer(address to, uint amount) virtual external returns (bool) { + return false; + } + } + + contract Token is ERC21, ERC20 { + /// @inheritdoc ERC20 + /// @dev override dev comment + /// @notice override notice + function transfer(address to, uint amount) override(ERC21, ERC20) external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "notice": "Transfer ``amount`` from ``msg.sender`` to ``to``." + } + } + })ABCDEF"; + + char const *natspec2 = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "notice": "override notice" + } + } + })ABCDEF"; + + checkNatspec(sourceCode, "ERC20", natspec, true); + checkNatspec(sourceCode, "Token", natspec2, true); +} + BOOST_AUTO_TEST_CASE(dev_inherit_parameter_mismatch) { char const *sourceCode = R"( @@ -1517,6 +1983,80 @@ BOOST_AUTO_TEST_CASE(user_inherit_parameter_mismatch) checkNatspec(sourceCode, "Token", natspec2, true); } +BOOST_AUTO_TEST_CASE(dev_explicit_inehrit_complex) +{ + char const *sourceCode1 = R"( + interface ERC20 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer + /// @dev test + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + + interface ERC21 { + /// Transfer ``amount`` from ``msg.sender`` to ``to``. + /// @author Programmer2 + /// @dev test2 + /// @param to address to transfer to + /// @param amount amount to transfer + function transfer(address to, uint amount) external returns (bool); + } + )"; + + char const *sourceCode2 = R"( + import "Interfaces.sol" as myInterfaces; + + contract Token is myInterfaces.ERC20, myInterfaces.ERC21 { + /// @inheritdoc myInterfaces.ERC20 + function transfer(address too, uint amount) + override(myInterfaces.ERC20, myInterfaces.ERC21) external returns (bool) { + return false; + } + } + )"; + + char const *natspec = R"ABCDEF({ + "methods": + { + "transfer(address,uint256)": + { + "author": "Programmer", + "details": "test", + "params": + { + "amount": "amount to transfer", + "to": "address to transfer to" + } + } + } + })ABCDEF"; + + m_compilerStack.reset(); + m_compilerStack.setSources({ + {"Interfaces.sol", "pragma solidity >=0.0;\n" + std::string(sourceCode1)}, + {"Testfile.sol", "pragma solidity >=0.0;\n" + std::string(sourceCode2)} + }); + + m_compilerStack.setEVMVersion(solidity::test::CommonOptions::get().evmVersion()); + + BOOST_REQUIRE_MESSAGE(m_compilerStack.parseAndAnalyze(), "Parsing contract failed"); + + Json::Value generatedDocumentation = m_compilerStack.natspecDev("Token"); + Json::Value expectedDocumentation; + util::jsonParseStrict(natspec, expectedDocumentation); + + expectedDocumentation["version"] = Json::Value(Natspec::c_natspecVersion); + expectedDocumentation["kind"] = Json::Value("dev"); + + BOOST_CHECK_MESSAGE( + expectedDocumentation == generatedDocumentation, + "Expected:\n" << expectedDocumentation.toStyledString() << + "\n but got:\n" << generatedDocumentation.toStyledString() + ); +} + } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc.sol b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc.sol new file mode 100644 index 000000000000..678872d7a00f --- /dev/null +++ b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc.sol @@ -0,0 +1,7 @@ +contract C { + /// @inheritdoc X + function f() internal { + } +} +// ---- +// DocstringParsingError 9397: (17-34): Documentation tag @inheritdoc references inexistent contract "X". diff --git a/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc2.sol b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc2.sol new file mode 100644 index 000000000000..4d97ff93d43c --- /dev/null +++ b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc2.sol @@ -0,0 +1,10 @@ +contract D { +} + +contract C is D { + /// @inheritdoc D + function f() internal { + } +} +// ---- +// DocstringParsingError 4682: (38-55): Documentation tag @inheritdoc references contract "D", but the contract does not contain a function that is overridden by this function. diff --git a/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc3.sol b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc3.sol new file mode 100644 index 000000000000..d00e1e7ea1ac --- /dev/null +++ b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc3.sol @@ -0,0 +1,11 @@ +contract D { + struct S { uint a; } +} + +contract C is D { + /// @inheritdoc D.S + function f() internal { + } +} +// ---- +// DocstringParsingError 1430: (63-82): Documentation tag @inheritdoc reference "D.S" is not a contract. diff --git a/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc_wrong_type.sol b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc_wrong_type.sol new file mode 100644 index 000000000000..510f6622fdca --- /dev/null +++ b/test/libsolidity/syntaxTests/natspec/invalid/docstring_inheritdoc_wrong_type.sol @@ -0,0 +1,8 @@ +contract C { + struct S { uint a; } + /// @inheritdoc S + function f() internal { + } +} +// ---- +// DocstringParsingError 1430: (42-59): Documentation tag @inheritdoc reference "S" is not a contract. diff --git a/test/libsolidity/syntaxTests/natspec/invalid/inherit_doc_events.sol b/test/libsolidity/syntaxTests/natspec/invalid/inherit_doc_events.sol new file mode 100644 index 000000000000..e7fb1a822fc9 --- /dev/null +++ b/test/libsolidity/syntaxTests/natspec/invalid/inherit_doc_events.sol @@ -0,0 +1,17 @@ +contract ERC20 { + /// @notice This event is emitted when a transfer occurs. + /// @param from The source account. + /// @param to The destination account. + /// @param amount The amount. + /// @dev A test case! + event Transfer(address indexed from, address indexed to, uint amount); +} + +contract A is ERC20 { + /// @inheritdoc ERC20 + event Transfer(); +} + + +// ---- +// DocstringParsingError 6546: (305-326): Documentation tag @inheritdoc not valid for events.