Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: BACK-8055 Detect unused definitions #154

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/erc7730/lint/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod

from erc7730.common.output import OutputAdder
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor


Expand All @@ -13,5 +14,5 @@ class ERC7730Linter(ABC):
"""

@abstractmethod
def lint(self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
def lint(self, descriptor: ResolvedERC7730Descriptor | InputERC7730Descriptor, out: OutputAdder) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be changed like this, linter runs on resolved descriptors. This only tricks the (python) linter but at runtime it takes resolved descriptors (see main class)

raise NotImplementedError()
25 changes: 14 additions & 11 deletions src/erc7730/lint/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from erc7730.lint.lint_base import MultiLinter
from erc7730.lint.lint_transaction_type_classifier import ClassifyTransactionTypeLinter
from erc7730.lint.lint_validate_abi import ValidateABILinter
from erc7730.lint.lint_validate_definitions import DefinitionLinter
from erc7730.lint.lint_validate_display_fields import ValidateDisplayFieldsLinter
from erc7730.list.list import get_erc7730_files
from erc7730.model.input.descriptor import InputERC7730Descriptor
Expand Down Expand Up @@ -50,13 +51,8 @@ def lint_all(paths: list[Path], out: OutputAdder) -> int:
:param out: output adder
:return: number of files checked
"""
linter = MultiLinter(
[
ValidateABILinter(),
ValidateDisplayFieldsLinter(),
ClassifyTransactionTypeLinter(),
]
)
input_linter = DefinitionLinter()
output_linter = MultiLinter([ValidateABILinter(), ValidateDisplayFieldsLinter(), ClassifyTransactionTypeLinter()])

files = list(get_erc7730_files(*paths, out=out))

Expand All @@ -70,7 +66,11 @@ def label(f: Path) -> Path | None:
print(f"🔍 checking {len(files)} descriptor files…\n")

with ThreadPoolExecutor() as executor:
for future in (executor.submit(lint_file, file, linter, out, label(file)) for file in files):
for future in (executor.submit(lint_file, file, input_linter, out, label(file)) for file in files):
future.result()

with ThreadPoolExecutor() as executor:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why run twice ?

for future in (executor.submit(lint_file, file, output_linter, out, label(file)) for file in files):
future.result()

