| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- 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") || 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);
- }
- }
- }
- };
|