Skip to content

Commit e37030b

Browse files
committed
feat(oxlint-config): add func-style rule
1 parent 4772a8f commit e37030b

4 files changed

Lines changed: 160 additions & 13 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
function isFunctionExpression(node) {
2+
return node?.type === 'ArrowFunctionExpression'
3+
|| node?.type === 'FunctionExpression'
4+
}
5+
6+
function pushComputedKeys(stack, classBody) {
7+
classBody.body.forEach((member) => {
8+
if (member.computed) {
9+
stack.push(member.key)
10+
}
11+
})
12+
}
13+
14+
function pushChildren(stack, node) {
15+
Object.entries(node).forEach(([key, value]) => {
16+
if (key === 'parent' || typeof value !== 'object' || value === null) {
17+
return
18+
}
19+
20+
if (Array.isArray(value)) {
21+
value.forEach(item => stack.push(item))
22+
} else {
23+
stack.push(value)
24+
}
25+
})
26+
}
27+
28+
/**
29+
* Check if a function references `this`, `arguments` or `new.target` from its own scope,
30+
* so it can't be converted to an arrow function.
31+
* @param {object} functionNode
32+
* @returns {boolean} Whether the function uses its own bindings.
33+
*/
34+
function usesFunctionBindings(functionNode) {
35+
const stack = [...functionNode.params, functionNode.body]
36+
37+
while (stack.length > 0) {
38+
const node = stack.pop()
39+
40+
if (!node || typeof node.type !== 'string') {
41+
continue
42+
}
43+
44+
switch (node.type) {
45+
case 'ThisExpression':
46+
return true
47+
case 'MetaProperty':
48+
if (node.meta.name === 'new') {
49+
return true
50+
}
51+
52+
continue
53+
case 'Identifier':
54+
if (node.name === 'arguments') {
55+
return true
56+
}
57+
58+
continue
59+
// nested regular functions and class members rebind `this` and `arguments`
60+
case 'FunctionDeclaration':
61+
case 'FunctionExpression':
62+
continue
63+
case 'ClassBody':
64+
pushComputedKeys(stack, node)
65+
continue
66+
case 'MemberExpression':
67+
stack.push(node.object)
68+
69+
if (node.computed) {
70+
stack.push(node.property)
71+
}
72+
73+
continue
74+
case 'Property':
75+
case 'PropertyDefinition':
76+
case 'MethodDefinition':
77+
if (node.computed) {
78+
stack.push(node.key)
79+
}
80+
81+
stack.push(node.value)
82+
continue
83+
default:
84+
pushChildren(stack, node)
85+
}
86+
}
87+
88+
return false
89+
}
90+
91+
function checkDeclarator(context, declarator) {
92+
if (
93+
isFunctionExpression(declarator.init)
94+
&& declarator.id.type === 'Identifier'
95+
&& !declarator.id.typeAnnotation
96+
) {
97+
context.report({
98+
node: declarator.id,
99+
message: 'Use a function declaration instead of assigning a function to a constant at the top level.'
100+
})
101+
}
102+
}
103+
104+
export default {
105+
meta: {
106+
type: 'suggestion',
107+
docs: {
108+
description: 'Enforce function declarations at the top level and arrow functions assigned to constants inside functions.'
109+
},
110+
schema: []
111+
},
112+
create(context) {
113+
let depth = 0
114+
115+
return {
116+
FunctionDeclaration(node) {
117+
if (depth > 0 && !node.generator && !usesFunctionBindings(node)) {
118+
context.report({
119+
node: node.id ?? node,
120+
message: 'Use an arrow function assigned to a constant instead of a nested function declaration.'
121+
})
122+
}
123+
124+
depth++
125+
},
126+
'FunctionDeclaration:exit'() {
127+
depth--
128+
},
129+
FunctionExpression() {
130+
depth++
131+
},
132+
'FunctionExpression:exit'() {
133+
depth--
134+
},
135+
ArrowFunctionExpression() {
136+
depth++
137+
},
138+
'ArrowFunctionExpression:exit'() {
139+
depth--
140+
},
141+
StaticBlock() {
142+
depth++
143+
},
144+
'StaticBlock:exit'() {
145+
depth--
146+
},
147+
VariableDeclaration(node) {
148+
if (depth === 0 && node.kind === 'const') {
149+
node.declarations.forEach(declarator => checkDeclarator(context, declarator))
150+
}
151+
}
152+
}
153+
}
154+
}

‎packages/oxlint-config/src/plugin/index.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import extensionsRule from './extensions.js'
2+
import funcStyleRule from './func-style.js'
23
import importOrderRule from './import-order.js'
34
import memberOrderingRule from './member-ordering.js'
45
import namedExportOrderRule from './named-export-order.js'
@@ -12,6 +13,7 @@ export default {
1213
},
1314
rules: {
1415
'extensions': extensionsRule,
16+
'func-style': funcStyleRule,
1517
'import-order': importOrderRule,
1618
'member-ordering': memberOrderingRule,
1719
'named-export-order': namedExportOrderRule,

‎packages/oxlint-config/src/plugin/naming-convention.js‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,7 @@ export default {
224224
},
225225
create(context) {
226226
const options = context.options
227-
228-
function check(name, node, selector, modifiers = []) {
227+
const check = (name, node, selector, modifiers = []) => {
229228
const option = getSelectorOptions(options, selector, modifiers)
230229

231230
if (!option || isValidName(name, option)) {
@@ -237,14 +236,12 @@ export default {
237236
message: `Name "${name}" must match one of these formats: ${getExpectedFormats(option.format)}.`
238237
})
239238
}
240-
241-
function checkPattern(pattern, selector) {
239+
const checkPattern = (pattern, selector) => {
242240
for (const item of getIdentifierNames(pattern)) {
243241
check(item.name, item.node, selector)
244242
}
245243
}
246-
247-
function checkProperty(node, selector) {
244+
const checkProperty = (node, selector) => {
248245
const name = getKeyName(node.key)
249246

250247
if (name === null) {

‎packages/oxlint-config/src/subconfigs/basic.js‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,6 @@ export default {
223223
'eslint/prefer-template': 'error',
224224
'eslint/symbol-description': 'error',
225225
'eslint/func-names': 'error',
226-
'eslint/func-style': [
227-
'error',
228-
'declaration',
229-
{
230-
allowArrowFunctions: true
231-
}
232-
],
233226
'eslint/max-nested-callbacks': ['error', 4],
234227
'eslint/max-params': ['error', 6],
235228
'eslint/new-cap': [
@@ -245,6 +238,7 @@ export default {
245238
'eslint/operator-assignment': ['error', 'always'],
246239
'eslint/prefer-object-spread': 'error',
247240
'eslint/unicode-bom': 'error',
241+
'trigen/func-style': 'error',
248242
'trigen/naming-convention': [
249243
'error',
250244
{

0 commit comments

Comments
 (0)