Skip to content

Commit 21a3339

Browse files
pfhayeslencioni
authored andcommitted
Rule for requiring HTML entities to be escaped (#681)
1 parent a41ca47 commit 21a3339

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
9595
* [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of `React.render`
9696
* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
9797
* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
98+
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup
9899
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
99100
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
100101
* [react/prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components

docs/rules/no-unescaped-entities.md

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Prevent invalid characters from appearing in markup (no-unescaped-entities)
2+
3+
This rule prevents characters that you may have meant as JSX escape characters
4+
from being accidentally injected as a text node in JSX statements.
5+
6+
For example, if one were to misplace their closing `>` in a tag:
7+
8+
```jsx
9+
<MyComponent
10+
name="name"
11+
type="string"
12+
foo="bar"> {/* oops! */}
13+
x="y">
14+
Body Text
15+
</MyComponent>
16+
```
17+
18+
The body text of this would render as `x="y"> Body Text`, which is probably not
19+
what was intended. This rule requires that these special characters are
20+
escaped if they appear in the body of a tag.
21+
22+
Another example is when one accidentally includes an extra closing brace.
23+
24+
```jsx
25+
<MyComponent>{'Text'}}</MyComponent>
26+
```
27+
28+
The extra brace will be rendered, and the body text will be `Text}`.
29+
30+
This rule will also check for `"` and `'`, which might be accidentally included
31+
when the closing `>` is in the wrong place.
32+
33+
```jsx
34+
<MyComponent
35+
a="b"> {/* oops! */}
36+
c="d"
37+
Intended body text
38+
</MyComponent>
39+
```
40+
41+
The preferred way to include one of these characters is to use the HTML escape code.
42+
43+
- `>` can be replaced with `&gt;`
44+
- `"` can be replaced with `&quot;`, `&ldquo;` or `&rdquo;`
45+
- `'` can be replaced with `&apos;`, `&lsquo;` or `&rsquo;`
46+
- `}` can be replaced with `&#125;`
47+
48+
Alternatively, you can include the literal character inside a subexpression
49+
(such as `<div>{'>'}</div>`.
50+
51+
The characters `<` and `{` should also be escaped, but they are not checked by this
52+
rule because it is a syntax error to include those tokens inside of a tag.
53+
54+
## Rule Details
55+
56+
The following patterns are considered warnings:
57+
58+
```jsx
59+
<div> > </div>
60+
```
61+
62+
The following patterns are not considered warnings:
63+
64+
```jsx
65+
<div> &gt; </div>
66+
```
67+
68+
```jsx
69+
<div> {'>'} </div>
70+
```

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var rules = {
2121
'no-did-mount-set-state': require('./lib/rules/no-did-mount-set-state'),
2222
'no-did-update-set-state': require('./lib/rules/no-did-update-set-state'),
2323
'no-render-return-value': require('./lib/rules/no-render-return-value'),
24+
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
2425
'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'),
2526
'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
2627
'jsx-handler-names': require('./lib/rules/jsx-handler-names'),

lib/rules/no-unescaped-entities.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @fileoverview HTML special characters should be escaped.
3+
* @author Patrick Hayes
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Rule Definition
9+
// ------------------------------------------------------------------------------
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+
var DEFAULTS = ['>', '"', '\'', '}'];
15+
16+
module.exports = {
17+
meta: {
18+
docs: {
19+
description: 'Detect unescaped HTML entities, which might represent malformed tags',
20+
category: 'Possible Errors',
21+
recommended: false
22+
},
23+
schema: [{
24+
type: 'object',
25+
properties: {
26+
forbid: {
27+
type: 'array',
28+
items: {
29+
type: 'string'
30+
}
31+
}
32+
},
33+
additionalProperties: false
34+
}]
35+
},
36+
37+
create: function(context) {
38+
function isInvalidEntity(node) {
39+
var configuration = context.options[0] || {};
40+
var entities = configuration.forbid || DEFAULTS;
41+
42+
// HTML entites are already escaped in node.value (as well as node.raw),
43+
// so pull the raw text from context.getSourceCode()
44+
for (var i = node.loc.start.line; i <= node.loc.end.line; i++) {
45+
var rawLine = context.getSourceCode().lines[i - 1];
46+
var start = 0;
47+
var end = rawLine.length;
48+
if (i === node.loc.start.line) {
49+
start = node.loc.start.column;
50+
}
51+
if (i === node.loc.end.line) {
52+
end = node.loc.end.column;
53+
}
54+
rawLine = rawLine.substring(start, end);
55+
for (var j = 0; j < entities.length; j++) {
56+
for (var index = 0; index < rawLine.length; index++) {
57+
var c = rawLine[index];
58+
if (c === entities[j]) {
59+
context.report({
60+
loc: {line: i, column: start + index},
61+
message: 'HTML entities must be escaped.',
62+
node: node
63+
});
64+
}
65+
}
66+
}
67+
}
68+
}
69+
70+
return {
71+
Literal: function(node) {
72+
if (node.type === 'Literal' && node.parent.type === 'JSXElement') {
73+
if (isInvalidEntity(node)) {
74+
context.report(node, 'HTML entities must be escaped.');
75+
}
76+
}
77+
}
78+
};
79+
}
80+
};
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* @fileoverview Tests for no-unescaped-entities
3+
* @author Patrick Hayes
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
var rule = require('../../../lib/rules/no-unescaped-entities');
12+
var RuleTester = require('eslint').RuleTester;
13+
var parserOptions = {
14+
ecmaFeatures: {
15+
jsx: true
16+
}
17+
};
18+
19+
// ------------------------------------------------------------------------------
20+
// Tests
21+
// ------------------------------------------------------------------------------
22+
23+
var ruleTester = new RuleTester();
24+
ruleTester.run('no-unescaped-entities', rule, {
25+
26+
valid: [
27+
{
28+
code: [
29+
'var Hello = React.createClass({',
30+
' render: function() {',
31+
' return (',
32+
' <div/>',
33+
' );',
34+
' }',
35+
'});'
36+
].join('\n'),
37+
parserOptions: parserOptions
38+
}, {
39+
code: [
40+
'var Hello = React.createClass({',
41+
' render: function() {',
42+
' return <div>Here is some text!</div>;',
43+
' }',
44+
'});'
45+
].join('\n'),
46+
parserOptions: parserOptions
47+
}, {
48+
code: [
49+
'var Hello = React.createClass({',
50+
' render: function() {',
51+
' return <div>I&rsquo;ve escaped some entities: &gt; &lt; &amp;</div>;',
52+
' }',
53+
'});'
54+
].join('\n'),
55+
parserOptions: parserOptions
56+
}, {
57+
code: [
58+
'var Hello = React.createClass({',
59+
' render: function() {',
60+
' return <div>first line is ok',
61+
' so is second',
62+
' and here are some escaped entities: &gt; &lt; &amp;</div>;',
63+
' }',
64+
'});'
65+
].join('\n'),
66+
parserOptions: parserOptions
67+
}, {
68+
code: [
69+
'var Hello = React.createClass({',
70+
' render: function() {',
71+
' return <div>{">" + "<" + "&" + \'"\'}</div>;',
72+
' },',
73+
'});'
74+
].join('\n'),
75+
parserOptions: parserOptions
76+
}
77+
],
78+
79+
invalid: [
80+
{
81+
code: [
82+
'var Hello = React.createClass({',
83+
' render: function() {',
84+
' return <div>></div>;',
85+
' }',
86+
'});'
87+
].join('\n'),
88+
parserOptions: parserOptions,
89+
errors: [{message: 'HTML entities must be escaped.'}]
90+
}, {
91+
code: [
92+
'var Hello = React.createClass({',
93+
' render: function() {',
94+
' return <div>first line is ok',
95+
' so is second',
96+
' and here are some bad entities: ></div>',
97+
' }',
98+
'});'
99+
].join('\n'),
100+
parserOptions: parserOptions,
101+
errors: [{message: 'HTML entities must be escaped.'}]
102+
}, {
103+
code: [
104+
'var Hello = React.createClass({',
105+
' render: function() {',
106+
' return <div>\'</div>;',
107+
' }',
108+
'});'
109+
].join('\n'),
110+
parserOptions: parserOptions,
111+
errors: [{message: 'HTML entities must be escaped.'}]
112+
}, {
113+
code: [
114+
'var Hello = React.createClass({',
115+
' render: function() {',
116+
' return <div>Multiple errors: \'>></div>;',
117+
' }',
118+
'});'
119+
].join('\n'),
120+
parserOptions: parserOptions,
121+
errors: [
122+
{message: 'HTML entities must be escaped.'},
123+
{message: 'HTML entities must be escaped.'},
124+
{message: 'HTML entities must be escaped.'}
125+
]
126+
}, {
127+
code: [
128+
'var Hello = React.createClass({',
129+
' render: function() {',
130+
' return <div>{"Unbalanced braces"}}</div>;',
131+
' }',
132+
'});'
133+
].join('\n'),
134+
parserOptions: parserOptions,
135+
errors: [{message: 'HTML entities must be escaped.'}]
136+
}
137+
]
138+
});

0 commit comments

Comments
 (0)