index.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  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 === 0) return;
  61. const openBrace = sourceCode.getFirstToken(node);
  62. let closingBraceToken = sourceCode.getTokenAfter(properties[properties.length - 1]);
  63. while (closingBraceToken && closingBraceToken.value !== "}") {
  64. closingBraceToken = sourceCode.getTokenAfter(closingBraceToken);
  65. }
  66. const closingBrace = closingBraceToken;
  67. if (openBrace.loc.start.line === closingBrace.loc.end.line) return;
  68. const firstProperty = properties[0];
  69. if (openBrace.loc.end.line !== firstProperty.loc.start.line - 1) {
  70. context.report({
  71. node: firstProperty,
  72. message: "First property should be exactly on the next line",
  73. fix(fixer) {
  74. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  75. const baseIndent = openBraceLine.match(/^\s*/)[0];
  76. const indent = baseIndent + " ";
  77. return fixer.replaceTextRange(
  78. [
  79. openBrace.range[1],
  80. firstProperty.range[0]
  81. ],
  82. "\n" + indent
  83. );
  84. }
  85. });
  86. }
  87. if (properties.length > 1) {
  88. for (let i = 0; i < properties.length - 1; i++) {
  89. const current = properties[i];
  90. const next = properties[i + 1];
  91. if (current.loc.end.line !== next.loc.start.line - 1) {
  92. context.report({
  93. node: next,
  94. message: "Each property should be exactly on the next line (no empty lines)",
  95. fix(fixer) {
  96. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  97. const baseIndent = openBraceLine.match(/^\s*/)[0];
  98. const indent = baseIndent + " ";
  99. const comma = sourceCode.getTokenBefore(next);
  100. return fixer.replaceTextRange(
  101. [
  102. comma.range[1],
  103. next.range[0]
  104. ],
  105. "\n" + indent
  106. );
  107. }
  108. });
  109. }
  110. }
  111. }
  112. const lastProperty = properties[properties.length - 1];
  113. if (lastProperty.loc.end.line !== closingBrace.loc.start.line - 1) {
  114. context.report({
  115. node: closingBrace,
  116. message: "Object closing brace should be exactly on the next line",
  117. fix(fixer) {
  118. const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
  119. const baseIndent = openBraceLine.match(/^\s*/)[0];
  120. // Be careful about trailing comma
  121. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  122. const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
  123. const startRange = hasTrailingComma ? tokenAfter.range[1] : lastProperty.range[1];
  124. return fixer.replaceTextRange(
  125. [
  126. startRange,
  127. closingBrace.range[0]
  128. ],
  129. "\n" + baseIndent
  130. );
  131. }
  132. });
  133. }
  134. const tokenAfter = sourceCode.getTokenAfter(lastProperty);
  135. if (tokenAfter && tokenAfter.value === ",") {
  136. context.report({
  137. node: lastProperty,
  138. message: "No trailing comma in objects",
  139. fix(fixer) {
  140. return fixer.remove(tokenAfter);
  141. }
  142. });
  143. }
  144. }
  145. return {
  146. ObjectExpression(node) {
  147. checkProperties(node, node.properties);
  148. },
  149. ObjectPattern(node) {
  150. checkProperties(node, node.properties);
  151. }
  152. };
  153. }
  154. },
  155. "multiline-jsx-attributes": {
  156. meta: {
  157. type: "layout",
  158. fixable: "whitespace",
  159. schema: []
  160. },
  161. create(context) {
  162. const sourceCode = context.getSourceCode();
  163. return {
  164. JSXOpeningElement(node) {
  165. if (node.attributes.length === 0) return;
  166. const firstToken = sourceCode.getFirstToken(node);
  167. const tagNameToken = sourceCode.getTokenAfter(firstToken);
  168. const firstAttr = node.attributes[0];
  169. if (tagNameToken.loc.end.line !== firstAttr.loc.start.line - 1) {
  170. context.report({
  171. node: firstAttr,
  172. message: "First JSX attribute should be exactly on the next line",
  173. fix(fixer) {
  174. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  175. const baseIndent = openBraceLine.match(/^\s*/)[0];
  176. const indent = baseIndent + " ";
  177. return fixer.replaceTextRange(
  178. [
  179. tagNameToken.range[1],
  180. firstAttr.range[0]
  181. ],
  182. "\n" + indent
  183. );
  184. }
  185. });
  186. }
  187. for (let i = 0; i < node.attributes.length - 1; i++) {
  188. const current = node.attributes[i];
  189. const next = node.attributes[i + 1];
  190. if (current.loc.end.line !== next.loc.start.line - 1) {
  191. context.report({
  192. node: next,
  193. message: "Each JSX attribute should be exactly on the next line (no empty lines)",
  194. fix(fixer) {
  195. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  196. const baseIndent = openBraceLine.match(/^\s*/)[0];
  197. const indent = baseIndent + " ";
  198. return fixer.replaceTextRange(
  199. [
  200. current.range[1],
  201. next.range[0]
  202. ],
  203. "\n" + indent
  204. );
  205. }
  206. });
  207. }
  208. }
  209. const lastAttr = node.attributes[node.attributes.length - 1];
  210. const allTokens = [];
  211. let currentToken = sourceCode.getTokenAfter(lastAttr);
  212. while (currentToken && currentToken.range[1] <= node.range[1]) {
  213. allTokens.push(currentToken);
  214. currentToken = sourceCode.getTokenAfter(currentToken);
  215. }
  216. const closingBracket = allTokens[allTokens.length - 1];
  217. // Check if self-closing slash is present
  218. let hasSelfClosingSlash = false;
  219. let slashToken = null;
  220. if (node.selfClosing && allTokens.length >= 2) {
  221. slashToken = allTokens[allTokens.length - 2];
  222. if (slashToken && slashToken.value === "/") {
  223. hasSelfClosingSlash = true;
  224. }
  225. }
  226. if (hasSelfClosingSlash) {
  227. if (lastAttr.loc.end.line !== slashToken.loc.start.line - 1) {
  228. context.report({
  229. node: slashToken,
  230. message: "JSX closing slash should be exactly on the next line",
  231. fix(fixer) {
  232. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  233. const baseIndent = openBraceLine.match(/^\s*/)[0];
  234. return fixer.replaceTextRange(
  235. [
  236. lastAttr.range[1],
  237. slashToken.range[0]
  238. ],
  239. "\n" + baseIndent
  240. );
  241. }
  242. });
  243. }
  244. } else {
  245. if (lastAttr.loc.end.line !== closingBracket.loc.start.line - 1) {
  246. context.report({
  247. node: closingBracket,
  248. message: "JSX closing bracket should be exactly on the next line",
  249. fix(fixer) {
  250. const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
  251. const baseIndent = openBraceLine.match(/^\s*/)[0];
  252. return fixer.replaceTextRange(
  253. [
  254. lastAttr.range[1],
  255. closingBracket.range[0]
  256. ],
  257. "\n" + baseIndent
  258. );
  259. }
  260. });
  261. }
  262. }
  263. }
  264. };
  265. }
  266. },
  267. "multiline-array-elements": {
  268. meta: {
  269. type: "layout",
  270. fixable: "whitespace",
  271. schema: []
  272. },
  273. create(context) {
  274. const sourceCode = context.getSourceCode();
  275. return {
  276. ArrayExpression(node) {
  277. if (node.elements.length === 0) return;
  278. const openBracket = sourceCode.getFirstToken(node);
  279. const closingBracket = sourceCode.getLastToken(node);
  280. if (openBracket.loc.start.line === closingBracket.loc.end.line) return;
  281. const firstElement = node.elements[0];
  282. if (openBracket.loc.end.line !== firstElement.loc.start.line - 1) {
  283. context.report({
  284. node: firstElement,
  285. message: "First array element should be exactly on the next line",
  286. fix(fixer) {
  287. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  288. const baseIndent = openBracketLine.match(/^\s*/)[0];
  289. const indent = baseIndent + " ";
  290. return fixer.replaceTextRange(
  291. [
  292. openBracket.range[1],
  293. firstElement.range[0]
  294. ],
  295. "\n" + indent
  296. );
  297. }
  298. });
  299. }
  300. for (let i = 0; i < node.elements.length - 1; i++) {
  301. const current = node.elements[i];
  302. const next = node.elements[i + 1];
  303. if (!current || !next) continue;
  304. if (current.loc.end.line !== next.loc.start.line - 1) {
  305. context.report({
  306. node: next,
  307. message: "Each array element should be exactly on the next line (no empty lines)",
  308. fix(fixer) {
  309. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  310. const baseIndent = openBracketLine.match(/^\s*/)[0];
  311. const indent = baseIndent + " ";
  312. const comma = sourceCode.getTokenBefore(next);
  313. return fixer.replaceTextRange(
  314. [
  315. comma.range[1],
  316. next.range[0]
  317. ],
  318. "\n" + indent
  319. );
  320. }
  321. });
  322. }
  323. }
  324. const lastElement = node.elements[node.elements.length - 1];
  325. if (lastElement && lastElement.loc.end.line !== closingBracket.loc.start.line - 1) {
  326. context.report({
  327. node: closingBracket,
  328. message: "Array closing bracket should be exactly on the next line",
  329. fix(fixer) {
  330. const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
  331. const baseIndent = openBracketLine.match(/^\s*/)[0];
  332. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  333. const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
  334. const startRange = hasTrailingComma ? tokenAfter.range[1] : lastElement.range[1];
  335. return fixer.replaceTextRange(
  336. [
  337. startRange,
  338. closingBracket.range[0]
  339. ],
  340. "\n" + baseIndent
  341. );
  342. }
  343. });
  344. }
  345. const tokenAfter = sourceCode.getTokenAfter(lastElement);
  346. if (tokenAfter && tokenAfter.value === "," && tokenAfter.range[1] <= closingBracket.range[0]) {
  347. context.report({
  348. node: lastElement,
  349. message: "No trailing comma in arrays",
  350. fix(fixer) {
  351. return fixer.remove(tokenAfter);
  352. }
  353. });
  354. }
  355. }
  356. };
  357. }
  358. },
  359. "custom-import-order": {
  360. meta: {
  361. type: "suggestion",
  362. fixable: "code",
  363. schema: []
  364. },
  365. create(context) {
  366. const sourceCode = context.getSourceCode();
  367. const imports = [];
  368. return {
  369. ImportDeclaration(node) {
  370. imports.push(node);
  371. },
  372. "Program:exit"() {
  373. if (imports.length <= 1) return;
  374. const categorized = categorizeImports(imports, sourceCode);
  375. for (let i = 0; i < imports.length - 1; i++) {
  376. const current = imports[i];
  377. const next = imports[i + 1];
  378. const currentCategory = getImportCategory(current, sourceCode);
  379. const nextCategory = getImportCategory(next, sourceCode);
  380. if (currentCategory > nextCategory) {
  381. context.report({
  382. node: next,
  383. message: `Import from "${next.source.value}" should come before "${current.source.value}"`,
  384. fix(fixer) {
  385. return fixImportOrder(fixer, imports, categorized, sourceCode);
  386. }
  387. });
  388. break;
  389. }
  390. }
  391. }
  392. };
  393. function getImportCategory(node, sourceCode) {
  394. const source = node.source.value;
  395. const code = sourceCode.getText(node);
  396. if (source === "react") return 1;
  397. if (source === "react-native") return 2;
  398. if (source.startsWith("./")) {
  399. if (code.includes("import type") || source.includes(".types") || source.includes("/type")) {
  400. return 3.1;
  401. }
  402. if (source.includes("style") || source.includes("Style")) {
  403. return 3.2;
  404. }
  405. return 3.3;
  406. }
  407. const specifiers = node.specifiers
  408. .filter(s => s.type === "ImportSpecifier")
  409. .map(s => s.imported ? s.imported.name : "");
  410. const hasNCoreHook = specifiers.some(name => name.startsWith("NCore"));
  411. const hasUseHook = specifiers.some(name => name.startsWith("use"));
  412. if (hasNCoreHook) return 4.1;
  413. if (hasUseHook) return 4.2;
  414. if (code.includes("import type") || source.includes("/types/") || source.includes("/type") || source.includes(".types")) {
  415. return 5;
  416. }
  417. if (source.startsWith("@/") || source.startsWith("~/")) return 6.3;
  418. if (!source.startsWith(".")) return 6.1;
  419. if (source.startsWith("../")) return 6.2;
  420. return 6.4;
  421. }
  422. function categorizeImports(imports, sourceCode) {
  423. return imports.map(imp => ({
  424. node: imp,
  425. category: getImportCategory(imp, sourceCode),
  426. source: imp.source.value
  427. })).sort((a, b) => {
  428. if (a.category !== b.category) {
  429. return a.category - b.category;
  430. }
  431. return a.source.localeCompare(b.source);
  432. });
  433. }
  434. function fixImportOrder(fixer, imports, categorized, sourceCode) {
  435. const firstImport = imports[0];
  436. const lastImport = imports[imports.length - 1];
  437. const rangeStart = firstImport.range[0];
  438. const rangeEnd = lastImport.range[1];
  439. let newImports = "";
  440. categorized.forEach((imp, index) => {
  441. newImports += sourceCode.getText(imp.node);
  442. if (index < categorized.length - 1) {
  443. newImports += "\n";
  444. }
  445. });
  446. return fixer.replaceTextRange([
  447. rangeStart,
  448. rangeEnd
  449. ], newImports);
  450. }
  451. }
  452. },
  453. "multiline-jsx-children": {
  454. meta: {
  455. type: "layout",
  456. fixable: "whitespace",
  457. schema: []
  458. },
  459. create(context) {
  460. return {
  461. JSXElement(node) {
  462. if (!node.closingElement) return;
  463. if (node.children.length === 0) return;
  464. const meaningfulChildren = node.children.filter(child => {
  465. if (child.type === "JSXText" && child.value.trim() === "") return false;
  466. return true;
  467. });
  468. if (meaningfulChildren.length === 0) return;
  469. const openingElement = node.openingElement;
  470. const closingElement = node.closingElement;
  471. const isMultilineOpening = openingElement.loc.start.line !== openingElement.loc.end.line;
  472. if (!isMultilineOpening) return;
  473. const sourceCode = context.getSourceCode();
  474. if (meaningfulChildren.length === 1 && meaningfulChildren[0].type === "JSXText") {
  475. const child = meaningfulChildren[0];
  476. const text = sourceCode.getText(child);
  477. const hasLeadingNewline = /^\s*\n/.test(text);
  478. const hasTrailingNewline = /\n\s*$/.test(text);
  479. if (!hasLeadingNewline || !hasTrailingNewline) {
  480. context.report({
  481. node: child,
  482. message: "Multiline JSX element text content must be on its own line",
  483. fix(fixer) {
  484. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  485. const baseIndent = openBracketLine.match(/^\s*/)[0];
  486. const indent = baseIndent + " ";
  487. return fixer.replaceText(child, "\n" + indent + text.trim() + "\n" + baseIndent);
  488. }
  489. });
  490. }
  491. return;
  492. }
  493. const firstChild = meaningfulChildren[0];
  494. const openingEndLine = openingElement.loc.end.line;
  495. let needsFirstChildFix = false;
  496. if (firstChild.type === "JSXText") {
  497. const text = sourceCode.getText(firstChild);
  498. if (!/^\s*\n/.test(text)) needsFirstChildFix = true;
  499. } else if (firstChild.loc.start.line === openingEndLine) {
  500. needsFirstChildFix = true;
  501. }
  502. if (needsFirstChildFix) {
  503. context.report({
  504. node: firstChild,
  505. message: "First child of a multiline JSX element must be on a new line",
  506. fix(fixer) {
  507. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  508. const baseIndent = openBracketLine.match(/^\s*/)[0];
  509. const indent = baseIndent + " ";
  510. if (firstChild.type === "JSXText") {
  511. const text = sourceCode.getText(firstChild);
  512. return fixer.replaceText(firstChild, "\n" + indent + text.replace(/^\s+/, ""));
  513. }
  514. return fixer.insertTextBefore(firstChild, "\n" + indent);
  515. }
  516. });
  517. }
  518. const lastChild = meaningfulChildren[meaningfulChildren.length - 1];
  519. let needsLastChildFix = false;
  520. if (lastChild.type === "JSXText") {
  521. const text = sourceCode.getText(lastChild);
  522. if (!/\n\s*$/.test(text)) needsLastChildFix = true;
  523. } else if (lastChild.loc.end.line === closingElement.loc.start.line) {
  524. needsLastChildFix = true;
  525. }
  526. if (needsLastChildFix) {
  527. context.report({
  528. node: closingElement,
  529. message: "Closing tag of a multiline JSX element must be on a new line",
  530. fix(fixer) {
  531. const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
  532. const baseIndent = openBracketLine.match(/^\s*/)[0];
  533. if (lastChild.type === "JSXText") {
  534. const text = sourceCode.getText(lastChild);
  535. return fixer.replaceText(lastChild, text.replace(/\s+$/, "") + "\n" + baseIndent);
  536. }
  537. return fixer.insertTextBefore(closingElement, "\n" + baseIndent);
  538. }
  539. });
  540. }
  541. }
  542. };
  543. }
  544. },
  545. "no-jsx-parens-in-return": {
  546. meta: {
  547. type: "layout",
  548. fixable: "code",
  549. schema: []
  550. },
  551. create(context) {
  552. return {
  553. ReturnStatement(node) {
  554. if (!node.argument) return;
  555. if (node.argument.type === "JSXElement" || node.argument.type === "JSXFragment") {
  556. const sourceCode = context.getSourceCode();
  557. const returnToken = sourceCode.getFirstToken(node);
  558. const firstChildToken = sourceCode.getFirstToken(node.argument);
  559. const tokensBetween = sourceCode.getTokensBetween(returnToken, firstChildToken);
  560. const openParen = tokensBetween.find(t => t.value === "(");
  561. if (openParen) {
  562. const closeParen = sourceCode.getTokenAfter(node.argument);
  563. if (closeParen && closeParen.value === ")") {
  564. context.report({
  565. node: node.argument,
  566. message: "Returned JSX element should not be wrapped in parentheses",
  567. fix(fixer) {
  568. const fixes = [
  569. fixer.remove(closeParen)
  570. ];
  571. if (returnToken.loc.end.line !== firstChildToken.loc.start.line) {
  572. fixes.push(fixer.replaceTextRange([
  573. returnToken.range[1],
  574. firstChildToken.range[0]
  575. ], " "));
  576. } else {
  577. fixes.push(fixer.remove(openParen));
  578. }
  579. return fixes;
  580. }
  581. });
  582. }
  583. } else if (returnToken.loc.end.line !== firstChildToken.loc.start.line) {
  584. context.report({
  585. node: node.argument,
  586. message: "Returned JSX element must start on the same line as 'return' to avoid Automatic Semicolon Insertion (ASI)",
  587. fix(fixer) {
  588. return fixer.replaceTextRange([
  589. returnToken.range[1],
  590. firstChildToken.range[0]
  591. ], " ");
  592. }
  593. });
  594. }
  595. }
  596. },
  597. ArrowFunctionExpression(node) {
  598. if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") {
  599. const sourceCode = context.getSourceCode();
  600. const arrowToken = sourceCode.getTokenBefore(node.body, t => t.value === "=>");
  601. const firstChildToken = sourceCode.getFirstToken(node.body);
  602. const tokensBetween = sourceCode.getTokensBetween(arrowToken, firstChildToken);
  603. const openParen = tokensBetween.find(t => t.value === "(");
  604. if (openParen) {
  605. const closeParen = sourceCode.getTokenAfter(node.body);
  606. if (closeParen && closeParen.value === ")") {
  607. context.report({
  608. node: node.body,
  609. message: "Implicitly returned JSX element should not be wrapped in parentheses",
  610. fix(fixer) {
  611. const fixes = [
  612. fixer.remove(closeParen)
  613. ];
  614. if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) {
  615. fixes.push(fixer.replaceTextRange([
  616. arrowToken.range[1],
  617. firstChildToken.range[0]
  618. ], " "));
  619. } else {
  620. fixes.push(fixer.remove(openParen));
  621. }
  622. return fixes;
  623. }
  624. });
  625. }
  626. } else if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) {
  627. context.report({
  628. node: node.body,
  629. message: "Implicitly returned JSX element must start on the same line as '=>'",
  630. fix(fixer) {
  631. return fixer.replaceTextRange([
  632. arrowToken.range[1],
  633. firstChildToken.range[0]
  634. ], " ");
  635. }
  636. });
  637. }
  638. }
  639. }
  640. };
  641. }
  642. },
  643. "no-newline-after-jsx": {
  644. meta: {
  645. type: "layout",
  646. fixable: "whitespace",
  647. schema: []
  648. },
  649. create(context) {
  650. function checkPunctuation(node) {
  651. const sourceCode = context.getSourceCode();
  652. const lastToken = sourceCode.getLastToken(node);
  653. const nextToken = sourceCode.getTokenAfter(node);
  654. if (nextToken && (nextToken.value === ";" || nextToken.value === "," || nextToken.value === ")")) {
  655. if (lastToken.loc.end.line !== nextToken.loc.start.line) {
  656. if (sourceCode.commentsExistBetween(node, nextToken)) return;
  657. context.report({
  658. node: nextToken,
  659. message: `Punctuation '${nextToken.value}' should be on the same line as the preceding JSX element`,
  660. fix(fixer) {
  661. return fixer.replaceTextRange([
  662. node.range[1],
  663. nextToken.range[0]
  664. ], "");
  665. }
  666. });
  667. }
  668. }
  669. }
  670. return {
  671. JSXElement: checkPunctuation,
  672. JSXFragment: checkPunctuation
  673. };
  674. }
  675. },
  676. "no-static-inline-styles": {
  677. meta: {
  678. type: "suggestion",
  679. messages: {
  680. staticStyle: "Static inline styles are not allowed. Please move them to a stylesheet file."
  681. },
  682. schema: []
  683. },
  684. create(context) {
  685. function checkProperty(propertyNode) {
  686. if (!propertyNode || propertyNode.type !== "Property") return;
  687. const value = propertyNode.value;
  688. if (!value) return;
  689. let isStatic = false;
  690. if (value.type === "Literal") {
  691. if (typeof value.value === "string" || typeof value.value === "number") {
  692. isStatic = true;
  693. }
  694. } else if (value.type === "UnaryExpression" && value.operator === "-") {
  695. if (value.argument && value.argument.type === "Literal" && typeof value.argument.value === "number") {
  696. isStatic = true;
  697. }
  698. } else if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
  699. isStatic = true;
  700. }
  701. if (isStatic) {
  702. context.report({
  703. node: propertyNode,
  704. messageId: "staticStyle"
  705. });
  706. }
  707. }
  708. function checkObjectExpression(node) {
  709. if (node.type !== "ObjectExpression") return;
  710. node.properties.forEach(checkProperty);
  711. }
  712. function checkStyleValue(node) {
  713. if (!node) return;
  714. if (node.type === "ObjectExpression") {
  715. checkObjectExpression(node);
  716. } else if (node.type === "ArrayExpression") {
  717. node.elements.forEach(element => {
  718. if (element && element.type === "ObjectExpression") {
  719. checkObjectExpression(element);
  720. }
  721. });
  722. }
  723. }
  724. return {
  725. JSXAttribute(node) {
  726. const attrName = node.name.name;
  727. if (typeof attrName !== "string") return;
  728. if (attrName === "style" || attrName.endsWith("Style")) {
  729. if (node.value && node.value.type === "JSXExpressionContainer") {
  730. checkStyleValue(node.value.expression);
  731. }
  732. }
  733. }
  734. };
  735. }
  736. },
  737. "jsx-ternary-formatting": {
  738. meta: {
  739. type: "layout",
  740. fixable: "whitespace",
  741. schema: []
  742. },
  743. create(context) {
  744. return {
  745. JSXExpressionContainer(node) {
  746. if (node.expression.type !== "ConditionalExpression") return;
  747. const sourceCode = context.getSourceCode();
  748. const openBrace = sourceCode.getFirstToken(node);
  749. const closeBrace = sourceCode.getLastToken(node);
  750. const cond = node.expression;
  751. let hasJSX = false;
  752. if (
  753. cond.consequent.type === "JSXElement" ||
  754. cond.consequent.type === "JSXFragment" ||
  755. cond.alternate.type === "JSXElement" ||
  756. cond.alternate.type === "JSXFragment"
  757. ) {
  758. hasJSX = true;
  759. }
  760. if (!hasJSX) return;
  761. const checkAndRemoveParens = (nodeToUnparen) => {
  762. const currentToken = sourceCode.getTokenBefore(nodeToUnparen);
  763. const afterToken = sourceCode.getTokenAfter(nodeToUnparen);
  764. let removed = false;
  765. while (currentToken && currentToken.value === "(" && afterToken && afterToken.value === ")") {
  766. context.report({
  767. node: currentToken,
  768. message: "Parentheses are not allowed around JSX in ternaries.",
  769. fix(fixer) {
  770. return [
  771. fixer.remove(currentToken),
  772. fixer.remove(afterToken)
  773. ];
  774. }
  775. });
  776. removed = true;
  777. break;
  778. }
  779. return removed;
  780. };
  781. let hasParens = false;
  782. if (cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment") {
  783. if (checkAndRemoveParens(cond.consequent)) hasParens = true;
  784. }
  785. if (cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment") {
  786. if (checkAndRemoveParens(cond.alternate)) hasParens = true;
  787. }
  788. if (hasParens) return; // Wait for the next fix pass to format the whitespace properly
  789. const questionToken = sourceCode.getTokenAfter(cond.test, {
  790. filter: t => t.value === "?"
  791. });
  792. const colonToken = sourceCode.getTokenAfter(cond.consequent, {
  793. filter: t => t.value === ":"
  794. });
  795. const getIndent = (token) => {
  796. const line = sourceCode.lines[token.loc.start.line - 1];
  797. return line.match(/^\s*/)[0];
  798. };
  799. const openIndent = getIndent(openBrace);
  800. const baseIndent = openIndent + " ";
  801. const innerIndent = baseIndent + " ";
  802. const checkAndFix = (token1, token2, expectedWhitespace) => {
  803. let textBetween = sourceCode.getText().slice(token1.range[1], token2.range[0]);
  804. textBetween = textBetween.replace(/\r\n/g, "\n");
  805. if (textBetween !== expectedWhitespace) {
  806. context.report({
  807. node: token2,
  808. message: "Incorrect ternary formatting",
  809. fix(fixer) {
  810. return fixer.replaceTextRange([
  811. token1.range[1],
  812. token2.range[0]
  813. ], expectedWhitespace);
  814. }
  815. });
  816. }
  817. };
  818. const firstTestToken = sourceCode.getTokenAfter(openBrace);
  819. checkAndFix(openBrace, firstTestToken, "\n" + baseIndent);
  820. const firstConsequentToken = sourceCode.getTokenAfter(questionToken);
  821. checkAndFix(questionToken, firstConsequentToken, "\n" + innerIndent);
  822. const firstColonToken = colonToken;
  823. const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
  824. checkAndFix(lastConsequentToken, firstColonToken, "\n" + baseIndent);
  825. const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
  826. checkAndFix(colonToken, firstAlternateToken, "\n" + innerIndent);
  827. const lastAlternateToken = sourceCode.getTokenBefore(closeBrace);
  828. checkAndFix(lastAlternateToken, closeBrace, "\n" + openIndent);
  829. }
  830. };
  831. }
  832. }
  833. };