index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. module.exports = {
  2. "multiline-import-specifiers": {
  3. meta: {
  4. type: "layout",
  5. fixable: "whitespace",
  6. schema: []
  7. },
  8. create(context) {
  9. return {
  10. ImportDeclaration(node) {
  11. const specifiers = node.specifiers.filter(
  12. s => s.type === "ImportSpecifier"
  13. );
  14. if (specifiers.length <= 1) return;
  15. const sourceCode = context.getSourceCode();
  16. for (let i = 0; i < specifiers.length - 1; i++) {
  17. const current = specifiers[i];
  18. const next = specifiers[i + 1];
  19. if (current.loc.end.line === next.loc.start.line) {
  20. context.report({
  21. node: next,
  22. message: "Each import specifier should be on a new line",
  23. fix(fixer) {
  24. const comma = sourceCode.getTokenBefore(next);
  25. return fixer.replaceTextRange(
  26. [
  27. comma.range[1],
  28. next.range[0]
  29. ],
  30. "\n "
  31. );
  32. }
  33. });
  34. }
  35. }
  36. const lastSpecifier = specifiers[specifiers.length - 1];
  37. const tokenAfter = sourceCode.getTokenAfter(lastSpecifier);
  38. if (tokenAfter && tokenAfter.value === ",") {
  39. context.report({
  40. node: lastSpecifier,
  41. message: "No trailing comma in imports",
  42. fix(fixer) {
  43. return fixer.remove(tokenAfter);
  44. }
  45. });
  46. }
  47. }
  48. };
  49. }
  50. },
  51. "multiline-object-properties": {
  52. meta: {
  53. type: "layout",
  54. fixable: "whitespace",
  55. schema: []
  56. },
  57. create(context) {
  58. const sourceCode = context.getSourceCode();
  59. function checkProperties(node, properties) {
  60. if (properties.length <= 1) return;
  61. for (let i = 0; i < properties.length - 1; i++) {
  62. const current = properties[i];
  63. const next = properties[i + 1];
  64. if (current.loc.end.line === next.loc.start.line) {
  65. context.report({
  66. node: next,
  67. message: "Each property should be on a new line",
  68. fix(fixer) {
  69. const openBrace = sourceCode.getFirstToken(node);
  70. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  71. const baseIndent = openBraceLine.match(/^\s*/)[0];
  72. const indent = baseIndent + " ";
  73. const comma = sourceCode.getTokenBefore(next);
  74. return fixer.replaceTextRange(
  75. [
  76. comma.range[1],
  77. next.range[0]
  78. ],
  79. "\n" + indent
  80. );
  81. }
  82. });
  83. }
  84. }
  85. const lastProperty = properties[properties.length - 1];
  86. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  87. if (tokenAfter && tokenAfter.value === ",") {
  88. context.report({
  89. node: lastProperty,
  90. message: "No trailing comma in objects",
  91. fix(fixer) {
  92. return fixer.remove(tokenAfter);
  93. }
  94. });
  95. }
  96. }
  97. return {
  98. ObjectExpression(node) {
  99. checkProperties(node, node.properties);
  100. },
  101. ObjectPattern(node) {
  102. checkProperties(node, node.properties);
  103. }
  104. };
  105. }
  106. },
  107. "multiline-jsx-attributes": {
  108. meta: {
  109. type: "layout",
  110. fixable: "whitespace",
  111. schema: []
  112. },
  113. create(context) {
  114. const sourceCode = context.getSourceCode();
  115. return {
  116. JSXOpeningElement(node) {
  117. if (node.attributes.length <= 1) return;
  118. const firstToken = sourceCode.getFirstToken(node);
  119. const tagNameToken = sourceCode.getTokenAfter(firstToken);
  120. const firstAttr = node.attributes[0];
  121. if (tagNameToken.loc.end.line === firstAttr.loc.start.line) {
  122. context.report({
  123. node: firstAttr,
  124. message: "First JSX attribute should be on a new line",
  125. fix(fixer) {
  126. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  127. const baseIndent = openBraceLine.match(/^\s*/)[0];
  128. const indent = baseIndent + " ";
  129. return fixer.replaceTextRange(
  130. [
  131. tagNameToken.range[1],
  132. firstAttr.range[0]
  133. ],
  134. "\n" + indent
  135. );
  136. }
  137. });
  138. }
  139. for (let i = 0; i < node.attributes.length - 1; i++) {
  140. const current = node.attributes[i];
  141. const next = node.attributes[i + 1];
  142. if (current.loc.end.line === next.loc.start.line) {
  143. context.report({
  144. node: next,
  145. message: "Each JSX attribute should be on a new line",
  146. fix(fixer) {
  147. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  148. const baseIndent = openBraceLine.match(/^\s*/)[0];
  149. const indent = baseIndent + " ";
  150. return fixer.replaceTextRange(
  151. [
  152. current.range[1],
  153. next.range[0]
  154. ],
  155. "\n" + indent
  156. );
  157. }
  158. });
  159. }
  160. }
  161. const lastAttr = node.attributes[node.attributes.length - 1];
  162. const allTokens = [];
  163. let currentToken = sourceCode.getTokenAfter(lastAttr);
  164. while (currentToken && currentToken.range[1] <= node.range[1]) {
  165. allTokens.push(currentToken);
  166. currentToken = sourceCode.getTokenAfter(currentToken);
  167. }
  168. const closingBracket = allTokens[allTokens.length - 1];
  169. if (lastAttr.loc.end.line === closingBracket.loc.start.line) {
  170. context.report({
  171. node: closingBracket,
  172. message: "JSX closing bracket should be on a new line",
  173. fix(fixer) {
  174. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  175. const baseIndent = openBraceLine.match(/^\s*/)[0];
  176. if (node.selfClosing && allTokens.length >= 2) {
  177. const slashToken = allTokens[allTokens.length - 2];
  178. if (slashToken && slashToken.value === "/") {
  179. return fixer.replaceTextRange(
  180. [
  181. lastAttr.range[1],
  182. slashToken.range[0]
  183. ],
  184. "\n" + baseIndent
  185. );
  186. }
  187. }
  188. return fixer.replaceTextRange(
  189. [
  190. lastAttr.range[1],
  191. closingBracket.range[0]
  192. ],
  193. "\n" + baseIndent
  194. );
  195. }
  196. });
  197. }
  198. }
  199. };
  200. }
  201. },
  202. "multiline-array-elements": {
  203. meta: {
  204. type: "layout",
  205. fixable: "whitespace",
  206. schema: []
  207. },
  208. create(context) {
  209. const sourceCode = context.getSourceCode();
  210. return {
  211. ArrayExpression(node) {
  212. if (node.elements.length <= 1) return;
  213. const openBracket = sourceCode.getFirstToken(node);
  214. const firstElement = node.elements[0];
  215. if (openBracket.loc.end.line === firstElement.loc.start.line) {
  216. context.report({
  217. node: firstElement,
  218. message: "First array element should be on a new line",
  219. fix(fixer) {
  220. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  221. const baseIndent = openBracketLine.match(/^\s*/)[0];
  222. const indent = baseIndent + " ";
  223. return fixer.replaceTextRange(
  224. [
  225. openBracket.range[1],
  226. firstElement.range[0]
  227. ],
  228. "\n" + indent
  229. );
  230. }
  231. });
  232. }
  233. for (let i = 0; i < node.elements.length - 1; i++) {
  234. const current = node.elements[i];
  235. const next = node.elements[i + 1];
  236. if (!current || !next) continue;
  237. if (current.loc.end.line === next.loc.start.line) {
  238. context.report({
  239. node: next,
  240. message: "Each array element should be on a new line",
  241. fix(fixer) {
  242. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  243. const baseIndent = openBracketLine.match(/^\s*/)[0];
  244. const indent = baseIndent + " ";
  245. const comma = sourceCode.getTokenBefore(next);
  246. return fixer.replaceTextRange(
  247. [
  248. comma.range[1],
  249. next.range[0]
  250. ],
  251. "\n" + indent
  252. );
  253. }
  254. });
  255. }
  256. }
  257. const lastElement = node.elements[node.elements.length - 1];
  258. const closingBracket = sourceCode.getLastToken(node);
  259. if (lastElement && lastElement.loc.end.line === closingBracket.loc.start.line) {
  260. context.report({
  261. node: closingBracket,
  262. message: "Array closing bracket should be on a new line",
  263. fix(fixer) {
  264. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  265. const baseIndent = openBracketLine.match(/^\s*/)[0];
  266. return fixer.replaceTextRange(
  267. [
  268. lastElement.range[1],
  269. closingBracket.range[0]
  270. ],
  271. "\n" + baseIndent
  272. );
  273. }
  274. });
  275. }
  276. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  277. if (tokenAfter && tokenAfter.value === "," && tokenAfter.range[1] <= closingBracket.range[0]) {
  278. context.report({
  279. node: lastElement,
  280. message: "No trailing comma in arrays",
  281. fix(fixer) {
  282. return fixer.remove(tokenAfter);
  283. }
  284. });
  285. }
  286. }
  287. };
  288. }
  289. },
  290. "custom-import-order": {
  291. meta: {
  292. type: "suggestion",
  293. fixable: "code",
  294. schema: []
  295. },
  296. create(context) {
  297. const sourceCode = context.getSourceCode();
  298. const imports = [];
  299. return {
  300. ImportDeclaration(node) {
  301. imports.push(node);
  302. },
  303. "Program:exit"() {
  304. if (imports.length <= 1) return;
  305. const categorized = categorizeImports(imports, sourceCode);
  306. for (let i = 0; i < imports.length - 1; i++) {
  307. const current = imports[i];
  308. const next = imports[i + 1];
  309. const currentCategory = getImportCategory(current, sourceCode);
  310. const nextCategory = getImportCategory(next, sourceCode);
  311. if (currentCategory > nextCategory) {
  312. context.report({
  313. node: next,
  314. message: `Import from "${next.source.value}" should come before "${current.source.value}"`,
  315. fix(fixer) {
  316. return fixImportOrder(fixer, imports, categorized, sourceCode);
  317. }
  318. });
  319. break;
  320. }
  321. }
  322. }
  323. };
  324. function getImportCategory(node, sourceCode) {
  325. const source = node.source.value;
  326. const code = sourceCode.getText(node);
  327. if (source === "react") return 1;
  328. if (source === "react-native") return 2;
  329. if (source.startsWith("./")) {
  330. if (code.includes("import type") || source.includes(".types")) {
  331. return 3.1;
  332. }
  333. if (source.includes("style") || source.includes("Style")) {
  334. return 3.2;
  335. }
  336. return 3.3;
  337. }
  338. const specifiers = node.specifiers
  339. .filter(s => s.type === "ImportSpecifier")
  340. .map(s => s.imported ? s.imported.name : "");
  341. const hasNCoreHook = specifiers.some(name => name.startsWith("NCore"));
  342. const hasUseHook = specifiers.some(name => name.startsWith("use"));
  343. if (hasNCoreHook) return 4.1;
  344. if (hasUseHook) return 4.2;
  345. if (code.includes("import type") || source.includes("/types/") || source.includes("/type") || source.includes(".types")) {
  346. return 5;
  347. }
  348. if (source.startsWith("@/") || source.startsWith("~/")) return 6.3;
  349. if (!source.startsWith(".")) return 6.1;
  350. if (source.startsWith("../")) return 6.2;
  351. return 6.4;
  352. }
  353. function categorizeImports(imports, sourceCode) {
  354. return imports.map(imp => ({
  355. node: imp,
  356. category: getImportCategory(imp, sourceCode),
  357. source: imp.source.value
  358. })).sort((a, b) => {
  359. if (a.category !== b.category) {
  360. return a.category - b.category;
  361. }
  362. return a.source.localeCompare(b.source);
  363. });
  364. }
  365. function fixImportOrder(fixer, imports, categorized, sourceCode) {
  366. const firstImport = imports[0];
  367. const lastImport = imports[imports.length - 1];
  368. const rangeStart = firstImport.range[0];
  369. const rangeEnd = lastImport.range[1];
  370. let newImports = "";
  371. categorized.forEach((imp, index) => {
  372. newImports += sourceCode.getText(imp.node);
  373. if (index < categorized.length - 1) {
  374. newImports += "\n";
  375. }
  376. });
  377. return fixer.replaceTextRange([
  378. rangeStart,
  379. rangeEnd
  380. ], newImports);
  381. }
  382. }
  383. }
  384. };