module.exports = { "multiline-import-specifiers": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { return { ImportDeclaration(node) { const specifiers = node.specifiers.filter( s => s.type === "ImportSpecifier" ); if (specifiers.length <= 1) return; const sourceCode = context.getSourceCode(); for (let i = 0; i < specifiers.length - 1; i++) { const current = specifiers[i]; const next = specifiers[i + 1]; if (current.loc.end.line === next.loc.start.line) { context.report({ node: next, message: "Each import specifier should be on a new line", fix(fixer) { const comma = sourceCode.getTokenBefore(next); return fixer.replaceTextRange( [ comma.range[1], next.range[0] ], "\n " ); } }); } } const lastSpecifier = specifiers[specifiers.length - 1]; const tokenAfter = sourceCode.getTokenAfter(lastSpecifier); if (tokenAfter && tokenAfter.value === ",") { context.report({ node: lastSpecifier, message: "No trailing comma in imports", fix(fixer) { return fixer.remove(tokenAfter); } }); } } }; } }, "multiline-object-properties": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); function checkProperties(node, properties) { if (properties.length <= 1) return; for (let i = 0; i < properties.length - 1; i++) { const current = properties[i]; const next = properties[i + 1]; if (current.loc.end.line === next.loc.start.line) { context.report({ node: next, message: "Each property should be on a new line", fix(fixer) { const openBrace = sourceCode.getFirstToken(node); const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; const indent = baseIndent + " "; const comma = sourceCode.getTokenBefore(next); return fixer.replaceTextRange( [ comma.range[1], next.range[0] ], "\n" + indent ); } }); } } const lastProperty = properties[properties.length - 1]; const tokenAfter = sourceCode.getTokenAfter(lastProperty); if (tokenAfter && tokenAfter.value === ",") { context.report({ node: lastProperty, message: "No trailing comma in objects", fix(fixer) { return fixer.remove(tokenAfter); } }); } } return { ObjectExpression(node) { checkProperties(node, node.properties); }, ObjectPattern(node) { checkProperties(node, node.properties); } }; } }, "multiline-jsx-attributes": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); return { JSXOpeningElement(node) { if (node.attributes.length <= 1) return; const firstToken = sourceCode.getFirstToken(node); const tagNameToken = sourceCode.getTokenAfter(firstToken); const firstAttr = node.attributes[0]; if (tagNameToken.loc.end.line === firstAttr.loc.start.line) { context.report({ node: firstAttr, message: "First JSX attribute should be on a new line", fix(fixer) { const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; const indent = baseIndent + " "; return fixer.replaceTextRange( [ tagNameToken.range[1], firstAttr.range[0] ], "\n" + indent ); } }); } for (let i = 0; i < node.attributes.length - 1; i++) { const current = node.attributes[i]; const next = node.attributes[i + 1]; if (current.loc.end.line === next.loc.start.line) { context.report({ node: next, message: "Each JSX attribute should be on a new line", fix(fixer) { const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; const indent = baseIndent + " "; return fixer.replaceTextRange( [ current.range[1], next.range[0] ], "\n" + indent ); } }); } } const lastAttr = node.attributes[node.attributes.length - 1]; const allTokens = []; let currentToken = sourceCode.getTokenAfter(lastAttr); while (currentToken && currentToken.range[1] <= node.range[1]) { allTokens.push(currentToken); currentToken = sourceCode.getTokenAfter(currentToken); } const closingBracket = allTokens[allTokens.length - 1]; if (lastAttr.loc.end.line === closingBracket.loc.start.line) { context.report({ node: closingBracket, message: "JSX closing bracket should be on a new line", fix(fixer) { const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; if (node.selfClosing && allTokens.length >= 2) { const slashToken = allTokens[allTokens.length - 2]; if (slashToken && slashToken.value === "/") { return fixer.replaceTextRange( [ lastAttr.range[1], slashToken.range[0] ], "\n" + baseIndent ); } } return fixer.replaceTextRange( [ lastAttr.range[1], closingBracket.range[0] ], "\n" + baseIndent ); } }); } } }; } }, "multiline-array-elements": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); return { ArrayExpression(node) { if (node.elements.length <= 1) return; const openBracket = sourceCode.getFirstToken(node); const firstElement = node.elements[0]; if (openBracket.loc.end.line === firstElement.loc.start.line) { context.report({ node: firstElement, message: "First array element should be on a new line", fix(fixer) { const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; const indent = baseIndent + " "; return fixer.replaceTextRange( [ openBracket.range[1], firstElement.range[0] ], "\n" + indent ); } }); } for (let i = 0; i < node.elements.length - 1; i++) { const current = node.elements[i]; const next = node.elements[i + 1]; if (!current || !next) continue; if (current.loc.end.line === next.loc.start.line) { context.report({ node: next, message: "Each array element should be on a new line", fix(fixer) { const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; const indent = baseIndent + " "; const comma = sourceCode.getTokenBefore(next); return fixer.replaceTextRange( [ comma.range[1], next.range[0] ], "\n" + indent ); } }); } } const lastElement = node.elements[node.elements.length - 1]; const closingBracket = sourceCode.getLastToken(node); if (lastElement && lastElement.loc.end.line === closingBracket.loc.start.line) { context.report({ node: closingBracket, message: "Array closing bracket should be on a new line", fix(fixer) { const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; return fixer.replaceTextRange( [ lastElement.range[1], closingBracket.range[0] ], "\n" + baseIndent ); } }); } const tokenAfter = sourceCode.getTokenAfter(lastElement); if (tokenAfter && tokenAfter.value === "," && tokenAfter.range[1] <= closingBracket.range[0]) { context.report({ node: lastElement, message: "No trailing comma in arrays", fix(fixer) { return fixer.remove(tokenAfter); } }); } } }; } }, "custom-import-order": { meta: { type: "suggestion", fixable: "code", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); const imports = []; return { ImportDeclaration(node) { imports.push(node); }, "Program:exit"() { if (imports.length <= 1) return; const categorized = categorizeImports(imports, sourceCode); for (let i = 0; i < imports.length - 1; i++) { const current = imports[i]; const next = imports[i + 1]; const currentCategory = getImportCategory(current, sourceCode); const nextCategory = getImportCategory(next, sourceCode); if (currentCategory > nextCategory) { context.report({ node: next, message: `Import from "${next.source.value}" should come before "${current.source.value}"`, fix(fixer) { return fixImportOrder(fixer, imports, categorized, sourceCode); } }); break; } } } }; function getImportCategory(node, sourceCode) { const source = node.source.value; const code = sourceCode.getText(node); if (source === "react") return 1; if (source === "react-native") return 2; if (source.startsWith("./")) { if (code.includes("import type") || source.includes(".types")) { return 3.1; } if (source.includes("style") || source.includes("Style")) { return 3.2; } return 3.3; } const specifiers = node.specifiers .filter(s => s.type === "ImportSpecifier") .map(s => s.imported ? s.imported.name : ""); const hasNCoreHook = specifiers.some(name => name.startsWith("NCore")); const hasUseHook = specifiers.some(name => name.startsWith("use")); if (hasNCoreHook) return 4.1; if (hasUseHook) return 4.2; if (code.includes("import type") || source.includes("/types/") || source.includes("/type") || source.includes(".types")) { return 5; } if (source.startsWith("@/") || source.startsWith("~/")) return 6.3; if (!source.startsWith(".")) return 6.1; if (source.startsWith("../")) return 6.2; return 6.4; } function categorizeImports(imports, sourceCode) { return imports.map(imp => ({ node: imp, category: getImportCategory(imp, sourceCode), source: imp.source.value })).sort((a, b) => { if (a.category !== b.category) { return a.category - b.category; } return a.source.localeCompare(b.source); }); } function fixImportOrder(fixer, imports, categorized, sourceCode) { const firstImport = imports[0]; const lastImport = imports[imports.length - 1]; const rangeStart = firstImport.range[0]; const rangeEnd = lastImport.range[1]; let newImports = ""; categorized.forEach((imp, index) => { newImports += sourceCode.getText(imp.node); if (index < categorized.length - 1) { newImports += "\n"; } }); return fixer.replaceTextRange([ rangeStart, rangeEnd ], newImports); } } } };