Repository: crytic/slither

Issue:
[Bug]: `cache-array-length` throws "StateVariable object has no attribute view"
### Describe the issue:

We have this [github workflow file ](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431075215/workflow)that runs slither in a CI runner

Recently, since the release of slither `0.9.5`, this workflow started to [fail](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431075215/jobs/9877217707) with the following error:
```
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 137, in _is_loop_referencing_array_length
    if isinstance(op, HighLevelCall) and not op.function.view and not op.function.pure:
AttributeError: 'StateVariable' object has no attribute 'view'
```

The latest slither release (`0.9.5`) introduced a new `cache-array-length` [detector](https://github.com/crytic/slither/pull/1694)

If I [exclude](https://github.com/rndquu/ubiquity-dollar/pull/18/commits/1a07aba316fb0188095ccee12da5929a1be2dad5) the `cache-array-length` from slither config then the workflow [works fine](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431150479/jobs/9877337547) again

What am I doing wrong?

### Code example to reproduce the issue:

Check this [PR](https://github.com/rndquu/ubiquity-dollar/pull/18)

There are 2 commits:
- the 1st one([ci: run slither on PR once](https://github.com/rndquu/ubiquity-dollar/pull/18/commits/8774373fffa55a9c9ae1629468c4fb2970c76881)) simply runs a [slither workflow](https://github.com/rndquu/ubiquity-dollar/blob/ci/fix-slither/.github/workflows/slither-analysis.yml) which produces an [error](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431075215/jobs/9877217707)
- the 2nd one ([ci: exclude cache-array-length detector](https://github.com/rndquu/ubiquity-dollar/pull/18/commits/1a07aba316fb0188095ccee12da5929a1be2dad5)) excludes the `cache-array-length` detector and the workflow works fine

### Version:

`0.9.5`

### Relevant log output:

```
Run yarn workspace @ubiquity/contracts run test:slither
  yarn workspace @ubiquity/contracts run test:slither
  shell: /usr/bin/bash -e {0}
  env:
    pythonLocation: /opt/hostedtoolcache/Python/3.10.1[2](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431180883/jobs/9877385161#step:7:2)/x64
    PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/[3](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431180883/jobs/9877385161#step:7:3).10.12/x6[4](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431180883/jobs/9877385161#step:7:4)/lib/pkgconfig
    Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.12/x64
    Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.12/x64
    Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.12/x64
    LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.10.12/x64/lib
    MSYS: winsymlinks:nativestrict
'forge clean' running (wd: /home/runner/work/ubiquity-dollar/ubiquity-dollar/packages/contracts)
'forge build --build-info' running (wd: /home/runner/work/ubiquity-dollar/ubiquity-dollar/packages/contracts)
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 814, in main_impl
    ) = process_all(filename, args, detector_classes, printer_classes)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 102, in process_all
    ) = process_single(compilation, args, detector_classes, printer_classes)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 82, in process_single
    return _process(slither, detector_classes, printer_classes)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 132, in _process
    detector_resultss = slither.run_detectors()
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/slither.py", line 226, in run_detectors
    results = [d.detect() for d in self._detectors]
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/slither.py", line 226, in <listcomp>
    results = [d.detect() for d in self._detectors]
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/abstract_detector.py", line 187, in detect
    for r in [output.data for output in self._detect()]:
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 209, in _detect
    non_optimal_array_len_usages = CacheArrayLength._get_non_optimal_array_len_usages(
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 201, in _get_non_optimal_array_len_usages
    CacheArrayLength._get_non_optimal_array_len_usages_for_function(f)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 188, in _get_non_optimal_array_len_usages_for_function
    CacheArrayLength._handle_loops(f.nodes, non_optimal_array_len_usages)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 177, in _handle_loops
    if not CacheArrayLength._is_loop_referencing_array_length(
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 142, in _is_loop_referencing_array_length
    if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 142, in _is_loop_referencing_array_length
    if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 142, in _is_loop_referencing_array_length
    if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth):
  [Previous line repeated 1 more time]
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 137, in _is_loop_referencing_array_length
    if isinstance(op, HighLevelCall) and not op.function.view and not op.function.pure:
AttributeError: 'StateVariable' object has no attribute 'view'
ERROR:root:Error in .
ERROR:root:Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 814, in main_impl
    ) = process_all(filename, args, detector_classes, printer_classes)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 102, in process_all
    ) = process_single(compilation, args, detector_classes, printer_classes)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 82, in process_single
    return _process(slither, detector_classes, printer_classes)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/__main__.py", line 132, in _process
    detector_resultss = slither.run_detectors()
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/slither.py", line 226, in run_detectors
    results = [d.detect() for d in self._detectors]
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/slither.py", line 226, in <listcomp>
    results = [d.detect() for d in self._detectors]
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/abstract_detector.py", line 187, in detect
    for r in [output.data for output in self._detect()]:
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 209, in _detect
    non_optimal_array_len_usages = CacheArrayLength._get_non_optimal_array_len_usages(
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 201, in _get_non_optimal_array_len_usages
    CacheArrayLength._get_non_optimal_array_len_usages_for_function(f)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 188, in _get_non_optimal_array_len_usages_for_function
    CacheArrayLength._handle_loops(f.nodes, non_optimal_array_len_usages)
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 177, in _handle_loops
    if not CacheArrayLength._is_loop_referencing_array_length(
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 142, in _is_loop_referencing_array_length
    if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 142, in _is_loop_referencing_array_length
    if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 142, in _is_loop_referencing_array_length
    if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth):
  [Previous line repeated 1 more time]
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/site-packages/slither/detectors/operations/cache_array_length.py", line 137, in _is_loop_referencing_array_length
    if isinstance(op, HighLevelCall) and not op.function.view and not op.function.pure:
AttributeError: 'StateVariable' object has no attribute 'view'

Error: Process completed with exit code 2[5](https://github.com/rndquu/ubiquity-dollar/actions/runs/5431180883/jobs/9877385161#step:7:5)5.
```


Discussion:

Resolving patch:
diff --git a/slither/detectors/operations/cache_array_length.py b/slither/detectors/operations/cache_array_length.py
--- a/slither/detectors/operations/cache_array_length.py
+++ b/slither/detectors/operations/cache_array_length.py
@@ -4,7 +4,6 @@
 from slither.core.declarations import Function
 from slither.core.expressions import BinaryOperation, Identifier, MemberAccess, UnaryOperation
 from slither.core.solidity_types import ArrayType
-from slither.core.source_mapping.source_mapping import SourceMapping
 from slither.core.variables import StateVariable
 from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
 from slither.slithir.operations import Length, Delete, HighLevelCall
@@ -134,7 +133,12 @@ def _is_loop_referencing_array_length(
                         and op.expression.expression.value == array
                     ):
                         return True
-                if isinstance(op, HighLevelCall) and not op.function.view and not op.function.pure:
+                if (
+                    isinstance(op, HighLevelCall)
+                    and isinstance(op.function, Function)
+                    and not op.function.view
+                    and not op.function.pure
+                ):
                     return True
 
         for son in node.sons:
@@ -144,7 +148,7 @@ def _is_loop_referencing_array_length(
         return False
 
     @staticmethod
-    def _handle_loops(nodes: List[Node], non_optimal_array_len_usages: List[SourceMapping]) -> None:
+    def _handle_loops(nodes: List[Node], non_optimal_array_len_usages: List[Node]) -> None:
         """
         For each loop, checks if it has a comparison with `length` array member and, if it has, checks whether that
         array size could potentially change in that loop.
@@ -180,21 +184,21 @@ def _handle_loops(nodes: List[Node], non_optimal_array_len_usages: List[SourceMa
                     non_optimal_array_len_usages.append(if_node)
 
     @staticmethod
-    def _get_non_optimal_array_len_usages_for_function(f: Function) -> List[SourceMapping]:
+    def _get_non_optimal_array_len_usages_for_function(f: Function) -> List[Node]:
         """
         Finds non-optimal usages of array length in loop conditions in a given function.
         """
-        non_optimal_array_len_usages: List[SourceMapping] = []
+        non_optimal_array_len_usages: List[Node] = []
         CacheArrayLength._handle_loops(f.nodes, non_optimal_array_len_usages)
 
         return non_optimal_array_len_usages
 
     @staticmethod
-    def _get_non_optimal_array_len_usages(functions: List[Function]) -> List[SourceMapping]:
+    def _get_non_optimal_array_len_usages(functions: List[Function]) -> List[Node]:
         """
         Finds non-optimal usages of array length in loop conditions in given functions.
         """
-        non_optimal_array_len_usages: List[SourceMapping] = []
+        non_optimal_array_len_usages: List[Node] = []
 
         for f in functions:
             non_optimal_array_len_usages += (
@@ -211,9 +215,10 @@ def _detect(self):
         )
         for usage in non_optimal_array_len_usages:
             info = [
-                "Loop condition at ",
-                usage,
-                " should use cached array length instead of referencing `length` member "
+                "Loop condition ",
+                f"`{usage.source_mapping.content}` ",
+                f"({usage.source_mapping}) ",
+                "should use cached array length instead of referencing `length` member "
                 "of the storage array.\n ",
             ]
             res = self.generate_result(info)


Test patch:
diff --git a/tests/e2e/detectors/snapshots/detectors__detector_CacheArrayLength_0_8_17_CacheArrayLength_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_CacheArrayLength_0_8_17_CacheArrayLength_sol__0.txt
--- a/tests/e2e/detectors/snapshots/detectors__detector_CacheArrayLength_0_8_17_CacheArrayLength_sol__0.txt
+++ b/tests/e2e/detectors/snapshots/detectors__detector_CacheArrayLength_0_8_17_CacheArrayLength_sol__0.txt
@@ -1,18 +1,20 @@
-Loop condition at i < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#36) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `j < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#109) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at i_scope_22 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#166) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#161) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at j_scope_11 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#108) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#172) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at i_scope_6 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#79) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `j < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#126) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at i_scope_21 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#160) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `k < array2.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#133) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at k_scope_9 < array2.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#98) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#68) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at k_scope_17 < array2.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#132) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `k < array2.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#99) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at j_scope_15 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#125) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#167) should use cached array length instead of referencing `length` member of the storage array.
  
-Loop condition at i_scope_4 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#67) should use cached array length instead of referencing `length` member of the storage array.
+Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#37) should use cached array length instead of referencing `length` member of the storage array.
+ 
+Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#80) should use cached array length instead of referencing `length` member of the storage array.
  
diff --git a/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol b/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol
--- a/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol
+++ b/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol
@@ -9,6 +9,7 @@ contract CacheArrayLength
 
     S[] array;
     S[] array2;
+    uint public x;
 
     function h() external
     {
@@ -167,5 +168,10 @@ contract CacheArrayLength
         {
             this.h_view();
         }
+        // array not modified and it cannot be changed in a function call since x is a public state variable
+        for (uint i = 0; i < array.length; i++) // warning should appear
+        {
+            this.x();
+        }
     }   
 }
\ No newline at end of file
diff --git a/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol-0.8.17.zip b/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol-0.8.17.zip
Binary files a/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol-0.8.17.zip and b/tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol-0.8.17.zip differ