no-unescaped-entities.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  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 jsxUtil = require('../util/jsx');
  8. // ------------------------------------------------------------------------------
  9. // Rule Definition
  10. // ------------------------------------------------------------------------------
  11. // NOTE: '<' and '{' are also problematic characters, but they do not need
  12. // to be included here because it is a syntax error when these characters are
  13. // included accidentally.
  14. const DEFAULTS = [{
  15. char: '>',
  16. alternatives: ['&gt;']
  17. }, {
  18. char: '"',
  19. alternatives: ['&quot;', '&ldquo;', '&#34;', '&rdquo;']
  20. }, {
  21. char: '\'',
  22. alternatives: ['&apos;', '&lsquo;', '&#39;', '&rsquo;']
  23. }, {
  24. char: '}',
  25. alternatives: ['&#125;']
  26. }];
  27. module.exports = {
  28. meta: {
  29. docs: {
  30. description: 'Detect unescaped HTML entities, which might represent malformed tags',
  31. category: 'Possible Errors',
  32. recommended: true,
  33. url: docsUrl('no-unescaped-entities')
  34. },
  35. messages: {
  36. unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
  37. unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.'
  38. },
  39. schema: [{
  40. type: 'object',
  41. properties: {
  42. forbid: {
  43. type: 'array',
  44. items: {
  45. oneOf: [{
  46. type: 'string'
  47. }, {
  48. type: 'object',
  49. properties: {
  50. char: {
  51. type: 'string'
  52. },
  53. alternatives: {
  54. type: 'array',
  55. uniqueItems: true,
  56. items: {
  57. type: 'string'
  58. }
  59. }
  60. }
  61. }]
  62. }
  63. }
  64. },
  65. additionalProperties: false
  66. }]
  67. },
  68. create(context) {
  69. function reportInvalidEntity(node) {
  70. const configuration = context.options[0] || {};
  71. const entities = configuration.forbid || DEFAULTS;
  72. // HTML entites are already escaped in node.value (as well as node.raw),
  73. // so pull the raw text from context.getSourceCode()
  74. for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
  75. let rawLine = context.getSourceCode().lines[i - 1];
  76. let start = 0;
  77. let end = rawLine.length;
  78. if (i === node.loc.start.line) {
  79. start = node.loc.start.column;
  80. }
  81. if (i === node.loc.end.line) {
  82. end = node.loc.end.column;
  83. }
  84. rawLine = rawLine.substring(start, end);
  85. for (let j = 0; j < entities.length; j++) {
  86. for (let index = 0; index < rawLine.length; index++) {
  87. const c = rawLine[index];
  88. if (typeof entities[j] === 'string') {
  89. if (c === entities[j]) {
  90. context.report({
  91. node,
  92. loc: {line: i, column: start + index},
  93. messageId: 'unescapedEntity',
  94. data: {
  95. entity: entities[j]
  96. }
  97. });
  98. }
  99. } else if (c === entities[j].char) {
  100. context.report({
  101. node,
  102. loc: {line: i, column: start + index},
  103. messageId: 'unescapedEntityAlts',
  104. data: {
  105. entity: entities[j].char,
  106. alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', ')
  107. }
  108. });
  109. }
  110. }
  111. }
  112. }
  113. }
  114. return {
  115. 'Literal, JSXText'(node) {
  116. if (jsxUtil.isJSX(node.parent)) {
  117. reportInvalidEntity(node);
  118. }
  119. }
  120. };
  121. }
  122. };