Files
lifeforge/eslint/padding-react-hooks.ts
2026-06-25 08:53:36 +08:00

158 lines
4.6 KiB
TypeScript

import type { TSESTree } from '@typescript-eslint/utils'
import type { Rule } from 'eslint'
const rule: Rule.RuleModule = {
meta: {
type: 'layout',
docs: {
description: 'Enforce padding lines between and around React hook calls.',
category: 'Stylistic Issues'
},
fixable: 'whitespace',
schema: [],
messages: {
noBlankLineBetweenHooks: 'Expected no blank line between hook calls.',
blankLineRequiredAfterHook: 'Expected blank line after hook calls.',
blankLineRequiredBeforeHook: 'Expected blank line before hook calls.'
}
},
create(context) {
const sourceCode = context.sourceCode
function isHookCall(node: any): boolean {
if (!node || node.type !== 'CallExpression') return false
const callee = node.callee
if (callee.type === 'Identifier') {
return /^use[A-Z]/.test(callee.name)
}
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier'
) {
return /^use[A-Z]/.test(callee.property.name)
}
return false
}
function isHookStatement(statement: TSESTree.Statement): boolean {
if (statement.type === 'ExpressionStatement') {
return isHookCall(
(statement as TSESTree.ExpressionStatement).expression
)
}
if (statement.type === 'VariableDeclaration') {
return (statement as TSESTree.VariableDeclaration).declarations.some(
decl => isHookCall(decl.init)
)
}
return false
}
function isSingleLineHookStatement(statement: TSESTree.Statement): boolean {
return (
isHookStatement(statement) &&
statement.loc?.start.line === statement.loc?.end.line
)
}
function isConstDeclaration(statement: TSESTree.Statement): boolean {
return (
statement.type === 'VariableDeclaration' &&
(statement as TSESTree.VariableDeclaration).kind === 'const'
)
}
function hasBlankLineBetween(startLine: number, endLine: number): boolean {
for (let line = startLine + 1; line < endLine; line++) {
const lineText = sourceCode.lines[line - 1]
if (lineText.trim() === '') {
return true
}
}
return false
}
function checkStatements(statements: TSESTree.Statement[]) {
for (let i = 0; i < statements.length - 1; i++) {
const current = statements[i]
const next = statements[i + 1]
const currentIsHook = isSingleLineHookStatement(current)
const nextIsHook = isSingleLineHookStatement(next)
const currentIsConst = isConstDeclaration(current)
const nextIsConst = isConstDeclaration(next)
const currentIsExpr = current.type === 'ExpressionStatement'
const nextIsExpr = next.type === 'ExpressionStatement'
const currentEndLine = current.loc?.end.line
const nextStartLine = next.loc?.start.line
if (currentEndLine === undefined || nextStartLine === undefined)
continue
if (currentIsHook && nextIsHook) {
if (hasBlankLineBetween(currentEndLine, nextStartLine)) {
context.report({
node: next,
messageId: 'noBlankLineBetweenHooks',
fix(fixer) {
const rangeStart = current.range?.[1]
const rangeEnd = next.range?.[0]
if (rangeStart === undefined || rangeEnd === undefined)
return null
const textBetween = sourceCode.text.slice(rangeStart, rangeEnd)
const fixedText = textBetween.replace(/\r?\n\s*\r?\n/g, '\n')
return fixer.replaceTextRange([rangeStart, rangeEnd], fixedText)
}
})
}
} else if (currentIsHook && (nextIsConst || nextIsExpr)) {
if (!hasBlankLineBetween(currentEndLine, nextStartLine)) {
context.report({
node: next,
messageId: 'blankLineRequiredAfterHook',
fix(fixer) {
return fixer.insertTextBefore(next, '\n')
}
})
}
} else if ((currentIsConst || currentIsExpr) && nextIsHook) {
if (!hasBlankLineBetween(currentEndLine, nextStartLine)) {
context.report({
node: next,
messageId: 'blankLineRequiredBeforeHook',
fix(fixer) {
return fixer.insertTextBefore(next, '\n')
}
})
}
}
}
}
return {
BlockStatement(node: any) {
checkStatements(node.body)
},
Program(node: any) {
checkStatements(node.body)
}
}
}
}
export default rule