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