index.js 80 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983
  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 keyName = propertyNode.key && propertyNode.key.name;
  807. const allowedStaticProps = new Set([
  808. "alignSelf",
  809. "textAlign",
  810. "display",
  811. "position",
  812. "flexDirection",
  813. "justifyContent",
  814. "alignItems",
  815. "flexWrap",
  816. "overflow",
  817. "textDecorationLine",
  818. "textTransform",
  819. "fontStyle",
  820. "fontWeight",
  821. "resizeMode",
  822. "lineHeight",
  823. "flex",
  824. "flexGrow",
  825. "flexShrink"
  826. ]);
  827. if (keyName && allowedStaticProps.has(keyName)) {
  828. return;
  829. }
  830. const value = propertyNode.value;
  831. if (!value) return;
  832. if (value.type === "Literal" && value.value === "transparent") {
  833. return;
  834. }
  835. let isStatic = false;
  836. if (value.type === "Literal") {
  837. if (typeof value.value === "string" || typeof value.value === "number") {
  838. isStatic = true;
  839. }
  840. } else if (value.type === "UnaryExpression" && value.operator === "-") {
  841. if (value.argument && value.argument.type === "Literal" && typeof value.argument.value === "number") {
  842. isStatic = true;
  843. }
  844. } else if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
  845. isStatic = true;
  846. }
  847. if (isStatic) {
  848. context.report({
  849. messageId: "staticStyle",
  850. node: propertyNode
  851. });
  852. }
  853. }
  854. function checkObjectExpression(node) {
  855. if (node.type !== "ObjectExpression") return;
  856. node.properties.forEach(checkProperty);
  857. }
  858. function checkStyleValue(node) {
  859. if (!node) return;
  860. if (node.type === "ObjectExpression") {
  861. checkObjectExpression(node);
  862. } else if (node.type === "ArrayExpression") {
  863. node.elements.forEach(element => {
  864. if (element && element.type === "ObjectExpression") {
  865. checkObjectExpression(element);
  866. }
  867. });
  868. }
  869. }
  870. return {
  871. VariableDeclarator(node) {
  872. if (node.id && node.id.name === "styles" && node.init && node.init.type === "ObjectExpression") {
  873. let parent = node.parent;
  874. let isInsideUseStyles = false;
  875. while (parent) {
  876. if (parent.type === "VariableDeclarator" && parent.id && parent.id.name === "useStyles") {
  877. isInsideUseStyles = true;
  878. break;
  879. }
  880. if (parent.type === "FunctionDeclaration" && parent.id && parent.id.name === "useStyles") {
  881. isInsideUseStyles = true;
  882. break;
  883. }
  884. parent = parent.parent;
  885. }
  886. if (isInsideUseStyles) {
  887. node.init.properties.forEach(styleClass => {
  888. let styleObj = styleClass.value;
  889. if (styleObj && styleObj.type === "TSAsExpression") {
  890. styleObj = styleObj.expression;
  891. }
  892. if (styleObj && styleObj.type === "ObjectExpression") {
  893. styleObj.properties.forEach(prop => {
  894. checkProperty(prop);
  895. });
  896. }
  897. });
  898. }
  899. }
  900. },
  901. JSXAttribute(node) {
  902. const attrName = node.name.name;
  903. if (typeof attrName !== "string") return;
  904. if (attrName === "style" || attrName.endsWith("Style")) {
  905. if (node.value && node.value.type === "JSXExpressionContainer") {
  906. checkStyleValue(node.value.expression);
  907. }
  908. }
  909. }
  910. };
  911. },
  912. meta: {
  913. messages: {
  914. staticStyle: "Static inline styles are not allowed. Please move them to a stylesheet file."
  915. },
  916. type: "suggestion",
  917. schema: []
  918. }
  919. },
  920. "sort-jsx-props": {
  921. create(context) {
  922. return {
  923. JSXOpeningElement(node) {
  924. const attributes = node.attributes;
  925. if (!attributes || attributes.length <= 1) return;
  926. const sourceCode = context.getSourceCode();
  927. const getAttrLength = (attr) => {
  928. let maxLen = 0;
  929. for (let i = attr.loc.start.line - 1; i < attr.loc.end.line; i++) {
  930. const lineLen = sourceCode.lines[i].trimRight().length;
  931. if (lineLen > maxLen) {
  932. maxLen = lineLen;
  933. }
  934. }
  935. return maxLen;
  936. };
  937. const groups = [];
  938. let currentGroup = [];
  939. for (const attr of attributes) {
  940. if (attr.type === "JSXSpreadAttribute") {
  941. if (currentGroup.length > 1) {
  942. groups.push(currentGroup);
  943. }
  944. currentGroup = [];
  945. } else {
  946. currentGroup.push(attr);
  947. }
  948. }
  949. if (currentGroup.length > 1) {
  950. groups.push(currentGroup);
  951. }
  952. for (const group of groups) {
  953. const sortedGroup = [...group].sort((a, b) => {
  954. return getAttrLength(b) - getAttrLength(a);
  955. });
  956. let isSorted = true;
  957. for (let i = 0; i < group.length; i++) {
  958. if (group[i] !== sortedGroup[i]) {
  959. isSorted = false;
  960. break;
  961. }
  962. }
  963. if (!isSorted) {
  964. context.report({
  965. fix(fixer) {
  966. const firstAttr = group[0];
  967. const startLoc = firstAttr.loc.start;
  968. const indent = " ".repeat(startLoc.column);
  969. const newText = sortedGroup.map(attr => sourceCode.getText(attr)).join("\n" + indent);
  970. const start = group[0].range[0];
  971. const end = group[group.length - 1].range[1];
  972. return fixer.replaceTextRange([
  973. start,
  974. end
  975. ], newText);
  976. },
  977. message: "JSX attributes should be sorted from longest to shortest.",
  978. node: group[0]
  979. });
  980. }
  981. }
  982. }
  983. };
  984. },
  985. meta: {
  986. fixable: "code",
  987. type: "layout",
  988. schema: []
  989. }
  990. },
  991. "sort-translations": {
  992. create(context) {
  993. const sourceCode = context.getSourceCode();
  994. function checkObject(node) {
  995. if (!node.properties || node.properties.length <= 1) return;
  996. let hasIsRTL = false;
  997. let hasLocale = false;
  998. let translationsProp = null;
  999. for (const prop of node.properties) {
  1000. const keyNode = prop.key;
  1001. if (!keyNode) continue;
  1002. let keyName = "";
  1003. if (keyNode.type === "Identifier") keyName = keyNode.name;
  1004. else if (keyNode.type === "Literal" || keyNode.type === "JSONLiteral") keyName = keyNode.value;
  1005. if (keyName === "isRTL") hasIsRTL = true;
  1006. if (keyName === "locale") hasLocale = true;
  1007. if (keyName === "translations") translationsProp = prop;
  1008. }
  1009. if (hasIsRTL && hasLocale && translationsProp) {
  1010. const translationsObj = translationsProp.value;
  1011. if (!translationsObj || !translationsObj.properties || translationsObj.properties.length <= 1) return;
  1012. const props = translationsObj.properties;
  1013. const sortedProps = [...props].sort((a, b) => {
  1014. const lenA = sourceCode.getText(a.key).length + sourceCode.getText(a.value).length;
  1015. const lenB = sourceCode.getText(b.key).length + sourceCode.getText(b.value).length;
  1016. if (lenA !== lenB) {
  1017. return lenB - lenA;
  1018. }
  1019. const keyA = a.key.value || a.key.name || "";
  1020. const keyB = b.key.value || b.key.name || "";
  1021. return keyA.localeCompare(keyB);
  1022. });
  1023. let isSorted = true;
  1024. let errorNode = null;
  1025. for (let i = 0; i < props.length; i++) {
  1026. if (props[i] !== sortedProps[i]) {
  1027. isSorted = false;
  1028. errorNode = props[i];
  1029. break;
  1030. }
  1031. }
  1032. if (!isSorted) {
  1033. context.report({
  1034. message: "Translations keys must be sorted by length (longest to shortest).",
  1035. fix(fixer) {
  1036. const firstProp = props[0];
  1037. const lastProp = props[props.length - 1];
  1038. const startLoc = firstProp.loc.start;
  1039. const indent = " ".repeat(startLoc.column);
  1040. let newText = "";
  1041. sortedProps.forEach((p, index) => {
  1042. newText += sourceCode.getText(p);
  1043. if (index < sortedProps.length - 1) {
  1044. newText += ",\n" + indent;
  1045. }
  1046. });
  1047. return fixer.replaceTextRange([
  1048. firstProp.range[0],
  1049. lastProp.range[1]
  1050. ], newText);
  1051. },
  1052. node: errorNode
  1053. });
  1054. }
  1055. }
  1056. }
  1057. return {
  1058. JSONObjectExpression: checkObject,
  1059. ObjectExpression: checkObject
  1060. };
  1061. },
  1062. meta: {
  1063. fixable: "code",
  1064. type: "layout",
  1065. schema: []
  1066. }
  1067. },
  1068. "sort-import-specifiers": {
  1069. create(context) {
  1070. return {
  1071. ExportNamedDeclaration(node) {
  1072. checkSpecifiers(node, "ExportSpecifier", "Export");
  1073. },
  1074. ImportDeclaration(node) {
  1075. checkSpecifiers(node, "ImportSpecifier", "Import");
  1076. }
  1077. };
  1078. function checkSpecifiers(node, specifierType, typeName) {
  1079. if (!node.specifiers) return;
  1080. const specifiers = node.specifiers.filter(
  1081. s => s.type === specifierType
  1082. );
  1083. if (specifiers.length <= 1) return;
  1084. const sourceCode = context.getSourceCode();
  1085. const getSpecLength = (spec) => {
  1086. return sourceCode.getText(spec).replace(/\s+/g, "").length;
  1087. };
  1088. const sortedSpecifiers = [...specifiers].sort((a, b) => {
  1089. return getSpecLength(b) - getSpecLength(a);
  1090. });
  1091. let isSorted = true;
  1092. for (let i = 0; i < specifiers.length; i++) {
  1093. if (specifiers[i] !== sortedSpecifiers[i]) {
  1094. isSorted = false;
  1095. break;
  1096. }
  1097. }
  1098. if (!isSorted) {
  1099. context.report({
  1100. fix(fixer) {
  1101. const firstSpec = specifiers[0];
  1102. const lastSpec = specifiers[specifiers.length - 1];
  1103. const startLoc = firstSpec.loc.start;
  1104. let indent = " ";
  1105. if (firstSpec.loc.start.line !== node.loc.start.line) {
  1106. indent = " ".repeat(startLoc.column);
  1107. }
  1108. const newText = sortedSpecifiers.map(spec => sourceCode.getText(spec)).join(",\n" + indent);
  1109. return fixer.replaceTextRange([
  1110. firstSpec.range[0],
  1111. lastSpec.range[1]
  1112. ], newText);
  1113. },
  1114. message: `${typeName} specifiers should be sorted from longest to shortest.`,
  1115. node
  1116. });
  1117. }
  1118. }
  1119. },
  1120. meta: {
  1121. fixable: "code",
  1122. type: "layout",
  1123. schema: []
  1124. }
  1125. },
  1126. "jsx-ternary-formatting": {
  1127. create(context) {
  1128. return {
  1129. JSXExpressionContainer(node) {
  1130. if (node.expression.type !== "ConditionalExpression") return;
  1131. const sourceCode = context.getSourceCode();
  1132. const openBrace = sourceCode.getFirstToken(node);
  1133. const closeBrace = sourceCode.getLastToken(node);
  1134. const cond = node.expression;
  1135. let hasJSX = false;
  1136. if (
  1137. cond.consequent.type === "JSXElement" ||
  1138. cond.consequent.type === "JSXFragment" ||
  1139. cond.alternate.type === "JSXElement" ||
  1140. cond.alternate.type === "JSXFragment"
  1141. ) {
  1142. hasJSX = true;
  1143. }
  1144. if (!hasJSX) return;
  1145. const checkAndRemoveParens = (nodeToUnparen) => {
  1146. const currentToken = sourceCode.getTokenBefore(nodeToUnparen);
  1147. const afterToken = sourceCode.getTokenAfter(nodeToUnparen);
  1148. let removed = false;
  1149. while (currentToken && currentToken.value === "(" && afterToken && afterToken.value === ")") {
  1150. context.report({
  1151. message: "Parentheses are not allowed around JSX in ternaries.",
  1152. fix(fixer) {
  1153. return [
  1154. fixer.remove(currentToken),
  1155. fixer.remove(afterToken)
  1156. ];
  1157. },
  1158. node: currentToken
  1159. });
  1160. removed = true;
  1161. break;
  1162. }
  1163. return removed;
  1164. };
  1165. let hasParens = false;
  1166. if (cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment") {
  1167. if (checkAndRemoveParens(cond.consequent)) hasParens = true;
  1168. }
  1169. if (cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment") {
  1170. if (checkAndRemoveParens(cond.alternate)) hasParens = true;
  1171. }
  1172. if (hasParens) return;
  1173. const questionToken = sourceCode.getTokenAfter(cond.test, {
  1174. filter: t => t.value === "?"
  1175. });
  1176. const colonToken = sourceCode.getTokenAfter(cond.consequent, {
  1177. filter: t => t.value === ":"
  1178. });
  1179. const getIndent = (token) => {
  1180. const line = sourceCode.lines[token.loc.start.line - 1];
  1181. return line.match(/^\s*/)[0];
  1182. };
  1183. const openIndent = getIndent(openBrace);
  1184. const baseIndent = openIndent + " ";
  1185. const innerIndent = baseIndent + " ";
  1186. const checkAndFix = (token1, token2, expectedWhitespace) => {
  1187. let textBetween = sourceCode.getText().slice(token1.range[1], token2.range[0]);
  1188. textBetween = textBetween.replace(/\r\n/g, "\n");
  1189. if (textBetween !== expectedWhitespace) {
  1190. context.report({
  1191. message: "Incorrect ternary formatting",
  1192. fix(fixer) {
  1193. return fixer.replaceTextRange([
  1194. token1.range[1],
  1195. token2.range[0]
  1196. ], expectedWhitespace);
  1197. },
  1198. node: token2
  1199. });
  1200. }
  1201. };
  1202. const firstTestToken = sourceCode.getTokenAfter(openBrace);
  1203. checkAndFix(openBrace, firstTestToken, "\n" + baseIndent);
  1204. const firstConsequentToken = sourceCode.getTokenAfter(questionToken);
  1205. checkAndFix(questionToken, firstConsequentToken, "\n" + innerIndent);
  1206. const firstColonToken = colonToken;
  1207. const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
  1208. checkAndFix(lastConsequentToken, firstColonToken, "\n" + baseIndent);
  1209. const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
  1210. checkAndFix(colonToken, firstAlternateToken, "\n" + innerIndent);
  1211. const lastAlternateToken = sourceCode.getTokenBefore(closeBrace);
  1212. checkAndFix(lastAlternateToken, closeBrace, "\n" + openIndent);
  1213. }
  1214. };
  1215. },
  1216. meta: {
  1217. fixable: "whitespace",
  1218. type: "layout",
  1219. schema: []
  1220. }
  1221. },
  1222. "multiline-jsx-children": {
  1223. create(context) {
  1224. return {
  1225. JSXElement(node) {
  1226. if (!node.closingElement) return;
  1227. if (node.children.length === 0) return;
  1228. const meaningfulChildren = node.children.filter(child => {
  1229. if (child.type === "JSXText" && child.value.trim() === "") return false;
  1230. return true;
  1231. });
  1232. if (meaningfulChildren.length === 0) return;
  1233. const openingElement = node.openingElement;
  1234. const closingElement = node.closingElement;
  1235. const isMultilineOpening = openingElement.loc.start.line !== openingElement.loc.end.line;
  1236. if (!isMultilineOpening) return;
  1237. const sourceCode = context.getSourceCode();
  1238. if (meaningfulChildren.length === 1 && meaningfulChildren[0].type === "JSXText") {
  1239. const child = meaningfulChildren[0];
  1240. const text = sourceCode.getText(child);
  1241. const hasLeadingNewline = /^\s*\n/.test(text);
  1242. const hasTrailingNewline = /\n\s*$/.test(text);
  1243. if (!hasLeadingNewline || !hasTrailingNewline) {
  1244. context.report({
  1245. fix(fixer) {
  1246. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  1247. const baseIndent = openBracketLine.match(/^\s*/)[0];
  1248. const indent = baseIndent + " ";
  1249. return fixer.replaceText(child, "\n" + indent + text.trim() + "\n" + baseIndent);
  1250. },
  1251. message: "Multiline JSX element text content must be on its own line",
  1252. node: child
  1253. });
  1254. }
  1255. return;
  1256. }
  1257. const firstChild = meaningfulChildren[0];
  1258. const openingEndLine = openingElement.loc.end.line;
  1259. let needsFirstChildFix = false;
  1260. if (firstChild.type === "JSXText") {
  1261. const text = sourceCode.getText(firstChild);
  1262. if (!/^\s*\n/.test(text)) needsFirstChildFix = true;
  1263. } else if (firstChild.loc.start.line === openingEndLine) {
  1264. needsFirstChildFix = true;
  1265. }
  1266. if (needsFirstChildFix) {
  1267. context.report({
  1268. fix(fixer) {
  1269. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  1270. const baseIndent = openBracketLine.match(/^\s*/)[0];
  1271. const indent = baseIndent + " ";
  1272. if (firstChild.type === "JSXText") {
  1273. const text = sourceCode.getText(firstChild);
  1274. return fixer.replaceText(firstChild, "\n" + indent + text.replace(/^\s+/, ""));
  1275. }
  1276. return fixer.insertTextBefore(firstChild, "\n" + indent);
  1277. },
  1278. message: "First child of a multiline JSX element must be on a new line",
  1279. node: firstChild
  1280. });
  1281. }
  1282. const lastChild = meaningfulChildren[meaningfulChildren.length - 1];
  1283. let needsLastChildFix = false;
  1284. if (lastChild.type === "JSXText") {
  1285. const text = sourceCode.getText(lastChild);
  1286. if (!/\n\s*$/.test(text)) needsLastChildFix = true;
  1287. } else if (lastChild.loc.end.line === closingElement.loc.start.line) {
  1288. needsLastChildFix = true;
  1289. }
  1290. if (needsLastChildFix) {
  1291. context.report({
  1292. fix(fixer) {
  1293. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  1294. const baseIndent = openBracketLine.match(/^\s*/)[0];
  1295. if (lastChild.type === "JSXText") {
  1296. const text = sourceCode.getText(lastChild);
  1297. return fixer.replaceText(lastChild, text.replace(/\s+$/, "") + "\n" + baseIndent);
  1298. }
  1299. return fixer.insertTextBefore(closingElement, "\n" + baseIndent);
  1300. },
  1301. message: "Closing tag of a multiline JSX element must be on a new line",
  1302. node: closingElement
  1303. });
  1304. }
  1305. }
  1306. };
  1307. },
  1308. meta: {
  1309. fixable: "whitespace",
  1310. type: "layout",
  1311. schema: []
  1312. }
  1313. },
  1314. "ncore-sort-types": {
  1315. create(context) {
  1316. const sourceCode = context.getSourceCode();
  1317. function sortMembers(node, members) {
  1318. if (!members || members.length <= 1) return;
  1319. const getLen = (n) => {
  1320. let maxLen = 0;
  1321. for (let i = n.loc.start.line - 1; i < n.loc.end.line; i++) {
  1322. const lineLen = sourceCode.lines[i].trimRight().length;
  1323. if (lineLen > maxLen) {
  1324. maxLen = lineLen;
  1325. }
  1326. }
  1327. return maxLen;
  1328. };
  1329. const initialSorted = members.map((member, i) => ({
  1330. length: getLen(member),
  1331. originalIndex: i,
  1332. node: member
  1333. })).sort((a, b) => {
  1334. if (a.length !== b.length) return b.length - a.length;
  1335. return a.originalIndex - b.originalIndex;
  1336. });
  1337. let changed = false;
  1338. for (let i = 0; i < members.length; i++) {
  1339. if (members[i] !== initialSorted[i].node) {
  1340. changed = true;
  1341. break;
  1342. }
  1343. }
  1344. if (changed) {
  1345. context.report({
  1346. fix(fixer) {
  1347. const fixes = [];
  1348. for (let i = 0; i < members.length; i++) {
  1349. fixes.push(fixer.replaceText(members[i], sourceCode.getText(initialSorted[i].node)));
  1350. }
  1351. return fixes;
  1352. },
  1353. message: "Type properties should be sorted by length (longest to shortest).",
  1354. node: node
  1355. });
  1356. }
  1357. }
  1358. return {
  1359. TSTypeLiteral(node) {
  1360. sortMembers(node, node.members);
  1361. },
  1362. TSInterfaceBody(node) {
  1363. sortMembers(node, node.body);
  1364. }
  1365. };
  1366. },
  1367. meta: {
  1368. fixable: "code",
  1369. type: "layout",
  1370. schema: []
  1371. }
  1372. },
  1373. "multiline-array-elements": {
  1374. create(context) {
  1375. const sourceCode = context.getSourceCode();
  1376. return {
  1377. "ArrayExpression, ArrayPattern"(node) {
  1378. if (node.elements.length === 0) return;
  1379. const openBracket = sourceCode.getFirstToken(node);
  1380. const closingBracket = sourceCode.getLastToken(node);
  1381. if (openBracket.loc.start.line === closingBracket.loc.end.line) {
  1382. let isJSXProp = false;
  1383. let current = node;
  1384. while (current.parent) {
  1385. if (current.parent.type === "JSXExpressionContainer") {
  1386. isJSXProp = true;
  1387. break;
  1388. }
  1389. current = current.parent;
  1390. }
  1391. if (!isJSXProp) return;
  1392. }
  1393. const firstElement = node.elements[0];
  1394. if (firstElement && openBracket.loc.end.line !== firstElement.loc.start.line - 1) {
  1395. context.report({
  1396. fix(fixer) {
  1397. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  1398. const baseIndent = openBracketLine.match(/^\s*/)[0];
  1399. const indent = baseIndent + " ";
  1400. return fixer.replaceTextRange(
  1401. [
  1402. openBracket.range[1],
  1403. firstElement.range[0]
  1404. ],
  1405. "\n" + indent
  1406. );
  1407. },
  1408. message: "First array element should be exactly on the next line",
  1409. node: firstElement
  1410. });
  1411. }
  1412. for (let i = 0; i < node.elements.length - 1; i++) {
  1413. const current = node.elements[i];
  1414. const next = node.elements[i + 1];
  1415. if (!current || !next) continue;
  1416. if (current.loc.end.line !== next.loc.start.line - 1) {
  1417. context.report({
  1418. message: "Each array element should be exactly on the next line (no empty lines)",
  1419. fix(fixer) {
  1420. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  1421. const baseIndent = openBracketLine.match(/^\s*/)[0];
  1422. const indent = baseIndent + " ";
  1423. const comma = sourceCode.getTokenBefore(next);
  1424. return fixer.replaceTextRange(
  1425. [
  1426. comma.range[1],
  1427. next.range[0]
  1428. ],
  1429. "\n" + indent
  1430. );
  1431. },
  1432. node: next
  1433. });
  1434. }
  1435. }
  1436. const lastElement = node.elements[node.elements.length - 1];
  1437. if (lastElement && lastElement.loc.end.line !== closingBracket.loc.start.line - 1) {
  1438. context.report({
  1439. fix(fixer) {
  1440. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  1441. const baseIndent = openBracketLine.match(/^\s*/)[0];
  1442. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  1443. const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
  1444. const startRange = hasTrailingComma ? tokenAfter.range[1] : lastElement.range[1];
  1445. return fixer.replaceTextRange(
  1446. [
  1447. startRange,
  1448. closingBracket.range[0]
  1449. ],
  1450. "\n" + baseIndent
  1451. );
  1452. },
  1453. message: "Array closing bracket should be exactly on the next line",
  1454. node: closingBracket
  1455. });
  1456. }
  1457. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  1458. if (tokenAfter && tokenAfter.value === "," && tokenAfter.range[1] <= closingBracket.range[0]) {
  1459. context.report({
  1460. message: "No trailing comma in arrays",
  1461. fix(fixer) {
  1462. return fixer.remove(tokenAfter);
  1463. },
  1464. node: lastElement
  1465. });
  1466. }
  1467. }
  1468. };
  1469. },
  1470. meta: {
  1471. fixable: "whitespace",
  1472. type: "layout",
  1473. schema: []
  1474. }
  1475. },
  1476. "multiline-jsx-attributes": {
  1477. create(context) {
  1478. const sourceCode = context.getSourceCode();
  1479. return {
  1480. JSXOpeningElement(node) {
  1481. if (node.attributes.length === 0) return;
  1482. const firstToken = sourceCode.getFirstToken(node);
  1483. const tagNameToken = sourceCode.getTokenAfter(firstToken);
  1484. const firstAttr = node.attributes[0];
  1485. if (tagNameToken.loc.end.line !== firstAttr.loc.start.line - 1) {
  1486. context.report({
  1487. fix(fixer) {
  1488. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  1489. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1490. const indent = baseIndent + " ";
  1491. return fixer.replaceTextRange(
  1492. [
  1493. tagNameToken.range[1],
  1494. firstAttr.range[0]
  1495. ],
  1496. "\n" + indent
  1497. );
  1498. },
  1499. message: "First JSX attribute should be exactly on the next line",
  1500. node: firstAttr
  1501. });
  1502. }
  1503. for (let i = 0; i < node.attributes.length - 1; i++) {
  1504. const current = node.attributes[i];
  1505. const next = node.attributes[i + 1];
  1506. if (current.loc.end.line !== next.loc.start.line - 1) {
  1507. context.report({
  1508. message: "Each JSX attribute should be exactly on the next line (no empty lines)",
  1509. fix(fixer) {
  1510. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  1511. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1512. const indent = baseIndent + " ";
  1513. return fixer.replaceTextRange(
  1514. [
  1515. current.range[1],
  1516. next.range[0]
  1517. ],
  1518. "\n" + indent
  1519. );
  1520. },
  1521. node: next
  1522. });
  1523. }
  1524. }
  1525. const lastAttr = node.attributes[node.attributes.length - 1];
  1526. const allTokens = [];
  1527. let currentToken = sourceCode.getTokenAfter(lastAttr);
  1528. while (currentToken && currentToken.range[1] <= node.range[1]) {
  1529. allTokens.push(currentToken);
  1530. currentToken = sourceCode.getTokenAfter(currentToken);
  1531. }
  1532. const closingBracket = allTokens[allTokens.length - 1];
  1533. let hasSelfClosingSlash = false;
  1534. let slashToken = null;
  1535. if (node.selfClosing && allTokens.length >= 2) {
  1536. slashToken = allTokens[allTokens.length - 2];
  1537. if (slashToken && slashToken.value === "/") {
  1538. hasSelfClosingSlash = true;
  1539. }
  1540. }
  1541. if (hasSelfClosingSlash) {
  1542. if (lastAttr.loc.end.line !== slashToken.loc.start.line - 1) {
  1543. context.report({
  1544. fix(fixer) {
  1545. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  1546. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1547. return fixer.replaceTextRange(
  1548. [
  1549. lastAttr.range[1],
  1550. slashToken.range[0]
  1551. ],
  1552. "\n" + baseIndent
  1553. );
  1554. },
  1555. message: "JSX closing slash should be exactly on the next line",
  1556. node: slashToken
  1557. });
  1558. }
  1559. } else {
  1560. if (lastAttr.loc.end.line !== closingBracket.loc.start.line - 1) {
  1561. context.report({
  1562. fix(fixer) {
  1563. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  1564. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1565. return fixer.replaceTextRange(
  1566. [
  1567. lastAttr.range[1],
  1568. closingBracket.range[0]
  1569. ],
  1570. "\n" + baseIndent
  1571. );
  1572. },
  1573. message: "JSX closing bracket should be exactly on the next line",
  1574. node: closingBracket
  1575. });
  1576. }
  1577. }
  1578. }
  1579. };
  1580. },
  1581. meta: {
  1582. fixable: "whitespace",
  1583. type: "layout",
  1584. schema: []
  1585. }
  1586. },
  1587. "multiline-object-properties": {
  1588. create(context) {
  1589. const sourceCode = context.getSourceCode();
  1590. function checkProperties(node, properties) {
  1591. if (properties.length === 0) return;
  1592. const openBrace = sourceCode.getFirstToken(node);
  1593. let closingBraceToken = sourceCode.getTokenAfter(properties[properties.length - 1]);
  1594. while (closingBraceToken && closingBraceToken.value !== "}") {
  1595. closingBraceToken = sourceCode.getTokenAfter(closingBraceToken);
  1596. }
  1597. const closingBrace = closingBraceToken;
  1598. if (openBrace.loc.start.line === closingBrace.loc.end.line) return;
  1599. const firstProperty = properties[0];
  1600. if (openBrace.loc.end.line !== firstProperty.loc.start.line - 1) {
  1601. context.report({
  1602. fix(fixer) {
  1603. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  1604. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1605. const indent = baseIndent + " ";
  1606. return fixer.replaceTextRange(
  1607. [
  1608. openBrace.range[1],
  1609. firstProperty.range[0]
  1610. ],
  1611. "\n" + indent
  1612. );
  1613. },
  1614. message: "First property should be exactly on the next line",
  1615. node: firstProperty
  1616. });
  1617. }
  1618. if (properties.length > 1) {
  1619. for (let i = 0; i < properties.length - 1; i++) {
  1620. const current = properties[i];
  1621. const next = properties[i + 1];
  1622. if (current.loc.end.line !== next.loc.start.line - 1) {
  1623. context.report({
  1624. message: "Each property should be exactly on the next line (no empty lines)",
  1625. fix(fixer) {
  1626. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  1627. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1628. const indent = baseIndent + " ";
  1629. const comma = sourceCode.getTokenBefore(next);
  1630. return fixer.replaceTextRange(
  1631. [
  1632. comma.range[1],
  1633. next.range[0]
  1634. ],
  1635. "\n" + indent
  1636. );
  1637. },
  1638. node: next
  1639. });
  1640. }
  1641. }
  1642. }
  1643. const lastProperty = properties[properties.length - 1];
  1644. if (lastProperty.loc.end.line !== closingBrace.loc.start.line - 1) {
  1645. context.report({
  1646. fix(fixer) {
  1647. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  1648. const baseIndent = openBraceLine.match(/^\s*/)[0];
  1649. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  1650. const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
  1651. const startRange = hasTrailingComma ? tokenAfter.range[1] : lastProperty.range[1];
  1652. return fixer.replaceTextRange(
  1653. [
  1654. startRange,
  1655. closingBrace.range[0]
  1656. ],
  1657. "\n" + baseIndent
  1658. );
  1659. },
  1660. message: "Object closing brace should be exactly on the next line",
  1661. node: closingBrace
  1662. });
  1663. }
  1664. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  1665. if (tokenAfter && tokenAfter.value === ",") {
  1666. context.report({
  1667. message: "No trailing comma in objects",
  1668. fix(fixer) {
  1669. return fixer.remove(tokenAfter);
  1670. },
  1671. node: lastProperty
  1672. });
  1673. }
  1674. }
  1675. return {
  1676. ObjectExpression(node) {
  1677. checkProperties(node, node.properties);
  1678. },
  1679. ObjectPattern(node) {
  1680. checkProperties(node, node.properties);
  1681. },
  1682. TSTypeLiteral(node) {
  1683. checkProperties(node, node.members);
  1684. }
  1685. };
  1686. },
  1687. meta: {
  1688. fixable: "whitespace",
  1689. type: "layout",
  1690. schema: []
  1691. }
  1692. },
  1693. "jsx-no-blank-lines": {
  1694. create(context) {
  1695. return {
  1696. JSXText(node) {
  1697. const text = node.value;
  1698. if (!/^\s*$/.test(text)) {
  1699. return;
  1700. }
  1701. const newlines = text.match(/\n/g);
  1702. if (newlines && newlines.length > 1) {
  1703. context.report({
  1704. message: "No blank lines allowed between JSX elements",
  1705. fix(fixer) {
  1706. const match = text.match(/\n[ \t]*$/);
  1707. if (match) {
  1708. return fixer.replaceText(node, match[0]);
  1709. }
  1710. },
  1711. node
  1712. });
  1713. }
  1714. }
  1715. };
  1716. },
  1717. meta: {
  1718. fixable: "whitespace",
  1719. type: "layout",
  1720. schema: []
  1721. }
  1722. }
  1723. };