string-content.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. 'use strict';
  2. const quoteString = require('./utils/quote-string.js');
  3. const escapeTemplateElementRaw = require('./utils/escape-template-element-raw.js');
  4. const {replaceTemplateElement} = require('./fix/index.js');
  5. const defaultMessage = 'Prefer `{{suggest}}` over `{{match}}`.';
  6. const SUGGESTION_MESSAGE_ID = 'replace';
  7. const messages = {
  8. [SUGGESTION_MESSAGE_ID]: 'Replace `{{match}}` with `{{suggest}}`.',
  9. };
  10. const ignoredIdentifier = new Set([
  11. 'gql',
  12. 'html',
  13. 'svg',
  14. ]);
  15. const ignoredMemberExpressionObject = new Set([
  16. 'styled',
  17. ]);
  18. const isIgnoredTag = node => {
  19. if (!node.parent || !node.parent.parent || !node.parent.parent.tag) {
  20. return false;
  21. }
  22. const {tag} = node.parent.parent;
  23. if (tag.type === 'Identifier' && ignoredIdentifier.has(tag.name)) {
  24. return true;
  25. }
  26. if (tag.type === 'MemberExpression') {
  27. const {object} = tag;
  28. if (
  29. object.type === 'Identifier'
  30. && ignoredMemberExpressionObject.has(object.name)
  31. ) {
  32. return true;
  33. }
  34. }
  35. return false;
  36. };
  37. function getReplacements(patterns) {
  38. return Object.entries(patterns)
  39. .map(([match, options]) => {
  40. if (typeof options === 'string') {
  41. options = {
  42. suggest: options,
  43. };
  44. }
  45. return {
  46. match,
  47. regex: new RegExp(match, 'gu'),
  48. fix: true,
  49. ...options,
  50. };
  51. });
  52. }
  53. /** @param {import('eslint').Rule.RuleContext} context */
  54. const create = context => {
  55. const {patterns} = {
  56. patterns: {},
  57. ...context.options[0],
  58. };
  59. const replacements = getReplacements(patterns);
  60. if (replacements.length === 0) {
  61. return {};
  62. }
  63. return {
  64. 'Literal, TemplateElement': node => {
  65. const {type, value, raw} = node;
  66. let string;
  67. if (type === 'Literal') {
  68. string = value;
  69. } else if (!isIgnoredTag(node)) {
  70. string = value.raw;
  71. }
  72. if (!string || typeof string !== 'string') {
  73. return;
  74. }
  75. const replacement = replacements.find(({regex}) => regex.test(string));
  76. if (!replacement) {
  77. return;
  78. }
  79. const {fix: autoFix, message = defaultMessage, match, suggest, regex} = replacement;
  80. const messageData = {
  81. match,
  82. suggest,
  83. };
  84. const problem = {
  85. node,
  86. message,
  87. data: messageData,
  88. };
  89. const fixed = string.replace(regex, suggest);
  90. const fix = type === 'Literal'
  91. ? fixer => fixer.replaceText(
  92. node,
  93. quoteString(fixed, raw[0]),
  94. )
  95. : fixer => replaceTemplateElement(
  96. fixer,
  97. node,
  98. escapeTemplateElementRaw(fixed),
  99. );
  100. if (autoFix) {
  101. problem.fix = fix;
  102. } else {
  103. problem.suggest = [
  104. {
  105. messageId: SUGGESTION_MESSAGE_ID,
  106. data: messageData,
  107. fix,
  108. },
  109. ];
  110. }
  111. return problem;
  112. },
  113. };
  114. };
  115. const schema = [
  116. {
  117. type: 'object',
  118. additionalProperties: false,
  119. properties: {
  120. patterns: {
  121. type: 'object',
  122. additionalProperties: {
  123. anyOf: [
  124. {
  125. type: 'string',
  126. },
  127. {
  128. type: 'object',
  129. required: [
  130. 'suggest',
  131. ],
  132. properties: {
  133. suggest: {
  134. type: 'string',
  135. },
  136. fix: {
  137. type: 'boolean',
  138. // Default: true
  139. },
  140. message: {
  141. type: 'string',
  142. // Default: ''
  143. },
  144. },
  145. additionalProperties: false,
  146. },
  147. ],
  148. }},
  149. },
  150. },
  151. ];
  152. /** @type {import('eslint').Rule.RuleModule} */
  153. module.exports = {
  154. create,
  155. meta: {
  156. type: 'suggestion',
  157. docs: {
  158. description: 'Enforce better string content.',
  159. },
  160. fixable: 'code',
  161. hasSuggestions: true,
  162. schema,
  163. messages,
  164. },
  165. };