Skip to content

Commit 8547ad8

Browse files
Merge pull request #705 from protofire/fix-func-name-mixcase-for-constants
fix: exception for snake case in interface
2 parents 0952d8c + 8445f78 commit 8547ad8

File tree

3 files changed

+297
-4
lines changed

3 files changed

+297
-4
lines changed

docs/rules/naming/func-name-mixedcase.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ This rule accepts a string option for rule severity. Must be one of "error", "wa
2626
}
2727
```
2828

29+
### Notes
30+
- SNAKE_CASE allowed only in interfaces when matching constant/immutable getter signatures.
31+
- Return must be elementary, enum, UDVT, or contract/interface.
2932

3033
## Examples
3134
This rule does not have examples.

lib/rules/naming/func-name-mixedcase.js

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ const meta = {
88
docs: {
99
description: 'Function name must be in mixedCase.',
1010
category: 'Style Guide Rules',
11+
notes: [
12+
{
13+
note: 'SNAKE_CASE allowed only in interfaces when matching constant/immutable getter signatures.',
14+
},
15+
{
16+
note: 'Return must be elementary, enum, UDVT, or contract/interface.',
17+
},
18+
],
1119
},
1220

1321
recommended: true,
@@ -19,13 +27,66 @@ const meta = {
1927
class FuncNameMixedcaseChecker extends BaseChecker {
2028
constructor(reporter) {
2129
super(reporter, ruleId, meta)
30+
this._ifaceDepth = 0
31+
}
32+
33+
ContractDefinition(node) {
34+
if (node.kind === 'interface') this._ifaceDepth++
35+
}
36+
37+
'ContractDefinition:exit'(node) {
38+
if (node.kind === 'interface') this._ifaceDepth--
2239
}
2340

2441
FunctionDefinition(node) {
25-
if (naming.isNotMixedCase(node.name) && !node.isConstructor) {
42+
if (node.isConstructor) return
43+
44+
// Strict exception: only getters of constant/immutable in interfaces
45+
const inInterface = this._ifaceDepth > 0
46+
const noParams = (node.parameters?.length || 0) === 0
47+
const isView = node.stateMutability === 'view'
48+
const isSnake = naming.isUpperSnakeCase(node.name)
49+
50+
const returns = node.returnParameters || []
51+
52+
// check return values
53+
const isReturnValid = this.hasSingleAllowedReturn(returns)
54+
55+
if (inInterface && noParams && isView && isSnake && isReturnValid) {
56+
return // allowed
57+
}
58+
59+
if (naming.isNotMixedCase(node.name)) {
2660
this.error(node, 'Function name must be in mixedCase')
2761
}
2862
}
63+
64+
hasSingleAllowedReturn(returns) {
65+
// more than 1 element, not allowed
66+
if (returns.length !== 1) return false
67+
const ret = returns[0]
68+
const tn = ret?.typeName
69+
70+
// check return element and type
71+
if (!tn || !tn.type) return false
72+
73+
// exclude arrays and tuples
74+
if (tn.type === 'ArrayTypeName' || tn.type === 'TupleTypeName') return false
75+
76+
// elementary: bool, address, int/uint*, bytes*, string...
77+
if (tn.type === 'ElementaryTypeName') return true
78+
79+
// check if it's a user-defined type without storage location (contract/interface, enum, UDVT)
80+
if (tn.type === 'UserDefinedTypeName') {
81+
const hasStorage = !!ret.storageLocation // 'memory' | 'calldata' | undefined
82+
return !hasStorage
83+
}
84+
85+
// optional: some older parsers separate address/bytes
86+
if (tn.type === 'AddressTypeName' || tn.type === 'BytesTypeName') return true
87+
88+
return false
89+
}
2990
}
3091

3192
module.exports = FuncNameMixedcaseChecker

test/rules/naming/func-name-mixedcase.js

Lines changed: 232 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const assert = require('assert')
22
const linter = require('../../../lib/index')
3+
const { multiLine } = require('../../common/contract-builder')
34
const contractWith = require('../../common/contract-builder').contractWith
5+
const { assertErrorCount, assertNoErrors } = require('../../common/asserts')
46

57
describe('Linter - func-name-mixedcase', () => {
68
it('should raise incorrect func name error', () => {
@@ -10,7 +12,7 @@ describe('Linter - func-name-mixedcase', () => {
1012
rules: { 'func-name-mixedcase': 'error' },
1113
})
1214

13-
assert.equal(report.errorCount, 1)
15+
assertErrorCount(report, 1)
1416
assert.ok(report.messages[0].message.includes('mixedCase'))
1517
})
1618

@@ -21,7 +23,7 @@ describe('Linter - func-name-mixedcase', () => {
2123
rules: { 'func-name-mixedcase': 'error' },
2224
})
2325

24-
assert.equal(report.errorCount, 0)
26+
assertNoErrors(report)
2527
})
2628

2729
describe('Function names with $ character', () => {
@@ -38,8 +40,235 @@ describe('Linter - func-name-mixedcase', () => {
3840
rules: { 'func-name-mixedcase': 'error' },
3941
})
4042

41-
assert.equal(report.errorCount, 0)
43+
assertNoErrors(report)
4244
})
4345
}
4446
})
47+
48+
describe('Interface function names representing a constant', () => {
49+
it('should NOT raise mixedCase name error', () => {
50+
const code = multiLine(
51+
'// SPDX-License-Identifier: Apache-2.0',
52+
'pragma solidity ^0.8.0;',
53+
'',
54+
'interface IA {',
55+
'/// @dev This is a constant state variable',
56+
'function START_TIME() external view returns (uint256);',
57+
'}',
58+
'',
59+
'contract A is IA {',
60+
' /// @inheritdoc IA',
61+
' uint256 public constant override START_TIME = 1;',
62+
'}'
63+
)
64+
65+
const report = linter.processStr(code, {
66+
rules: { 'func-name-mixedcase': 'error' },
67+
})
68+
69+
assertNoErrors(report)
70+
})
71+
72+
it('should fail when inside interface but returning multiple unnamed values', () => {
73+
const code = multiLine(
74+
'pragma solidity ^0.8.20;',
75+
'interface IBad {',
76+
' function START_TIME() external view returns (uint256, uint256);',
77+
'}'
78+
)
79+
80+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
81+
82+
assertErrorCount(report, 1)
83+
assert.ok(report.messages[0].message.includes('mixedCase'))
84+
})
85+
86+
it('should fail when SNAKE_CASE in interface is missing `view` (not a getter-like signature)', () => {
87+
const code = multiLine(
88+
'pragma solidity ^0.8.20;',
89+
'interface IBad {',
90+
' function START_TIME() external returns (uint256);',
91+
'}'
92+
)
93+
94+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
95+
96+
assertErrorCount(report, 1)
97+
assert.ok(report.messages[0].message.includes('mixedCase'))
98+
})
99+
100+
it('should fail when SNAKE_CASE interface function has parameters (not a getter-like signature)', () => {
101+
const code = multiLine(
102+
'pragma solidity ^0.8.20;',
103+
'interface IBad {',
104+
' function START_TIME(address who) external view returns (uint256);',
105+
'}'
106+
)
107+
108+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
109+
110+
assertErrorCount(report, 1)
111+
assert.ok(report.messages[0].message.includes('mixedCase'))
112+
})
113+
114+
it('should fail when SNAKE_CASE in non-interface contract functions', () => {
115+
const code = contractWith('function START_TIME() public view returns (uint256) { return 1; }')
116+
117+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
118+
119+
assertErrorCount(report, 1)
120+
assert.ok(report.messages[0].message.includes('mixedCase'))
121+
})
122+
123+
it('should fail when inside interface but returning multiple named values', () => {
124+
const code = multiLine(
125+
'pragma solidity ^0.8.20;',
126+
'interface IBad {',
127+
' function START_TIME() external view returns (uint256 a, uint256 b);',
128+
'}'
129+
)
130+
131+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
132+
133+
assertErrorCount(report, 1)
134+
assert.ok(report.messages[0].message.includes('mixedCase'))
135+
})
136+
137+
it('allows $ in mixedCase names (regression of former tests)', () => {
138+
const code = contractWith('function aFunc$1Nam23e () public {}')
139+
140+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
141+
142+
assertNoErrors(report)
143+
})
144+
145+
it('allows IERC20 as return (contract/interface type)', () => {
146+
const code = multiLine(
147+
'pragma solidity ^0.8.20;',
148+
'interface IERC20 { function totalSupply() external view returns (uint256); }',
149+
'interface I {',
150+
' function TOKEN() external view returns (IERC20);',
151+
'}'
152+
)
153+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
154+
155+
assertNoErrors(report)
156+
})
157+
158+
it('allows UDVT like UD60x18 as return', () => {
159+
const code = multiLine(
160+
'pragma solidity ^0.8.20;',
161+
'type UD60x18 is uint256;',
162+
'interface I {',
163+
' function UNLOCK_PERCENTAGE() external view returns (UD60x18);',
164+
'}'
165+
)
166+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
167+
168+
assertNoErrors(report)
169+
})
170+
171+
it('allows enum as return', () => {
172+
const code = multiLine(
173+
'pragma solidity ^0.8.20;',
174+
'enum Status { Init, Live, Done }',
175+
'interface I {',
176+
' function CURRENT_STATUS() external view returns (Status);',
177+
'}'
178+
)
179+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
180+
181+
assertNoErrors(report)
182+
})
183+
184+
it('rejects struct as return (UserDefinedTypeName with storageLocation)', () => {
185+
const code = multiLine(
186+
'pragma solidity ^0.8.20;',
187+
'struct Info { uint256 a; uint256 b; }',
188+
'interface I {',
189+
' function DATA() external view returns (Info memory);',
190+
'}'
191+
)
192+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
193+
194+
assertErrorCount(report, 1)
195+
assert.ok(report.messages[0].message.includes('mixedCase'))
196+
})
197+
198+
it('rejects array as return', () => {
199+
const code = multiLine(
200+
'pragma solidity ^0.8.20;',
201+
'interface I {',
202+
' function VALUES() external view returns (uint256[] memory);',
203+
'}'
204+
)
205+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
206+
assertErrorCount(report, 1)
207+
assert.ok(report.messages[0].message.includes('mixedCase'))
208+
})
209+
210+
it('rejects multiple returns (tuple)', () => {
211+
const code = multiLine(
212+
'pragma solidity ^0.8.20;',
213+
'interface I {',
214+
' function START_TIME() external view returns (uint256, uint256);',
215+
'}'
216+
)
217+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
218+
assertErrorCount(report, 1)
219+
assert.ok(report.messages[0].message.includes('mixedCase'))
220+
})
221+
222+
it('rejects pure (only view allowed for constant/immutable getters)', () => {
223+
const code = multiLine(
224+
'pragma solidity ^0.8.20;',
225+
'interface I {',
226+
' function MAGIC_NUMBER() external pure returns (uint256);',
227+
'}'
228+
)
229+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
230+
assertErrorCount(report, 1)
231+
assert.ok(report.messages[0].message.includes('mixedCase'))
232+
})
233+
234+
it('still allows mixedCase with $ (regression)', () => {
235+
const code = multiLine(
236+
'pragma solidity ^0.8.20;',
237+
'interface I {',
238+
' function aFunc$1Nam23e() external view returns (uint256);',
239+
'}'
240+
)
241+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
242+
243+
assertNoErrors(report)
244+
})
245+
246+
it('allows string as return (elementary type)', () => {
247+
const code = multiLine(
248+
'pragma solidity ^0.8.20;',
249+
'interface I {',
250+
' function NAME() external view returns (string memory);',
251+
'}'
252+
)
253+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
254+
255+
assertNoErrors(report)
256+
})
257+
258+
it('rejects IERC20 return when declared in a non-interface contract', () => {
259+
const code = multiLine(
260+
'pragma solidity ^0.8.20;',
261+
'interface IERC20 { function totalSupply() external view returns (uint256); }',
262+
'contract C {',
263+
' function TOKEN() external view returns (IERC20) {',
264+
' return IERC20(address(0));',
265+
' }',
266+
'}'
267+
)
268+
const report = linter.processStr(code, { rules: { 'func-name-mixedcase': 'error' } })
269+
assertErrorCount(report, 1)
270+
271+
assert.ok(report.messages[0].message.includes('mixedCase'))
272+
})
273+
})
45274
})

0 commit comments

Comments
 (0)