Repository: crytic/slither

Issue:
Support top-level structs and enums
The full set of allowed top-level constructs is given by the sourceUnit key here:
https://solidity.readthedocs.io/en/v0.6.8/grammar.html#language-grammar

Slither seems to only support the subset here:
https://github.com/crytic/slither/blob/master/slither/solc_parsing/slitherSolc.py#L105


Discussion:
Hi @dileepfrog, thanks for reporting this.

We will add its support asap

Resolving patch:
diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py
--- a/slither/core/declarations/contract.py
+++ b/slither/core/declarations/contract.py
@@ -51,6 +51,8 @@ def __init__(self):
         self._is_upgradeable = None
         self._is_upgradeable_proxy = None
 
+        self._is_top_level = False
+
 
         self._initial_state_variables = [] # ssa
 
@@ -935,6 +937,19 @@ def is_incorrectly_constructed(self):
         """
         return self._is_incorrectly_parsed
 
+    @property
+    def is_top_level(self) -> bool:
+        """
+        The "TopLevel" contract is used to hold structures and enums defined at the top level
+        ie. structures and enums that are represented outside of any contract
+        :return:
+        """
+        return self._is_top_level
+
+    @is_top_level.setter
+    def is_top_level(self, t: bool):
+        self._is_top_level = t
+
     # endregion
     ###################################################################################
     ###################################################################################
diff --git a/slither/solc_parsing/slitherSolc.py b/slither/solc_parsing/slitherSolc.py
--- a/slither/solc_parsing/slitherSolc.py
+++ b/slither/solc_parsing/slitherSolc.py
@@ -2,6 +2,10 @@
 import json
 import re
 import logging
+from typing import Optional, List
+
+from slither.core.declarations import Contract
+from slither.exceptions import SlitherException
 
 logging.basicConfig()
 logger = logging.getLogger("SlitherSolcParsing")
@@ -25,6 +29,8 @@ def __init__(self, filename):
 
         self._is_compact_ast = False
 
+        self._top_level_contracts_counter = 0
+
     ###################################################################################
     ###################################################################################
     # region AST
@@ -102,7 +108,11 @@ def _parse_contracts_from_loaded_json(self, data_loaded, filename):
 
         for contract_data in data_loaded[self.get_children()]:
 
-            assert contract_data[self.get_key()] in ['ContractDefinition', 'PragmaDirective', 'ImportDirective']
+            assert contract_data[self.get_key()] in ['ContractDefinition',
+                                                     'PragmaDirective',
+                                                     'ImportDirective',
+                                                     'StructDefinition',
+                                                     'EnumDefinition']
             if contract_data[self.get_key()] == 'ContractDefinition':
                 contract = ContractSolc04(self, contract_data)
                 if 'src' in contract_data:
@@ -123,6 +133,30 @@ def _parse_contracts_from_loaded_json(self, data_loaded, filename):
                 import_directive.set_offset(contract_data['src'], self)
                 self._import_directives.append(import_directive)
 
+            elif contract_data[self.get_key()] in ['StructDefinition', 'EnumDefinition']:
+                # This can only happen for top-level structure and enum
+                # They were introduced with 0.6.5
+                assert self._is_compact_ast # Do not support top level definition for legacy AST
+                fake_contract_data = {
+                    'name': f'SlitherInternalTopLevelContract{self._top_level_contracts_counter}',
+                    'id': -1000, # TODO: determine if collission possible
+                    'linearizedBaseContracts': [],
+                    'fullyImplemented': True,
+                    'contractKind': 'SLitherInternal'
+                }
+                self._top_level_contracts_counter += 1
+                top_level_contract = ContractSolc04(self, fake_contract_data)
+                top_level_contract.is_top_level = True
+                top_level_contract.set_offset(contract_data['src'], self)
+
+                if contract_data[self.get_key()] == 'StructDefinition':
+                    top_level_contract._structuresNotParsed.append(contract_data)  # Todo add proper setters
+                else:
+                    top_level_contract._enumsNotParsed.append(contract_data)  # Todo add proper setters
+
+                self._contractsNotParsed.append(top_level_contract)
+
+
     def _parse_source_unit(self, data, filename):
         if data[self.get_key()] != 'SourceUnit':
             return -1  # handle solc prior 0.3.6
@@ -178,6 +212,9 @@ def _analyze_contracts(self):
         # First we save all the contracts in a dict
         # the key is the contractid
         for contract in self._contractsNotParsed:
+            if contract.name.startswith('SlitherInternalTopLevelContract') and not contract.is_top_level:
+                raise SlitherException("""Your codebase has a contract named 'SlitherInternalTopLevelContract'.
+Please rename it, this name is reserved for Slither's internals""")
             if contract.name in self._contracts:
                 if contract.id != self._contracts[contract.name].id:
                     self._contract_name_collisions[contract.name].append(contract.source_mapping_str)


Test patch: