jsx-filename-extension.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. /**
  2. * @fileoverview Restrict file extensions that may contain JSX
  3. * @author Joe Lencioni
  4. */
  5. 'use strict';
  6. const path = require('path');
  7. const docsUrl = require('../util/docsUrl');
  8. const report = require('../util/report');
  9. // ------------------------------------------------------------------------------
  10. // Constants
  11. // ------------------------------------------------------------------------------
  12. const DEFAULTS = {
  13. allow: 'always',
  14. extensions: ['.jsx'],
  15. ignoreFilesWithoutCode: false,
  16. };
  17. // ------------------------------------------------------------------------------
  18. // Rule Definition
  19. // ------------------------------------------------------------------------------
  20. const messages = {
  21. noJSXWithExtension: 'JSX not allowed in files with extension \'{{ext}}\'',
  22. extensionOnlyForJSX: 'Only files containing JSX may use the extension \'{{ext}}\'',
  23. };
  24. /** @type {import('eslint').Rule.RuleModule} */
  25. module.exports = {
  26. meta: {
  27. docs: {
  28. description: 'Disallow file extensions that may contain JSX',
  29. category: 'Stylistic Issues',
  30. recommended: false,
  31. url: docsUrl('jsx-filename-extension'),
  32. },
  33. messages,
  34. schema: [{
  35. type: 'object',
  36. properties: {
  37. allow: {
  38. enum: ['always', 'as-needed'],
  39. },
  40. extensions: {
  41. type: 'array',
  42. items: {
  43. type: 'string',
  44. },
  45. },
  46. ignoreFilesWithoutCode: {
  47. type: 'boolean',
  48. },
  49. },
  50. additionalProperties: false,
  51. }],
  52. },
  53. create(context) {
  54. const filename = context.getFilename();
  55. let jsxNode;
  56. if (filename === '<text>') {
  57. // No need to traverse any nodes.
  58. return {};
  59. }
  60. const allow = (context.options[0] && context.options[0].allow) || DEFAULTS.allow;
  61. const allowedExtensions = (context.options[0] && context.options[0].extensions) || DEFAULTS.extensions;
  62. const ignoreFilesWithoutCode = (context.options[0] && context.options[0].ignoreFilesWithoutCode)
  63. || DEFAULTS.ignoreFilesWithoutCode;
  64. const isAllowedExtension = allowedExtensions.some((extension) => filename.slice(-extension.length) === extension);
  65. function handleJSX(node) {
  66. if (!jsxNode) {
  67. jsxNode = node;
  68. }
  69. }
  70. // --------------------------------------------------------------------------
  71. // Public
  72. // --------------------------------------------------------------------------
  73. return {
  74. JSXElement: handleJSX,
  75. JSXFragment: handleJSX,
  76. 'Program:exit'(node) {
  77. if (jsxNode) {
  78. if (!isAllowedExtension) {
  79. report(context, messages.noJSXWithExtension, 'noJSXWithExtension', {
  80. node: jsxNode,
  81. data: {
  82. ext: path.extname(filename),
  83. },
  84. });
  85. }
  86. return;
  87. }
  88. if (isAllowedExtension && allow === 'as-needed') {
  89. if (ignoreFilesWithoutCode && node.body.length === 0) {
  90. return;
  91. }
  92. report(context, messages.extensionOnlyForJSX, 'extensionOnlyForJSX', {
  93. node,
  94. data: {
  95. ext: path.extname(filename),
  96. },
  97. });
  98. }
  99. },
  100. };
  101. },
  102. };