Skip to content

Commit 1776e18

Browse files
committedNov 1, 2022
feat: text-escaping rule; fixes #864
1 parent da1c85f commit 1776e18

File tree

8 files changed

+685
-4
lines changed

8 files changed

+685
-4
lines changed
 

‎.README/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -587,4 +587,5 @@ selector).
587587
{"gitdown": "include", "file": "./rules/require-yields-check.md"}
588588
{"gitdown": "include", "file": "./rules/sort-tags.md"}
589589
{"gitdown": "include", "file": "./rules/tag-lines.md"}
590+
{"gitdown": "include", "file": "./rules/text-escaping.md"}
590591
{"gitdown": "include", "file": "./rules/valid-types.md"}

‎.README/rules/text-escaping.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
### `text-escaping`
2+
3+
This rule can auto-escape certain characters that are input within block and
4+
tag descriptions.
5+
6+
This rule may be desirable if your text is known not to contain HTML or
7+
Markdown and you therefore do not wish for it to be accidentally interpreted
8+
as such by the likes of Visual Studio Code or if you wish to view it escaped
9+
within it or your documentation.
10+
11+
#### Options
12+
13+
##### `escapeHTML`
14+
15+
This option escapes all `<` and `&` characters (except those followed by
16+
whitespace which are treated as literals by Visual Studio Code).
17+
18+
##### `escapeMarkdown`
19+
20+
This option escapes the first backtick (`` ` ``) in a paired sequence.
21+
22+
|||
23+
|---|---|
24+
|Context|everywhere|
25+
|Tags|``|
26+
|Recommended|false|
27+
|Settings||
28+
|Options||
29+
30+
<!-- assertions textEscaping -->

‎README.md

+152-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ JSDoc linting rules for ESLint.
7272
* [`require-yields-check`](#user-content-eslint-plugin-jsdoc-rules-require-yields-check)
7373
* [`sort-tags`](#user-content-eslint-plugin-jsdoc-rules-sort-tags)
7474
* [`tag-lines`](#user-content-eslint-plugin-jsdoc-rules-tag-lines)
75+
* [`text-escaping`](#user-content-eslint-plugin-jsdoc-rules-text-escaping)
7576
* [`valid-types`](#user-content-eslint-plugin-jsdoc-rules-valid-types)
7677

7778

@@ -22100,6 +22101,155 @@ The following patterns are not considered problems:
2210022101
````
2210122102

2210222103

22104+
<a name="user-content-eslint-plugin-jsdoc-rules-text-escaping"></a>
22105+
<a name="eslint-plugin-jsdoc-rules-text-escaping"></a>
22106+
### <code>text-escaping</code>
22107+
22108+
This rule can auto-escape certain characters that are input within block and
22109+
tag descriptions.
22110+
22111+
This rule may be desirable if your text is known not to contain HTML or
22112+
Markdown and you therefore do not wish for it to be accidentally interpreted
22113+
as such by the likes of Visual Studio Code or if you wish to view it escaped
22114+
within it or your documentation.
22115+
22116+
<a name="user-content-eslint-plugin-jsdoc-rules-text-escaping-options-42"></a>
22117+
<a name="eslint-plugin-jsdoc-rules-text-escaping-options-42"></a>
22118+
#### Options
22119+
22120+
<a name="user-content-eslint-plugin-jsdoc-rules-text-escaping-options-42-escapehtml"></a>
22121+
<a name="eslint-plugin-jsdoc-rules-text-escaping-options-42-escapehtml"></a>
22122+
##### <code>escapeHTML</code>
22123+
22124+
This option escapes all `<` and `&` characters (except those followed by
22125+
whitespace which are treated as literals by Visual Studio Code).
22126+
22127+
<a name="user-content-eslint-plugin-jsdoc-rules-text-escaping-options-42-escapemarkdown"></a>
22128+
<a name="eslint-plugin-jsdoc-rules-text-escaping-options-42-escapemarkdown"></a>
22129+
##### <code>escapeMarkdown</code>
22130+
22131+
This option escapes the first backtick (`` ` ``) in a paired sequence.
22132+
22133+
|||
22134+
|---|---|
22135+
|Context|everywhere|
22136+
|Tags|``|
22137+
|Recommended|false|
22138+
|Settings||
22139+
|Options||
22140+
22141+
The following patterns are considered problems:
22142+
22143+
````js
22144+
/**
22145+
* Some things to escape: <a> and &gt; and `test`
22146+
*/
22147+
// Message: You must include either `escapeHTML` or `escapeMarkdown`
22148+
22149+
/**
22150+
* Some things to escape: <a> and &gt; and &#xabc; and `test`
22151+
*/
22152+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22153+
// Message: You have unescaped HTML characters < or &
22154+
22155+
/**
22156+
* Some things to escape: <a> and &gt; and `test`
22157+
*/
22158+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22159+
// Message: You have unescaped Markdown backtick sequences
22160+
22161+
/**
22162+
* Some things to escape:
22163+
* <a> and &gt; and `test`
22164+
*/
22165+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22166+
// Message: You have unescaped HTML characters < or &
22167+
22168+
/**
22169+
* Some things to escape:
22170+
* <a> and &gt; and `test`
22171+
*/
22172+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22173+
// Message: You have unescaped Markdown backtick sequences
22174+
22175+
/**
22176+
* @param {SomeType} aName Some things to escape: <a> and &gt; and `test`
22177+
*/
22178+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22179+
// Message: You have unescaped HTML characters < or & in a tag
22180+
22181+
/**
22182+
* @param {SomeType} aName Some things to escape: <a> and &gt; and `test`
22183+
*/
22184+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22185+
// Message: You have unescaped Markdown backtick sequences in a tag
22186+
22187+
/**
22188+
* @param {SomeType} aName Some things to escape:
22189+
* <a> and &gt; and `test`
22190+
*/
22191+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22192+
// Message: You have unescaped HTML characters < or & in a tag
22193+
22194+
/**
22195+
* @param {SomeType} aName Some things to escape:
22196+
* <a> and &gt; and `test`
22197+
*/
22198+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22199+
// Message: You have unescaped Markdown backtick sequences in a tag
22200+
````
22201+
22202+
The following patterns are not considered problems:
22203+
22204+
````js
22205+
/**
22206+
* Some things to escape: &lt;a> and &gt; and `test`
22207+
*/
22208+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22209+
22210+
/**
22211+
* Some things to escape: <a> and &gt; and \`test`
22212+
*/
22213+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22214+
22215+
/**
22216+
* Some things to escape: < and &
22217+
*/
22218+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22219+
22220+
/**
22221+
* Some things to escape: `
22222+
*/
22223+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22224+
22225+
/**
22226+
* @param {SomeType} aName Some things to escape: &lt;a> and &gt; and `test`
22227+
*/
22228+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22229+
22230+
/**
22231+
* @param {SomeType} aName Some things to escape: <a> and &gt; and \`test`
22232+
*/
22233+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22234+
22235+
/**
22236+
* @param {SomeType} aName Some things to escape: < and &
22237+
*/
22238+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22239+
22240+
/**
22241+
* @param {SomeType} aName Some things to escape: `
22242+
*/
22243+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeMarkdown":true}]
22244+
22245+
/**
22246+
* Nothing
22247+
* to escape
22248+
*/
22249+
// "jsdoc/text-escaping": ["error"|"warn", {"escapeHTML":true}]
22250+
````
22251+
22252+
2210322253
<a name="user-content-eslint-plugin-jsdoc-rules-valid-types"></a>
2210422254
<a name="eslint-plugin-jsdoc-rules-valid-types"></a>
2210522255
### <code>valid-types</code>
@@ -22181,8 +22331,8 @@ for valid types (based on the tag's `type` value), and either portion checked
2218122331
for presence (based on `false` `name` or `type` values or their `required`
2218222332
value). See the setting for more details.
2218322333

22184-
<a name="user-content-eslint-plugin-jsdoc-rules-valid-types-options-42"></a>
22185-
<a name="eslint-plugin-jsdoc-rules-valid-types-options-42"></a>
22334+
<a name="user-content-eslint-plugin-jsdoc-rules-valid-types-options-43"></a>
22335+
<a name="eslint-plugin-jsdoc-rules-valid-types-options-43"></a>
2218622336
#### Options
2218722337

2218822338
- `allowEmptyNamepaths` (default: true) - Set to `false` to bulk disallow

‎src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import requireYields from './rules/requireYields';
4646
import requireYieldsCheck from './rules/requireYieldsCheck';
4747
import sortTags from './rules/sortTags';
4848
import tagLines from './rules/tagLines';
49+
import textEscaping from './rules/textEscaping';
4950
import validTypes from './rules/validTypes';
5051

5152
export default {
@@ -103,6 +104,7 @@ export default {
103104
'jsdoc/require-yields-check': 'warn',
104105
'jsdoc/sort-tags': 'off',
105106
'jsdoc/tag-lines': 'warn',
107+
'jsdoc/text-escaping': 'off',
106108
'jsdoc/valid-types': 'warn',
107109
},
108110
},
@@ -156,6 +158,7 @@ export default {
156158
'require-yields-check': requireYieldsCheck,
157159
'sort-tags': sortTags,
158160
'tag-lines': tagLines,
161+
'text-escaping': textEscaping,
159162
'valid-types': validTypes,
160163
},
161164
};

