diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index 2d2d10b042..95b05aa6b9 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -110,6 +110,8 @@ def __init__(self, compilation_unit: "SlitherCompilationUnit", scope: "FileScope Dict["StateVariable", Set[Union["StateVariable", "Function"]]] ] = None + self._comments: Optional[str] = None + ################################################################################### ################################################################################### # region General's properties @@ -165,6 +167,31 @@ def is_library(self) -> bool: def is_library(self, is_library: bool): self._is_library = is_library + @property + def comments(self) -> Optional[str]: + """ + Return the comments associated with the contract. + + When using comments, avoid strict text matching, as the solc behavior might change. + For example, for old solc version, the first space after the * is not kept, i.e: + + * @title Test Contract + * @dev Test comment + + Returns + - " @title Test Contract\n @dev Test comment" for newest versions + - "@title Test Contract\n@dev Test comment" for older versions + + + Returns: + the comment as a string + """ + return self._comments + + @comments.setter + def comments(self, comments: str): + self._comments = comments + # endregion ################################################################################### ################################################################################### diff --git a/slither/solc_parsing/declarations/contract.py b/slither/solc_parsing/declarations/contract.py index 475c3fab29..e63dbe68ff 100644 --- a/slither/solc_parsing/declarations/contract.py +++ b/slither/solc_parsing/declarations/contract.py @@ -780,12 +780,35 @@ def delete_content(self): self._customErrorParsed = [] def _handle_comment(self, attributes: Dict) -> None: + """ + Save the contract comment in self.comments + And handle custom slither comments + + Args: + attributes: + + Returns: + + """ + # Old solc versions store the comment in attributes["documentation"] + # More recent ones store it in attributes["documentation"]["text"] if ( "documentation" in attributes and attributes["documentation"] is not None - and "text" in attributes["documentation"] + and ( + "text" in attributes["documentation"] + or isinstance(attributes["documentation"], str) + ) ): - candidates = attributes["documentation"]["text"].replace("\n", ",").split(",") + text = ( + attributes["documentation"] + if isinstance(attributes["documentation"], str) + else attributes["documentation"]["text"] + ) + self._contract.comments = text + + # Look for custom comments + candidates = text.replace("\n", ",").split(",") for candidate in candidates: if "@custom:security isDelegatecallProxy" in candidate: diff --git a/tests/custom_comments/contract_comment.sol b/tests/custom_comments/contract_comment.sol new file mode 100644 index 0000000000..8f0fb5233b --- /dev/null +++ b/tests/custom_comments/contract_comment.sol @@ -0,0 +1,7 @@ +/** + * @title Test Contract + * @dev Test comment + */ +contract A{ + +} \ No newline at end of file diff --git a/tests/test_features.py b/tests/test_features.py index d29a5eb6af..7c2db8a3e2 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -31,7 +31,7 @@ def test_node() -> None: def test_collision() -> None: - + solc_select.switch_global_version("0.8.0", always_install=True) standard_json = SolcStandardJson() standard_json.add_source_file("./tests/collisions/a.sol") standard_json.add_source_file("./tests/collisions/b.sol") @@ -43,6 +43,7 @@ def test_collision() -> None: def test_cycle() -> None: + solc_select.switch_global_version("0.8.0", always_install=True) slither = Slither("./tests/test_cyclic_import/a.sol") _run_all_detectors(slither) @@ -74,6 +75,36 @@ def test_upgradeable_comments() -> None: assert v1.upgradeable_version == "version_1" +def test_contract_comments() -> None: + comments = " @title Test Contract\n @dev Test comment" + + solc_select.switch_global_version("0.8.10", always_install=True) + slither = Slither("./tests/custom_comments/contract_comment.sol") + compilation_unit = slither.compilation_units[0] + contract = compilation_unit.get_contract_from_name("A")[0] + + assert contract.comments == comments + + # Old solc versions have a different parsing of comments + # the initial space (after *) is also not kept on every line + comments = "@title Test Contract\n@dev Test comment" + solc_select.switch_global_version("0.5.16", always_install=True) + slither = Slither("./tests/custom_comments/contract_comment.sol") + compilation_unit = slither.compilation_units[0] + contract = compilation_unit.get_contract_from_name("A")[0] + + assert contract.comments == comments + + # Test with legacy AST + comments = "@title Test Contract\n@dev Test comment" + solc_select.switch_global_version("0.5.16", always_install=True) + slither = Slither("./tests/custom_comments/contract_comment.sol", solc_force_legacy_json=True) + compilation_unit = slither.compilation_units[0] + contract = compilation_unit.get_contract_from_name("A")[0] + + assert contract.comments == comments + + def test_using_for_top_level_same_name() -> None: solc_select.switch_global_version("0.8.15", always_install=True) slither = Slither("./tests/ast-parsing/using-for-3-0.8.0.sol")