@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
package/index.mjs CHANGED
@@ -1,14 +1,13 @@
1
1
  import stylistic from '@stylistic/eslint-plugin'
2
- import stylisticJsx from '@stylistic/eslint-plugin-jsx'
3
- import tsplugin from '@typescript-eslint/eslint-plugin'
4
2
  import importPlugin from 'eslint-plugin-import'
5
- import noInlineMultilineTypesPlugin from 'eslint-plugin-no-inline-multiline-types'
6
- import whitespacedPlugin from 'eslint-plugin-whitespaced'
7
- import omitPlugin from 'eslint-plugin-omit-unnecessary'
3
+ import noInlineMultilineTypesPlugin from './plugins/no-inline-types/index.mjs'
4
+ import whitespacedPlugin from './plugins/whitespaced/index.mjs'
5
+ import omitPlugin from './plugins/omit/index.mjs'
8
6
  import react from 'eslint-plugin-react'
9
7
  import reactHooks from 'eslint-plugin-react-hooks'
10
8
  import globals from 'globals'
11
9
  import tseslint from 'typescript-eslint'
10
+ import reactStrictPlugin from './plugins/react-strict/index.mjs'
12
11
  import { rules } from './rules.mjs'
13
12
 
14
13
 
@@ -17,10 +16,11 @@ const plugins = {
17
16
  'react-hooks': reactHooks,
18
17
  'import': importPlugin,
19
18
  '@stylistic': stylistic,
20
- '@typescript-eslint': tsplugin,
19
+ '@typescript-eslint': tseslint.plugin,
21
20
  'omit': omitPlugin,
22
21
  'no-inline-types': noInlineMultilineTypesPlugin,
23
22
  'whitespaced': whitespacedPlugin,
23
+ 'react-strict': reactStrictPlugin,
24
24
  }
25
25
 
26
26
  export { rules }
@@ -38,17 +38,7 @@ export const baseConfig = {
38
38
  'import/internal-regex': '^@/(.+)',
39
39
  },
40
40
  ignores: [ '**/node_modules/**' ],
41
- plugins: {
42
- ...plugins,
43
- // Merge stylistic JSX plugin into the main stylistic namespace
44
- '@stylistic': {
45
- ...stylistic,
46
- rules: {
47
- ...stylistic.rules,
48
- ...stylisticJsx.rules,
49
- }
50
- }
51
- },
41
+ plugins,
52
42
  rules,
53
43
  }
54
44
 
@@ -60,9 +50,5 @@ export const config = tseslint.config(
60
50
  ...tseslint.configs.recommended,
61
51
  baseConfig)
62
52
 
63
- /**
64
- * @summary ESLint configuration
65
- * @description Opinionated yet functional AF base config for ESLint
66
- */
67
53
 
68
54
  export default config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuomashatakka/eslint-config",
3
- "version": "2.6.2",
3
+ "version": "3.1.0",
4
4
  "description": "Default eslint configuration",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -26,28 +26,20 @@
26
26
  "typescript": "*"
27
27
  },
28
28
  "dependencies": {
29
- "eslint": "^9.31.0",
30
- "@stylistic/eslint-plugin": "^5.2.0",
31
- "@stylistic/eslint-plugin-jsx": "^2.13.0",
32
- "typescript-eslint": "^8.37.0",
33
- "globals": "^16.3.0",
34
- "typescript": "^5.8.3",
29
+ "@stylistic/eslint-plugin": "^5.10.0",
30
+ "@types/react": "^19.2.14",
31
+ "eslint": "^9.39.0",
32
+ "eslint-plugin-import": "^2.32.0",
35
33
  "eslint-plugin-react": "^7.37.5",
36
34
  "eslint-plugin-react-hooks": "^5.2.0",
37
- "eslint-plugin-import": "^2.32.0",
38
- "@typescript-eslint/eslint-plugin": "^8.37.0",
39
- "eslint-plugin-no-inline-multiline-types": "^0.0.5",
40
- "eslint-plugin-omit-unnecessary": "^0.0.3",
41
- "eslint-plugin-whitespaced": "^1.0.2"
35
+ "globals": "^16.5.0",
36
+ "typescript": "^5.9.0",
37
+ "typescript-eslint": "^8.58.0"
42
38
  },
