index.js 79 KB

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