index.js 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681
  1. module.exports = {
  2. "multiline-import-specifiers": {
  3. meta: {
  4. type: "layout",
  5. fixable: "whitespace",
  6. schema: []
  7. },
  8. create(context) {
  9. return {
  10. ImportDeclaration(node) {
  11. const specifiers = node.specifiers.filter(
  12. s => s.type === "ImportSpecifier"
  13. );
  14. if (specifiers.length === 0) return;
  15. const sourceCode = context.getSourceCode();
  16. const openBrace = sourceCode.getFirstToken(node, t => t.value === "{");
  17. const closeBrace = sourceCode.getLastToken(node, t => t.value === "}");
  18. if (!openBrace || !closeBrace) return;
  19. const firstSpecifier = specifiers[0];
  20. if (firstSpecifier.loc.start.line - openBrace.loc.end.line !== 1) {
  21. context.report({
  22. node: firstSpecifier,
  23. message: "There should be exactly one newline after '{'.",
  24. fix(fixer) {
  25. return fixer.replaceTextRange([
  26. openBrace.range[1],
  27. firstSpecifier.range[0]
  28. ], "\n ");
  29. }
  30. });
  31. }
  32. for (let i = 0; i < specifiers.length - 1; i++) {
  33. const current = specifiers[i];
  34. const next = specifiers[i + 1];
  35. if (next.loc.start.line - current.loc.end.line !== 1) {
  36. context.report({
  37. node: next,
  38. message: "Each import specifier should be on a new line without blank lines between them.",
  39. fix(fixer) {
  40. const comma = sourceCode.getTokenBefore(next);
  41. return fixer.replaceTextRange(
  42. [
  43. comma.range[1],
  44. next.range[0]
  45. ],
  46. "\n "
  47. );
  48. }
  49. });
  50. }
  51. }
  52. const lastSpecifier = specifiers[specifiers.length - 1];
  53. const tokenAfter = sourceCode.getTokenAfter(lastSpecifier);
  54. if (tokenAfter && tokenAfter.value === ",") {
  55. context.report({
  56. node: lastSpecifier,
  57. message: "No trailing comma in imports",
  58. fix(fixer) {
  59. return fixer.remove(tokenAfter);
  60. }
  61. });
  62. }
  63. const tokenBeforeClose = sourceCode.getTokenBefore(closeBrace);
  64. if (closeBrace.loc.start.line - tokenBeforeClose.loc.end.line !== 1) {
  65. context.report({
  66. node: closeBrace,
  67. message: "There should be exactly one newline before '}'.",
  68. fix(fixer) {
  69. return fixer.replaceTextRange([
  70. tokenBeforeClose.range[1],
  71. closeBrace.range[0]
  72. ], "\n");
  73. }
  74. });
  75. }
  76. }
  77. };
  78. }
  79. },
  80. "multiline-object-properties": {
  81. meta: {
  82. type: "layout",
  83. fixable: "whitespace",
  84. schema: []
  85. },
  86. create(context) {
  87. const sourceCode = context.getSourceCode();
  88. function checkProperties(node, properties) {
  89. if (properties.length === 0) return;
  90. const openBrace = sourceCode.getFirstToken(node);
  91. let closingBraceToken = sourceCode.getTokenAfter(properties[properties.length - 1]);
  92. while (closingBraceToken && closingBraceToken.value !== "}") {
  93. closingBraceToken = sourceCode.getTokenAfter(closingBraceToken);
  94. }
  95. const closingBrace = closingBraceToken;
  96. if (openBrace.loc.start.line === closingBrace.loc.end.line) return;
  97. const firstProperty = properties[0];
  98. if (openBrace.loc.end.line !== firstProperty.loc.start.line - 1) {
  99. context.report({
  100. node: firstProperty,
  101. message: "First property should be exactly on the next line",
  102. fix(fixer) {
  103. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  104. const baseIndent = openBraceLine.match(/^\s*/)[0];
  105. const indent = baseIndent + " ";
  106. return fixer.replaceTextRange(
  107. [
  108. openBrace.range[1],
  109. firstProperty.range[0]
  110. ],
  111. "\n" + indent
  112. );
  113. }
  114. });
  115. }
  116. if (properties.length > 1) {
  117. for (let i = 0; i < properties.length - 1; i++) {
  118. const current = properties[i];
  119. const next = properties[i + 1];
  120. if (current.loc.end.line !== next.loc.start.line - 1) {
  121. context.report({
  122. node: next,
  123. message: "Each property should be exactly on the next line (no empty lines)",
  124. fix(fixer) {
  125. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  126. const baseIndent = openBraceLine.match(/^\s*/)[0];
  127. const indent = baseIndent + " ";
  128. const comma = sourceCode.getTokenBefore(next);
  129. return fixer.replaceTextRange(
  130. [
  131. comma.range[1],
  132. next.range[0]
  133. ],
  134. "\n" + indent
  135. );
  136. }
  137. });
  138. }
  139. }
  140. }
  141. const lastProperty = properties[properties.length - 1];
  142. if (lastProperty.loc.end.line !== closingBrace.loc.start.line - 1) {
  143. context.report({
  144. node: closingBrace,
  145. message: "Object closing brace should be exactly on the next line",
  146. fix(fixer) {
  147. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  148. const baseIndent = openBraceLine.match(/^\s*/)[0];
  149. // Be careful about trailing comma
  150. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  151. const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
  152. const startRange = hasTrailingComma ? tokenAfter.range[1] : lastProperty.range[1];
  153. return fixer.replaceTextRange(
  154. [
  155. startRange,
  156. closingBrace.range[0]
  157. ],
  158. "\n" + baseIndent
  159. );
  160. }
  161. });
  162. }
  163. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  164. if (tokenAfter && tokenAfter.value === ",") {
  165. context.report({
  166. node: lastProperty,
  167. message: "No trailing comma in objects",
  168. fix(fixer) {
  169. return fixer.remove(tokenAfter);
  170. }
  171. });
  172. }
  173. }
  174. return {
  175. ObjectExpression(node) {
  176. checkProperties(node, node.properties);
  177. },
  178. ObjectPattern(node) {
  179. checkProperties(node, node.properties);
  180. }
  181. };
  182. }
  183. },
  184. "multiline-jsx-attributes": {
  185. meta: {
  186. type: "layout",
  187. fixable: "whitespace",
  188. schema: []
  189. },
  190. create(context) {
  191. const sourceCode = context.getSourceCode();
  192. return {
  193. JSXOpeningElement(node) {
  194. if (node.attributes.length === 0) return;
  195. const firstToken = sourceCode.getFirstToken(node);
  196. const tagNameToken = sourceCode.getTokenAfter(firstToken);
  197. const firstAttr = node.attributes[0];
  198. if (tagNameToken.loc.end.line !== firstAttr.loc.start.line - 1) {
  199. context.report({
  200. node: firstAttr,
  201. message: "First JSX attribute should be exactly on the next line",
  202. fix(fixer) {
  203. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  204. const baseIndent = openBraceLine.match(/^\s*/)[0];
  205. const indent = baseIndent + " ";
  206. return fixer.replaceTextRange(
  207. [
  208. tagNameToken.range[1],
  209. firstAttr.range[0]
  210. ],
  211. "\n" + indent
  212. );
  213. }
  214. });
  215. }
  216. for (let i = 0; i < node.attributes.length - 1; i++) {
  217. const current = node.attributes[i];
  218. const next = node.attributes[i + 1];
  219. if (current.loc.end.line !== next.loc.start.line - 1) {
  220. context.report({
  221. node: next,
  222. message: "Each JSX attribute should be exactly on the next line (no empty lines)",
  223. fix(fixer) {
  224. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  225. const baseIndent = openBraceLine.match(/^\s*/)[0];
  226. const indent = baseIndent + " ";
  227. return fixer.replaceTextRange(
  228. [
  229. current.range[1],
  230. next.range[0]
  231. ],
  232. "\n" + indent
  233. );
  234. }
  235. });
  236. }
  237. }
  238. const lastAttr = node.attributes[node.attributes.length - 1];
  239. const allTokens = [];
  240. let currentToken = sourceCode.getTokenAfter(lastAttr);
  241. while (currentToken && currentToken.range[1] <= node.range[1]) {
  242. allTokens.push(currentToken);
  243. currentToken = sourceCode.getTokenAfter(currentToken);
  244. }
  245. const closingBracket = allTokens[allTokens.length - 1];
  246. // Check if self-closing slash is present
  247. let hasSelfClosingSlash = false;
  248. let slashToken = null;
  249. if (node.selfClosing && allTokens.length >= 2) {
  250. slashToken = allTokens[allTokens.length - 2];
  251. if (slashToken && slashToken.value === "/") {
  252. hasSelfClosingSlash = true;
  253. }
  254. }
  255. if (hasSelfClosingSlash) {
  256. if (lastAttr.loc.end.line !== slashToken.loc.start.line - 1) {
  257. context.report({
  258. node: slashToken,
  259. message: "JSX closing slash should be exactly on the next line",
  260. fix(fixer) {
  261. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  262. const baseIndent = openBraceLine.match(/^\s*/)[0];
  263. return fixer.replaceTextRange(
  264. [
  265. lastAttr.range[1],
  266. slashToken.range[0]
  267. ],
  268. "\n" + baseIndent
  269. );
  270. }
  271. });
  272. }
  273. } else {
  274. if (lastAttr.loc.end.line !== closingBracket.loc.start.line - 1) {
  275. context.report({
  276. node: closingBracket,
  277. message: "JSX closing bracket should be exactly on the next line",
  278. fix(fixer) {
  279. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  280. const baseIndent = openBraceLine.match(/^\s*/)[0];
  281. return fixer.replaceTextRange(
  282. [
  283. lastAttr.range[1],
  284. closingBracket.range[0]
  285. ],
  286. "\n" + baseIndent
  287. );
  288. }
  289. });
  290. }
  291. }
  292. }
  293. };
  294. }
  295. },
  296. "multiline-array-elements": {
  297. meta: {
  298. type: "layout",
  299. fixable: "whitespace",
  300. schema: []
  301. },
  302. create(context) {
  303. const sourceCode = context.getSourceCode();
  304. return {
  305. "ArrayExpression, ArrayPattern"(node) {
  306. if (node.elements.length === 0) return;
  307. const openBracket = sourceCode.getFirstToken(node);
  308. const closingBracket = sourceCode.getLastToken(node);
  309. if (openBracket.loc.start.line === closingBracket.loc.end.line) return;
  310. const firstElement = node.elements[0];
  311. if (openBracket.loc.end.line !== firstElement.loc.start.line - 1) {
  312. context.report({
  313. node: firstElement,
  314. message: "First array element should be exactly on the next line",
  315. fix(fixer) {
  316. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  317. const baseIndent = openBracketLine.match(/^\s*/)[0];
  318. const indent = baseIndent + " ";
  319. return fixer.replaceTextRange(
  320. [
  321. openBracket.range[1],
  322. firstElement.range[0]
  323. ],
  324. "\n" + indent
  325. );
  326. }
  327. });
  328. }
  329. for (let i = 0; i < node.elements.length - 1; i++) {
  330. const current = node.elements[i];
  331. const next = node.elements[i + 1];
  332. if (!current || !next) continue;
  333. if (current.loc.end.line !== next.loc.start.line - 1) {
  334. context.report({
  335. node: next,
  336. message: "Each array element should be exactly on the next line (no empty lines)",
  337. fix(fixer) {
  338. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  339. const baseIndent = openBracketLine.match(/^\s*/)[0];
  340. const indent = baseIndent + " ";
  341. const comma = sourceCode.getTokenBefore(next);
  342. return fixer.replaceTextRange(
  343. [
  344. comma.range[1],
  345. next.range[0]
  346. ],
  347. "\n" + indent
  348. );
  349. }
  350. });
  351. }
  352. }
  353. const lastElement = node.elements[node.elements.length - 1];
  354. if (lastElement && lastElement.loc.end.line !== closingBracket.loc.start.line - 1) {
  355. context.report({
  356. node: closingBracket,
  357. message: "Array closing bracket should be exactly on the next line",
  358. fix(fixer) {
  359. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  360. const baseIndent = openBracketLine.match(/^\s*/)[0];
  361. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  362. const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
  363. const startRange = hasTrailingComma ? tokenAfter.range[1] : lastElement.range[1];
  364. return fixer.replaceTextRange(
  365. [
  366. startRange,
  367. closingBracket.range[0]
  368. ],
  369. "\n" + baseIndent
  370. );
  371. }
  372. });
  373. }
  374. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  375. if (tokenAfter && tokenAfter.value === "," && tokenAfter.range[1] <= closingBracket.range[0]) {
  376. context.report({
  377. node: lastElement,
  378. message: "No trailing comma in arrays",
  379. fix(fixer) {
  380. return fixer.remove(tokenAfter);
  381. }
  382. });
  383. }
  384. }
  385. };
  386. }
  387. },
  388. "custom-import-order": {
  389. meta: {
  390. type: "suggestion",
  391. fixable: "code",
  392. schema: []
  393. },
  394. create(context) {
  395. const sourceCode = context.getSourceCode();
  396. const imports = [];
  397. return {
  398. ImportDeclaration(node) {
  399. imports.push(node);
  400. },
  401. "Program:exit"() {
  402. if (imports.length <= 1) return;
  403. const categorized = categorizeImports(imports, sourceCode);
  404. let isSorted = true;
  405. let errorNode = null;
  406. for (let i = 0; i < imports.length; i++) {
  407. if (imports[i] !== categorized[i].node) {
  408. isSorted = false;
  409. errorNode = imports[i];
  410. break;
  411. }
  412. }
  413. if (!isSorted) {
  414. context.report({
  415. node: errorNode,
  416. message: "Imports are not sorted correctly according to custom rules.",
  417. fix(fixer) {
  418. return fixImportOrder(fixer, imports, categorized, sourceCode);
  419. }
  420. });
  421. }
  422. }
  423. };
  424. function getImportCategory(node, sourceCode) {
  425. const source = node.source.value;
  426. const code = sourceCode.getText(node);
  427. if (source === "react") return 1;
  428. if (source === "react-native") return 2;
  429. if (source.startsWith("./")) {
  430. if (code.includes("import type") || source.includes(".types") || source.includes("/type")) {
  431. return 3.1;
  432. }
  433. if (source.includes("style") || source.includes("Style")) {
  434. return 3.2;
  435. }
  436. return 3.3;
  437. }
  438. const specifiers = node.specifiers
  439. .filter(s => s.type === "ImportSpecifier")
  440. .map(s => s.imported ? s.imported.name : "");
  441. const hasNCoreHook = specifiers.some(name => name.startsWith("NCore"));
  442. const hasUseHook = specifiers.some(name => name.startsWith("use"));
  443. if (hasNCoreHook) return 4.1;
  444. if (hasUseHook) return 4.2;
  445. if (code.includes("import type") || source.includes("/types/") || source.includes("/type") || source.includes(".types")) {
  446. return 5;
  447. }
  448. if (source.startsWith("@/") || source.startsWith("~/")) return 6.3;
  449. if (!source.startsWith(".")) return 6.1;
  450. if (source.startsWith("../")) return 6.2;
  451. return 6.4;
  452. }
  453. function categorizeImports(imports, sourceCode) {
  454. return imports.map(imp => {
  455. const isDefaultImport = imp.specifiers.some(s => s.type === "ImportDefaultSpecifier");
  456. const textLength = sourceCode.getText(imp).replace(/\s+/g, "").length;
  457. return {
  458. node: imp,
  459. category: getImportCategory(imp, sourceCode),
  460. source: imp.source.value,
  461. isDefaultImport,
  462. textLength
  463. };
  464. }).sort((a, b) => {
  465. if (a.category !== b.category) {
  466. return a.category - b.category;
  467. }
  468. if (a.category >= 6) {
  469. if (a.isDefaultImport !== b.isDefaultImport) {
  470. return a.isDefaultImport ? 1 : -1;
  471. }
  472. if (a.textLength !== b.textLength) {
  473. return b.textLength - a.textLength;
  474. }
  475. }
  476. return a.source.localeCompare(b.source);
  477. });
  478. }
  479. function fixImportOrder(fixer, imports, categorized, sourceCode) {
  480. const firstImport = imports[0];
  481. const lastImport = imports[imports.length - 1];
  482. const rangeStart = firstImport.range[0];
  483. const rangeEnd = lastImport.range[1];
  484. let newImports = "";
  485. categorized.forEach((imp, index) => {
  486. newImports += sourceCode.getText(imp.node);
  487. if (index < categorized.length - 1) {
  488. newImports += "\n";
  489. }
  490. });
  491. return fixer.replaceTextRange([
  492. rangeStart,
  493. rangeEnd
  494. ], newImports);
  495. }
  496. }
  497. },
  498. "multiline-jsx-children": {
  499. meta: {
  500. type: "layout",
  501. fixable: "whitespace",
  502. schema: []
  503. },
  504. create(context) {
  505. return {
  506. JSXElement(node) {
  507. if (!node.closingElement) return;
  508. if (node.children.length === 0) return;
  509. const meaningfulChildren = node.children.filter(child => {
  510. if (child.type === "JSXText" && child.value.trim() === "") return false;
  511. return true;
  512. });
  513. if (meaningfulChildren.length === 0) return;
  514. const openingElement = node.openingElement;
  515. const closingElement = node.closingElement;
  516. const isMultilineOpening = openingElement.loc.start.line !== openingElement.loc.end.line;
  517. if (!isMultilineOpening) return;
  518. const sourceCode = context.getSourceCode();
  519. if (meaningfulChildren.length === 1 && meaningfulChildren[0].type === "JSXText") {
  520. const child = meaningfulChildren[0];
  521. const text = sourceCode.getText(child);
  522. const hasLeadingNewline = /^\s*\n/.test(text);
  523. const hasTrailingNewline = /\n\s*$/.test(text);
  524. if (!hasLeadingNewline || !hasTrailingNewline) {
  525. context.report({
  526. node: child,
  527. message: "Multiline JSX element text content must be on its own line",
  528. fix(fixer) {
  529. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  530. const baseIndent = openBracketLine.match(/^\s*/)[0];
  531. const indent = baseIndent + " ";
  532. return fixer.replaceText(child, "\n" + indent + text.trim() + "\n" + baseIndent);
  533. }
  534. });
  535. }
  536. return;
  537. }
  538. const firstChild = meaningfulChildren[0];
  539. const openingEndLine = openingElement.loc.end.line;
  540. let needsFirstChildFix = false;
  541. if (firstChild.type === "JSXText") {
  542. const text = sourceCode.getText(firstChild);
  543. if (!/^\s*\n/.test(text)) needsFirstChildFix = true;
  544. } else if (firstChild.loc.start.line === openingEndLine) {
  545. needsFirstChildFix = true;
  546. }
  547. if (needsFirstChildFix) {
  548. context.report({
  549. node: firstChild,
  550. message: "First child of a multiline JSX element must be on a new line",
  551. fix(fixer) {
  552. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  553. const baseIndent = openBracketLine.match(/^\s*/)[0];
  554. const indent = baseIndent + " ";
  555. if (firstChild.type === "JSXText") {
  556. const text = sourceCode.getText(firstChild);
  557. return fixer.replaceText(firstChild, "\n" + indent + text.replace(/^\s+/, ""));
  558. }
  559. return fixer.insertTextBefore(firstChild, "\n" + indent);
  560. }
  561. });
  562. }
  563. const lastChild = meaningfulChildren[meaningfulChildren.length - 1];
  564. let needsLastChildFix = false;
  565. if (lastChild.type === "JSXText") {
  566. const text = sourceCode.getText(lastChild);
  567. if (!/\n\s*$/.test(text)) needsLastChildFix = true;
  568. } else if (lastChild.loc.end.line === closingElement.loc.start.line) {
  569. needsLastChildFix = true;
  570. }
  571. if (needsLastChildFix) {
  572. context.report({
  573. node: closingElement,
  574. message: "Closing tag of a multiline JSX element must be on a new line",
  575. fix(fixer) {
  576. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  577. const baseIndent = openBracketLine.match(/^\s*/)[0];
  578. if (lastChild.type === "JSXText") {
  579. const text = sourceCode.getText(lastChild);
  580. return fixer.replaceText(lastChild, text.replace(/\s+$/, "") + "\n" + baseIndent);
  581. }
  582. return fixer.insertTextBefore(closingElement, "\n" + baseIndent);
  583. }
  584. });
  585. }
  586. }
  587. };
  588. }
  589. },
  590. "no-jsx-parens-in-return": {
  591. meta: {
  592. type: "layout",
  593. fixable: "code",
  594. schema: []
  595. },
  596. create(context) {
  597. return {
  598. ReturnStatement(node) {
  599. if (!node.argument) return;
  600. if (node.argument.type === "JSXElement" || node.argument.type === "JSXFragment") {
  601. const sourceCode = context.getSourceCode();
  602. const returnToken = sourceCode.getFirstToken(node);
  603. const firstChildToken = sourceCode.getFirstToken(node.argument);
  604. const tokensBetween = sourceCode.getTokensBetween(returnToken, firstChildToken);
  605. const openParen = tokensBetween.find(t => t.value === "(");
  606. if (openParen) {
  607. const closeParen = sourceCode.getTokenAfter(node.argument);
  608. if (closeParen && closeParen.value === ")") {
  609. context.report({
  610. node: node.argument,
  611. message: "Returned JSX element should not be wrapped in parentheses",
  612. fix(fixer) {
  613. const fixes = [
  614. fixer.remove(closeParen)
  615. ];
  616. if (returnToken.loc.end.line !== firstChildToken.loc.start.line) {
  617. fixes.push(fixer.replaceTextRange([
  618. returnToken.range[1],
  619. firstChildToken.range[0]
  620. ], " "));
  621. } else {
  622. fixes.push(fixer.remove(openParen));
  623. }
  624. return fixes;
  625. }
  626. });
  627. }
  628. } else if (returnToken.loc.end.line !== firstChildToken.loc.start.line) {
  629. context.report({
  630. node: node.argument,
  631. message: "Returned JSX element must start on the same line as 'return' to avoid Automatic Semicolon Insertion (ASI)",
  632. fix(fixer) {
  633. return fixer.replaceTextRange([
  634. returnToken.range[1],
  635. firstChildToken.range[0]
  636. ], " ");
  637. }
  638. });
  639. }
  640. }
  641. },
  642. ArrowFunctionExpression(node) {
  643. if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") {
  644. const sourceCode = context.getSourceCode();
  645. const arrowToken = sourceCode.getTokenBefore(node.body, t => t.value === "=>");
  646. const firstChildToken = sourceCode.getFirstToken(node.body);
  647. const tokensBetween = sourceCode.getTokensBetween(arrowToken, firstChildToken);
  648. const openParen = tokensBetween.find(t => t.value === "(");
  649. if (openParen) {
  650. const closeParen = sourceCode.getTokenAfter(node.body);
  651. if (closeParen && closeParen.value === ")") {
  652. context.report({
  653. node: node.body,
  654. message: "Implicitly returned JSX element should not be wrapped in parentheses",
  655. fix(fixer) {
  656. const fixes = [
  657. fixer.remove(closeParen)
  658. ];
  659. if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) {
  660. fixes.push(fixer.replaceTextRange([
  661. arrowToken.range[1],
  662. firstChildToken.range[0]
  663. ], " "));
  664. } else {
  665. fixes.push(fixer.remove(openParen));
  666. }
  667. return fixes;
  668. }
  669. });
  670. }
  671. } else if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) {
  672. context.report({
  673. node: node.body,
  674. message: "Implicitly returned JSX element must start on the same line as '=>'",
  675. fix(fixer) {
  676. return fixer.replaceTextRange([
  677. arrowToken.range[1],
  678. firstChildToken.range[0]
  679. ], " ");
  680. }
  681. });
  682. }
  683. }
  684. }
  685. };
  686. }
  687. },
  688. "no-newline-after-jsx": {
  689. meta: {
  690. type: "layout",
  691. fixable: "whitespace",
  692. schema: []
  693. },
  694. create(context) {
  695. function checkPunctuation(node) {
  696. const sourceCode = context.getSourceCode();
  697. const lastToken = sourceCode.getLastToken(node);
  698. const nextToken = sourceCode.getTokenAfter(node);
  699. if (nextToken && (nextToken.value === ";" || nextToken.value === "," || nextToken.value === ")")) {
  700. if (lastToken.loc.end.line !== nextToken.loc.start.line) {
  701. if (sourceCode.commentsExistBetween(node, nextToken)) return;
  702. context.report({
  703. node: nextToken,
  704. message: `Punctuation '${nextToken.value}' should be on the same line as the preceding JSX element`,
  705. fix(fixer) {
  706. return fixer.replaceTextRange([
  707. node.range[1],
  708. nextToken.range[0]
  709. ], "");
  710. }
  711. });
  712. }
  713. }
  714. }
  715. return {
  716. JSXElement: checkPunctuation,
  717. JSXFragment: checkPunctuation
  718. };
  719. }
  720. },
  721. "no-static-inline-styles": {
  722. meta: {
  723. type: "suggestion",
  724. messages: {
  725. staticStyle: "Static inline styles are not allowed. Please move them to a stylesheet file."
  726. },
  727. schema: []
  728. },
  729. create(context) {
  730. function checkProperty(propertyNode) {
  731. if (!propertyNode || propertyNode.type !== "Property") return;
  732. const value = propertyNode.value;
  733. if (!value) return;
  734. let isStatic = false;
  735. if (value.type === "Literal") {
  736. if (typeof value.value === "string" || typeof value.value === "number") {
  737. isStatic = true;
  738. }
  739. } else if (value.type === "UnaryExpression" && value.operator === "-") {
  740. if (value.argument && value.argument.type === "Literal" && typeof value.argument.value === "number") {
  741. isStatic = true;
  742. }
  743. } else if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
  744. isStatic = true;
  745. }
  746. if (isStatic) {
  747. context.report({
  748. node: propertyNode,
  749. messageId: "staticStyle"
  750. });
  751. }
  752. }
  753. function checkObjectExpression(node) {
  754. if (node.type !== "ObjectExpression") return;
  755. node.properties.forEach(checkProperty);
  756. }
  757. function checkStyleValue(node) {
  758. if (!node) return;
  759. if (node.type === "ObjectExpression") {
  760. checkObjectExpression(node);
  761. } else if (node.type === "ArrayExpression") {
  762. node.elements.forEach(element => {
  763. if (element && element.type === "ObjectExpression") {
  764. checkObjectExpression(element);
  765. }
  766. });
  767. }
  768. }
  769. return {
  770. JSXAttribute(node) {
  771. const attrName = node.name.name;
  772. if (typeof attrName !== "string") return;
  773. if (attrName === "style" || attrName.endsWith("Style")) {
  774. if (node.value && node.value.type === "JSXExpressionContainer") {
  775. checkStyleValue(node.value.expression);
  776. }
  777. }
  778. }
  779. };
  780. }
  781. },
  782. "jsx-ternary-formatting": {
  783. meta: {
  784. type: "layout",
  785. fixable: "whitespace",
  786. schema: []
  787. },
  788. create(context) {
  789. return {
  790. JSXExpressionContainer(node) {
  791. if (node.expression.type !== "ConditionalExpression") return;
  792. const sourceCode = context.getSourceCode();
  793. const openBrace = sourceCode.getFirstToken(node);
  794. const closeBrace = sourceCode.getLastToken(node);
  795. const cond = node.expression;
  796. let hasJSX = false;
  797. if (
  798. cond.consequent.type === "JSXElement" ||
  799. cond.consequent.type === "JSXFragment" ||
  800. cond.alternate.type === "JSXElement" ||
  801. cond.alternate.type === "JSXFragment"
  802. ) {
  803. hasJSX = true;
  804. }
  805. if (!hasJSX) return;
  806. const checkAndRemoveParens = (nodeToUnparen) => {
  807. const currentToken = sourceCode.getTokenBefore(nodeToUnparen);
  808. const afterToken = sourceCode.getTokenAfter(nodeToUnparen);
  809. let removed = false;
  810. while (currentToken && currentToken.value === "(" && afterToken && afterToken.value === ")") {
  811. context.report({
  812. node: currentToken,
  813. message: "Parentheses are not allowed around JSX in ternaries.",
  814. fix(fixer) {
  815. return [
  816. fixer.remove(currentToken),
  817. fixer.remove(afterToken)
  818. ];
  819. }
  820. });
  821. removed = true;
  822. break;
  823. }
  824. return removed;
  825. };
  826. let hasParens = false;
  827. if (cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment") {
  828. if (checkAndRemoveParens(cond.consequent)) hasParens = true;
  829. }
  830. if (cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment") {
  831. if (checkAndRemoveParens(cond.alternate)) hasParens = true;
  832. }
  833. if (hasParens) return; // Wait for the next fix pass to format the whitespace properly
  834. const questionToken = sourceCode.getTokenAfter(cond.test, {
  835. filter: t => t.value === "?"
  836. });
  837. const colonToken = sourceCode.getTokenAfter(cond.consequent, {
  838. filter: t => t.value === ":"
  839. });
  840. const getIndent = (token) => {
  841. const line = sourceCode.lines[token.loc.start.line - 1];
  842. return line.match(/^\s*/)[0];
  843. };
  844. const openIndent = getIndent(openBrace);
  845. const baseIndent = openIndent + " ";
  846. const innerIndent = baseIndent + " ";
  847. const checkAndFix = (token1, token2, expectedWhitespace) => {
  848. let textBetween = sourceCode.getText().slice(token1.range[1], token2.range[0]);
  849. textBetween = textBetween.replace(/\r\n/g, "\n");
  850. if (textBetween !== expectedWhitespace) {
  851. context.report({
  852. node: token2,
  853. message: "Incorrect ternary formatting",
  854. fix(fixer) {
  855. return fixer.replaceTextRange([
  856. token1.range[1],
  857. token2.range[0]
  858. ], expectedWhitespace);
  859. }
  860. });
  861. }
  862. };
  863. const firstTestToken = sourceCode.getTokenAfter(openBrace);
  864. checkAndFix(openBrace, firstTestToken, "\n" + baseIndent);
  865. const firstConsequentToken = sourceCode.getTokenAfter(questionToken);
  866. checkAndFix(questionToken, firstConsequentToken, "\n" + innerIndent);
  867. const firstColonToken = colonToken;
  868. const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
  869. checkAndFix(lastConsequentToken, firstColonToken, "\n" + baseIndent);
  870. const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
  871. checkAndFix(colonToken, firstAlternateToken, "\n" + innerIndent);
  872. const lastAlternateToken = sourceCode.getTokenBefore(closeBrace);
  873. checkAndFix(lastAlternateToken, closeBrace, "\n" + openIndent);
  874. }
  875. };
  876. }
  877. },
  878. "jsx-no-blank-lines": {
  879. meta: {
  880. type: "layout",
  881. fixable: "whitespace",
  882. schema: []
  883. },
  884. create(context) {
  885. return {
  886. JSXText(node) {
  887. const text = node.value;
  888. if (!/^\s*$/.test(text)) {
  889. return;
  890. }
  891. const newlines = text.match(/\n/g);
  892. if (newlines && newlines.length > 1) {
  893. context.report({
  894. node,
  895. message: "No blank lines allowed between JSX elements",
  896. fix(fixer) {
  897. const match = text.match(/\n[ \t]*$/);
  898. if (match) {
  899. return fixer.replaceText(node, match[0]);
  900. }
  901. }
  902. });
  903. }
  904. }
  905. };
  906. }
  907. },
  908. "sort-jsx-props": {
  909. meta: {
  910. type: "layout",
  911. fixable: "code",
  912. schema: []
  913. },
  914. create(context) {
  915. return {
  916. JSXOpeningElement(node) {
  917. const attributes = node.attributes;
  918. if (!attributes || attributes.length <= 1) return;
  919. const sourceCode = context.getSourceCode();
  920. const getAttrLength = (attr) => {
  921. const text = sourceCode.getText(attr);
  922. return text.replace(/\s+/g, "").length;
  923. };
  924. const groups = [];
  925. let currentGroup = [];
  926. for (const attr of attributes) {
  927. if (attr.type === "JSXSpreadAttribute") {
  928. if (currentGroup.length > 1) {
  929. groups.push(currentGroup);
  930. }
  931. currentGroup = [];
  932. } else {
  933. currentGroup.push(attr);
  934. }
  935. }
  936. if (currentGroup.length > 1) {
  937. groups.push(currentGroup);
  938. }
  939. for (const group of groups) {
  940. const sortedGroup = [...group].sort((a, b) => {
  941. return getAttrLength(b) - getAttrLength(a);
  942. });
  943. let isSorted = true;
  944. for (let i = 0; i < group.length; i++) {
  945. if (group[i] !== sortedGroup[i]) {
  946. isSorted = false;
  947. break;
  948. }
  949. }
  950. if (!isSorted) {
  951. context.report({
  952. node: group[0],
  953. message: "JSX attributes should be sorted from longest to shortest.",
  954. fix(fixer) {
  955. const firstAttr = group[0];
  956. const startLoc = firstAttr.loc.start;
  957. const indent = " ".repeat(startLoc.column);
  958. const newText = sortedGroup.map(attr => sourceCode.getText(attr)).join("\n" + indent);
  959. const start = group[0].range[0];
  960. const end = group[group.length - 1].range[1];
  961. return fixer.replaceTextRange([
  962. start,
  963. end
  964. ], newText);
  965. }
  966. });
  967. }
  968. }
  969. }
  970. };
  971. }
  972. },
  973. "sort-import-specifiers": {
  974. meta: {
  975. type: "layout",
  976. fixable: "code",
  977. schema: []
  978. },
  979. create(context) {
  980. return {
  981. ImportDeclaration(node) {
  982. checkSpecifiers(node, "ImportSpecifier", "Import");
  983. },
  984. ExportNamedDeclaration(node) {
  985. checkSpecifiers(node, "ExportSpecifier", "Export");
  986. }
  987. };
  988. function checkSpecifiers(node, specifierType, typeName) {
  989. if (!node.specifiers) return;
  990. const specifiers = node.specifiers.filter(
  991. s => s.type === specifierType
  992. );
  993. if (specifiers.length <= 1) return;
  994. const sourceCode = context.getSourceCode();
  995. const getSpecLength = (spec) => {
  996. return sourceCode.getText(spec).replace(/\s+/g, "").length;
  997. };
  998. const sortedSpecifiers = [...specifiers].sort((a, b) => {
  999. return getSpecLength(b) - getSpecLength(a);
  1000. });
  1001. let isSorted = true;
  1002. for (let i = 0; i < specifiers.length; i++) {
  1003. if (specifiers[i] !== sortedSpecifiers[i]) {
  1004. isSorted = false;
  1005. break;
  1006. }
  1007. }
  1008. if (!isSorted) {
  1009. context.report({
  1010. node,
  1011. message: `${typeName} specifiers should be sorted from longest to shortest.`,
  1012. fix(fixer) {
  1013. const firstSpec = specifiers[0];
  1014. const lastSpec = specifiers[specifiers.length - 1];
  1015. const startLoc = firstSpec.loc.start;
  1016. let indent = " ";
  1017. if (firstSpec.loc.start.line !== node.loc.start.line) {
  1018. indent = " ".repeat(startLoc.column);
  1019. }
  1020. const newText = sortedSpecifiers.map(spec => sourceCode.getText(spec)).join(",\n" + indent);
  1021. return fixer.replaceTextRange([
  1022. firstSpec.range[0],
  1023. lastSpec.range[1]
  1024. ], newText);
  1025. }
  1026. });
  1027. }
  1028. }
  1029. }
  1030. },
  1031. "sort-translations": {
  1032. meta: {
  1033. type: "layout",
  1034. fixable: "code",
  1035. schema: []
  1036. },
  1037. create(context) {
  1038. const sourceCode = context.getSourceCode();
  1039. function checkObject(node) {
  1040. if (!node.properties || node.properties.length <= 1) return;
  1041. let hasIsRTL = false;
  1042. let hasLocale = false;
  1043. let translationsProp = null;
  1044. for (const prop of node.properties) {
  1045. const keyNode = prop.key;
  1046. if (!keyNode) continue;
  1047. let keyName = "";
  1048. if (keyNode.type === "Identifier") keyName = keyNode.name;
  1049. else if (keyNode.type === "Literal" || keyNode.type === "JSONLiteral") keyName = keyNode.value;
  1050. if (keyName === "isRTL") hasIsRTL = true;
  1051. if (keyName === "locale") hasLocale = true;
  1052. if (keyName === "translations") translationsProp = prop;
  1053. }
  1054. if (hasIsRTL && hasLocale && translationsProp) {
  1055. const translationsObj = translationsProp.value;
  1056. if (!translationsObj || !translationsObj.properties || translationsObj.properties.length <= 1) return;
  1057. const props = translationsObj.properties;
  1058. const sortedProps = [...props].sort((a, b) => {
  1059. const lenA = sourceCode.getText(a.key).length + sourceCode.getText(a.value).length;
  1060. const lenB = sourceCode.getText(b.key).length + sourceCode.getText(b.value).length;
  1061. if (lenA !== lenB) {
  1062. return lenB - lenA;
  1063. }
  1064. const keyA = a.key.value || a.key.name || "";
  1065. const keyB = b.key.value || b.key.name || "";
  1066. return keyA.localeCompare(keyB);
  1067. });
  1068. let isSorted = true;
  1069. let errorNode = null;
  1070. for (let i = 0; i < props.length; i++) {
  1071. if (props[i] !== sortedProps[i]) {
  1072. isSorted = false;
  1073. errorNode = props[i];
  1074. break;
  1075. }
  1076. }
  1077. if (!isSorted) {
  1078. context.report({
  1079. node: errorNode,
  1080. message: "Translations keys must be sorted by length (longest to shortest).",
  1081. fix(fixer) {
  1082. const firstProp = props[0];
  1083. const lastProp = props[props.length - 1];
  1084. const startLoc = firstProp.loc.start;
  1085. const indent = " ".repeat(startLoc.column);
  1086. let newText = "";
  1087. sortedProps.forEach((p, index) => {
  1088. newText += sourceCode.getText(p);
  1089. if (index < sortedProps.length - 1) {
  1090. newText += ",\n" + indent;
  1091. }
  1092. });
  1093. return fixer.replaceTextRange([
  1094. firstProp.range[0],
  1095. lastProp.range[1]
  1096. ], newText);
  1097. }
  1098. });
  1099. }
  1100. }
  1101. }
  1102. return {
  1103. ObjectExpression: checkObject,
  1104. JSONObjectExpression: checkObject
  1105. };
  1106. }
  1107. },
  1108. "ncore-sort-styles": {
  1109. meta: {
  1110. type: "layout",
  1111. fixable: "code",
  1112. schema: []
  1113. },
  1114. create(context) {
  1115. const sourceCode = context.getSourceCode();
  1116. const shorthandDependencies = {
  1117. borderRadius: [
  1118. "borderTopLeftRadius",
  1119. "borderTopRightRadius",
  1120. "borderBottomLeftRadius",
  1121. "borderBottomRightRadius",
  1122. "borderTopStartRadius",
  1123. "borderTopEndRadius",
  1124. "borderBottomStartRadius",
  1125. "borderBottomEndRadius"
  1126. ],
  1127. padding: [
  1128. "paddingHorizontal",
  1129. "paddingVertical",
  1130. "paddingTop",
  1131. "paddingRight",
  1132. "paddingBottom",
  1133. "paddingLeft",
  1134. "paddingStart",
  1135. "paddingEnd"
  1136. ],
  1137. margin: [
  1138. "marginHorizontal",
  1139. "marginVertical",
  1140. "marginTop",
  1141. "marginRight",
  1142. "marginBottom",
  1143. "marginLeft",
  1144. "marginStart",
  1145. "marginEnd"
  1146. ],
  1147. borderWidth: [
  1148. "borderTopWidth",
  1149. "borderRightWidth",
  1150. "borderBottomWidth",
  1151. "borderLeftWidth",
  1152. "borderStartWidth",
  1153. "borderEndWidth"
  1154. ],
  1155. borderColor: [
  1156. "borderTopColor",
  1157. "borderRightColor",
  1158. "borderBottomColor",
  1159. "borderLeftColor",
  1160. "borderStartColor",
  1161. "borderEndColor"
  1162. ],
  1163. borderStyle: [
  1164. "borderTopStyle",
  1165. "borderRightStyle",
  1166. "borderBottomStyle",
  1167. "borderLeftStyle"
  1168. ],
  1169. paddingHorizontal: [
  1170. "paddingLeft",
  1171. "paddingRight",
  1172. "paddingStart",
  1173. "paddingEnd"
  1174. ],
  1175. marginHorizontal: [
  1176. "marginLeft",
  1177. "marginRight",
  1178. "marginStart",
  1179. "marginEnd"
  1180. ],
  1181. flex: [
  1182. "flexGrow",
  1183. "flexShrink",
  1184. "flexBasis"
  1185. ],
  1186. paddingVertical: [
  1187. "paddingTop",
  1188. "paddingBottom"
  1189. ],
  1190. marginVertical: [
  1191. "marginTop",
  1192. "marginBottom"
  1193. ]
  1194. };
  1195. function isStyleObject(node) {
  1196. if (node.properties.length <= 1) return false;
  1197. if (node.parent.type === "CallExpression" &&
  1198. node.parent.callee.type === "MemberExpression" &&
  1199. node.parent.callee.object.name === "StyleSheet" &&
  1200. node.parent.callee.property.name === "create") {
  1201. return true;
  1202. }
  1203. if (node.parent.type === "Property" &&
  1204. node.parent.parent.type === "ObjectExpression" &&
  1205. node.parent.parent.parent.type === "CallExpression" &&
  1206. node.parent.parent.parent.callee.type === "MemberExpression" &&
  1207. node.parent.parent.parent.callee.object.name === "StyleSheet") {
  1208. return true;
  1209. }
  1210. const knownStyleKeys = new Set([
  1211. "alignItems",
  1212. "justifyContent",
  1213. "flexDirection",
  1214. "flexWrap",
  1215. "flex",
  1216. "padding",
  1217. "margin",
  1218. "paddingTop",
  1219. "marginTop",
  1220. "paddingBottom",
  1221. "marginBottom",
  1222. "paddingLeft",
  1223. "marginLeft",
  1224. "paddingRight",
  1225. "marginRight",
  1226. "paddingHorizontal",
  1227. "marginHorizontal",
  1228. "paddingVertical",
  1229. "marginVertical",
  1230. "backgroundColor",
  1231. "color",
  1232. "fontSize",
  1233. "fontWeight",
  1234. "lineHeight",
  1235. "textAlign",
  1236. "borderWidth",
  1237. "borderRadius",
  1238. "borderColor",
  1239. "borderStyle",
  1240. "position",
  1241. "top",
  1242. "bottom",
  1243. "left",
  1244. "right",
  1245. "display",
  1246. "width",
  1247. "height",
  1248. "minWidth",
  1249. "minHeight",
  1250. "maxWidth",
  1251. "maxHeight",
  1252. "alignSelf"
  1253. ]);
  1254. let hasStyleKey = false;
  1255. for (const prop of node.properties) {
  1256. if (prop.type === "Property" && prop.key.type === "Identifier") {
  1257. if (knownStyleKeys.has(prop.key.name)) {
  1258. hasStyleKey = true;
  1259. break;
  1260. }
  1261. }
  1262. }
  1263. if (hasStyleKey) return true;
  1264. if (node.parent.type === "VariableDeclarator" &&
  1265. (node.parent.id.name === "styles" || node.parent.id.name === "stylesheet" || node.parent.id.name === "style")) {
  1266. return true;
  1267. }
  1268. if (node.parent.type === "TSAsExpression" || node.parent.type === "TypeAssertion") {
  1269. const typeName = node.parent.typeAnnotation?.typeName?.name;
  1270. if (typeName && (typeName === "ViewStyle" || typeName === "TextStyle" || typeName === "ImageStyle")) {
  1271. return true;
  1272. }
  1273. }
  1274. if (node.parent.type === "Property" && node.parent.parent.type === "ObjectExpression") {
  1275. if (node.parent.parent.parent.type === "VariableDeclarator" &&
  1276. (node.parent.parent.parent.id.name === "styles" || node.parent.parent.parent.id.name === "stylesheet")) {
  1277. return true;
  1278. }
  1279. }
  1280. return false;
  1281. }
  1282. function getSortedProperties(properties) {
  1283. const webStyles = [];
  1284. const others = [];
  1285. for (const p of properties) {
  1286. if (p.type === "SpreadElement" && p.argument.type === "CallExpression" && p.argument.callee.name === "webStyle") {
  1287. webStyles.push(p);
  1288. } else {
  1289. others.push(p);
  1290. }
  1291. }
  1292. const chunks = [];
  1293. let currentChunk = [];
  1294. const chunkSeparators = [];
  1295. for (const p of others) {
  1296. if (p.type === "SpreadElement") {
  1297. chunks.push(currentChunk);
  1298. chunkSeparators.push(p);
  1299. currentChunk = [];
  1300. } else {
  1301. currentChunk.push(p);
  1302. }
  1303. }
  1304. chunks.push(currentChunk);
  1305. function sortChunk(chunk) {
  1306. if (chunk.length <= 1) return chunk;
  1307. const getLen = (node) => sourceCode.getText(node).length;
  1308. const initialSorted = chunk.map((node, i) => ({
  1309. node,
  1310. originalIndex: i,
  1311. length: getLen(node)
  1312. })).sort((a, b) => {
  1313. if (a.length !== b.length) return b.length - a.length;
  1314. return a.originalIndex - b.originalIndex;
  1315. });
  1316. const propNames = chunk.map(p => {
  1317. if (p.type === "Property") {
  1318. if (p.key.type === "Identifier") return p.key.name;
  1319. if (p.key.type === "Literal") return p.key.value;
  1320. }
  1321. return null;
  1322. });
  1323. const prerequisites = new Map();
  1324. for (let i = 0; i < chunk.length; i++) {
  1325. prerequisites.set(i, new Set());
  1326. }
  1327. for (let i = 0; i < propNames.length; i++) {
  1328. const nameA = propNames[i];
  1329. if (!nameA) continue;
  1330. const deps = shorthandDependencies[nameA];
  1331. if (deps) {
  1332. for (let j = 0; j < propNames.length; j++) {
  1333. if (i === j) continue;
  1334. if (deps.includes(propNames[j])) {
  1335. prerequisites.get(j).add(i);
  1336. }
  1337. }
  1338. }
  1339. }
  1340. const result = [];
  1341. const placedIndices = new Set();
  1342. const pool = [...initialSorted];
  1343. while (pool.length > 0) {
  1344. let placedAny = false;
  1345. for (let i = 0; i < pool.length; i++) {
  1346. const item = pool[i];
  1347. const itemOriginalIndex = item.originalIndex;
  1348. const reqs = prerequisites.get(itemOriginalIndex);
  1349. let canPlace = true;
  1350. for (const req of reqs) {
  1351. if (!placedIndices.has(req)) {
  1352. canPlace = false;
  1353. break;
  1354. }
  1355. }
  1356. if (canPlace) {
  1357. result.push(item.node);
  1358. placedIndices.add(itemOriginalIndex);
  1359. pool.splice(i, 1);
  1360. placedAny = true;
  1361. break;
  1362. }
  1363. }
  1364. if (!placedAny) {
  1365. const item = pool.shift();
  1366. result.push(item.node);
  1367. placedIndices.add(item.originalIndex);
  1368. }
  1369. }
  1370. return result;
  1371. }
  1372. const finalResult = [];
  1373. for (let i = 0; i < chunks.length; i++) {
  1374. finalResult.push(...sortChunk(chunks[i]));
  1375. if (i < chunkSeparators.length) {
  1376. finalResult.push(chunkSeparators[i]);
  1377. }
  1378. }
  1379. finalResult.push(...webStyles);
  1380. return finalResult;
  1381. }
  1382. return {
  1383. ObjectExpression(node) {
  1384. if (!isStyleObject(node)) return;
  1385. const oldOrder = node.properties;
  1386. if (oldOrder.length <= 1) return;
  1387. const newOrder = getSortedProperties(oldOrder);
  1388. let changed = false;
  1389. for (let i = 0; i < oldOrder.length; i++) {
  1390. if (oldOrder[i] !== newOrder[i]) {
  1391. changed = true;
  1392. break;
  1393. }
  1394. }
  1395. if (changed) {
  1396. context.report({
  1397. node: node,
  1398. message: "Style properties should be sorted by length (longest to shortest), with shorthand properties appropriately ordered and spread elements at the bottom.",
  1399. fix(fixer) {
  1400. const fixes = [];
  1401. for (let i = 0; i < oldOrder.length; i++) {
  1402. fixes.push(fixer.replaceText(oldOrder[i], sourceCode.getText(newOrder[i])));
  1403. }
  1404. return fixes;
  1405. }
  1406. });
  1407. }
  1408. }
  1409. };
  1410. } },
  1411. "ncore-sort-types": {
  1412. meta: {
  1413. type: "layout",
  1414. fixable: "code",
  1415. schema: []
  1416. },
  1417. create(context) {
  1418. const sourceCode = context.getSourceCode();
  1419. function sortMembers(node, members) {
  1420. if (!members || members.length <= 1) return;
  1421. const getLen = (n) => {
  1422. const text = sourceCode.getText(n);
  1423. const lines = text.split(/\r?\n/);
  1424. let maxLen = 0;
  1425. for (const line of lines) {
  1426. const trimmed = line.trim();
  1427. if (trimmed.length > maxLen) {
  1428. maxLen = trimmed.length;
  1429. }
  1430. }
  1431. return maxLen;
  1432. };
  1433. const initialSorted = members.map((member, i) => ({
  1434. node: member,
  1435. originalIndex: i,
  1436. length: getLen(member)
  1437. })).sort((a, b) => {
  1438. if (a.length !== b.length) return b.length - a.length;
  1439. return a.originalIndex - b.originalIndex;
  1440. });
  1441. let changed = false;
  1442. for (let i = 0; i < members.length; i++) {
  1443. if (members[i] !== initialSorted[i].node) {
  1444. changed = true;
  1445. break;
  1446. }
  1447. }
  1448. if (changed) {
  1449. context.report({
  1450. node: node,
  1451. message: "Type properties should be sorted by length (longest to shortest).",
  1452. fix(fixer) {
  1453. const fixes = [];
  1454. for (let i = 0; i < members.length; i++) {
  1455. fixes.push(fixer.replaceText(members[i], sourceCode.getText(initialSorted[i].node)));
  1456. }
  1457. return fixes;
  1458. }
  1459. });
  1460. }
  1461. }
  1462. return {
  1463. TSInterfaceBody(node) {
  1464. sortMembers(node, node.body);
  1465. },
  1466. TSTypeLiteral(node) {
  1467. sortMembers(node, node.members);
  1468. }
  1469. };
  1470. }
  1471. }
  1472. };