@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.
Files changed (45) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/.github/workflows/publish.yml +45 -0
  3. package/AGENTS.md +29 -0
  4. package/bun.lock +60 -102
  5. package/eslint.config.mjs +1 -0
  6. package/index.mjs +7 -21
  7. package/package.json +11 -19
  8. package/plugins/no-inline-types/index.mjs +11 -0
  9. package/plugins/no-inline-types/rules/no-inline-multiline-types.mjs +181 -0
  10. package/plugins/omit/index.mjs +8 -0
  11. package/plugins/omit/rules/omit-unnecessary-parens-brackets.mjs +329 -0
  12. package/plugins/omit/utils.mjs +91 -0
  13. package/plugins/react-strict/index.mjs +19 -0
  14. package/plugins/react-strict/rules/jsx-prop-layout.mjs +100 -0
  15. package/plugins/react-strict/rules/no-complex-jsx-map.mjs +66 -0
  16. package/plugins/react-strict/rules/no-jsx-value-calculations.mjs +99 -0
  17. package/plugins/react-strict/rules/no-nested-divs.mjs +59 -0
  18. package/plugins/react-strict/rules/no-style-prop.mjs +43 -0
  19. package/plugins/react-strict/rules/prefer-no-use-effect.mjs +26 -0
  20. package/plugins/whitespaced/index.mjs +15 -0
  21. package/plugins/whitespaced/rules/aligned-assignments.mjs +385 -0
  22. package/plugins/whitespaced/rules/block-padding.mjs +289 -0
  23. package/plugins/whitespaced/rules/class-property-grouping.mjs +370 -0
  24. package/plugins/whitespaced/rules/consistent-line-spacing.mjs +266 -0
  25. package/plugins/whitespaced/rules/multiline-format.mjs +533 -0
  26. package/rules.mjs +101 -95
  27. package/test/fixtures/basic-javascript.js +5 -4
  28. package/test/fixtures/complex-patterns.ts +9 -7
  29. package/test/fixtures/edge-cases.js +12 -7
  30. package/test/fixtures/jsx-formatting.jsx +5 -4
  31. package/test/fixtures/omit-parens.invalid.ts +12 -0
  32. package/test/fixtures/omit-parens.valid.ts +13 -0
  33. package/test/fixtures/react-component.tsx +7 -6
  34. package/test/fixtures/react-strict.invalid.tsx +31 -0
  35. package/test/fixtures/react-strict.valid.tsx +76 -0
  36. package/test/fixtures/whitespaced-docstring.invalid.ts +10 -0
  37. package/test/fixtures/whitespaced-docstring.valid.ts +16 -0
  38. package/test/fixtures/whitespaced-members.invalid.ts +22 -0
  39. package/test/fixtures/whitespaced-members.valid.ts +13 -0
  40. package/test/fixtures/whitespaced-multiline.invalid.ts +8 -0
  41. package/test/fixtures/whitespaced-multiline.valid.ts +15 -0
  42. package/test/fixtures/whitespaced-types.valid.ts +5 -0
  43. package/test/fixtures/whitespaced.valid.ts +45 -0
  44. package/test/format-cases.mjs +13 -14
  45. 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
+ }