diff --git a/docs/static-analysis/printers/modifiers.md b/docs/static-analysis/printers/modifiers.md new file mode 100644 index 000000000..ba03b02bd --- /dev/null +++ b/docs/static-analysis/printers/modifiers.md @@ -0,0 +1,17 @@ +# Modifiers printer + +Prints modifiers with their usages. + +## Example + +
+--8<-- "docs/static-analysis/printers/modifiers.svg" +
+ +## Parameters + +| Command-line name | TOML name | Type | Default value | Description | +|---------------------------------------------------------------------------|--------------------------------|-------------|---------------|-----------------------------------------------------------------| +| `--name` (multiple) | `names` | `List[str]` | `[]` | Modifier names. | +| `--canonical-names`/
`--no-canonical-names` | `canonical_names` | `bool` | `True` | Whether to print (full) canonical names instead of local names. | +| `--code-snippets`/
`--no-code-snippets` | `code_snippets` | `bool` | `True` | Whether to print modifier source code snippets. | diff --git a/docs/static-analysis/printers/modifiers.svg b/docs/static-analysis/printers/modifiers.svg new file mode 100644 index 000000000..157205f66 --- /dev/null +++ b/docs/static-analysis/printers/modifiers.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wake print modifiers + + + + + + + + + + ╭──── Modifier noDelegateCall ────╮╭── Invocations ───╮ +modifiernoDelegateCall(){modifyPosition +checkNotDelegateCall();take +_;donate +}settle +╰─────────────────────────────────╯swap +mint +╰──────────────────╯ +╭───────── Modifier onlyOwner ─────────╮╭─────── Invocations ────────╮ +modifieronlyOwner(){setProtocolFeeController +if(msg.sender!=owner)setOwner +revertInvalidCaller();╰────────────────────────────╯ +_; +} +╰──────────────────────────────────────╯ +╭─────── Modifier onlyByLocker ────────╮╭── Invocations ───╮ +modifieronlyByLocker(){mint +addresslocker=modifyPosition +lockData.getActiveLock();donate +if(msg.sender!=locker)take +revertLockedBy(locker);settle +_;swap +}╰──────────────────╯ +╰──────────────────────────────────────╯ + + + + diff --git a/mkdocs.yml b/mkdocs.yml index a5632988e..b253efe65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,6 +90,7 @@ nav: - Imports graph: 'static-analysis/printers/imports-graph.md' - Inheritance graph: 'static-analysis/printers/inheritance-graph.md' - Inheritance tree: 'static-analysis/printers/inheritance-tree.md' + - Modifiers: 'static-analysis/printers/modifiers.md' - State changes: 'static-analysis/printers/state-changes.md' - Storage layout: 'static-analysis/printers/storage-layout.md' - Tokens: 'static-analysis/printers/tokens.md' diff --git a/wake_printers/__init__.py b/wake_printers/__init__.py index c34718fc2..b2cd0f91e 100644 --- a/wake_printers/__init__.py +++ b/wake_printers/__init__.py @@ -3,6 +3,7 @@ from .imports_graph import ImportsGraphPrinter from .inheritance_graph import InheritanceGraphPrinter from .inheritance_tree import InheritanceTreePrinter +from .modifiers import ModifiersPrinter from .state_changes import StateChangesPrinter from .storage_layout import StorageLayoutPrinter from .tokens import TokensPrinter diff --git a/wake_printers/modifiers.py b/wake_printers/modifiers.py new file mode 100644 index 000000000..603bc8c7a --- /dev/null +++ b/wake_printers/modifiers.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from typing import Iterable, List, Set, Tuple + +import networkx as nx +import rich_click as click +from rich import print + +import wake.ir as ir +import wake.ir.types as types +from wake.cli import SolidityName +from wake.printers import Printer, printer + + +class ModifiersPrinter(Printer): + _names: Set[str] + _canonical_names: bool + _snippets: bool + _modifiers: List[ir.ModifierDefinition] + + def __init__(self): + self._modifiers = [] + + def _generate_layout( + self, mod: ir.ModifierDefinition, invocations: Iterable[ir.FunctionDefinition] + ): + from os import get_terminal_size + + from rich.columns import Columns + from rich.markup import render + from rich.panel import Panel + from rich.syntax import Syntax + + # Get the terminal size + terminal_width, _ = get_terminal_size() + # Calculate the width for each panel + panel_width = terminal_width // 2 - 1 + # Create a Syntax object for the modifier code with Python syntax highlighting + code_syntax = Syntax( + mod.source, + "solidity", + dedent=True, + line_numbers=False, + tab_size=2, + word_wrap=True, + ) + # Create a panel for the modifier code with the calculated width + mod_name = mod.canonical_name if self._canonical_names else mod.name + code_panel = Panel( + code_syntax, + title=render(f"Modifier [link={self.generate_link(mod)}]{mod_name}[/link]"), + width=panel_width, + expand=False, + ) + # Create a panel for the invocations with the calculated width + if self._canonical_names: + invocations_str = "\n".join( + [ + f"- [link={self.generate_link(invocation)}]{invocation.canonical_name}[/]" + for invocation in invocations + ] + ) + else: + invocations_str = "\n".join( + [ + f"- [link={self.generate_link(invocation)}]{invocation.name}[/]" + for invocation in invocations + ] + ) + invocations_panel = Panel( + invocations_str, title="Invocations", width=panel_width, expand=False + ) + # Create a Columns layout with the two panels + columns = Columns([code_panel, invocations_panel], equal=True, expand=False) + + print(columns) + + def print(self) -> None: + if not self._snippets: + for modifier in sorted(self._modifiers, key=lambda m: m.canonical_name): + functions: Set[ir.FunctionDefinition] = set() + for ref in modifier.references: + if isinstance(ref, ir.IdentifierPathPart): + ref = ref.underlying_node + elif isinstance(ref, ir.ExternalReference): + # should not happen + continue + p = ref.parent + if not isinstance(p, ir.ModifierInvocation): + self.logger.warning( + f"Unexpected modifier reference parent: {p}\n{p.source}" + ) + continue + functions.add(p.parent) + + if len(functions) == 0: + print( + f"[link={self.generate_link(modifier)}]{modifier.canonical_name}[/link] is not used anywhere" + ) + else: + print( + f"[link={self.generate_link(modifier)}]{modifier.canonical_name}[/link] is used in:" + ) + for function in sorted(functions, key=lambda f: f.canonical_name): + print( + f" [link={self.generate_link(function)}]{function.canonical_name}[/link]" + ) + + return + + for modifier in sorted(self._modifiers, key=lambda m: m.canonical_name): + functions = set() + for ref in modifier.references: + if isinstance(ref, ir.IdentifierPathPart): + ref = ref.underlying_node + elif isinstance(ref, ir.ExternalReference): + # should not happen + continue + p = ref.parent + if not isinstance(p, ir.ModifierInvocation): + self.logger.warning( + f"Unexpected modifier reference parent: {p}\n{p.source}" + ) + continue + functions.add(p.parent) + self._generate_layout(modifier, functions) + + def visit_modifier_definition(self, node: ir.ModifierDefinition): + if ( + len(self._names) == 0 + or node.name in self._names + or node.canonical_name in self._names + ): + self._modifiers.append(node) + + @printer.command(name="modifiers") + @click.option( + "--name", + "-n", + "names", + type=SolidityName("modifier", case_sensitive=False), + multiple=True, + help="Modifier names", + ) + @click.option( + "--canonical-names/--no-canonical-names", + default=True, + help="Use (full) canonical names instead of local names", + ) + @click.option( + "--snippets/--no-snippets", + default=True, + help="Show code snippets of modifiers", + ) + def cli( + self, names: Tuple[str, ...], canonical_names: bool, snippets: bool + ) -> None: + """ + Print modifiers and their usage. + """ + self._names = set(names) + self._canonical_names = canonical_names + self._snippets = snippets