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 === 0) return; const sourceCode = context.getSourceCode(); const openBrace = sourceCode.getFirstToken(node, t => t.value === "{"); const closeBrace = sourceCode.getLastToken(node, t => t.value === "}"); if (!openBrace || !closeBrace) return; const firstSpecifier = specifiers[0]; if (firstSpecifier.loc.start.line - openBrace.loc.end.line !== 1) { context.report({ node: firstSpecifier, message: "There should be exactly one newline after '{'.", fix(fixer) { return fixer.replaceTextRange([ openBrace.range[1], firstSpecifier.range[0] ], "\n "); } }); } for (let i = 0; i < specifiers.length - 1; i++) { const current = specifiers[i]; const next = specifiers[i + 1]; if (next.loc.start.line - current.loc.end.line !== 1) { context.report({ node: next, message: "Each import specifier should be on a new line without blank lines between them.", 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); } }); } const tokenBeforeClose = sourceCode.getTokenBefore(closeBrace); if (closeBrace.loc.start.line - tokenBeforeClose.loc.end.line !== 1) { context.report({ node: closeBrace, message: "There should be exactly one newline before '}'.", fix(fixer) { return fixer.replaceTextRange([ tokenBeforeClose.range[1], closeBrace.range[0] ], "\n"); } }); } } }; } }, "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, ArrayPattern"(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); let isSorted = true; let errorNode = null; for (let i = 0; i < imports.length; i++) { if (imports[i] !== categorized[i].node) { isSorted = false; errorNode = imports[i]; break; } } if (!isSorted) { context.report({ node: errorNode, message: "Imports are not sorted correctly according to custom rules.", fix(fixer) { return fixImportOrder(fixer, imports, categorized, sourceCode); } }); } } }; 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 => { const isDefaultImport = imp.specifiers.some(s => s.type === "ImportDefaultSpecifier"); const textLength = sourceCode.getText(imp).replace(/\s+/g, "").length; return { node: imp, category: getImportCategory(imp, sourceCode), source: imp.source.value, isDefaultImport, textLength }; }).sort((a, b) => { if (a.category !== b.category) { return a.category - b.category; } if (a.category >= 6) { if (a.isDefaultImport !== b.isDefaultImport) { return a.isDefaultImport ? 1 : -1; } if (a.textLength !== b.textLength) { return b.textLength - a.textLength; } } 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); } } }, VariableDeclarator(node) { if (node.id && node.id.name === "styles" && node.init && node.init.type === "ObjectExpression") { let parent = node.parent; let isInsideUseStyles = false; while (parent) { if (parent.type === "VariableDeclarator" && parent.id && parent.id.name === "useStyles") { isInsideUseStyles = true; break; } if (parent.type === "FunctionDeclaration" && parent.id && parent.id.name === "useStyles") { isInsideUseStyles = true; break; } parent = parent.parent; } if (isInsideUseStyles) { node.init.properties.forEach(styleClass => { let styleObj = styleClass.value; if (styleObj && styleObj.type === "TSAsExpression") { styleObj = styleObj.expression; } if (styleObj && styleObj.type === "ObjectExpression") { styleObj.properties.forEach(prop => { checkProperty(prop); }); } }); } } } }; } }, "jsx-ternary-formatting": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { return { JSXExpressionContainer(node) { if (node.expression.type !== "ConditionalExpression") return; const sourceCode = context.getSourceCode(); const openBrace = sourceCode.getFirstToken(node); const closeBrace = sourceCode.getLastToken(node); const cond = node.expression; let hasJSX = false; if ( cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment" || cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment" ) { hasJSX = true; } if (!hasJSX) return; const checkAndRemoveParens = (nodeToUnparen) => { const currentToken = sourceCode.getTokenBefore(nodeToUnparen); const afterToken = sourceCode.getTokenAfter(nodeToUnparen); let removed = false; while (currentToken && currentToken.value === "(" && afterToken && afterToken.value === ")") { context.report({ node: currentToken, message: "Parentheses are not allowed around JSX in ternaries.", fix(fixer) { return [ fixer.remove(currentToken), fixer.remove(afterToken) ]; } }); removed = true; break; } return removed; }; let hasParens = false; if (cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment") { if (checkAndRemoveParens(cond.consequent)) hasParens = true; } if (cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment") { if (checkAndRemoveParens(cond.alternate)) hasParens = true; } if (hasParens) return; // Wait for the next fix pass to format the whitespace properly const questionToken = sourceCode.getTokenAfter(cond.test, { filter: t => t.value === "?" }); const colonToken = sourceCode.getTokenAfter(cond.consequent, { filter: t => t.value === ":" }); const getIndent = (token) => { const line = sourceCode.lines[token.loc.start.line - 1]; return line.match(/^\s*/)[0]; }; const openIndent = getIndent(openBrace); const baseIndent = openIndent + " "; const innerIndent = baseIndent + " "; const checkAndFix = (token1, token2, expectedWhitespace) => { let textBetween = sourceCode.getText().slice(token1.range[1], token2.range[0]); textBetween = textBetween.replace(/\r\n/g, "\n"); if (textBetween !== expectedWhitespace) { context.report({ node: token2, message: "Incorrect ternary formatting", fix(fixer) { return fixer.replaceTextRange([ token1.range[1], token2.range[0] ], expectedWhitespace); } }); } }; const firstTestToken = sourceCode.getTokenAfter(openBrace); checkAndFix(openBrace, firstTestToken, "\n" + baseIndent); const firstConsequentToken = sourceCode.getTokenAfter(questionToken); checkAndFix(questionToken, firstConsequentToken, "\n" + innerIndent); const firstColonToken = colonToken; const lastConsequentToken = sourceCode.getTokenBefore(colonToken); checkAndFix(lastConsequentToken, firstColonToken, "\n" + baseIndent); const firstAlternateToken = sourceCode.getTokenAfter(colonToken); checkAndFix(colonToken, firstAlternateToken, "\n" + innerIndent); const lastAlternateToken = sourceCode.getTokenBefore(closeBrace); checkAndFix(lastAlternateToken, closeBrace, "\n" + openIndent); } }; } }, "jsx-no-blank-lines": { meta: { type: "layout", fixable: "whitespace", schema: [] }, create(context) { return { JSXText(node) { const text = node.value; if (!/^\s*$/.test(text)) { return; } const newlines = text.match(/\n/g); if (newlines && newlines.length > 1) { context.report({ node, message: "No blank lines allowed between JSX elements", fix(fixer) { const match = text.match(/\n[ \t]*$/); if (match) { return fixer.replaceText(node, match[0]); } } }); } } }; } }, "sort-jsx-props": { meta: { type: "layout", fixable: "code", schema: [] }, create(context) { return { JSXOpeningElement(node) { const attributes = node.attributes; if (!attributes || attributes.length <= 1) return; const sourceCode = context.getSourceCode(); const getAttrLength = (attr) => { const text = sourceCode.getText(attr); return text.replace(/\s+/g, "").length; }; const groups = []; let currentGroup = []; for (const attr of attributes) { if (attr.type === "JSXSpreadAttribute") { if (currentGroup.length > 1) { groups.push(currentGroup); } currentGroup = []; } else { currentGroup.push(attr); } } if (currentGroup.length > 1) { groups.push(currentGroup); } for (const group of groups) { const sortedGroup = [...group].sort((a, b) => { return getAttrLength(b) - getAttrLength(a); }); let isSorted = true; for (let i = 0; i < group.length; i++) { if (group[i] !== sortedGroup[i]) { isSorted = false; break; } } if (!isSorted) { context.report({ node: group[0], message: "JSX attributes should be sorted from longest to shortest.", fix(fixer) { const firstAttr = group[0]; const startLoc = firstAttr.loc.start; const indent = " ".repeat(startLoc.column); const newText = sortedGroup.map(attr => sourceCode.getText(attr)).join("\n" + indent); const start = group[0].range[0]; const end = group[group.length - 1].range[1]; return fixer.replaceTextRange([ start, end ], newText); } }); } } } }; } }, "sort-import-specifiers": { meta: { type: "layout", fixable: "code", schema: [] }, create(context) { return { ImportDeclaration(node) { checkSpecifiers(node, "ImportSpecifier", "Import"); }, ExportNamedDeclaration(node) { checkSpecifiers(node, "ExportSpecifier", "Export"); } }; function checkSpecifiers(node, specifierType, typeName) { if (!node.specifiers) return; const specifiers = node.specifiers.filter( s => s.type === specifierType ); if (specifiers.length <= 1) return; const sourceCode = context.getSourceCode(); const getSpecLength = (spec) => { return sourceCode.getText(spec).replace(/\s+/g, "").length; }; const sortedSpecifiers = [...specifiers].sort((a, b) => { return getSpecLength(b) - getSpecLength(a); }); let isSorted = true; for (let i = 0; i < specifiers.length; i++) { if (specifiers[i] !== sortedSpecifiers[i]) { isSorted = false; break; } } if (!isSorted) { context.report({ node, message: `${typeName} specifiers should be sorted from longest to shortest.`, fix(fixer) { const firstSpec = specifiers[0]; const lastSpec = specifiers[specifiers.length - 1]; const startLoc = firstSpec.loc.start; let indent = " "; if (firstSpec.loc.start.line !== node.loc.start.line) { indent = " ".repeat(startLoc.column); } const newText = sortedSpecifiers.map(spec => sourceCode.getText(spec)).join(",\n" + indent); return fixer.replaceTextRange([ firstSpec.range[0], lastSpec.range[1] ], newText); } }); } } } }, "sort-translations": { meta: { type: "layout", fixable: "code", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); function checkObject(node) { if (!node.properties || node.properties.length <= 1) return; let hasIsRTL = false; let hasLocale = false; let translationsProp = null; for (const prop of node.properties) { const keyNode = prop.key; if (!keyNode) continue; let keyName = ""; if (keyNode.type === "Identifier") keyName = keyNode.name; else if (keyNode.type === "Literal" || keyNode.type === "JSONLiteral") keyName = keyNode.value; if (keyName === "isRTL") hasIsRTL = true; if (keyName === "locale") hasLocale = true; if (keyName === "translations") translationsProp = prop; } if (hasIsRTL && hasLocale && translationsProp) { const translationsObj = translationsProp.value; if (!translationsObj || !translationsObj.properties || translationsObj.properties.length <= 1) return; const props = translationsObj.properties; const sortedProps = [...props].sort((a, b) => { const lenA = sourceCode.getText(a.key).length + sourceCode.getText(a.value).length; const lenB = sourceCode.getText(b.key).length + sourceCode.getText(b.value).length; if (lenA !== lenB) { return lenB - lenA; } const keyA = a.key.value || a.key.name || ""; const keyB = b.key.value || b.key.name || ""; return keyA.localeCompare(keyB); }); let isSorted = true; let errorNode = null; for (let i = 0; i < props.length; i++) { if (props[i] !== sortedProps[i]) { isSorted = false; errorNode = props[i]; break; } } if (!isSorted) { context.report({ node: errorNode, message: "Translations keys must be sorted by length (longest to shortest).", fix(fixer) { const firstProp = props[0]; const lastProp = props[props.length - 1]; const startLoc = firstProp.loc.start; const indent = " ".repeat(startLoc.column); let newText = ""; sortedProps.forEach((p, index) => { newText += sourceCode.getText(p); if (index < sortedProps.length - 1) { newText += ",\n" + indent; } }); return fixer.replaceTextRange([ firstProp.range[0], lastProp.range[1] ], newText); } }); } } } return { ObjectExpression: checkObject, JSONObjectExpression: checkObject }; } }, "ncore-sort-styles": { meta: { type: "layout", fixable: "code", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); const shorthandDependencies = { borderRadius: [ "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius", "borderTopStartRadius", "borderTopEndRadius", "borderBottomStartRadius", "borderBottomEndRadius" ], padding: [ "paddingHorizontal", "paddingVertical", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "paddingStart", "paddingEnd" ], margin: [ "marginHorizontal", "marginVertical", "marginTop", "marginRight", "marginBottom", "marginLeft", "marginStart", "marginEnd" ], borderWidth: [ "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderStartWidth", "borderEndWidth" ], borderColor: [ "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor", "borderStartColor", "borderEndColor" ], borderStyle: [ "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle" ], paddingHorizontal: [ "paddingLeft", "paddingRight", "paddingStart", "paddingEnd" ], marginHorizontal: [ "marginLeft", "marginRight", "marginStart", "marginEnd" ], flex: [ "flexGrow", "flexShrink", "flexBasis" ], paddingVertical: [ "paddingTop", "paddingBottom" ], marginVertical: [ "marginTop", "marginBottom" ] }; function isStyleObject(node) { if (node.properties.length <= 1) return false; if (node.parent.type === "CallExpression" && node.parent.callee.type === "MemberExpression" && node.parent.callee.object.name === "StyleSheet" && node.parent.callee.property.name === "create") { return true; } if (node.parent.type === "Property" && node.parent.parent.type === "ObjectExpression" && node.parent.parent.parent.type === "CallExpression" && node.parent.parent.parent.callee.type === "MemberExpression" && node.parent.parent.parent.callee.object.name === "StyleSheet") { return true; } const knownStyleKeys = new Set([ "alignItems", "justifyContent", "flexDirection", "flexWrap", "flex", "padding", "margin", "paddingTop", "marginTop", "paddingBottom", "marginBottom", "paddingLeft", "marginLeft", "paddingRight", "marginRight", "paddingHorizontal", "marginHorizontal", "paddingVertical", "marginVertical", "backgroundColor", "color", "fontSize", "fontWeight", "lineHeight", "textAlign", "borderWidth", "borderRadius", "borderColor", "borderStyle", "position", "top", "bottom", "left", "right", "display", "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", "alignSelf" ]); let hasStyleKey = false; for (const prop of node.properties) { if (prop.type === "Property" && prop.key.type === "Identifier") { if (knownStyleKeys.has(prop.key.name)) { hasStyleKey = true; break; } } } if (hasStyleKey) return true; if (node.parent.type === "VariableDeclarator" && (node.parent.id.name === "styles" || node.parent.id.name === "stylesheet" || node.parent.id.name === "style")) { return true; } if (node.parent.type === "TSAsExpression" || node.parent.type === "TypeAssertion") { const typeName = node.parent.typeAnnotation?.typeName?.name; if (typeName && (typeName === "ViewStyle" || typeName === "TextStyle" || typeName === "ImageStyle")) { return true; } } if (node.parent.type === "Property" && node.parent.parent.type === "ObjectExpression") { if (node.parent.parent.parent.type === "VariableDeclarator" && (node.parent.parent.parent.id.name === "styles" || node.parent.parent.parent.id.name === "stylesheet")) { return true; } } return false; } function getSortedProperties(properties) { const webStyles = []; const others = []; for (const p of properties) { if (p.type === "SpreadElement" && p.argument.type === "CallExpression" && p.argument.callee.name === "webStyle") { webStyles.push(p); } else { others.push(p); } } const chunks = []; let currentChunk = []; const chunkSeparators = []; for (const p of others) { if (p.type === "SpreadElement") { chunks.push(currentChunk); chunkSeparators.push(p); currentChunk = []; } else { currentChunk.push(p); } } chunks.push(currentChunk); function sortChunk(chunk) { if (chunk.length <= 1) return chunk; const getLen = (node) => sourceCode.getText(node).length; const initialSorted = chunk.map((node, i) => ({ node, originalIndex: i, length: getLen(node) })).sort((a, b) => { if (a.length !== b.length) return b.length - a.length; return a.originalIndex - b.originalIndex; }); const propNames = chunk.map(p => { if (p.type === "Property") { if (p.key.type === "Identifier") return p.key.name; if (p.key.type === "Literal") return p.key.value; } return null; }); const prerequisites = new Map(); for (let i = 0; i < chunk.length; i++) { prerequisites.set(i, new Set()); } for (let i = 0; i < propNames.length; i++) { const nameA = propNames[i]; if (!nameA) continue; const deps = shorthandDependencies[nameA]; if (deps) { for (let j = 0; j < propNames.length; j++) { if (i === j) continue; if (deps.includes(propNames[j])) { prerequisites.get(j).add(i); } } } } const result = []; const placedIndices = new Set(); const pool = [...initialSorted]; while (pool.length > 0) { let placedAny = false; for (let i = 0; i < pool.length; i++) { const item = pool[i]; const itemOriginalIndex = item.originalIndex; const reqs = prerequisites.get(itemOriginalIndex); let canPlace = true; for (const req of reqs) { if (!placedIndices.has(req)) { canPlace = false; break; } } if (canPlace) { result.push(item.node); placedIndices.add(itemOriginalIndex); pool.splice(i, 1); placedAny = true; break; } } if (!placedAny) { const item = pool.shift(); result.push(item.node); placedIndices.add(item.originalIndex); } } return result; } const finalResult = []; for (let i = 0; i < chunks.length; i++) { finalResult.push(...sortChunk(chunks[i])); if (i < chunkSeparators.length) { finalResult.push(chunkSeparators[i]); } } finalResult.push(...webStyles); return finalResult; } return { ObjectExpression(node) { if (!isStyleObject(node)) return; const oldOrder = node.properties; if (oldOrder.length <= 1) return; const newOrder = getSortedProperties(oldOrder); let changed = false; for (let i = 0; i < oldOrder.length; i++) { if (oldOrder[i] !== newOrder[i]) { changed = true; break; } } if (changed) { context.report({ node: node, message: "Style properties should be sorted by length (longest to shortest), with shorthand properties appropriately ordered and spread elements at the bottom.", fix(fixer) { const fixes = []; for (let i = 0; i < oldOrder.length; i++) { fixes.push(fixer.replaceText(oldOrder[i], sourceCode.getText(newOrder[i]))); } return fixes; } }); } } }; } }, "ncore-sort-types": { meta: { type: "layout", fixable: "code", schema: [] }, create(context) { const sourceCode = context.getSourceCode(); function sortMembers(node, members) { if (!members || members.length <= 1) return; const getLen = (n) => { const text = sourceCode.getText(n); const lines = text.split(/\r?\n/); let maxLen = 0; for (const line of lines) { const trimmed = line.trim(); if (trimmed.length > maxLen) { maxLen = trimmed.length; } } return maxLen; }; const initialSorted = members.map((member, i) => ({ node: member, originalIndex: i, length: getLen(member) })).sort((a, b) => { if (a.length !== b.length) return b.length - a.length; return a.originalIndex - b.originalIndex; }); let changed = false; for (let i = 0; i < members.length; i++) { if (members[i] !== initialSorted[i].node) { changed = true; break; } } if (changed) { context.report({ node: node, message: "Type properties should be sorted by length (longest to shortest).", fix(fixer) { const fixes = []; for (let i = 0; i < members.length; i++) { fixes.push(fixer.replaceText(members[i], sourceCode.getText(initialSorted[i].node))); } return fixes; } }); } } return { TSInterfaceBody(node) { sortMembers(node, node.body); }, TSTypeLiteral(node) { sortMembers(node, node.members); } }; } } };