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 === 0) return; const openBrace = sourceCode.getFirstToken(node); let closingBraceToken = sourceCode.getTokenAfter(properties[properties.length - 1]); while (closingBraceToken && closingBraceToken.value !== "}") { closingBraceToken = sourceCode.getTokenAfter(closingBraceToken); } const closingBrace = closingBraceToken; if (openBrace.loc.start.line === closingBrace.loc.end.line) return; const firstProperty = properties[0]; if (openBrace.loc.end.line !== firstProperty.loc.start.line - 1) { context.report({ node: firstProperty, message: "First property should be exactly on the next line", fix(fixer) { const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; const indent = baseIndent + " "; return fixer.replaceTextRange( [ openBrace.range[1], firstProperty.range[0] ], "\n" + indent ); } }); } if (properties.length > 1) { 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 - 1) { context.report({ node: next, message: "Each property should be exactly on the next line (no empty lines)", fix(fixer) { 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]; if (lastProperty.loc.end.line !== closingBrace.loc.start.line - 1) { context.report({ node: closingBrace, message: "Object closing brace should be exactly on the next line", fix(fixer) { const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; // Be careful about trailing comma const tokenAfter = sourceCode.getTokenAfter(lastProperty); const hasTrailingComma = tokenAfter && tokenAfter.value === ","; const startRange = hasTrailingComma ? tokenAfter.range[1] : lastProperty.range[1]; return fixer.replaceTextRange( [ startRange, closingBrace.range[0] ], "\n" + baseIndent ); } }); } 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 === 0) 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 - 1) { context.report({ node: firstAttr, message: "First JSX attribute should be exactly on the next 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 - 1) { context.report({ node: next, message: "Each JSX attribute should be exactly on the next line (no empty lines)", 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]; // Check if self-closing slash is present let hasSelfClosingSlash = false; let slashToken = null; if (node.selfClosing && allTokens.length >= 2) { slashToken = allTokens[allTokens.length - 2]; if (slashToken && slashToken.value === "/") { hasSelfClosingSlash = true; } } if (hasSelfClosingSlash) { if (lastAttr.loc.end.line !== slashToken.loc.start.line - 1) { context.report({ node: slashToken, message: "JSX closing slash should be exactly on the next line", fix(fixer) { const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; return fixer.replaceTextRange( [ lastAttr.range[1], slashToken.range[0] ], "\n" + baseIndent ); } }); } } else { if (lastAttr.loc.end.line !== closingBracket.loc.start.line - 1) { context.report({ node: closingBracket, message: "JSX closing bracket should be exactly on the next line", fix(fixer) { const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1]; const baseIndent = openBraceLine.match(/^\s*/)[0]; 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 === 0) return; const openBracket = sourceCode.getFirstToken(node); const closingBracket = sourceCode.getLastToken(node); if (openBracket.loc.start.line === closingBracket.loc.end.line) return; const firstElement = node.elements[0]; if (openBracket.loc.end.line !== firstElement.loc.start.line - 1) { context.report({ node: firstElement, message: "First array element should be exactly on the next 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 - 1) { context.report({ node: next, message: "Each array element should be exactly on the next line (no empty lines)", 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]; if (lastElement && lastElement.loc.end.line !== closingBracket.loc.start.line - 1) { context.report({ node: closingBracket, message: "Array closing bracket should be exactly on the next line", fix(fixer) { const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; const tokenAfter = sourceCode.getTokenAfter(lastElement); const hasTrailingComma = tokenAfter && tokenAfter.value === ","; const startRange = hasTrailingComma ? tokenAfter.range[1] : lastElement.range[1]; return fixer.replaceTextRange( [ startRange, 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") || source.includes("/type")) { 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); } } }, "multiline-jsx-children": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { return { JSXElement(node) { if (!node.closingElement) return; if (node.children.length === 0) return; const meaningfulChildren = node.children.filter(child => { if (child.type === "JSXText" && child.value.trim() === "") return false; return true; }); if (meaningfulChildren.length === 0) return; const openingElement = node.openingElement; const closingElement = node.closingElement; const isMultilineOpening = openingElement.loc.start.line !== openingElement.loc.end.line; if (!isMultilineOpening) return; const sourceCode = context.getSourceCode(); if (meaningfulChildren.length === 1 && meaningfulChildren[0].type === "JSXText") { const child = meaningfulChildren[0]; const text = sourceCode.getText(child); const hasLeadingNewline = /^\s*\n/.test(text); const hasTrailingNewline = /\n\s*$/.test(text); if (!hasLeadingNewline || !hasTrailingNewline) { context.report({ node: child, message: "Multiline JSX element text content must be on its own line", fix(fixer) { const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; const indent = baseIndent + " "; return fixer.replaceText(child, "\n" + indent + text.trim() + "\n" + baseIndent); } }); } return; } const firstChild = meaningfulChildren[0]; const openingEndLine = openingElement.loc.end.line; let needsFirstChildFix = false; if (firstChild.type === "JSXText") { const text = sourceCode.getText(firstChild); if (!/^\s*\n/.test(text)) needsFirstChildFix = true; } else if (firstChild.loc.start.line === openingEndLine) { needsFirstChildFix = true; } if (needsFirstChildFix) { context.report({ node: firstChild, message: "First child of a multiline JSX element must be on a new line", fix(fixer) { const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; const indent = baseIndent + " "; if (firstChild.type === "JSXText") { const text = sourceCode.getText(firstChild); return fixer.replaceText(firstChild, "\n" + indent + text.replace(/^\s+/, "")); } return fixer.insertTextBefore(firstChild, "\n" + indent); } }); } const lastChild = meaningfulChildren[meaningfulChildren.length - 1]; let needsLastChildFix = false; if (lastChild.type === "JSXText") { const text = sourceCode.getText(lastChild); if (!/\n\s*$/.test(text)) needsLastChildFix = true; } else if (lastChild.loc.end.line === closingElement.loc.start.line) { needsLastChildFix = true; } if (needsLastChildFix) { context.report({ node: closingElement, message: "Closing tag of a multiline JSX element must be on a new line", fix(fixer) { const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1]; const baseIndent = openBracketLine.match(/^\s*/)[0]; if (lastChild.type === "JSXText") { const text = sourceCode.getText(lastChild); return fixer.replaceText(lastChild, text.replace(/\s+$/, "") + "\n" + baseIndent); } return fixer.insertTextBefore(closingElement, "\n" + baseIndent); } }); } } }; } }, "no-jsx-parens-in-return": { meta: { type: "layout", fixable: "code", schema: [] }, create(context) { return { ReturnStatement(node) { if (!node.argument) return; if (node.argument.type === "JSXElement" || node.argument.type === "JSXFragment") { const sourceCode = context.getSourceCode(); const returnToken = sourceCode.getFirstToken(node); const firstChildToken = sourceCode.getFirstToken(node.argument); const tokensBetween = sourceCode.getTokensBetween(returnToken, firstChildToken); const openParen = tokensBetween.find(t => t.value === "("); if (openParen) { const closeParen = sourceCode.getTokenAfter(node.argument); if (closeParen && closeParen.value === ")") { context.report({ node: node.argument, message: "Returned JSX element should not be wrapped in parentheses", fix(fixer) { const fixes = [ fixer.remove(closeParen) ]; if (returnToken.loc.end.line !== firstChildToken.loc.start.line) { fixes.push(fixer.replaceTextRange([ returnToken.range[1], firstChildToken.range[0] ], " ")); } else { fixes.push(fixer.remove(openParen)); } return fixes; } }); } } else if (returnToken.loc.end.line !== firstChildToken.loc.start.line) { context.report({ node: node.argument, message: "Returned JSX element must start on the same line as 'return' to avoid Automatic Semicolon Insertion (ASI)", fix(fixer) { return fixer.replaceTextRange([ returnToken.range[1], firstChildToken.range[0] ], " "); } }); } } }, ArrowFunctionExpression(node) { if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") { const sourceCode = context.getSourceCode(); const arrowToken = sourceCode.getTokenBefore(node.body, t => t.value === "=>"); const firstChildToken = sourceCode.getFirstToken(node.body); const tokensBetween = sourceCode.getTokensBetween(arrowToken, firstChildToken); const openParen = tokensBetween.find(t => t.value === "("); if (openParen) { const closeParen = sourceCode.getTokenAfter(node.body); if (closeParen && closeParen.value === ")") { context.report({ node: node.body, message: "Implicitly returned JSX element should not be wrapped in parentheses", fix(fixer) { const fixes = [ fixer.remove(closeParen) ]; if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) { fixes.push(fixer.replaceTextRange([ arrowToken.range[1], firstChildToken.range[0] ], " ")); } else { fixes.push(fixer.remove(openParen)); } return fixes; } }); } } else if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) { context.report({ node: node.body, message: "Implicitly returned JSX element must start on the same line as '=>'", fix(fixer) { return fixer.replaceTextRange([ arrowToken.range[1], firstChildToken.range[0] ], " "); } }); } } } }; } }, "no-newline-after-jsx": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { function checkPunctuation(node) { const sourceCode = context.getSourceCode(); const lastToken = sourceCode.getLastToken(node); const nextToken = sourceCode.getTokenAfter(node); if (nextToken && (nextToken.value === ";" || nextToken.value === "," || nextToken.value === ")")) { if (lastToken.loc.end.line !== nextToken.loc.start.line) { if (sourceCode.commentsExistBetween(node, nextToken)) return; context.report({ node: nextToken, message: `Punctuation '${nextToken.value}' should be on the same line as the preceding JSX element`, fix(fixer) { return fixer.replaceTextRange([ node.range[1], nextToken.range[0] ], ""); } }); } } } return { JSXElement: checkPunctuation, JSXFragment: checkPunctuation }; } }, "no-static-inline-styles": { meta: { type: "suggestion", messages: { staticStyle: "Static inline styles are not allowed. Please move them to a stylesheet file." }, schema: [] }, create(context) { function checkProperty(propertyNode) { if (!propertyNode || propertyNode.type !== "Property") return; const value = propertyNode.value; if (!value) return; let isStatic = false; if (value.type === "Literal") { if (typeof value.value === "string" || typeof value.value === "number") { isStatic = true; } } else if (value.type === "UnaryExpression" && value.operator === "-") { if (value.argument && value.argument.type === "Literal" && typeof value.argument.value === "number") { isStatic = true; } } else if (value.type === "TemplateLiteral" && value.expressions.length === 0) { isStatic = true; } if (isStatic) { context.report({ node: propertyNode, messageId: "staticStyle" }); } } function checkObjectExpression(node) { if (node.type !== "ObjectExpression") return; node.properties.forEach(checkProperty); } function checkStyleValue(node) { if (!node) return; if (node.type === "ObjectExpression") { checkObjectExpression(node); } else if (node.type === "ArrayExpression") { node.elements.forEach(element => { if (element && element.type === "ObjectExpression") { checkObjectExpression(element); } }); } } return { JSXAttribute(node) { const attrName = node.name.name; if (typeof attrName !== "string") return; if (attrName === "style" || attrName.endsWith("Style")) { if (node.value && node.value.type === "JSXExpressionContainer") { checkStyleValue(node.value.expression); } } } }; } } };