diff --git a/README.md b/README.md index a696ff080f..376c92296f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](# * [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of `React.render` * [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState` * [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute. +* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup * [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable) * [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types * [react/prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components diff --git a/docs/rules/no-unescaped-entities.md b/docs/rules/no-unescaped-entities.md new file mode 100644 index 0000000000..2040d2c07c --- /dev/null +++ b/docs/rules/no-unescaped-entities.md @@ -0,0 +1,70 @@ +# Prevent invalid characters from appearing in markup (no-unescaped-entities) + +This rule prevents characters that you may have meant as JSX escape characters +from being accidentally injected as a text node in JSX statements. + +For example, if one were to misplace their closing `>` in a tag: + +```jsx + {/* oops! */} + x="y"> + Body Text + +``` + +The body text of this would render as `x="y"> Body Text`, which is probably not +what was intended. This rule requires that these special characters are +escaped if they appear in the body of a tag. + +Another example is when one accidentally includes an extra closing brace. + +```jsx +{'Text'}} +``` + +The extra brace will be rendered, and the body text will be `Text}`. + +This rule will also check for `"` and `'`, which might be accidentally included +when the closing `>` is in the wrong place. + +```jsx + {/* oops! */} + c="d" + Intended body text + +``` + +The preferred way to include one of these characters is to use the HTML escape code. + +- `>` can be replaced with `>` +- `"` can be replaced with `"`, `“` or `”` +- `'` can be replaced with `'`, `‘` or `’` +- `}` can be replaced with `}` + +Alternatively, you can include the literal character inside a subexpression +(such as `
{'>'}
`. + +The characters `<` and `{` should also be escaped, but they are not checked by this +rule because it is a syntax error to include those tokens inside of a tag. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +
>
+``` + +The following patterns are not considered warnings: + +```jsx +
>
+``` + +```jsx +
{'>'}
+``` diff --git a/index.js b/index.js index a0ac8bf0b1..ff5d9d9eef 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ var rules = { 'no-did-mount-set-state': require('./lib/rules/no-did-mount-set-state'), 'no-did-update-set-state': require('./lib/rules/no-did-update-set-state'), 'no-render-return-value': require('./lib/rules/no-render-return-value'), + 'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'), 'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'), 'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'), 'jsx-handler-names': require('./lib/rules/jsx-handler-names'), diff --git a/lib/rules/no-unescaped-entities.js b/lib/rules/no-unescaped-entities.js new file mode 100644 index 0000000000..3548cb43ed --- /dev/null +++ b/lib/rules/no-unescaped-entities.js @@ -0,0 +1,80 @@ +/** + * @fileoverview HTML special characters should be escaped. + * @author Patrick Hayes + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +// NOTE: '<' and '{' are also problematic characters, but they do not need +// to be included here because it is a syntax error when these characters are +// included accidentally. +var DEFAULTS = ['>', '"', '\'', '}']; + +module.exports = { + meta: { + docs: { + description: 'Detect unescaped HTML entities, which might represent malformed tags', + category: 'Possible Errors', + recommended: false + }, + schema: [{ + type: 'object', + properties: { + forbid: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + }] + }, + + create: function(context) { + function isInvalidEntity(node) { + var configuration = context.options[0] || {}; + var entities = configuration.forbid || DEFAULTS; + + // HTML entites are already escaped in node.value (as well as node.raw), + // so pull the raw text from context.getSourceCode() + for (var i = node.loc.start.line; i <= node.loc.end.line; i++) { + var rawLine = context.getSourceCode().lines[i - 1]; + var start = 0; + var end = rawLine.length; + if (i === node.loc.start.line) { + start = node.loc.start.column; + } + if (i === node.loc.end.line) { + end = node.loc.end.column; + } + rawLine = rawLine.substring(start, end); + for (var j = 0; j < entities.length; j++) { + for (var index = 0; index < rawLine.length; index++) { + var c = rawLine[index]; + if (c === entities[j]) { + context.report({ + loc: {line: i, column: start + index}, + message: 'HTML entities must be escaped.', + node: node + }); + } + } + } + } + } + + return { + Literal: function(node) { + if (node.type === 'Literal' && node.parent.type === 'JSXElement') { + if (isInvalidEntity(node)) { + context.report(node, 'HTML entities must be escaped.'); + } + } + } + }; + } +}; diff --git a/tests/lib/rules/no-unescaped-entities.js b/tests/lib/rules/no-unescaped-entities.js new file mode 100644 index 0000000000..098dd4f76d --- /dev/null +++ b/tests/lib/rules/no-unescaped-entities.js @@ -0,0 +1,138 @@ +/** + * @fileoverview Tests for no-unescaped-entities + * @author Patrick Hayes + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/no-unescaped-entities'); +var RuleTester = require('eslint').RuleTester; +var parserOptions = { + ecmaFeatures: { + jsx: true + } +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester(); +ruleTester.run('no-unescaped-entities', rule, { + + valid: [ + { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return (', + '
', + ' );', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
Here is some text!
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
I’ve escaped some entities: > < &
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
first line is ok', + ' so is second', + ' and here are some escaped entities: > < &
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
{">" + "<" + "&" + \'"\'}
;', + ' },', + '});' + ].join('\n'), + parserOptions: parserOptions + } + ], + + invalid: [ + { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
>
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions, + errors: [{message: 'HTML entities must be escaped.'}] + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
first line is ok', + ' so is second', + ' and here are some bad entities: >
', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions, + errors: [{message: 'HTML entities must be escaped.'}] + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
\'
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions, + errors: [{message: 'HTML entities must be escaped.'}] + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
Multiple errors: \'>>
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions, + errors: [ + {message: 'HTML entities must be escaped.'}, + {message: 'HTML entities must be escaped.'}, + {message: 'HTML entities must be escaped.'} + ] + }, { + code: [ + 'var Hello = React.createClass({', + ' render: function() {', + ' return
{"Unbalanced braces"}}
;', + ' }', + '});' + ].join('\n'), + parserOptions: parserOptions, + errors: [{message: 'HTML entities must be escaped.'}] + } + ] +});