@tuomashatakka/eslint-config 2.6.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/publish.yml +45 -0
- package/AGENTS.md +29 -0
- package/bun.lock +60 -102
- package/eslint.config.mjs +1 -0
- package/index.mjs +7 -21
- package/package.json +11 -19
- package/plugins/no-inline-types/index.mjs +11 -0
- package/plugins/no-inline-types/rules/no-inline-multiline-types.mjs +181 -0
- package/plugins/omit/index.mjs +8 -0
- package/plugins/omit/rules/omit-unnecessary-parens-brackets.mjs +329 -0
- package/plugins/omit/utils.mjs +91 -0
- package/plugins/react-strict/index.mjs +19 -0
- package/plugins/react-strict/rules/jsx-prop-layout.mjs +100 -0
- package/plugins/react-strict/rules/no-complex-jsx-map.mjs +66 -0
- package/plugins/react-strict/rules/no-jsx-value-calculations.mjs +99 -0
- package/plugins/react-strict/rules/no-nested-divs.mjs +59 -0
- package/plugins/react-strict/rules/no-style-prop.mjs +43 -0
- package/plugins/react-strict/rules/prefer-no-use-effect.mjs +26 -0
- package/plugins/whitespaced/index.mjs +15 -0
- package/plugins/whitespaced/rules/aligned-assignments.mjs +385 -0
- package/plugins/whitespaced/rules/block-padding.mjs +289 -0
- package/plugins/whitespaced/rules/class-property-grouping.mjs +370 -0
- package/plugins/whitespaced/rules/consistent-line-spacing.mjs +266 -0
- package/plugins/whitespaced/rules/multiline-format.mjs +533 -0
- package/rules.mjs +101 -95
- package/test/fixtures/basic-javascript.js +5 -4
- package/test/fixtures/complex-patterns.ts +9 -7
- package/test/fixtures/edge-cases.js +12 -7
- package/test/fixtures/jsx-formatting.jsx +5 -4
- package/test/fixtures/omit-parens.invalid.ts +12 -0
- package/test/fixtures/omit-parens.valid.ts +13 -0
- package/test/fixtures/react-component.tsx +7 -6
- package/test/fixtures/react-strict.invalid.tsx +31 -0
- package/test/fixtures/react-strict.valid.tsx +76 -0
- package/test/fixtures/whitespaced-docstring.invalid.ts +10 -0
- package/test/fixtures/whitespaced-docstring.valid.ts +16 -0
- package/test/fixtures/whitespaced-members.invalid.ts +22 -0
- package/test/fixtures/whitespaced-members.valid.ts +13 -0
- package/test/fixtures/whitespaced-multiline.invalid.ts +8 -0
- package/test/fixtures/whitespaced-multiline.valid.ts +15 -0
- package/test/fixtures/whitespaced-types.valid.ts +5 -0
- package/test/fixtures/whitespaced.valid.ts +45 -0
- package/test/format-cases.mjs +13 -14
- package/test/test-runner.mjs +128 -47
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const RESERVED_WORDS = new Set([
|
|
2
|
+
'await',
|
|
3
|
+
'break',
|
|
4
|
+
'case',
|
|
5
|
+
'catch',
|
|
6
|
+
'class',
|
|
7
|
+
'const',
|
|
8
|
+
'continue',
|
|
9
|
+
'debugger',
|
|
10
|
+
'default',
|
|
11
|
+
'delete',
|
|
12
|
+
'do',
|
|
13
|
+
'else',
|
|
14
|
+
'enum',
|
|
15
|
+
'export',
|
|
16
|
+
'extends',
|
|
17
|
+
'false',
|
|
18
|
+
'finally',
|
|
19
|
+
'for',
|
|
20
|
+
'function',
|
|
21
|
+
'if',
|
|
22
|
+
'implements',
|
|
23
|
+
'import',
|
|
24
|
+
'in',
|
|
25
|
+
'instanceof',
|
|
26
|
+
'interface',
|
|
27
|
+
'let',
|
|
28
|
+
'new',
|
|
29
|
+
'null',
|
|
30
|
+
'package',
|
|
31
|
+
'private',
|
|
32
|
+
'protected',
|
|
33
|
+
'public',
|
|
34
|
+
'return',
|
|
35
|
+
'static',
|
|
36
|
+
'super',
|
|
37
|
+
'switch',
|
|
38
|
+
'this',
|
|
39
|
+
'throw',
|
|
40
|
+
'true',
|
|
41
|
+
'try',
|
|
42
|
+
'typeof',
|
|
43
|
+
'var',
|
|
44
|
+
'void',
|
|
45
|
+
'while',
|
|
46
|
+
'with',
|
|
47
|
+
'yield',
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
function isValidDotNotationIdentifier (name) {
|
|
55
|
+
const identifierRegex = /^[\p{L}\p{Nl}$_][\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$_]*$/u
|
|
56
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
57
|
+
return false
|
|
58
|
+
return identifierRegex.test(name) && !RESERVED_WORDS.has(name)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
function isDeclaration (statement) {
|
|
66
|
+
return statement.type === 'VariableDeclaration' ||
|
|
67
|
+
statement.type === 'FunctionDeclaration' ||
|
|
68
|
+
statement.type === 'ClassDeclaration'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function isParenthesized (node, sourceCode) {
|
|
76
|
+
const previousToken = sourceCode.getTokenBefore(node)
|
|
77
|
+
const nextToken = sourceCode.getTokenAfter(node)
|
|
78
|
+
|
|
79
|
+
return Boolean(
|
|
80
|
+
previousToken && nextToken &&
|
|
81
|
+
previousToken.value === '(' && previousToken.range[1] <= node.range[0] &&
|
|
82
|
+
nextToken.value === ')' && nextToken.range[0] >= node.range[1]
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
isParenthesized,
|
|
89
|
+
isDeclaration,
|
|
90
|
+
isValidDotNotationIdentifier,
|
|
91
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import noStyleProp from './rules/no-style-prop.mjs'
|
|
2
|
+
import noNestedDivs from './rules/no-nested-divs.mjs'
|
|
3
|
+
import noComplexJsxMap from './rules/no-complex-jsx-map.mjs'
|
|
4
|
+
import preferNoUseEffect from './rules/prefer-no-use-effect.mjs'
|
|
5
|
+
import noJsxValueCalculations from './rules/no-jsx-value-calculations.mjs'
|
|
6
|
+
import jsxPropLayout from './rules/jsx-prop-layout.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const rules = {
|
|
10
|
+
'no-style-prop': noStyleProp,
|
|
11
|
+
'no-nested-divs': noNestedDivs,
|
|
12
|
+
'no-complex-jsx-map': noComplexJsxMap,
|
|
13
|
+
'prefer-no-use-effect': preferNoUseEffect,
|
|
14
|
+
'no-jsx-value-calculations': noJsxValueCalculations,
|
|
15
|
+
'jsx-prop-layout': jsxPropLayout,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export default { rules }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const RESERVED_PROPS = new Set([
|
|
2
|
+
'key',
|
|
3
|
+
'ref',
|
|
4
|
+
])
|
|
5
|
+
const STYLE_PROPS = new Set([
|
|
6
|
+
'className',
|
|
7
|
+
'style',
|
|
8
|
+
'id',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function getPropName (attr) {
|
|
13
|
+
if (attr.type === 'JSXSpreadAttribute')
|
|
14
|
+
return null
|
|
15
|
+
|
|
16
|
+
if (attr.name.type === 'JSXIdentifier')
|
|
17
|
+
return attr.name.name
|
|
18
|
+
|
|
19
|
+
if (attr.name.type === 'JSXNamespacedName')
|
|
20
|
+
return `${attr.name.namespace.name}:${attr.name.name.name}`
|
|
21
|
+
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
function getPropGroup (name) {
|
|
27
|
+
if (!name)
|
|
28
|
+
return 4 // spread attributes — middle
|
|
29
|
+
|
|
30
|
+
if (RESERVED_PROPS.has(name))
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
if (STYLE_PROPS.has(name))
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
if (name.startsWith('data-') || name.startsWith('aria-'))
|
|
37
|
+
return 2
|
|
38
|
+
|
|
39
|
+
if (name.startsWith('on') && name[2] === name[2].toUpperCase())
|
|
40
|
+
return 5
|
|
41
|
+
|
|
42
|
+
return 3
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
meta: {
|
|
48
|
+
type: 'suggestion',
|
|
49
|
+
docs: { description: 'Enforce consistent JSX prop ordering: key/ref first, className/style next, data/aria attrs, then regular props, callbacks last' },
|
|
50
|
+
fixable: 'code',
|
|
51
|
+
schema: [],
|
|
52
|
+
messages: {
|
|
53
|
+
propOrder: '`{{ current }}` should be placed before `{{ previous }}` ({{ currentGroup }} props should come before {{ previousGroup }} props).',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
create (context) {
|
|
57
|
+
const groupNames = [
|
|
58
|
+
'reserved',
|
|
59
|
+
'style/class',
|
|
60
|
+
'data/aria',
|
|
61
|
+
'regular',
|
|
62
|
+
'spread',
|
|
63
|
+
'callback',
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
JSXOpeningElement (node) {
|
|
68
|
+
const attrs = node.attributes
|
|
69
|
+
|
|
70
|
+
if (attrs.length < 2)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
let lastGroup = -1
|
|
74
|
+
let lastName = null
|
|
75
|
+
|
|
76
|
+
for (const attr of attrs) {
|
|
77
|
+
const name = getPropName(attr)
|
|
78
|
+
const group = getPropGroup(name)
|
|
79
|
+
|
|
80
|
+
if (group < lastGroup) {
|
|
81
|
+
context.report({
|
|
82
|
+
node: attr,
|
|
83
|
+
messageId: 'propOrder',
|
|
84
|
+
data: {
|
|
85
|
+
current: name || '{...spread}',
|
|
86
|
+
previous: lastName || '{...spread}',
|
|
87
|
+
currentGroup: groupNames[group],
|
|
88
|
+
previousGroup: groupNames[lastGroup],
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lastGroup = group
|
|
95
|
+
lastName = name
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function isInsideJSXExpression (node) {
|
|
2
|
+
let current = node.parent
|
|
3
|
+
|
|
4
|
+
while (current) {
|
|
5
|
+
if (current.type === 'JSXExpressionContainer')
|
|
6
|
+
return true
|
|
7
|
+
|
|
8
|
+
current = current.parent
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function hasComplexBody (callbackBody) {
|
|
16
|
+
if (callbackBody.type === 'BlockStatement') {
|
|
17
|
+
const hasIfOrSwitch = callbackBody.body.some(stmt =>
|
|
18
|
+
stmt.type === 'IfStatement' || stmt.type === 'SwitchStatement')
|
|
19
|
+
|
|
20
|
+
if (hasIfOrSwitch)
|
|
21
|
+
return true
|
|
22
|
+
|
|
23
|
+
if (callbackBody.body.length > 3)
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'suggestion',
|
|
34
|
+
docs: { description: 'Disallow complex .map() callbacks with inline logic inside JSX' },
|
|
35
|
+
fixable: null,
|
|
36
|
+
schema: [],
|
|
37
|
+
messages: {
|
|
38
|
+
noComplexMap: 'Extract complex .map() callback into a separate component. Move conditional logic outside the JSX return block.',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
create (context) {
|
|
42
|
+
return {
|
|
43
|
+
CallExpression (node) {
|
|
44
|
+
if (
|
|
45
|
+
node.callee.type !== 'MemberExpression' ||
|
|
46
|
+
node.callee.property.type !== 'Identifier' ||
|
|
47
|
+
node.callee.property.name !== 'map'
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
if (!isInsideJSXExpression(node))
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
const callback = node.arguments[0]
|
|
55
|
+
|
|
56
|
+
if (!callback)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression') {
|
|
60
|
+
if (hasComplexBody(callback.body))
|
|
61
|
+
context.report({ node, messageId: 'noComplexMap' })
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
function isComponentFunction (node) {
|
|
2
|
+
let current = node
|
|
3
|
+
|
|
4
|
+
while (current) {
|
|
5
|
+
if (
|
|
6
|
+
current.type === 'ArrowFunctionExpression' ||
|
|
7
|
+
current.type === 'FunctionExpression' ||
|
|
8
|
+
current.type === 'FunctionDeclaration'
|
|
9
|
+
) {
|
|
10
|
+
const parent = current.parent
|
|
11
|
+
|
|
12
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
13
|
+
const name = parent.id.name
|
|
14
|
+
|
|
15
|
+
if (name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase())
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (current.type === 'FunctionDeclaration' && current.id) {
|
|
20
|
+
const name = current.id.name
|
|
21
|
+
|
|
22
|
+
if (name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase())
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
current = current.parent
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function isInsideReturnJSX (node) {
|
|
35
|
+
let current = node.parent
|
|
36
|
+
|
|
37
|
+
while (current) {
|
|
38
|
+
if (current.type === 'ReturnStatement')
|
|
39
|
+
return true
|
|
40
|
+
|
|
41
|
+
if (current.type === 'ArrowFunctionExpression' && current.body.type !== 'BlockStatement')
|
|
42
|
+
return true
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
current.type === 'FunctionDeclaration' ||
|
|
46
|
+
current.type === 'FunctionExpression' ||
|
|
47
|
+
current.type === 'ArrowFunctionExpression'
|
|
48
|
+
)
|
|
49
|
+
return false
|
|
50
|
+
|
|
51
|
+
current = current.parent
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
meta: {
|
|
60
|
+
type: 'suggestion',
|
|
61
|
+
docs: { description: 'Disallow value calculations and assignments inside JSX return blocks' },
|
|
62
|
+
fixable: null,
|
|
63
|
+
schema: [],
|
|
64
|
+
messages: {
|
|
65
|
+
noJsxCalculations: 'Move value calculations and assignments outside the return block. Compute values before the return statement.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
create (context) {
|
|
69
|
+
return {
|
|
70
|
+
JSXExpressionContainer (node) {
|
|
71
|
+
if (!isComponentFunction(node) || !isInsideReturnJSX(node))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
const expr = node.expression
|
|
75
|
+
|
|
76
|
+
if (!expr || expr.type === 'JSXEmptyExpression')
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if (expr.type === 'AssignmentExpression') {
|
|
80
|
+
context.report({ node: expr, messageId: 'noJsxCalculations' })
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (expr.type === 'SequenceExpression') {
|
|
85
|
+
const hasAssignment = expr.expressions.some(e => e.type === 'AssignmentExpression')
|
|
86
|
+
|
|
87
|
+
if (hasAssignment)
|
|
88
|
+
context.report({ node: expr, messageId: 'noJsxCalculations' })
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
VariableDeclaration (node) {
|
|
92
|
+
if (!isComponentFunction(node) || !isInsideReturnJSX(node))
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
context.report({ node, messageId: 'noJsxCalculations' })
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const SEMANTIC_ALTERNATIVES = [
|
|
2
|
+
'main',
|
|
3
|
+
'section',
|
|
4
|
+
'article',
|
|
5
|
+
'aside',
|
|
6
|
+
'header',
|
|
7
|
+
'footer',
|
|
8
|
+
'nav',
|
|
9
|
+
'figure',
|
|
10
|
+
'figcaption',
|
|
11
|
+
'details',
|
|
12
|
+
'summary',
|
|
13
|
+
'dialog',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function getElementName (node) {
|
|
18
|
+
if (node.type === 'JSXElement' && node.openingElement.name.type === 'JSXIdentifier')
|
|
19
|
+
return node.openingElement.name.name
|
|
20
|
+
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
meta: {
|
|
27
|
+
type: 'suggestion',
|
|
28
|
+
docs: { description: 'Disallow nested div elements; prefer semantic HTML5 tags' },
|
|
29
|
+
fixable: null,
|
|
30
|
+
schema: [],
|
|
31
|
+
messages: {
|
|
32
|
+
noNestedDivs: 'Avoid nesting <div> inside <div>. Use semantic HTML5 elements instead ({{ alternatives }}).',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
create (context) {
|
|
36
|
+
return {
|
|
37
|
+
JSXElement (node) {
|
|
38
|
+
const name = getElementName(node)
|
|
39
|
+
|
|
40
|
+
if (name !== 'div')
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
const parent = node.parent
|
|
44
|
+
|
|
45
|
+
if (parent.type !== 'JSXElement')
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
const parentName = getElementName(parent)
|
|
49
|
+
|
|
50
|
+
if (parentName === 'div')
|
|
51
|
+
context.report({
|
|
52
|
+
node: node.openingElement,
|
|
53
|
+
messageId: 'noNestedDivs',
|
|
54
|
+
data: { alternatives: SEMANTIC_ALTERNATIVES.join(', ') },
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const DRAG_DROP_HANDLERS = new Set([
|
|
2
|
+
'onDrag',
|
|
3
|
+
'onDragStart',
|
|
4
|
+
'onDragEnd',
|
|
5
|
+
'onDragEnter',
|
|
6
|
+
'onDragLeave',
|
|
7
|
+
'onDragOver',
|
|
8
|
+
'onDrop',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: { description: 'Disallow inline style props unless used with drag/drop interactions' },
|
|
16
|
+
fixable: null,
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
noStyleProp: 'Avoid inline style props. Use CSS classes or styled components instead. Inline styles are only acceptable for dynamic user interaction scenarios like drag/drop.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create (context) {
|
|
23
|
+
return {
|
|
24
|
+
JSXAttribute (node) {
|
|
25
|
+
if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'style')
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
const opening = node.parent
|
|
29
|
+
|
|
30
|
+
if (opening.type !== 'JSXOpeningElement')
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
const hasDragHandler = opening.attributes.some(attr =>
|
|
34
|
+
attr.type === 'JSXAttribute' &&
|
|
35
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
36
|
+
DRAG_DROP_HANDLERS.has(attr.name.name))
|
|
37
|
+
|
|
38
|
+
if (!hasDragHandler)
|
|
39
|
+
context.report({ node, messageId: 'noStyleProp' })
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: { description: 'Discourage useEffect in favor of React context, custom hooks, or event-driven patterns' },
|
|
5
|
+
fixable: null,
|
|
6
|
+
schema: [],
|
|
7
|
+
messages: {
|
|
8
|
+
preferNoUseEffect: 'Consider alternatives to useEffect. Extract side effects into React context, a custom hook in a separate module, or use event-driven patterns.',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
create (context) {
|
|
12
|
+
return {
|
|
13
|
+
CallExpression (node) {
|
|
14
|
+
const isDirectCall = node.callee.type === 'Identifier' &&
|
|
15
|
+
node.callee.name === 'useEffect'
|
|
16
|
+
|
|
17
|
+
const isMemberCall = node.callee.type === 'MemberExpression' &&
|
|
18
|
+
node.callee.property.type === 'Identifier' &&
|
|
19
|
+
node.callee.property.name === 'useEffect'
|
|
20
|
+
|
|
21
|
+
if (isDirectCall || isMemberCall)
|
|
22
|
+
context.report({ node, messageId: 'preferNoUseEffect' })
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Simplified ESLint plugin for whitespaced formatting
|
|
3
|
+
*
|
|
4
|
+
* Retained rules:
|
|
5
|
+
* - aligned-assignments: Enforce vertical alignment for variable assignments and type annotations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import alignedAssignments from './rules/aligned-assignments.mjs'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
rules: {
|
|
13
|
+
"aligned-assignments": alignedAssignments,
|
|
14
|
+
},
|
|
15
|
+
}
|