43
39
  "devDependencies": {
44
- "@eslint/compat": "^1.3.1",
45
- "@eslint/eslintrc": "^3.3.1",
46
- "@eslint/js": "^9.31.0",
47
- "@stylistic/eslint-plugin-ts": "^4.4.1",
40
+ "@eslint/eslintrc": "^3.3.5",
41
+ "@eslint/js": "^9.39.0",
48
42
  "@types/eslint__eslintrc": "^2.1.2",
49
- "@types/eslint__js": "^8.42.3",
50
- "@typescript-eslint/parser": "^8.37.0",
51
- "eslint-plugin-block-padding": "^0.0.3"
43
+ "@types/eslint__js": "^8.42.3"
52
44
  }
53
45
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @fileoverview Disallows inline type definitions that span multiple lines.
3
+ */
4
+ import noInline from './rules/no-inline-multiline-types.mjs'
5
+
6
+
7
+ export default {
8
+ rules: {
9
+ 'no-inline-multiline-types': noInline,
10
+ },
11
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @fileoverview Disallows inline TSTypeLiteral annotations that span multiple lines.
3
+ */
4
+
5
+ function toPascalCase (str) {
6
+ if (!str)
7
+ return ''
8
+ str = str.replace(/_([a-z])/g, (match, char) => char.toUpperCase())
9
+ return str.charAt(0).toUpperCase() + str.slice(1)
10
+ }
11
+
12
+
13
+ /** @type {import('eslint').Rule.RuleModule} */
14
+ const rule = {
15
+ meta: {
16
+ type: 'suggestion',
17
+ docs: {
18
+ description: 'Disallows inline TSTypeLiteral annotations',
19
+ recommended: false,
20
+ url: null,
21
+ },
22
+ fixable: "code",
23
+ schema: [],
24
+ messages: {
25
+ inlineType: 'Inline type literal annotations are disallowed. Extract to a named interface or type alias.',
26
+ },
27
+ },
28
+
29
+ create (context) {
30
+ const sourceCode = context.getSourceCode()
31
+
32
+
33
+ function findInsertionPointAncestor (startNode) {
34
+ let currentNode = startNode
35
+ const targetAncestorTypes = new Set([
36
+ 'VariableDeclaration',
37
+ 'FunctionDeclaration',
38
+ 'ClassDeclaration',
39
+ 'TSInterfaceDeclaration',
40
+ 'TSTypeAliasDeclaration',
41
+ ])
42
+ const validParentContextTypes = new Set([
43
+ 'Program',
44
+ 'BlockStatement',
45
+ 'ExportNamedDeclaration',
46
+ 'ExportDefaultDeclaration',
47
+ ])
48
+
49
+ while (currentNode.parent) {
50
+ if (targetAncestorTypes.has(currentNode.type) && currentNode.parent && validParentContextTypes.has(currentNode.parent.type)) {
51
+ if (currentNode.parent.type === 'ExportNamedDeclaration' || currentNode.parent.type === 'ExportDefaultDeclaration')
52
+ return currentNode.parent
53
+ return currentNode
54
+ }
55
+ if ((currentNode.type === 'ExportNamedDeclaration' || currentNode.type === 'ExportDefaultDeclaration') &&
56
+ currentNode.declaration && targetAncestorTypes.has(currentNode.declaration.type))
57
+ return currentNode
58
+
59
+ currentNode = currentNode.parent
60
+ if (currentNode.type === 'Program') {
61
+ if (targetAncestorTypes.has(startNode.type))
62
+ return startNode
63
+ break
64
+ }
65
+ }
66
+ return null
67
+ }
68
+
69
+
70
+ function deriveTypeName (typeAnnotationNode) {
71
+ const parent = typeAnnotationNode.parent
72
+ if (!parent)
73
+ return 'ExtractedType'
74
+
75
+ try {
76
+ if (parent.type === 'Identifier' && parent.parent && parent.parent.type === 'VariableDeclarator')
77
+ return toPascalCase(parent.name) + 'Type'
78
+
79
+ if (parent.type === 'Identifier' && parent.parent && (
80
+ parent.parent.type === 'FunctionDeclaration' ||
81
+ parent.parent.type === 'FunctionExpression' ||
82
+ parent.parent.type === 'ArrowFunctionExpression' ||
83
+ parent.parent.type === 'TSParameterProperty'
84
+ )) {
85
+ if (parent.parent.params && parent.parent.params.includes(parent))
86
+ return toPascalCase(parent.name) + 'Type'
87
+ if (parent.parent.type === 'TSParameterProperty' && parent.parent.parameter === parent)
88
+ return toPascalCase(parent.name) + 'Type'
89
+ }
90
+
91
+ if (parent.type === 'ObjectPattern' && parent.parent && (
92
+ parent.parent.type === 'FunctionDeclaration' ||
93
+ parent.parent.type === 'FunctionExpression' ||
94
+ parent.parent.type === 'ArrowFunctionExpression'
95
+ )) {
96
+ const funcName = parent.parent.id?.name
97
+ return funcName ? toPascalCase(funcName) + 'Props' : 'PropsType'
98
+ }
99
+
100
+ if (parent.type === 'ArrayPattern' && parent.parent && (
101
+ parent.parent.type === 'FunctionDeclaration' ||
102
+ parent.parent.type === 'FunctionExpression' ||
103
+ parent.parent.type === 'ArrowFunctionExpression'
104
+ ))
105
+ return 'ParamsType'
106
+
107
+ if (parent.type === 'PropertyDefinition' && parent.key && parent.key.type === 'Identifier')
108
+ return toPascalCase(parent.key.name) + 'Type'
109
+
110
+ if ((parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression') &&
111
+ parent.returnType === typeAnnotationNode) {
112
+ const funcName = parent.id?.name
113
+ if (!funcName && parent.parent && parent.parent.type === 'VariableDeclarator' && parent.parent.id.type === 'Identifier')
114
+ return toPascalCase(parent.parent.id.name) + 'ReturnType'
115
+ return toPascalCase(funcName || 'Function') + 'ReturnType'
116
+ }
117
+
118
+ if (parent.type === 'TSParameterProperty' && parent.parameter && parent.parameter.type === 'Identifier')
119
+ return toPascalCase(parent.parameter.name) + 'Type'
120
+ }
121
+ catch (e) {
122
+ console.error('Error deriving type name:', e)
123
+ }
124
+
125
+ return 'ExtractedType'
126
+ }
127
+
128
+
129
+ function checkMultilineLiteralInAnnotation (typeAnnotationNode) {
130
+ if (!typeAnnotationNode || typeAnnotationNode.type !== 'TSTypeAnnotation')
131
+ return
132
+
133
+ const literalNode = typeAnnotationNode.typeAnnotation
134
+
135
+ if (literalNode && literalNode.type === 'TSTypeLiteral')
136
+ context.report({
137
+ node: literalNode,
138
+ messageId: 'inlineType',
139
+ fix (fixer) {
140
+ const newTypeName = deriveTypeName(typeAnnotationNode)
141
+ const literalText = sourceCode.getText(literalNode)
142
+ const typeAliasString = `type ${newTypeName} = ${literalText};\n\n`
143
+ const insertionAncestor = findInsertionPointAncestor(typeAnnotationNode)
144
+
145
+ if (!insertionAncestor)
146
+ return null
147
+
148
+ return [
149
+ fixer.insertTextBefore(insertionAncestor, typeAliasString),
150
+ fixer.replaceText(literalNode, newTypeName),
151
+ ]
152
+ },
153
+ })
154
+ }
155
+
156
+
157
+ return {
158
+ VariableDeclarator (node) {
159
+ if (node.id && node.id.typeAnnotation)
160
+ checkMultilineLiteralInAnnotation(node.id.typeAnnotation)
161
+ },
162
+ PropertyDefinition (node) {
163
+ if (node.typeAnnotation)
164
+ checkMultilineLiteralInAnnotation(node.typeAnnotation)
165
+ },
166
+ ':function' (node) {
167
+ if (node.returnType)
168
+ checkMultilineLiteralInAnnotation(node.returnType)
169
+ if (node.params)
170
+ for (const param of node.params)
171
+ if (param && param.typeAnnotation)
172
+ checkMultilineLiteralInAnnotation(param.typeAnnotation)
173
+ else if (param.type === 'TSParameterProperty' && param.parameter && param.parameter.typeAnnotation)
174
+ checkMultilineLiteralInAnnotation(param.parameter.typeAnnotation)
175
+ },
176
+ }
177
+ },
178
+ }
179
+
180
+
181
+ export default rule
@@ -0,0 +1,8 @@
1
+ import omit from './rules/omit-unnecessary-parens-brackets.mjs'
2
+
3
+
4
+ export default {
5
+ rules: {
6
+ 'omit-unnecessary-parens-brackets': omit,
7
+ },
8
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @fileoverview Rule to omit unnecessary parentheses, brackets, and braces
3
+ */
4
+
5
+ import { isValidDotNotationIdentifier, isDeclaration, isParenthesized } from '../utils.mjs'
6
+
7
+
8
+ export default {
9
+ meta: {
10
+ type: 'suggestion',
11
+ docs: {
12
+ description: 'Omit unnecessary parentheses, brackets, and braces',
13
+ category: "Stylistic Issues",
14
+ recommended: false,
15
+ url: null,
16
+ },
17
+ fixable: "code",
18
+ schema: [],
19
+ messages: {
20
+ unnecessaryParens: "Unnecessary parentheses",
21
+ useDotNotation: "Use dot notation instead of bracket notation",
22
+ unnecessaryBraces: "Unnecessary curly braces",
23
+ }
24
+ },
25
+ create (context) {
26
+ const sourceCode = context.getSourceCode()
27
+
28
+
29
+ function checkDotNotation (node) {
30
+ if (node.computed &&
31
+ node.property.type === 'Literal' &&
32
+ typeof node.property.value === 'string' &&
33
+ isValidDotNotationIdentifier(node.property.value) &&
34
+ !node.optional
35
+ ) context.report({
36
+ node: node.property,
37
+ messageId: "useDotNotation",
38
+ fix(fixer) {
39
+ const propText = node.property.value
40
+ const openingBracket = sourceCode.getTokenBefore(node.property)
41
+ const closingBracket = sourceCode.getTokenAfter(node.property)
42
+
43
+ if (!openingBracket || openingBracket.value !== '[' || !closingBracket || closingBracket.value !== ']')
44
+ return null
45
+
46
+ const textBetweenObjectAndBracket = sourceCode.text.slice(node.object.range[1], openingBracket.range[0])
47
+ const textBetweenPropertyAndBracket = sourceCode.text.slice(node.property.range[1], closingBracket.range[0])
48
+
49
+ if (textBetweenObjectAndBracket.trim() !== '' || textBetweenPropertyAndBracket.trim() !== '')
50
+ return null
51
+
52
+ return fixer.replaceTextRange(
53
+ [openingBracket.range[0], closingBracket.range[1]],
54
+ `.${propText}`
55
+ )
56
+ }
57
+ })
58
+ }
59
+
60
+
61
+ function checkUnnecessaryParens (node) {
62
+ if (!node || !isParenthesized(node, sourceCode))
63
+ return
64
+
65
+ const parent = node.parent
66
+ const tokenBefore = sourceCode.getTokenBefore(node)
67
+ const tokenAfter = sourceCode.getTokenAfter(node)
68
+
69
+ if (!tokenBefore || !tokenAfter)
70
+ return
71
+
72
+ const reportAndFixUnnecessaryParens = reportedNode => {
73
+ context.report({
74
+ node: reportedNode,
75
+ loc: { start: tokenBefore.loc.start, end: tokenAfter.loc.end },
76
+ messageId: 'unnecessaryParens',
77
+ fix: (fixer) => [ fixer.remove(tokenBefore), fixer.remove(tokenAfter) ],
78
+ })
79
+ }
80
+
81
+ if (parent && isParenthesized(parent, sourceCode)) {
82
+ const parentTokenBefore = sourceCode.getTokenBefore(parent)
83
+ const parentTokenAfter = sourceCode.getTokenAfter(parent)
84
+ if (parentTokenBefore && parentTokenAfter &&
85
+ parentTokenBefore.range[0] === tokenBefore.range[0] - 1 &&
86
+ parentTokenAfter.range[1] === tokenAfter.range[1] + 1) {
87
+ reportAndFixUnnecessaryParens(node)
88
+ return
89
+ }
90
+ }
91
+
92
+ if (node.type === 'Literal' || node.type === 'Identifier') {
93
+ if (parent && (
94
+ parent.type === 'VariableDeclarator' && parent.init === node ||
95
+ parent.type === 'ReturnStatement' && parent.argument === node ||
96
+ parent.type === 'ExpressionStatement' && parent.expression === node ||
97
+ parent.type === 'ArrayExpression' && parent.elements.includes(node) ||
98
+ parent.type === 'Property' && parent.value === node && !parent.method && !parent.shorthand
99
+ )) {
100
+ reportAndFixUnnecessaryParens(node)
101
+ return
102
+ }
103
+ }
104
+
105
+ if (node.type === 'AwaitExpression' || node.type === 'YieldExpression') {
106
+ if (parent && (
107
+ parent.type === 'ExpressionStatement' ||
108
+ parent.type === 'VariableDeclarator' ||
109
+ parent.type === 'ReturnStatement' ||
110
+ parent.type === 'AssignmentExpression' && parent.right === node
111
+ )) {
112
+ reportAndFixUnnecessaryParens(node)
113
+ return
114
+ }
115
+ }
116
+
117
+ if (parent && parent.type === 'ArrowFunctionExpression' && parent.body === node) {
118
+ if (node.type !== 'ObjectExpression') {
119
+ reportAndFixUnnecessaryParens(node)
120
+ return
121
+ }
122
+ }
123
+
124
+ if (parent && parent.type === 'ReturnStatement' && parent.argument === node &&
125
+ (node.type === 'JSXElement' || node.type === 'JSXFragment')) {
126
+ context.report({
127
+ node,
128
+ loc: { start: tokenBefore.loc.start, end: tokenAfter.loc.end },
129
+ messageId: 'unnecessaryParens',
130
+ fix: (fixer) => {
131
+ const returnToken = sourceCode.getFirstToken(parent)
132
+ return [
133
+ fixer.replaceTextRange([ returnToken.range[1], node.range[0] ], ' '),
134
+ fixer.removeRange([ node.range[1], tokenAfter.range[1] ]),
135
+ ]
136
+ },
137
+ })
138
+ return
139
+ }
140
+ }
141
+
142
+
143
+ function checkUnnecessaryBraces (blockStatement, controllingNode) {
144
+ if (!blockStatement || blockStatement.type !== 'BlockStatement')
145
+ return
146
+
147
+ if (blockStatement.body.length !== 1)
148
+ return
149
+
150
+ const singleStatement = blockStatement.body[0]
151
+
152
+ if (isDeclaration(singleStatement))
153
+ return
154
+
155
+ if (controllingNode && controllingNode.type === 'IfStatement' &&
156
+ !controllingNode.alternate &&
157
+ singleStatement.type === 'IfStatement')
158
+ return
159
+
160
+ if (singleStatement.type === 'BlockStatement')
161
+ return
162
+
163
+ context.report({
164
+ node: blockStatement,
165
+ messageId: 'unnecessaryBraces',
166
+ fix (fixer) {
167
+ const firstToken = sourceCode.getFirstToken(blockStatement)
168
+ const lastToken = sourceCode.getLastToken(blockStatement)
169
+ const innerText = sourceCode.getText(singleStatement)
170
+
171
+ if (!firstToken || firstToken.value !== '{' || !lastToken || lastToken.value !== '}')
172
+ return null
173
+
174
+ const firstStatementToken = sourceCode.getFirstToken(singleStatement)
175
+ const lastStatementToken = sourceCode.getLastToken(singleStatement)
176
+
177
+ if (firstStatementToken && firstToken.range[1] < firstStatementToken.range[0]) {
178
+ const tokensBefore = sourceCode.getTokensBetween(firstToken, firstStatementToken, {
179
+ includeComments: false,
180
+ })
181
+ if (tokensBefore.length > 0)
182
+ return null
183
+ }
184
+
185
+ if (lastStatementToken && lastToken.range[1] < lastStatementToken.range[0]) {
186
+ const tokensAfter = sourceCode.getTokensBetween(lastStatementToken, lastToken, {
187
+ includeComments: false,
188
+ })
189
+ if (tokensAfter.length > 0)
190
+ return null
191
+ }
192
+
193
+ return fixer.replaceTextRange(blockStatement.range, innerText)
194
+ },
195
+ })
196
+ }
197
+
198
+
199
+ function checkArrowFunctionBraces (node) {
200
+ if (!node.body || node.body.type !== 'BlockStatement')
201
+ return
202
+
203
+ const blockBody = node.body
204
+ if (blockBody.body.length !== 1)
205
+ return
206
+
207
+ const singleStatement = blockBody.body[0]
208
+ if (singleStatement.type !== 'ReturnStatement')
209
+ return
210
+
211
+ if (!singleStatement.argument)
212
+ return
213
+
214
+ const returnedExpression = singleStatement.argument
215
+ const needsParens = returnedExpression.type === 'ObjectExpression'
216
+
217
+ context.report({
218
+ node: blockBody,
219
+ messageId: 'unnecessaryBraces',
220
+ fix (fixer) {
221
+ const arrowToken = sourceCode.getTokenBefore(blockBody)
222
+ if (!arrowToken || arrowToken.value !== '=>')
223
+ return null
224
+
225
+ const firstBlockToken = sourceCode.getFirstToken(blockBody)
226
+ const lastBlockToken = sourceCode.getLastToken(blockBody)
227
+ if (!firstBlockToken || firstBlockToken.value !== '{' || !lastBlockToken || lastBlockToken.value !== '}')
228
+ return null
229
+
230
+ let expressionText = sourceCode.getText(returnedExpression)
231
+
232
+ const textBeforeBlock = sourceCode.text.slice(arrowToken.range[1], firstBlockToken.range[0])
233
+ if (textBeforeBlock.trim())
234
+ return null
235
+
236
+ const returnToken = sourceCode.getFirstToken(singleStatement)
237
+ const textBeforeReturn = sourceCode.text.slice(firstBlockToken.range[1], returnToken.range[0])
238
+ const textAfterReturn = sourceCode.text.slice(returnToken.range[1], returnedExpression.range[0])
239
+ const textAfterExpr = sourceCode.text.slice(returnedExpression.range[1], lastBlockToken.range[0])
240
+
241
+ if (textBeforeReturn.trim() || textAfterReturn.trim() || textAfterExpr.trim())
242
+ return null
243
+
244
+ if (needsParens && !isParenthesized(returnedExpression, sourceCode))
245
+ expressionText = `(${expressionText})`
246
+
247
+ let replacementText = (needsParens || expressionText.startsWith('(') ? '' : ' ') + expressionText
248
+ replacementText = replacementText.replace(/\s+/g, ' ')
249
+ return fixer.replaceTextRange(blockBody.range, replacementText.trim())
250
+ },
251
+ })
252
+ }
253
+
254
+
255
+ return {
256
+ MemberExpression: checkDotNotation,
257
+
258
+ VariableDeclarator (node) {
259
+ checkUnnecessaryParens(node.init)
260
+ },
261
+ ExpressionStatement (node) {
262
+ checkUnnecessaryParens(node.expression)
263
+ },
264
+ ReturnStatement (node) {
265
+ checkUnnecessaryParens(node.argument)
266
+ },
267
+ BinaryExpression (node) {
268
+ checkUnnecessaryParens(node.left); checkUnnecessaryParens(node.right)
269
+ },
270
+ LogicalExpression (node) {
271
+ checkUnnecessaryParens(node.left); checkUnnecessaryParens(node.right)
272
+ },
273
+ UnaryExpression (node) {
274
+ checkUnnecessaryParens(node.argument)
275
+ },
276
+ AwaitExpression (node) {
277
+ checkUnnecessaryParens(node.argument)
278
+ },
279
+ YieldExpression (node) {
280
+ checkUnnecessaryParens(node.argument)
281
+ },
282
+ ConditionalExpression (node) {
283
+ checkUnnecessaryParens(node.test)
284
+ checkUnnecessaryParens(node.consequent)
285
+ checkUnnecessaryParens(node.alternate)
286
+ },
287
+ CallExpression (node) {
288
+ node.arguments.forEach(arg => checkUnnecessaryParens(arg))
289
+ checkUnnecessaryParens(node.callee)
290
+ },
291
+ NewExpression (node) {
292
+ node.arguments.forEach(arg => checkUnnecessaryParens(arg))
293
+ checkUnnecessaryParens(node.callee)
294
+ },
295
+ ArrayExpression (node) {
296
+ node.elements.forEach(arg => checkUnnecessaryParens(arg))
297
+ },
298
+ Property (node) {
299
+ checkUnnecessaryParens(node.value)
300
+ },
301
+
302
+ IfStatement (node) {
303
+ checkUnnecessaryBraces(node.consequent, node)
304
+ if (node.alternate && node.alternate.type !== 'IfStatement')
305
+ checkUnnecessaryBraces(node.alternate, node)
306
+ },
307
+ ForStatement (node) {
308
+ checkUnnecessaryBraces(node.body, node)
309
+ },
310
+ ForInStatement (node) {
311
+ checkUnnecessaryBraces(node.body, node)
312
+ },
313
+ ForOfStatement (node) {
314
+ checkUnnecessaryBraces(node.body, node)
315
+ },
316
+ WhileStatement (node) {
317
+ checkUnnecessaryBraces(node.body, node)
318
+ },
319
+ DoWhileStatement (node) {
320
+ checkUnnecessaryBraces(node.body, node)
321
+ },
322
+
323
+ ArrowFunctionExpression (node) {
324
+ checkUnnecessaryParens(node.body)
325
+ checkArrowFunctionBraces(node)
326
+ },
327
+ }
328
+ },
329
+ }