mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-27 22:36:06 +00:00
158 lines
4.6 KiB
TypeScript
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
|