index.js 71 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713
  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. VariableDeclarator(node) {
  780. if (node.id && node.id.name === "styles" && node.init && node.init.type === "ObjectExpression") {
  781. let parent = node.parent;
  782. let isInsideUseStyles = false;
  783. while (parent) {
  784. if (parent.type === "VariableDeclarator" && parent.id && parent.id.name === "useStyles") {
  785. isInsideUseStyles = true;
  786. break;
  787. }
  788. if (parent.type === "FunctionDeclaration" && parent.id && parent.id.name === "useStyles") {
  789. isInsideUseStyles = true;
  790. break;
  791. }
  792. parent = parent.parent;
  793. }
  794. if (isInsideUseStyles) {
  795. node.init.properties.forEach(styleClass => {
  796. let styleObj = styleClass.value;
  797. if (styleObj && styleObj.type === "TSAsExpression") {
  798. styleObj = styleObj.expression;
  799. }
  800. if (styleObj && styleObj.type === "ObjectExpression") {
  801. styleObj.properties.forEach(prop => {
  802. checkProperty(prop);
  803. });
  804. }
  805. });
  806. }
  807. }
  808. }
  809. };
  810. }
  811. },
  812. "jsx-ternary-formatting": {
  813. meta: {
  814. type: "layout",
  815. fixable: "whitespace",
  816. schema: []
  817. },
  818. create(context) {
  819. return {
  820. JSXExpressionContainer(node) {
  821. if (node.expression.type !== "ConditionalExpression") return;
  822. const sourceCode = context.getSourceCode();
  823. const openBrace = sourceCode.getFirstToken(node);
  824. const closeBrace = sourceCode.getLastToken(node);
  825. const cond = node.expression;
  826. let hasJSX = false;
  827. if (
  828. cond.consequent.type === "JSXElement" ||
  829. cond.consequent.type === "JSXFragment" ||
  830. cond.alternate.type === "JSXElement" ||
  831. cond.alternate.type === "JSXFragment"
  832. ) {
  833. hasJSX = true;
  834. }
  835. if (!hasJSX) return;
  836. const checkAndRemoveParens = (nodeToUnparen) => {
  837. const currentToken = sourceCode.getTokenBefore(nodeToUnparen);
  838. const afterToken = sourceCode.getTokenAfter(nodeToUnparen);
  839. let removed = false;
  840. while (currentToken && currentToken.value === "(" && afterToken && afterToken.value === ")") {
  841. context.report({
  842. node: currentToken,
  843. message: "Parentheses are not allowed around JSX in ternaries.",
  844. fix(fixer) {
  845. return [
  846. fixer.remove(currentToken),
  847. fixer.remove(afterToken)
  848. ];
  849. }
  850. });
  851. removed = true;
  852. break;
  853. }
  854. return removed;
  855. };
  856. let hasParens = false;
  857. if (cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment") {
  858. if (checkAndRemoveParens(cond.consequent)) hasParens = true;
  859. }
  860. if (cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment") {
  861. if (checkAndRemoveParens(cond.alternate)) hasParens = true;
  862. }
  863. if (hasParens) return; // Wait for the next fix pass to format the whitespace properly
  864. const questionToken = sourceCode.getTokenAfter(cond.test, {
  865. filter: t => t.value === "?"
  866. });
  867. const colonToken = sourceCode.getTokenAfter(cond.consequent, {
  868. filter: t => t.value === ":"
  869. });
  870. const getIndent = (token) => {
  871. const line = sourceCode.lines[token.loc.start.line - 1];
  872. return line.match(/^\s*/)[0];
  873. };
  874. const openIndent = getIndent(openBrace);
  875. const baseIndent = openIndent + " ";
  876. const innerIndent = baseIndent + " ";
  877. const checkAndFix = (token1, token2, expectedWhitespace) => {
  878. let textBetween = sourceCode.getText().slice(token1.range[1], token2.range[0]);
  879. textBetween = textBetween.replace(/\r\n/g, "\n");
  880. if (textBetween !== expectedWhitespace) {
  881. context.report({
  882. node: token2,
  883. message: "Incorrect ternary formatting",
  884. fix(fixer) {
  885. return fixer.replaceTextRange([
  886. token1.range[1],
  887. token2.range[0]
  888. ], expectedWhitespace);
  889. }
  890. });
  891. }
  892. };
  893. const firstTestToken = sourceCode.getTokenAfter(openBrace);
  894. checkAndFix(openBrace, firstTestToken, "\n" + baseIndent);
  895. const firstConsequentToken = sourceCode.getTokenAfter(questionToken);
  896. checkAndFix(questionToken, firstConsequentToken, "\n" + innerIndent);
  897. const firstColonToken = colonToken;
  898. const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
  899. checkAndFix(lastConsequentToken, firstColonToken, "\n" + baseIndent);
  900. const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
  901. checkAndFix(colonToken, firstAlternateToken, "\n" + innerIndent);
  902. const lastAlternateToken = sourceCode.getTokenBefore(closeBrace);
  903. checkAndFix(lastAlternateToken, closeBrace, "\n" + openIndent);
  904. }
  905. };
  906. }
  907. },
  908. "jsx-no-blank-lines": {
  909. meta: {
  910. type: "layout",
  911. fixable: "whitespace",
  912. schema: []
  913. },
  914. create(context) {
  915. return {
  916. JSXText(node) {
  917. const text = node.value;
  918. if (!/^\s*$/.test(text)) {
  919. return;
  920. }
  921. const newlines = text.match(/\n/g);
  922. if (newlines && newlines.length > 1) {
  923. context.report({
  924. node,
  925. message: "No blank lines allowed between JSX elements",
  926. fix(fixer) {
  927. const match = text.match(/\n[ \t]*$/);
  928. if (match) {
  929. return fixer.replaceText(node, match[0]);
  930. }
  931. }
  932. });
  933. }
  934. }
  935. };
  936. }
  937. },
  938. "sort-jsx-props": {
  939. meta: {
  940. type: "layout",
  941. fixable: "code",
  942. schema: []
  943. },
  944. create(context) {
  945. return {
  946. JSXOpeningElement(node) {
  947. const attributes = node.attributes;
  948. if (!attributes || attributes.length <= 1) return;
  949. const sourceCode = context.getSourceCode();
  950. const getAttrLength = (attr) => {
  951. const text = sourceCode.getText(attr);
  952. return text.replace(/\s+/g, "").length;
  953. };
  954. const groups = [];
  955. let currentGroup = [];
  956. for (const attr of attributes) {
  957. if (attr.type === "JSXSpreadAttribute") {
  958. if (currentGroup.length > 1) {
  959. groups.push(currentGroup);
  960. }
  961. currentGroup = [];
  962. } else {
  963. currentGroup.push(attr);
  964. }
  965. }
  966. if (currentGroup.length > 1) {
  967. groups.push(currentGroup);
  968. }
  969. for (const group of groups) {
  970. const sortedGroup = [...group].sort((a, b) => {
  971. return getAttrLength(b) - getAttrLength(a);
  972. });
  973. let isSorted = true;
  974. for (let i = 0; i < group.length; i++) {
  975. if (group[i] !== sortedGroup[i]) {
  976. isSorted = false;
  977. break;
  978. }
  979. }
  980. if (!isSorted) {
  981. context.report({
  982. node: group[0],
  983. message: "JSX attributes should be sorted from longest to shortest.",
  984. fix(fixer) {
  985. const firstAttr = group[0];
  986. const startLoc = firstAttr.loc.start;
  987. const indent = " ".repeat(startLoc.column);
  988. const newText = sortedGroup.map(attr => sourceCode.getText(attr)).join("\n" + indent);
  989. const start = group[0].range[0];
  990. const end = group[group.length - 1].range[1];
  991. return fixer.replaceTextRange([
  992. start,
  993. end
  994. ], newText);
  995. }
  996. });
  997. }
  998. }
  999. }
  1000. };
  1001. }
  1002. },
  1003. "sort-import-specifiers": {
  1004. meta: {
  1005. type: "layout",
  1006. fixable: "code",
  1007. schema: []
  1008. },
  1009. create(context) {
  1010. return {
  1011. ImportDeclaration(node) {
  1012. checkSpecifiers(node, "ImportSpecifier", "Import");
  1013. },
  1014. ExportNamedDeclaration(node) {
  1015. checkSpecifiers(node, "ExportSpecifier", "Export");
  1016. }
  1017. };
  1018. function checkSpecifiers(node, specifierType, typeName) {
  1019. if (!node.specifiers) return;
  1020. const specifiers = node.specifiers.filter(
  1021. s => s.type === specifierType
  1022. );
  1023. if (specifiers.length <= 1) return;
  1024. const sourceCode = context.getSourceCode();
  1025. const getSpecLength = (spec) => {
  1026. return sourceCode.getText(spec).replace(/\s+/g, "").length;
  1027. };
  1028. const sortedSpecifiers = [...specifiers].sort((a, b) => {
  1029. return getSpecLength(b) - getSpecLength(a);
  1030. });
  1031. let isSorted = true;
  1032. for (let i = 0; i < specifiers.length; i++) {
  1033. if (specifiers[i] !== sortedSpecifiers[i]) {
  1034. isSorted = false;
  1035. break;
  1036. }
  1037. }
  1038. if (!isSorted) {
  1039. context.report({
  1040. node,
  1041. message: `${typeName} specifiers should be sorted from longest to shortest.`,
  1042. fix(fixer) {
  1043. const firstSpec = specifiers[0];
  1044. const lastSpec = specifiers[specifiers.length - 1];
  1045. const startLoc = firstSpec.loc.start;
  1046. let indent = " ";
  1047. if (firstSpec.loc.start.line !== node.loc.start.line) {
  1048. indent = " ".repeat(startLoc.column);
  1049. }
  1050. const newText = sortedSpecifiers.map(spec => sourceCode.getText(spec)).join(",\n" + indent);
  1051. return fixer.replaceTextRange([
  1052. firstSpec.range[0],
  1053. lastSpec.range[1]
  1054. ], newText);
  1055. }
  1056. });
  1057. }
  1058. }
  1059. }
  1060. },
  1061. "sort-translations": {
  1062. meta: {
  1063. type: "layout",
  1064. fixable: "code",
  1065. schema: []
  1066. },
  1067. create(context) {
  1068. const sourceCode = context.getSourceCode();
  1069. function checkObject(node) {
  1070. if (!node.properties || node.properties.length <= 1) return;
  1071. let hasIsRTL = false;
  1072. let hasLocale = false;
  1073. let translationsProp = null;
  1074. for (const prop of node.properties) {
  1075. const keyNode = prop.key;
  1076. if (!keyNode) continue;
  1077. let keyName = "";
  1078. if (keyNode.type === "Identifier") keyName = keyNode.name;
  1079. else if (keyNode.type === "Literal" || keyNode.type === "JSONLiteral") keyName = keyNode.value;
  1080. if (keyName === "isRTL") hasIsRTL = true;
  1081. if (keyName === "locale") hasLocale = true;
  1082. if (keyName === "translations") translationsProp = prop;
  1083. }
  1084. if (hasIsRTL && hasLocale && translationsProp) {
  1085. const translationsObj = translationsProp.value;
  1086. if (!translationsObj || !translationsObj.properties || translationsObj.properties.length <= 1) return;
  1087. const props = translationsObj.properties;
  1088. const sortedProps = [...props].sort((a, b) => {
  1089. const lenA = sourceCode.getText(a.key).length + sourceCode.getText(a.value).length;
  1090. const lenB = sourceCode.getText(b.key).length + sourceCode.getText(b.value).length;
  1091. if (lenA !== lenB) {
  1092. return lenB - lenA;
  1093. }
  1094. const keyA = a.key.value || a.key.name || "";
  1095. const keyB = b.key.value || b.key.name || "";
  1096. return keyA.localeCompare(keyB);
  1097. });
  1098. let isSorted = true;
  1099. let errorNode = null;
  1100. for (let i = 0; i < props.length; i++) {
  1101. if (props[i] !== sortedProps[i]) {
  1102. isSorted = false;
  1103. errorNode = props[i];
  1104. break;
  1105. }
  1106. }
  1107. if (!isSorted) {
  1108. context.report({
  1109. node: errorNode,
  1110. message: "Translations keys must be sorted by length (longest to shortest).",
  1111. fix(fixer) {
  1112. const firstProp = props[0];
  1113. const lastProp = props[props.length - 1];
  1114. const startLoc = firstProp.loc.start;
  1115. const indent = " ".repeat(startLoc.column);
  1116. let newText = "";
  1117. sortedProps.forEach((p, index) => {
  1118. newText += sourceCode.getText(p);
  1119. if (index < sortedProps.length - 1) {
  1120. newText += ",\n" + indent;
  1121. }
  1122. });
  1123. return fixer.replaceTextRange([
  1124. firstProp.range[0],
  1125. lastProp.range[1]
  1126. ], newText);
  1127. }
  1128. });
  1129. }
  1130. }
  1131. }
  1132. return {
  1133. ObjectExpression: checkObject,
  1134. JSONObjectExpression: checkObject
  1135. };
  1136. }
  1137. },
  1138. "ncore-sort-styles": {
  1139. meta: {
  1140. type: "layout",
  1141. fixable: "code",
  1142. schema: []
  1143. },
  1144. create(context) {
  1145. const sourceCode = context.getSourceCode();
  1146. const shorthandDependencies = {
  1147. borderRadius: [
  1148. "borderTopLeftRadius",
  1149. "borderTopRightRadius",
  1150. "borderBottomLeftRadius",
  1151. "borderBottomRightRadius",
  1152. "borderTopStartRadius",
  1153. "borderTopEndRadius",
  1154. "borderBottomStartRadius",
  1155. "borderBottomEndRadius"
  1156. ],
  1157. padding: [
  1158. "paddingHorizontal",
  1159. "paddingVertical",
  1160. "paddingTop",
  1161. "paddingRight",
  1162. "paddingBottom",
  1163. "paddingLeft",
  1164. "paddingStart",
  1165. "paddingEnd"
  1166. ],
  1167. margin: [
  1168. "marginHorizontal",
  1169. "marginVertical",
  1170. "marginTop",
  1171. "marginRight",
  1172. "marginBottom",
  1173. "marginLeft",
  1174. "marginStart",
  1175. "marginEnd"
  1176. ],
  1177. borderWidth: [
  1178. "borderTopWidth",
  1179. "borderRightWidth",
  1180. "borderBottomWidth",
  1181. "borderLeftWidth",
  1182. "borderStartWidth",
  1183. "borderEndWidth"
  1184. ],
  1185. borderColor: [
  1186. "borderTopColor",
  1187. "borderRightColor",
  1188. "borderBottomColor",
  1189. "borderLeftColor",
  1190. "borderStartColor",
  1191. "borderEndColor"
  1192. ],
  1193. borderStyle: [
  1194. "borderTopStyle",
  1195. "borderRightStyle",
  1196. "borderBottomStyle",
  1197. "borderLeftStyle"
  1198. ],
  1199. paddingHorizontal: [
  1200. "paddingLeft",
  1201. "paddingRight",
  1202. "paddingStart",
  1203. "paddingEnd"
  1204. ],
  1205. marginHorizontal: [
  1206. "marginLeft",
  1207. "marginRight",
  1208. "marginStart",
  1209. "marginEnd"
  1210. ],
  1211. flex: [
  1212. "flexGrow",
  1213. "flexShrink",
  1214. "flexBasis"
  1215. ],
  1216. paddingVertical: [
  1217. "paddingTop",
  1218. "paddingBottom"
  1219. ],
  1220. marginVertical: [
  1221. "marginTop",
  1222. "marginBottom"
  1223. ]
  1224. };
  1225. function isStyleObject(node) {
  1226. if (node.properties.length <= 1) return false;
  1227. if (node.parent.type === "CallExpression" &&
  1228. node.parent.callee.type === "MemberExpression" &&
  1229. node.parent.callee.object.name === "StyleSheet" &&
  1230. node.parent.callee.property.name === "create") {
  1231. return true;
  1232. }
  1233. if (node.parent.type === "Property" &&
  1234. node.parent.parent.type === "ObjectExpression" &&
  1235. node.parent.parent.parent.type === "CallExpression" &&
  1236. node.parent.parent.parent.callee.type === "MemberExpression" &&
  1237. node.parent.parent.parent.callee.object.name === "StyleSheet") {
  1238. return true;
  1239. }
  1240. const knownStyleKeys = new Set([
  1241. "alignItems",
  1242. "justifyContent",
  1243. "flexDirection",
  1244. "flexWrap",
  1245. "flex",
  1246. "padding",
  1247. "margin",
  1248. "paddingTop",
  1249. "marginTop",
  1250. "paddingBottom",
  1251. "marginBottom",
  1252. "paddingLeft",
  1253. "marginLeft",
  1254. "paddingRight",
  1255. "marginRight",
  1256. "paddingHorizontal",
  1257. "marginHorizontal",
  1258. "paddingVertical",
  1259. "marginVertical",
  1260. "backgroundColor",
  1261. "color",
  1262. "fontSize",
  1263. "fontWeight",
  1264. "lineHeight",
  1265. "textAlign",
  1266. "borderWidth",
  1267. "borderRadius",
  1268. "borderColor",
  1269. "borderStyle",
  1270. "position",
  1271. "top",
  1272. "bottom",
  1273. "left",
  1274. "right",
  1275. "display",
  1276. "width",
  1277. "height",
  1278. "minWidth",
  1279. "minHeight",
  1280. "maxWidth",
  1281. "maxHeight",
  1282. "alignSelf"
  1283. ]);
  1284. let hasStyleKey = false;
  1285. for (const prop of node.properties) {
  1286. if (prop.type === "Property" && prop.key.type === "Identifier") {
  1287. if (knownStyleKeys.has(prop.key.name)) {
  1288. hasStyleKey = true;
  1289. break;
  1290. }
  1291. }
  1292. }
  1293. if (hasStyleKey) return true;
  1294. if (node.parent.type === "VariableDeclarator" &&
  1295. (node.parent.id.name === "styles" || node.parent.id.name === "stylesheet" || node.parent.id.name === "style")) {
  1296. return true;
  1297. }
  1298. if (node.parent.type === "TSAsExpression" || node.parent.type === "TypeAssertion") {
  1299. const typeName = node.parent.typeAnnotation?.typeName?.name;
  1300. if (typeName && (typeName === "ViewStyle" || typeName === "TextStyle" || typeName === "ImageStyle")) {
  1301. return true;
  1302. }
  1303. }
  1304. if (node.parent.type === "Property" && node.parent.parent.type === "ObjectExpression") {
  1305. if (node.parent.parent.parent.type === "VariableDeclarator" &&
  1306. (node.parent.parent.parent.id.name === "styles" || node.parent.parent.parent.id.name === "stylesheet")) {
  1307. return true;
  1308. }
  1309. }
  1310. return false;
  1311. }
  1312. function getSortedProperties(properties) {
  1313. const webStyles = [];
  1314. const others = [];
  1315. for (const p of properties) {
  1316. if (p.type === "SpreadElement" && p.argument.type === "CallExpression" && p.argument.callee.name === "webStyle") {
  1317. webStyles.push(p);
  1318. } else {
  1319. others.push(p);
  1320. }
  1321. }
  1322. const chunks = [];
  1323. let currentChunk = [];
  1324. const chunkSeparators = [];
  1325. for (const p of others) {
  1326. if (p.type === "SpreadElement") {
  1327. chunks.push(currentChunk);
  1328. chunkSeparators.push(p);
  1329. currentChunk = [];
  1330. } else {
  1331. currentChunk.push(p);
  1332. }
  1333. }
  1334. chunks.push(currentChunk);
  1335. function sortChunk(chunk) {
  1336. if (chunk.length <= 1) return chunk;
  1337. const getLen = (node) => sourceCode.getText(node).length;
  1338. const initialSorted = chunk.map((node, i) => ({
  1339. node,
  1340. originalIndex: i,
  1341. length: getLen(node)
  1342. })).sort((a, b) => {
  1343. if (a.length !== b.length) return b.length - a.length;
  1344. return a.originalIndex - b.originalIndex;
  1345. });
  1346. const propNames = chunk.map(p => {
  1347. if (p.type === "Property") {
  1348. if (p.key.type === "Identifier") return p.key.name;
  1349. if (p.key.type === "Literal") return p.key.value;
  1350. }
  1351. return null;
  1352. });
  1353. const prerequisites = new Map();
  1354. for (let i = 0; i < chunk.length; i++) {
  1355. prerequisites.set(i, new Set());
  1356. }
  1357. for (let i = 0; i < propNames.length; i++) {
  1358. const nameA = propNames[i];
  1359. if (!nameA) continue;
  1360. const deps = shorthandDependencies[nameA];
  1361. if (deps) {
  1362. for (let j = 0; j < propNames.length; j++) {
  1363. if (i === j) continue;
  1364. if (deps.includes(propNames[j])) {
  1365. prerequisites.get(j).add(i);
  1366. }
  1367. }
  1368. }
  1369. }
  1370. const result = [];
  1371. const placedIndices = new Set();
  1372. const pool = [...initialSorted];
  1373. while (pool.length > 0) {
  1374. let placedAny = false;
  1375. for (let i = 0; i < pool.length; i++) {
  1376. const item = pool[i];
  1377. const itemOriginalIndex = item.originalIndex;
  1378. const reqs = prerequisites.get(itemOriginalIndex);
  1379. let canPlace = true;
  1380. for (const req of reqs) {
  1381. if (!placedIndices.has(req)) {
  1382. canPlace = false;
  1383. break;
  1384. }
  1385. }
  1386. if (canPlace) {
  1387. result.push(item.node);
  1388. placedIndices.add(itemOriginalIndex);
  1389. pool.splice(i, 1);
  1390. placedAny = true;
  1391. break;
  1392. }
  1393. }
  1394. if (!placedAny) {
  1395. const item = pool.shift();
  1396. result.push(item.node);
  1397. placedIndices.add(item.originalIndex);
  1398. }
  1399. }
  1400. return result;
  1401. }
  1402. const finalResult = [];
  1403. for (let i = 0; i < chunks.length; i++) {
  1404. finalResult.push(...sortChunk(chunks[i]));
  1405. if (i < chunkSeparators.length) {
  1406. finalResult.push(chunkSeparators[i]);
  1407. }
  1408. }
  1409. finalResult.push(...webStyles);
  1410. return finalResult;
  1411. }
  1412. return {
  1413. ObjectExpression(node) {
  1414. if (!isStyleObject(node)) return;
  1415. const oldOrder = node.properties;
  1416. if (oldOrder.length <= 1) return;
  1417. const newOrder = getSortedProperties(oldOrder);
  1418. let changed = false;
  1419. for (let i = 0; i < oldOrder.length; i++) {
  1420. if (oldOrder[i] !== newOrder[i]) {
  1421. changed = true;
  1422. break;
  1423. }
  1424. }
  1425. if (changed) {
  1426. context.report({
  1427. node: node,
  1428. message: "Style properties should be sorted by length (longest to shortest), with shorthand properties appropriately ordered and spread elements at the bottom.",
  1429. fix(fixer) {
  1430. const fixes = [];
  1431. for (let i = 0; i < oldOrder.length; i++) {
  1432. fixes.push(fixer.replaceText(oldOrder[i], sourceCode.getText(newOrder[i])));
  1433. }
  1434. return fixes;
  1435. }
  1436. });
  1437. }
  1438. }
  1439. };
  1440. }
  1441. },
  1442. "ncore-sort-types": {
  1443. meta: {
  1444. type: "layout",
  1445. fixable: "code",
  1446. schema: []
  1447. },
  1448. create(context) {
  1449. const sourceCode = context.getSourceCode();
  1450. function sortMembers(node, members) {
  1451. if (!members || members.length <= 1) return;
  1452. const getLen = (n) => {
  1453. const text = sourceCode.getText(n);
  1454. const lines = text.split(/\r?\n/);
  1455. let maxLen = 0;
  1456. for (const line of lines) {
  1457. const trimmed = line.trim();
  1458. if (trimmed.length > maxLen) {
  1459. maxLen = trimmed.length;
  1460. }
  1461. }
  1462. return maxLen;
  1463. };
  1464. const initialSorted = members.map((member, i) => ({
  1465. node: member,
  1466. originalIndex: i,
  1467. length: getLen(member)
  1468. })).sort((a, b) => {
  1469. if (a.length !== b.length) return b.length - a.length;
  1470. return a.originalIndex - b.originalIndex;
  1471. });
  1472. let changed = false;
  1473. for (let i = 0; i < members.length; i++) {
  1474. if (members[i] !== initialSorted[i].node) {
  1475. changed = true;
  1476. break;
  1477. }
  1478. }
  1479. if (changed) {
  1480. context.report({
  1481. node: node,
  1482. message: "Type properties should be sorted by length (longest to shortest).",
  1483. fix(fixer) {
  1484. const fixes = [];
  1485. for (let i = 0; i < members.length; i++) {
  1486. fixes.push(fixer.replaceText(members[i], sourceCode.getText(initialSorted[i].node)));
  1487. }
  1488. return fixes;
  1489. }
  1490. });
  1491. }
  1492. }
  1493. return {
  1494. TSInterfaceBody(node) {
  1495. sortMembers(node, node.body);
  1496. },
  1497. TSTypeLiteral(node) {
  1498. sortMembers(node, node.members);
  1499. }
  1500. };
  1501. }
  1502. }
  1503. };