return len(files)
Expand All @@ -91,6 +91,9 @@ def lint_file(path: Path, linter: ERC7730Linter, out: OutputAdder, show_as: Path

with BufferAdder(file_out, prolog=f"➡️ checking [bold]{label}[/bold]…", epilog="") as out, ExceptionsToOutput(out):
input_descriptor = InputERC7730Descriptor.load(path)
resolved_descriptor = ERC7730InputToResolved().convert(input_descriptor, out)
if resolved_descriptor is not None:
linter.lint(resolved_descriptor, out)
if isinstance(linter, DefinitionLinter):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can see here that it doesn't fit. Interface is not good if you have to do special cases here

linter.lint(input_descriptor, out)
else:
resolved_descriptor = ERC7730InputToResolved().convert(input_descriptor, out)
if resolved_descriptor is not None:
linter.lint(resolved_descriptor, out)
3 changes: 2 additions & 1 deletion src/erc7730/lint/lint_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from erc7730.common.output import OutputAdder
from erc7730.lint import ERC7730Linter
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor


Expand All @@ -13,6 +14,6 @@ def __init__(self, linters: list[ERC7730Linter]):
self.lints = linters

@override
def lint(self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
def lint(self, descriptor: ResolvedERC7730Descriptor | InputERC7730Descriptor, out: OutputAdder) -> None:
for linter in self.lints:
linter.lint(descriptor, out)
20 changes: 11 additions & 9 deletions src/erc7730/lint/lint_transaction_type_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from erc7730.lint.classifier import TxClass
from erc7730.lint.classifier.abi_classifier import ABIClassifier
from erc7730.lint.classifier.eip712_classifier import EIP712Classifier
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.resolved.context import EIP712Schema, ResolvedContractContext, ResolvedEIP712Context
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor
from erc7730.model.resolved.display import ResolvedDisplay, ResolvedFormat
Expand All @@ -18,15 +19,16 @@ class ClassifyTransactionTypeLinter(ERC7730Linter):
"""

@override
def lint(self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
if descriptor.context is None:
return None
if (tx_class := self._determine_tx_class(descriptor)) is None:
# could not determine transaction type
return None
if (display := descriptor.display) is None:
return None
DisplayFormatChecker(tx_class, display).check(out)
def lint(self, descriptor: ResolvedERC7730Descriptor | InputERC7730Descriptor, out: OutputAdder) -> None:
if isinstance(descriptor, ResolvedERC7730Descriptor):
if descriptor.context is None:
return None
if (tx_class := self._determine_tx_class(descriptor)) is None:
# could not determine transaction type
return None
if (display := descriptor.display) is None:
return None
DisplayFormatChecker(tx_class, display).check(out)

@classmethod
def _determine_tx_class(cls, descriptor: ResolvedERC7730Descriptor) -> TxClass | None:
Expand Down
3 changes: 2 additions & 1 deletion src/erc7730/lint/lint_validate_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from erc7730.common.abi import compute_signature, get_functions
from erc7730.common.output import OutputAdder
from erc7730.lint import ERC7730Linter
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.resolved.context import ResolvedContractContext, ResolvedEIP712Context
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor

Expand All @@ -17,7 +18,7 @@ class ValidateABILinter(ERC7730Linter):
"""

@override
def lint(self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
def lint(self, descriptor: ResolvedERC7730Descriptor | InputERC7730Descriptor, out: OutputAdder) -> None:
if isinstance(descriptor.context, ResolvedEIP712Context):
return self._validate_eip712_schemas(descriptor.context, out)
if isinstance(descriptor.context, ResolvedContractContext):
Expand Down
31 changes: 31 additions & 0 deletions src/erc7730/lint/lint_validate_definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import final, override

from erc7730.common.output import OutputAdder
from erc7730.lint import ERC7730Linter
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.input.display import InputReference
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor


@final
class DefinitionLinter(ERC7730Linter):
"""Check that parameters under definitions are used in formats section"""

@override
def lint(self, descriptor: ResolvedERC7730Descriptor | InputERC7730Descriptor, out: OutputAdder) -> None:
if isinstance(descriptor, InputERC7730Descriptor) and descriptor.display.definitions is not None:
for name, _ in descriptor.display.definitions.items():
found = False
for _, format in descriptor.display.formats.items():
if found is False:
for field in format.fields:
if (
isinstance(field, InputReference)
and field.ref.__str__() == f"$.display.definitions.{name}"
):
found = True
if found is False:
out.error(
title="Unused field definition",
message=f"Field {name} is not used in descriptor formats.",
)
8 changes: 5 additions & 3 deletions src/erc7730/lint/lint_validate_display_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from erc7730.common.abi import function_to_selector
from erc7730.common.output import OutputAdder
from erc7730.lint import ERC7730Linter
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.paths import DataPath, Field
from erc7730.model.paths.path_ops import data_path_ends_with, path_starts_with, to_absolute
from erc7730.model.paths.path_schemas import (
Expand All @@ -27,9 +28,10 @@ class ValidateDisplayFieldsLinter(ERC7730Linter):
"""

@override
def lint(self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
self._validate_eip712_paths(descriptor, out)
self._validate_abi_paths(descriptor, out)
def lint(self, descriptor: ResolvedERC7730Descriptor | InputERC7730Descriptor, out: OutputAdder) -> None:
if isinstance(descriptor, ResolvedERC7730Descriptor):
self._validate_eip712_paths(descriptor, out)
self._validate_abi_paths(descriptor, out)

@classmethod
def _validate_eip712_paths(cls, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
Expand Down
71 changes: 71 additions & 0 deletions tests/lint/resources/calldata-unused-definitions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"context": {
"$id": "AugustusSwapper",
"contract" : {
"abi": "https://github.com/LedgerHQ/ledger-asset-dapps/blob/main/ethereum/paraswap/abis/0x1bd435f3c054b6e901b7b108a0ab7617c808677b.abi.json",
"deployments": [
{
"chainId": 1,
"address": "0x1bd435f3c054b6e901b7b108a0ab7617c808677b"
},
{
"chainId": 1,
"address": "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
},
{
"chainId": 56,
"address": "0x55a0e3b6579972055faa983482aceb4b251dcf15"
},
{
"chainId": 56,
"address": "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
},
{
"chainId": 137,
"address": "0x90249ed4d69d70e709ffcd8bee2c5a566f65dade"
},
{
"chainId": 137,
"address": "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
}
]
}
},

"metadata": {
"owner": "Paraswap"
},

"display": {
"definitions": {
"amountIn": {
"label": "Income Amount",
"format": "amount"
},
"amountOut": {
"label": "Outcome Amount",
"format": "amount"
},
"amountOutIn": {
"label": "Outcome/Income Amount",
"format": "amount"
}
},
"formats": {
"0xcfc0afeb" : {
"$id": "swapOnZeroXv2",
"fields": [
{
"path": "amountIn",
"$ref": "$.display.definitions.amountIn"
},
{
"path": "amountOut",
"$ref": "$.display.definitions.amountOut"
}
],
"required": ["amountIn", "amountOut"]
}
}
}
}
20 changes: 19 additions & 1 deletion tests/lint/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import pytest

from erc7730.lint.lint import lint_all_and_print_errors
from erc7730.common.output import SetOutputAdder
from erc7730.lint.lint import lint_all, lint_all_and_print_errors
from tests.cases import path_id
from tests.files import ERC7730_DESCRIPTORS

RESOURCES = Path(__file__).resolve().parent / "resources"


@pytest.mark.parametrize("input_file", ERC7730_DESCRIPTORS, ids=path_id)
def test_registry_files(input_file: Path) -> None:
Expand All @@ -18,3 +21,18 @@ def test_registry_files(input_file: Path) -> None:
pytest.skip("Descriptor uses literal constants instead of token paths, which is not supported yet")

assert lint_all_and_print_errors([input_file])


def test_unused_definition_detected_by_linter() -> None:
"""
Test unused field definition is detected within an ERC-7730 file
"""
output_adder = SetOutputAdder()

input_file = RESOURCES / "calldata-unused-definitions.json"

lint_all([input_file], output_adder)

assert output_adder.has_errors is True

assert "Unused field definition" in (set(map(lambda x: x.title, output_adder.outputs)))
Loading