Skip to content

Commit

Permalink
Rule for requiring HTML entities to be escaped (#681)
Browse files Browse the repository at this point in the history
  • Loading branch information
pfhayes authored and lencioni committed Sep 16, 2016
1 parent a41ca47 commit 21a3339
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions docs/rules/no-unescaped-entities.md
Original file line number Diff line number Diff line change
@@ -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
<MyComponent
name="name"
type="string"
foo="bar"> {/* oops! */}
x="y">
Body Text
</MyComponent>
```

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
<MyComponent>{'Text'}}</MyComponent>
```

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
<MyComponent
a="b"> {/* oops! */}
c="d"
Intended body text
</MyComponent>
```

The preferred way to include one of these characters is to use the HTML escape code.

- `>` can be replaced with `&gt;`
- `"` can be replaced with `&quot;`, `&ldquo;` or `&rdquo;`
- `'` can be replaced with `&apos;`, `&lsquo;` or `&rsquo;`
- `}` can be replaced with `&#125;`

Alternatively, you can include the literal character inside a subexpression
(such as `<div>{'>'}</div>`.

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
<div> > </div>
```

The following patterns are not considered warnings:

```jsx
<div> &gt; </div>
```

```jsx
<div> {'>'} </div>
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
80 changes: 80 additions & 0 deletions lib/rules/no-unescaped-entities.js
Original file line number Diff line number Diff line change
@@ -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.');
}
}
}
};
}
};
138 changes: 138 additions & 0 deletions tests/lib/rules/no-unescaped-entities.js
Original file line number Diff line number Diff line change
@@ -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 (',
' <div/>',
' );',
' }',
'});'
].join('\n'),
parserOptions: parserOptions
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>Here is some text!</div>;',
' }',
'});'
].join('\n'),
parserOptions: parserOptions
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>I&rsquo;ve escaped some entities: &gt; &lt; &amp;</div>;',
' }',
'});'
].join('\n'),
parserOptions: parserOptions
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>first line is ok',
' so is second',
' and here are some escaped entities: &gt; &lt; &amp;</div>;',
' }',
'});'
].join('\n'),
parserOptions: parserOptions
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>{">" + "<" + "&" + \'"\'}</div>;',
' },',
'});'
].join('\n'),
parserOptions: parserOptions
}
],

invalid: [
{
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>></div>;',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{message: 'HTML entities must be escaped.'}]
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>first line is ok',
' so is second',
' and here are some bad entities: ></div>',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{message: 'HTML entities must be escaped.'}]
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>\'</div>;',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{message: 'HTML entities must be escaped.'}]
}, {
code: [
'var Hello = React.createClass({',
' render: function() {',
' return <div>Multiple errors: \'>></div>;',
' }',
'});'
].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 <div>{"Unbalanced braces"}}</div>;',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{message: 'HTML entities must be escaped.'}]
}
]
});

0 comments on commit 21a3339

Please sign in to comment.