Skip to content

Getting started#

Implementing a custom detector or printer is a very efficient way to extend Wake's detection and analysis capabilities.

Both detectors and printers may be implemented as project-specific or global.

Tip

Built-in detectors and printers may serve as a good starting point for implementing custom detectors and printers.

Using a template#

The best way to get started is to use

wake init detector detector-name

or

wake init printer printer-name

commands, which will create a template detector or printer in ./detectors or ./printers respectively. By supplying the --global flag, the template will be created in $XDG_DATA_HOME/wake/global-detectors or $XDG_DATA_HOME/wake/global-printers instead.

Tip

If working in VS Code with the Tools for Solidity extension installed, the same can be achieved by running the following commands in the command palette:

  • Tools for Solidity: New Detector,
  • Tools for Solidity: New Global Detector,
  • Tools for Solidity: New Printer,
  • Tools for Solidity: New Global Printer.

Detector & printer structure#

Both template detectors and printers are implemented as a minimal Python class:

Detector Printer
class MyDetectorDetector(Detector):
    _detections: List[DetectorResult]

    def __init__(self) -> None:
        self._detections = []

    def detect(self) -> List[DetectorResult]:
        return self._detections

    @detector.command(name="my-detector")
    def cli(self) -> None:
        pass
class MyPrinterPrinter(Printer):
    def print(self) -> None:
        pass

    @printer.command(name="my-printer")
    def cli(self) -> None:
        pass

Detectors define the detect method, which returns a list of detections. Printers define the print method, which prints the results of the analysis.

Tip

Printers may print information from any method (even from __init__ and cli), not just from print.

On the contrary, printers do not need to print anything at all. For example, a printer may be used to generate a file with the results of the analysis.

Command-line interface#

Detector and Printer subclasses should implement a command-line interface (CLI) method using the Click library. The name of the Click command determines the name of the detector or printer. Both detectors and printers may accept additional arguments and options, for example:

@printer.command(name="my-printer")
@click.argument(
    "modifier",
    type=str,
    required=True,
    help="Name of the modifier to analyze.",
)
@click.option(
    "--follow-function-calls/--no-follow-function-calls",
    is_flag=True,
    default=False,
    help="Follow function calls in the modifier.",
)

See the Detector configuration and Printer configuration sections for how to set the values of arguments and options when running detectors and printers.

Default values for detectors

Detectors must always provide default values for all arguments and options.

This is because detectors may be run with wake detect all, where passing detector-specific arguments and options is not possible. The same is true for the LSP server, which runs detectors in the background.

Inherited attributes and methods#

Both Detector and Printer classes inherit from the Visitor class, which provides visit_ methods for all types of Solidity abstract syntax tree (AST) nodes.

Wake builds on the AST and provides an intermediate representation (IR) model, which is an extension of the AST with additional information and fixes for incorrect or missing information. Refer to the wake.ir API reference for more information.

The visit_ methods accept a single argument, which is the IR node to be visited, for example:

def visit_function_definition(self, node: ir.FunctionDefinition) -> None:
    pass

Visit functions are automatically called by the execution engine when running the detector or printer.

Additionally, there are two methods for generating links from an IR node or from a source code location:

def generate_link(self, node: ir.IrAbc) -> str:
    ...

def generate_link_from_line_col(self, path: Union[str, Path], line: int, col: int) -> str:
    ...

Example

The methods may be used in the following way:

def visit_function_definition(self, node: ir.FunctionDefinition) -> None:
    link = f"[link={self.generate_link(node)}]{node.canonical_name}[/link]"

Refer to the Rich documentation for more information about the syntax of console links.

Visit modes#

All detectors and printers accept unlimited number of paths to Solidity source code files and directories. The paths are passed as command-line arguments, for example:

wake detect my-detector contracts/utils

When there are any paths specified, the visit_ functions are called only for IR nodes in Solidity files in the specified paths. However, some detectors and printers may need to visit all IR nodes in the project, to perform the analysis correctly. In such cases, the detector or printer should override the visit_mode property and return "all" instead of the default "paths".

class MyDetectorDetector(Detector):
    ...

    @property
    def visit_mode(self) -> str:
        return "all"

When the visit_mode is set to "all", the detector or printer is responsible for filtering out the detections or printed information that are not relevant to the specified paths. For example:

class MyPrinterPrinter(Printer):
    ...

    @property
    def visit_mode(self):
        return "all"

    def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
        from wake.utils import is_relative_to

        if not any(is_relative_to(node.source_unit.file, p) for p in self.paths):
            return
        ...

Execution order#

The methods of detectors and printers are executed in the following order:

  1. __init__,
  2. Click command-line entry point (cli or any other method decorated with @detector.command() or @printer.command()),
  3. visit_mode,
  4. visit_ methods in an unspecified order,
  5. detect for detectors or print for printers.