string-content.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. 'use strict';
  2. const escapeString = require('./utils/escape-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 problem = {
  81. node,
  82. message,
  83. data: {
  84. match,
  85. suggest,
  86. },
  87. };
  88. const fixed = string.replace(regex, suggest);
  89. const fix = type === 'Literal'
  90. ? fixer => fixer.replaceText(
  91. node,
  92. escapeString(fixed, raw[0]),
  93. )
  94. : fixer => replaceTemplateElement(
  95. fixer,
  96. node,
  97. escapeTemplateElementRaw(fixed),
  98. );
  99. if (autoFix) {
  100. problem.fix = fix;
  101. } else {
  102. problem.suggest = [
  103. {
  104. messageId: SUGGESTION_MESSAGE_ID,
  105. fix,
  106. },
  107. ];
  108. }
  109. return problem;
  110. },
  111. };
  112. };
  113. const schema = [
  114. {
  115. type: 'object',
  116. additionalProperties: false,
  117. properties: {
  118. patterns: {
  119. type: 'object',
  120. additionalProperties: {
  121. anyOf: [
  122. {
  123. type: 'string',
  124. },
  125. {
  126. type: 'object',
  127. required: [
  128. 'suggest',
  129. ],
  130. properties: {
  131. suggest: {
  132. type: 'string',
  133. },
  134. fix: {
  135. type: 'boolean',
  136. // Default: true
  137. },
  138. message: {
  139. type: 'string',
  140. // Default: ''
  141. },
  142. },
  143. additionalProperties: false,
  144. },
  145. ],
  146. }},
  147. },
  148. },
  149. ];
  150. /** @type {import('eslint').Rule.RuleModule} */
  151. module.exports = {
  152. create,
  153. meta: {
  154. type: 'suggestion',
  155. docs: {
  156. description: 'Enforce better string content.',
  157. },
  158. fixable: 'code',
  159. hasSuggestions: true,
  160. schema,
  161. messages,
  162. },
  163. };