no-unescaped-entities.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /**
  2. * @fileoverview HTML special characters should be escaped.
  3. * @author Patrick Hayes
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const getSourceCode = require('../util/eslint').getSourceCode;
  8. const jsxUtil = require('../util/jsx');
  9. const report = require('../util/report');
  10. const getMessageData = require('../util/message');
  11. // ------------------------------------------------------------------------------
  12. // Rule Definition
  13. // ------------------------------------------------------------------------------
  14. // NOTE: '<' and '{' are also problematic characters, but they do not need
  15. // to be included here because it is a syntax error when these characters are
  16. // included accidentally.
  17. const DEFAULTS = [{
  18. char: '>',
  19. alternatives: ['&gt;'],
  20. }, {
  21. char: '"',
  22. alternatives: ['&quot;', '&ldquo;', '&#34;', '&rdquo;'],
  23. }, {
  24. char: '\'',
  25. alternatives: ['&apos;', '&lsquo;', '&#39;', '&rsquo;'],
  26. }, {
  27. char: '}',
  28. alternatives: ['&#125;'],
  29. }];
  30. const messages = {
  31. unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
  32. unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.',
  33. replaceWithAlt: 'Replace with `{{alt}}`.',
  34. };
  35. /** @type {import('eslint').Rule.RuleModule} */
  36. module.exports = {
  37. meta: {
  38. hasSuggestions: true,
  39. docs: {
  40. description: 'Disallow unescaped HTML entities from appearing in markup',
  41. category: 'Possible Errors',
  42. recommended: true,
  43. url: docsUrl('no-unescaped-entities'),
  44. },
  45. messages,
  46. schema: [{
  47. type: 'object',
  48. properties: {
  49. forbid: {
  50. type: 'array',
  51. items: {
  52. anyOf: [{
  53. type: 'string',
  54. }, {
  55. type: 'object',
  56. properties: {
  57. char: {
  58. type: 'string',
  59. },
  60. alternatives: {
  61. type: 'array',
  62. uniqueItems: true,
  63. items: {
  64. type: 'string',
  65. },
  66. },
  67. },
  68. }],
  69. },
  70. },
  71. },
  72. additionalProperties: false,
  73. }],
  74. },
  75. create(context) {
  76. function reportInvalidEntity(node) {
  77. const configuration = context.options[0] || {};
  78. const entities = configuration.forbid || DEFAULTS;
  79. // HTML entities are already escaped in node.value (as well as node.raw),
  80. // so pull the raw text from getSourceCode(context)
  81. for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
  82. let rawLine = getSourceCode(context).lines[i - 1];
  83. let start = 0;
  84. let end = rawLine.length;
  85. if (i === node.loc.start.line) {
  86. start = node.loc.start.column;
  87. }
  88. if (i === node.loc.end.line) {
  89. end = node.loc.end.column;
  90. }
  91. rawLine = rawLine.slice(start, end);
  92. for (let j = 0; j < entities.length; j++) {
  93. for (let index = 0; index < rawLine.length; index++) {
  94. const c = rawLine[index];
  95. if (typeof entities[j] === 'string') {
  96. if (c === entities[j]) {
  97. report(context, messages.unescapedEntity, 'unescapedEntity', {
  98. node,
  99. loc: { line: i, column: start + index },
  100. data: {
  101. entity: entities[j],
  102. },
  103. });
  104. }
  105. } else if (c === entities[j].char) {
  106. report(context, messages.unescapedEntityAlts, 'unescapedEntityAlts', {
  107. node,
  108. loc: { line: i, column: start + index },
  109. data: {
  110. entity: entities[j].char,
  111. alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', '),
  112. },
  113. suggest: entities[j].alternatives.map((alt) => Object.assign(
  114. getMessageData('replaceWithAlt', messages.replaceWithAlt),
  115. {
  116. data: { alt },
  117. fix(fixer) {
  118. const lineToChange = i - node.loc.start.line;
  119. const newText = node.raw.split('\n').map((line, idx) => {
  120. if (idx === lineToChange) {
  121. return line.slice(0, index) + alt + line.slice(index + 1);
  122. }
  123. return line;
  124. }).join('\n');
  125. return fixer.replaceText(node, newText);
  126. },
  127. }
  128. )),
  129. });
  130. }
  131. }
  132. }
  133. }
  134. }
  135. return {
  136. 'Literal, JSXText'(node) {
  137. if (jsxUtil.isJSX(node.parent)) {
  138. reportInvalidEntity(node);
  139. }
  140. },
  141. };
  142. },
  143. };