‎src/iterateJsdoc.js

+48-2
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const getUtils = (
144144
return jsdocUtils.getRegexFromString(str, requiredFlags);
145145
};
146146

147-
utils.getTagDescription = (tg) => {
147+
utils.getTagDescription = (tg, returnArray) => {
148148
const descriptions = [];
149149
tg.source.some(({
150150
tokens: {
@@ -179,7 +179,26 @@ const getUtils = (
179179
return false;
180180
});
181181

182-
return descriptions.join('\n');
182+
return returnArray ? descriptions : descriptions.join('\n');
183+
};
184+
185+
utils.setTagDescription = (tg, matcher, setter) => {
186+
let finalIdx = 0;
187+
tg.source.some(({
188+
tokens: {
189+
description,
190+
},
191+
}, idx) => {
192+
if (description && matcher.test(description)) {
193+
tg.source[idx].tokens.description = setter(description);
194+
finalIdx = idx;
195+
return true;
196+
}
197+
198+
return false;
199+
});
200+
201+
return finalIdx;
183202
};
184203

185204
utils.getDescription = () => {
@@ -207,10 +226,37 @@ const getUtils = (
207226

208227
return {
209228
description: descriptions.join('\n'),
229+
descriptions,
210230
lastDescriptionLine,
211231
};
212232
};
213233

234+
utils.setDescriptionLines = (matcher, setter) => {
235+
let finalIdx = 0;
236+
jsdoc.source.some(({
237+
tokens: {
238+
description,
239+
tag,
240+
end,
241+
},
242+
}, idx) => {
243+
// istanbul ignore if -- Already checked
244+
if (idx && (tag || end)) {
245+
return true;
246+
}
247+
248+
if (description && matcher.test(description)) {
249+
jsdoc.source[idx].tokens.description = setter(description);
250+
finalIdx = idx;
251+
return true;
252+
}
253+
254+
return false;
255+
});
256+
257+
return finalIdx;
258+
};
259+
214260
utils.changeTag = (tag, ...tokens) => {
215261
for (const [
216262
idx,

‎src/rules/textEscaping.js

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import iterateJsdoc from '../iterateJsdoc';
2+
3+
// We could disallow raw gt, quot, and apos, but allow for parity; but we do
4+
// not allow hex or decimal character references
5+
const htmlRegex = /(<|&(?!(?:amp|lt|gt|quot|apos);))(?=\S)/u;
6+
const markdownRegex = /(?<!\\)(`+)([^`]+)\1(?!`)/u;
7+
8+
const htmlReplacer = (desc) => {
9+
return desc.replace(new RegExp(htmlRegex, 'gu'), (_) => {
10+
if (_ === '<') {
11+
return '&lt;';
12+
}
13+
14+
return '&amp;';
15+
});
16+
};
17+
18+
const markdownReplacer = (desc) => {
19+
return desc.replace(new RegExp(markdownRegex, 'gu'), (_, backticks, encapsed) => {
20+
const bookend = '`'.repeat(backticks.length);
21+
return `\\${bookend}${encapsed}${bookend}`;
22+
});
23+
};
24+
25+
export default iterateJsdoc(({
26+
context,
27+
jsdoc,
28+
utils,
29+
}) => {
30+
const {
31+
escapeHTML,
32+
escapeMarkdown,
33+
} = context.options[0] || {};
34+
35+
if (!escapeHTML && !escapeMarkdown) {
36+
context.report({
37+
loc: {
38+
start: {
39+
column: 1,
40+
line: 1,
41+
},
42+
},
43+
message: 'You must include either `escapeHTML` or `escapeMarkdown`',
44+
});
45+
return;
46+
}
47+
48+
const {
49+
descriptions,
50+
} = utils.getDescription();
51+
52+
if (escapeHTML) {
53+
if (descriptions.some((desc) => {
54+
return htmlRegex.test(desc);
55+
})) {
56+
const line = utils.setDescriptionLines(htmlRegex, htmlReplacer);
57+
utils.reportJSDoc('You have unescaped HTML characters < or &', {
58+
line,
59+
}, () => {}, true);
60+
return;
61+
}
62+
63+
for (const tag of jsdoc.tags) {
64+
if (utils.getTagDescription(tag, true).some((desc) => {
65+
return htmlRegex.test(desc);
66+
})) {
67+
const line = utils.setTagDescription(tag, htmlRegex, htmlReplacer) +
68+
tag.source[0].number;
69+
utils.reportJSDoc('You have unescaped HTML characters < or & in a tag', {
70+
line,
71+
}, () => {}, true);
72+
}
73+
}
74+
75+
return;
76+
}
77+
78+
if (descriptions.some((desc) => {
79+
return markdownRegex.test(desc);
80+
})) {
81+
const line = utils.setDescriptionLines(markdownRegex, markdownReplacer);
82+
utils.reportJSDoc('You have unescaped Markdown backtick sequences', {
83+
line,
84+
}, () => {}, true);
85+
return;
86+
}
87+
88+
for (const tag of jsdoc.tags) {
89+
if (utils.getTagDescription(tag, true).some((desc) => {
90+
return markdownRegex.test(desc);
91+
})) {
92+
const line = utils.setTagDescription(
93+
tag, markdownRegex, markdownReplacer,
94+
) + tag.source[0].number;
95+
utils.reportJSDoc(
96+
'You have unescaped Markdown backtick sequences in a tag',
97+
{
98+
line,
99+
},
100+
() => {},
101+
true,
102+
);
103+
}
104+
}
105+
}, {
106+
iterateAllJsdocs: true,
107+
meta: {
108+
docs: {
109+
description: '',
110+
url: 'https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-text-escaping',
111+
},
112+
fixable: 'code',
113+
schema: [
114+
{
115+
additionalProperies: false,
116+
properties: {
117+
// Option properties here (or remove the object)
118+
escapeHTML: {
119+
type: 'boolean',
120+
},
121+
escapeMarkdown: {
122+
type: 'boolean',
123+
},
124+
},
125+
type: 'object',
126+
},
127+
],
128+
type: 'suggestion',
129+
},
130+
});

‎test/rules/assertions/textEscaping.js

+320
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
export default {
2+
invalid: [
3+
{
4+
code: `
5+
/**
6+
* Some things to escape: <a> and &gt; and \`test\`
7+
*/
8+
`,
9+
errors: [
10+
{
11+
line: 1,
12+
message: 'You must include either `escapeHTML` or `escapeMarkdown`',
13+
},
14+
],
15+
},
16+
{
17+
code: `
18+
/**
19+
* Some things to escape: <a> and &gt; and &#xabc; and \`test\`
20+
*/
21+
`,
22+
errors: [
23+
{
24+
line: 3,
25+
message: 'You have unescaped HTML characters < or &',
26+
},
27+
],
28+
options: [
29+
{
30+
escapeHTML: true,
31+
},
32+
],
33+
output: `
34+
/**
35+
* Some things to escape: &lt;a> and &gt; and &amp;#xabc; and \`test\`
36+
*/
37+
`,
38+
},
39+
{
40+
code: `
41+
/**
42+
* Some things to escape: <a> and &gt; and \`test\`
43+
*/
44+
`,
45+
errors: [
46+
{
47+
line: 3,
48+
message: 'You have unescaped Markdown backtick sequences',
49+
},
50+
],
51+
options: [
52+
{
53+
escapeMarkdown: true,
54+
},
55+
],
56+
output: `
57+
/**
58+
* Some things to escape: <a> and &gt; and \\\`test\`
59+
*/
60+
`,
61+
},
62+
{
63+
code: `
64+
/**
65+
* Some things to escape:
66+
* <a> and &gt; and \`test\`
67+
*/
68+
`,
69+
errors: [
70+
{
71+
line: 4,
72+
message: 'You have unescaped HTML characters < or &',
73+
},
74+
],
75+
options: [
76+
{
77+
escapeHTML: true,
78+
},
79+
],
80+
output: `
81+
/**
82+
* Some things to escape:
83+
* &lt;a> and &gt; and \`test\`
84+
*/
85+
`,
86+
},
87+
{
88+
code: `
89+
/**
90+
* Some things to escape:
91+
* <a> and &gt; and \`test\`
92+
*/
93+
`,
94+
errors: [
95+
{
96+
line: 4,
97+
message: 'You have unescaped Markdown backtick sequences',
98+
},
99+
],
100+
options: [
101+
{
102+
escapeMarkdown: true,
103+
},
104+
],
105+
output: `
106+
/**
107+
* Some things to escape:
108+
* <a> and &gt; and \\\`test\`
109+
*/
110+
`,
111+
},
112+
{
113+
code: `
114+
/**
115+
* @param {SomeType} aName Some things to escape: <a> and &gt; and \`test\`
116+
*/
117+
`,
118+
errors: [
119+
{
120+
line: 3,
121+
message: 'You have unescaped HTML characters < or & in a tag',
122+
},
123+
],
124+
options: [
125+
{
126+
escapeHTML: true,
127+
},
128+
],
129+
output: `
130+
/**
131+
* @param {SomeType} aName Some things to escape: &lt;a> and &gt; and \`test\`
132+
*/
133+
`,
134+
},
135+
{
136+
code: `
137+
/**
138+
* @param {SomeType} aName Some things to escape: <a> and &gt; and \`test\`
139+
*/
140+
`,
141+
errors: [
142+
{
143+
line: 3,
144+
message: 'You have unescaped Markdown backtick sequences in a tag',
145+
},
146+
],
147+
options: [
148+
{
149+
escapeMarkdown: true,
150+
},
151+
],
152+
output: `
153+
/**
154+
* @param {SomeType} aName Some things to escape: <a> and &gt; and \\\`test\`
155+
*/
156+
`,
157+
},
158+
{
159+
code: `
160+
/**
161+
* @param {SomeType} aName Some things to escape:
162+
* <a> and &gt; and \`test\`
163+
*/
164+
`,
165+
errors: [
166+
{
167+
line: 4,
168+
message: 'You have unescaped HTML characters < or & in a tag',
169+
},
170+
],
171+
options: [
172+
{
173+
escapeHTML: true,
174+
},
175+
],
176+
output: `
177+
/**
178+
* @param {SomeType} aName Some things to escape:
179+
* &lt;a> and &gt; and \`test\`
180+
*/
181+
`,
182+
},
183+
{
184+
code: `
185+
/**
186+
* @param {SomeType} aName Some things to escape:
187+
* <a> and &gt; and \`test\`
188+
*/
189+
`,
190+
errors: [
191+
{
192+
line: 4,
193+
message: 'You have unescaped Markdown backtick sequences in a tag',
194+
},
195+
],
196+
options: [
197+
{
198+
escapeMarkdown: true,
199+
},
200+
],
201+
output: `
202+
/**
203+
* @param {SomeType} aName Some things to escape:
204+
* <a> and &gt; and \\\`test\`
205+
*/
206+
`,
207+
},
208+
],
209+
valid: [
210+
{
211+
code: `
212+
/**
213+
* Some things to escape: &lt;a> and &gt; and \`test\`
214+
*/
215+
`,
216+
options: [
217+
{
218+
escapeHTML: true,
219+
},
220+
],
221+
},
222+
{
223+
code: `
224+
/**
225+
* Some things to escape: <a> and &gt; and \\\`test\`
226+
*/
227+
`,
228+
options: [
229+
{
230+
escapeMarkdown: true,
231+
},
232+
],
233+
},
234+
{
235+
code: `
236+
/**
237+
* Some things to escape: < and &
238+
*/
239+
`,
240+
options: [
241+
{
242+
escapeHTML: true,
243+
},
244+
],
245+
},
246+
{
247+
code: `
248+
/**
249+
* Some things to escape: \`
250+
*/
251+
`,
252+
options: [
253+
{
254+
escapeMarkdown: true,
255+
},
256+
],
257+
},
258+
{
259+
code: `
260+
/**
261+
* @param {SomeType} aName Some things to escape: &lt;a> and &gt; and \`test\`
262+
*/
263+
`,
264+
options: [
265+
{
266+
escapeHTML: true,
267+
},
268+
],
269+
},
270+
{
271+
code: `
272+
/**
273+
* @param {SomeType} aName Some things to escape: <a> and &gt; and \\\`test\`
274+
*/
275+
`,
276+
options: [
277+
{
278+
escapeMarkdown: true,
279+
},
280+
],
281+
},
282+
{
283+
code: `
284+
/**
285+
* @param {SomeType} aName Some things to escape: < and &
286+
*/
287+
`,
288+
options: [
289+
{
290+
escapeHTML: true,
291+
},
292+
],
293+
},
294+
{
295+
code: `
296+
/**
297+
* @param {SomeType} aName Some things to escape: \`
298+
*/
299+
`,
300+
options: [
301+
{
302+
escapeMarkdown: true,
303+
},
304+
],
305+
},
306+
{
307+
code: `
308+
/**
309+
* Nothing
310+
* to escape
311+
*/
312+
`,
313+
options: [
314+
{
315+
escapeHTML: true,
316+
},
317+
],
318+
},
319+
],
320+
};

‎test/rules/ruleNames.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@
4747
"require-yields-check",
4848
"sort-tags",
4949
"tag-lines",
50+
"text-escaping",
5051
"valid-types"
5152
]

0 commit comments

Comments
 (0)
Please sign in to comment.