Skip to content

Commit 0972a89

Browse files
fiskersindresorhus
andauthored
Add string-content rule (#496)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 49c4acf commit 0972a89

File tree

9 files changed

+464
-8
lines changed

9 files changed

+464
-8
lines changed

docs/rules/string-content.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Enforce better string content
2+
3+
Enforce certain things about the contents of strings. For example, you can enforce using `` instead of `'` to avoid escaping. Or you could block some words. The possibilities are endless.
4+
5+
This rule is fixable.
6+
7+
*It only reports one pattern per AST node at the time.*
8+
9+
## Fail
10+
11+
```js
12+
const foo = 'Someone\'s coming!';
13+
```
14+
15+
## Pass
16+
17+
```js
18+
const foo = 'Someone’s coming!';
19+
```
20+
21+
## Options
22+
23+
Type: `object`
24+
25+
### patterns
26+
27+
Type: `object`
28+
29+
Extend [default patterns](#default-pattern).
30+
31+
The example below:
32+
33+
- Disables the default `'``` replacement.
34+
- Adds a custom `unicorn``🦄` replacement.
35+
- Adds a custom `awesome``😎` replacement and a custom message.
36+
- Adds a custom `cool``😎` replacement, but disables auto fix.
37+
38+
```json
39+
{
40+
"unicorn/string-content": [
41+
"error",
42+
{
43+
"patterns": {
44+
"'": false,
45+
"unicorn": "🦄",
46+
"awesome": {
47+
"suggest": "😎",
48+
"message": "Please use `😎` instead of `awesome`."
49+
},
50+
"cool": {
51+
"suggest": "😎",
52+
"fix": false
53+
}
54+
}
55+
}
56+
]
57+
}
58+
```
59+
60+
The key of `patterns` is treated as a regex, so you must escape special characters.
61+
62+
For example, if you want to enforce `...```:
63+
64+
```json
65+
{
66+
"patterns": {
67+
"\\.\\.\\.": ""
68+
}
69+
}
70+
```
71+
72+
## Default Pattern
73+
74+
```json
75+
{
76+
"'": ""
77+
}
78+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ module.exports = {
6464
'unicorn/prefer-trim-start-end': 'error',
6565
'unicorn/prefer-type-error': 'error',
6666
'unicorn/prevent-abbreviations': 'error',
67+
'unicorn/string-content': 'off',
6768
'unicorn/throw-new-error': 'error'
6869
}
6970
}

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Configure it in `package.json`.
7979
"unicorn/prefer-trim-start-end": "error",
8080
"unicorn/prefer-type-error": "error",
8181
"unicorn/prevent-abbreviations": "error",
82+
"unicorn/string-content": "off",
8283
"unicorn/throw-new-error": "error"
8384
}
8485
}
@@ -132,6 +133,7 @@ Configure it in `package.json`.
132133
- [prefer-trim-start-end](docs/rules/prefer-trim-start-end.md) - Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. *(fixable)*
133134
- [prefer-type-error](docs/rules/prefer-type-error.md) - Enforce throwing `TypeError` in type checking conditions. *(fixable)*
134135
- [prevent-abbreviations](docs/rules/prevent-abbreviations.md) - Prevent abbreviations. *(partly fixable)*
136+
- [string-content](docs/rules/string-content.md) - Enforce better string content. *(fixable)*
135137
- [throw-new-error](docs/rules/throw-new-error.md) - Require `new` when throwing an error. *(fixable)*
136138

137139
## Deprecated Rules

rules/better-regex.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,17 @@ const create = context => {
6262
const newPattern = cleanRegexp(oldPattern, flags);
6363

6464
if (oldPattern !== newPattern) {
65-
// Escape backslash
66-
const fixed = quoteString(newPattern.replace(/\\/g, '\\\\'));
67-
6865
context.report({
6966
node,
7067
message,
7168
data: {
7269
original: oldPattern,
7370
optimized: newPattern
7471
},
75-
fix: fixer => fixer.replaceText(patternNode, fixed)
72+
fix: fixer => fixer.replaceText(
73+
patternNode,
74+
quoteString(newPattern)
75+
)
7676
});
7777
}
7878
}

rules/string-content.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use strict';
2+
const getDocumentationUrl = require('./utils/get-documentation-url');
3+
const quoteString = require('./utils/quote-string');
4+
const replaceTemplateElement = require('./utils/replace-template-element');
5+
const escapeTemplateElementRaw = require('./utils/escape-template-element-raw');
6+
7+
const defaultPatterns = {
8+
'\'': '’'
9+
};
10+
11+
const defaultMessage = 'Prefer `{{suggest}}` over `{{match}}`.';
12+
13+
function getReplacements(patterns) {
14+
return Object.entries({
15+
...defaultPatterns,
16+
...patterns
17+
})
18+
.filter(([, options]) => options !== false)
19+
.map(([match, options]) => {
20+
if (typeof options === 'string') {
21+
options = {
22+
suggest: options
23+
};
24+
}
25+
26+
return {
27+
match,
28+
regex: new RegExp(match, 'gu'),
29+
fix: true,
30+
...options
31+
};
32+
});
33+
}
34+
35+
const create = context => {
36+
const {patterns} = {
37+
patterns: {},
38+
...context.options[0]
39+
};
40+
const replacements = getReplacements(patterns);
41+
42+
if (replacements.length === 0) {
43+
return {};
44+
}
45+
46+
return {
47+
'Literal, TemplateElement': node => {
48+
const {type} = node;
49+
50+
let string;
51+
if (type === 'Literal') {
52+
string = node.value;
53+
if (typeof string !== 'string') {
54+
return;
55+
}
56+
} else {
57+
string = node.value.raw;
58+
}
59+
60+
if (!string) {
61+
return;
62+
}
63+
64+
const replacement = replacements.find(({regex}) => regex.test(string));
65+
66+
if (!replacement) {
67+
return;
68+
}
69+
70+
const {fix, message = defaultMessage, match, suggest} = replacement;
71+
const problem = {
72+
node,
73+
message,
74+
data: {
75+
match,
76+
suggest
77+
}
78+
};
79+
80+
if (!fix) {
81+
context.report(problem);
82+
return;
83+
}
84+
85+
const fixed = string.replace(replacement.regex, suggest);
86+
if (type === 'Literal') {
87+
problem.fix = fixer => fixer.replaceText(
88+
node,
89+
quoteString(fixed, node.raw[0])
90+
);
91+
} else {
92+
problem.fix = fixer => replaceTemplateElement(
93+
fixer,
94+
node,
95+
escapeTemplateElementRaw(fixed)
96+
);
97+
}
98+
99+
context.report(problem);
100+
}
101+
};
102+
};
103+
104+
const schema = [
105+
{
106+
type: 'object',
107+
properties: {
108+
patterns: {
109+
type: 'object',
110+
additionalProperties: {
111+
anyOf: [
112+
{
113+
enum: [
114+
false
115+
]
116+
},
117+
{
118+
type: 'string'
119+
},
120+
{
121+
type: 'object',
122+
required: [
123+
'suggest'
124+
],
125+
properties: {
126+
suggest: {
127+
type: 'string'
128+
},
129+
fix: {
130+
type: 'boolean'
131+
// Default: true
132+
},
133+
message: {
134+
type: 'string'
135+
// Default: ''
136+
}
137+
},
138+
additionalProperties: false
139+
}
140+
]
141+
}}
142+
},
143+
additionalProperties: false
144+
}
145+
];
146+
147+
module.exports = {
148+
create,
149+
meta: {
150+
type: 'suggestion',
151+
docs: {
152+
url: getDocumentationUrl(__filename)
153+
},
154+
fixable: 'code',
155+
schema
156+
}
157+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
module.exports = string => string.replace(
4+
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g,
5+
'\\$<symbol>'
6+
);

rules/utils/quote-string.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
'use strict';
22

33
/**
4-
Escape apostrophe and wrap the result in single quotes.
4+
Escape string and wrap the result in quotes.
55
66
@param {string} string - The string to be quoted.
7-
@returns {string} - The quoted string.
7+
@param {string} quote - The quote character.
8+
@returns {string} - The quoted and escaped string.
89
*/
9-
module.exports = string => `'${string.replace(/'/g, '\\\'')}'`;
10+
module.exports = (string, quote = '\'') => {
11+
const escaped = string
12+
.replace(/\\/g, '\\\\')
13+
.replace(/\r/g, '\\r')
14+
.replace(/\n/g, '\\n')
15+
.replace(new RegExp(quote, 'g'), `\\${quote}`);
16+
return quote + escaped + quote;
17+
};

test/prefer-replace-all.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ ruleTester.run('prefer-replace-all', rule, {
8686
},
8787
{
8888
code: 'foo.replace(/\\\\\\./g, bar)',
89-
output: 'foo.replaceAll(\'\\.\', bar)',
89+
output: 'foo.replaceAll(\'\\\\.\', bar)',
9090
errors: [error]
9191
}
9292
]

0 commit comments

Comments
 (0)