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

support ignoreConstructors option for no-empty-blocks #418

Conversation

hackartists
Copy link
Contributor

A new example

You can set ignoreConstructors simlar with func-visibility rule.

{
  "rules": {
    "no-empty-blocks": ["warn", {ignoreConstructors: true}]
  }
}

Related Issue

#242

pscott added a commit to snapshot-labs/sx-evm that referenced this pull request Mar 2, 2023
Orland0x added a commit to snapshot-labs/sx-evm that referenced this pull request Mar 2, 2023
…commit (#93)

* chore: remove global import warnings

* chore: fix 'Explicit mark state visibility' warnings

* chore: use capslock for constant names

* chore: Use camelCase for Contract name

* chore: use mixedCase for function name

* chore: fix unused variables

* chore: fix avoid low level calls

* chore: fix solhint no-inline-assembly

* chore: fix solhint no empty blocks. Could be better once protofire/solhint#418 gets merged

* chore: fix solhint max-states-count

* chore: fix compilation warnings

* chore: add forge build to pre-commit

* chore: add --deny-warnings to CI

* fix: remove --deny-warnings from husky

---------

Co-authored-by: Orland0x <37511817+Orland0x@users.noreply.github.com>
Copy link
Contributor

@juanpcapurro juanpcapurro left a comment

Choose a reason for hiding this comment

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

I like this implementation ✨

However there's a thing I'd like to discuss:

the following is valid with the ignoreConstructors option

constuctor() {}

however the code doesn't do anything useful, and IMO it'd be good practice to
get rid of it.

the case in which this rule (without the option herein defined) is annoying is
where you have a constructor with a member initialization list:

constructor(uint a) Base(a) {}

since the linter complains about the empty code block but you can't get rid of
the constructor definition, since it's necessary to call the base constructor.

Therefore I propose to change the default behaviour of the rule so it always
ignores constructors with both a member initialization list and an empty body
and also always raises an error on an empty constructor without said
initialization

quick patch on top of 584479629 to show what I mean:

diff --git a/lib/rules/best-practises/no-empty-blocks.js b/lib/rules/best-practises/no-empty-blocks.js
index ba14fa5..1cd88dc 100644
--- a/lib/rules/best-practises/no-empty-blocks.js
+++ b/lib/rules/best-practises/no-empty-blocks.js
@@ -54,0 +55 @@ class NoEmptyBlocksChecker extends BaseChecker {
+    node.body.modifiers = node.modifiers
@@ -58 +59 @@ class NoEmptyBlocksChecker extends BaseChecker {
-    if (node.isConstructor && this.ignoreConstructors) {
+    if (node.isConstructor && node.modifiers.length > 0) {
diff --git a/test/rules/best-practises/no-empty-blocks.js b/test/rules/best-practises/no-empty-blocks.js
index 83ae325..5727461 100644
--- a/test/rules/best-practises/no-empty-blocks.js
+++ b/test/rules/best-practises/no-empty-blocks.js
@@ -29,0 +30 @@ describe('Linter - no-empty-blocks', () => {
+    contractWith('constructor (uint256 a) Base(a) {  }'),
@@ -117,10 +117,0 @@ describe('Linter - no-empty-blocks', () => {
-  it('should not raise error for constructor when ignoreConstructors is set to true', () => {
-    const code = contractWith(`constructor () {}`)
-
-    const report = linter.processStr(code, {
-      rules: { 'no-empty-blocks': ['warn', { ignoreConstructors: true }] },
-    })
-
-    assertNoWarnings(report)
-  })
-

what do y'all think?

@hackartists
Copy link
Contributor Author

hackartists commented Mar 3, 2023

@juanpcapurro Thank you for your comments. I agree your comments because I mostly use empty constructor only for inherited contract.
However, it might have inteded empty block of constructor. It is because the solidity doesn't allow to skip constructor except for abstract contract. For example, you can often see as below code in javascript if the class don't need to do something when instantiation.

const instance = new Class();

In this case, javascript allow us to skip writing constructor, but the solidity.
As the above reason, it is the reason why I declare ignoreConstructors.

But, your comments can work for some, so how about adding an optional setting as belows.

Firstly, it will keep the current behavior as below.

{
      rules: { 'no-empty-blocks': ['warn', { ignoreConstructors: true }] },
}

Additinally, it allows to configure more detail as below.

{
      rules: { 'no-empty-blocks': ['warn', { ignoreConstructors: { mustHaveModifiers: true } }] },
}

How do you think about the above optional setting?
(Or we might consider about ignoreConstructors for all constructor and ignoreInheritedConstructors for contructors having modifiers.)

@juanpcapurro
Copy link
Contributor

However, it might have inteded empty block of constructor. It is because the solidity doesn't allow to skip constructor except for abstract contract

I'm not sure I follow you. I just did a small test with the default contract for a forge init and it doesn't have a constructor and compiles just fine:

pragma solidity ^0.8.13;

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}

Could you provide an example of a contract that doesn't compile because of not having a constructor (including solidity version)?

or do you mean something else?

@hackartists
Copy link
Contributor Author

@juanpcapurro oh, thank you, good to me. And I will reflect your feedback.

@hackartists hackartists force-pushed the feat/ignore-empty-block-for-constructor branch from b117715 to 9e95930 Compare March 8, 2023 07:49
@hackartists hackartists force-pushed the feat/ignore-empty-block-for-constructor branch from 9e95930 to c81ffe6 Compare March 8, 2023 07:53
@hackartists
Copy link
Contributor Author

hackartists commented Mar 8, 2023

@juanpcapurro I've pushed the commit reflecting your feedback.
By the commit, the configuration is reverted to same as origin, namely simple string.
Therefore, for all no-empty-block, it will disappear warning for inherited constructor having modifiers.

@juanpcapurro
Copy link
Contributor

@dbale-altoros Could you enable CI for this PR?

@dbale-altoros
Copy link
Collaborator

@dbale-altoros Could you enable CI for this PR?

@juanpcapurro done

Copy link
Contributor

@juanpcapurro juanpcapurro left a comment

Choose a reason for hiding this comment

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

the blocking issue currently is wether to modify the AST or not, and the redundant testcases are secondary

@@ -113,6 +115,26 @@ describe('Linter - no-empty-blocks', () => {
assertNoWarnings(report)
})

it('should raise error for empty block of [constructor]', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this whole hunk redundant with the two above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. They were redundant so I've removed them.

assertWarnsCount(report, 1)
})

it('should not raise error for empty block of inherited [constructor] having modifiers', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

what do the brackets mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically, it was removed. But I denote the syntax keywards by brackets as the way others do

@@ -27,7 +27,15 @@ class NoEmptyBlocksChecker extends BaseChecker {
this._validateContractPartsCount(node)
}

FunctionDefinition(node) {
node.body.shouldIgnore = node.isConstructor && node.modifiers.length > 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Keep in mind the AST is shared for all visitors, so this property will exist in the Blocks that are bodies of a function in all rule definitions that are executed after this one.

Therefore, an effort should be made not to pollute the AST with information not globally relevant.

My personally preferred approach would be to add the isConstructor and modifiers fields of the FunctionDefinition node to its body, and then assert isConstructors && modifiers.length > 0 in the listener for Block nodes.

Having those two names in the Block node makes it more clear what they are if they show up in the execution of another rule.

However, I'm not sure that it's a good practise to modify the AST that's gonna be used by other rules, and I'd like the opinion of @fvictorio on this topic. (I grepped a bit and couldn't find many usages of the node variable as an lvalue)

If it's problematic to modify the AST in this way, then you'd have to store the information of what blocks to ignore in the ComplexityScore class. One such way could be to have an object whose keys are a string that looks like ${range[0]-range[1]}, and it's value is wether to ignore the block or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

The way this is done in other rules is by adding a field to the checker instance. So:

this.shouldIgnore = ...

Agree that modifying the node is not a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@fvictorio @juanpcapurro I agree what you've mentioned. Howeve whether to be constructor could be validated in FunctionDefinition. Therefore, I needed to recognize if its block is passed by constructor or not. Do you have any other good idea to pass it to the block.

Copy link
Contributor

Choose a reason for hiding this comment

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

What about using node.parent in the Block callback to check if the block corresponds to a constructor, and then checking if there are modifiers?

Something similar to that is done in a following line to check if the block corresponds to a fallback function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@fvictorio Thank for good to me. I've change it to use node.parent and checked it working properly.

@dbale-altoros
Copy link
Collaborator

@hackartists thanks for this contribution!!
It is a great addition
Well done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants