Skip to content

Commit

Permalink
Add string-content rule (#496)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fisker and sindresorhus committed Mar 8, 2020
1 parent 49c4acf commit 0972a89
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 8 deletions.
78 changes: 78 additions & 0 deletions docs/rules/string-content.md
@@ -0,0 +1,78 @@
# Enforce better string content

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.

This rule is fixable.

*It only reports one pattern per AST node at the time.*

## Fail

```js
const foo = 'Someone\'s coming!';
```

## Pass

```js
const foo = 'Someone’s coming!';
```

## Options

Type: `object`

### patterns

Type: `object`

Extend [default patterns](#default-pattern).

The example below:

- Disables the default `'``` replacement.
- Adds a custom `unicorn``🦄` replacement.
- Adds a custom `awesome``😎` replacement and a custom message.
- Adds a custom `cool``😎` replacement, but disables auto fix.

```json
{
"unicorn/string-content": [
"error",
{
"patterns": {
"'": false,
"unicorn": "🦄",
"awesome": {
"suggest": "😎",
"message": "Please use `😎` instead of `awesome`."
},
"cool": {
"suggest": "😎",
"fix": false
}
}
}
]
}
```

The key of `patterns` is treated as a regex, so you must escape special characters.

For example, if you want to enforce `...```:

```json
{
"patterns": {
"\\.\\.\\.": ""
}
}
```

## Default Pattern

```json
{
"'": ""
}
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -64,6 +64,7 @@ module.exports = {
'unicorn/prefer-trim-start-end': 'error',
'unicorn/prefer-type-error': 'error',
'unicorn/prevent-abbreviations': 'error',
'unicorn/string-content': 'off',
'unicorn/throw-new-error': 'error'
}
}
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -79,6 +79,7 @@ Configure it in `package.json`.
"unicorn/prefer-trim-start-end": "error",
"unicorn/prefer-type-error": "error",
"unicorn/prevent-abbreviations": "error",
"unicorn/string-content": "off",
"unicorn/throw-new-error": "error"
}
}
Expand Down Expand Up @@ -132,6 +133,7 @@ Configure it in `package.json`.
- [prefer-trim-start-end](docs/rules/prefer-trim-start-end.md) - Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. *(fixable)*
- [prefer-type-error](docs/rules/prefer-type-error.md) - Enforce throwing `TypeError` in type checking conditions. *(fixable)*
- [prevent-abbreviations](docs/rules/prevent-abbreviations.md) - Prevent abbreviations. *(partly fixable)*
- [string-content](docs/rules/string-content.md) - Enforce better string content. *(fixable)*
- [throw-new-error](docs/rules/throw-new-error.md) - Require `new` when throwing an error. *(fixable)*

## Deprecated Rules
Expand Down
8 changes: 4 additions & 4 deletions rules/better-regex.js
Expand Up @@ -62,17 +62,17 @@ const create = context => {
const newPattern = cleanRegexp(oldPattern, flags);

if (oldPattern !== newPattern) {
// Escape backslash
const fixed = quoteString(newPattern.replace(/\\/g, '\\\\'));

context.report({
node,
message,
data: {
original: oldPattern,
optimized: newPattern
},
fix: fixer => fixer.replaceText(patternNode, fixed)
fix: fixer => fixer.replaceText(
patternNode,
quoteString(newPattern)
)
});
}
}
Expand Down
157 changes: 157 additions & 0 deletions rules/string-content.js
@@ -0,0 +1,157 @@
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const quoteString = require('./utils/quote-string');
const replaceTemplateElement = require('./utils/replace-template-element');
const escapeTemplateElementRaw = require('./utils/escape-template-element-raw');

const defaultPatterns = {
'\'': '’'
};

const defaultMessage = 'Prefer `{{suggest}}` over `{{match}}`.';

function getReplacements(patterns) {
return Object.entries({
...defaultPatterns,
...patterns
})
.filter(([, options]) => options !== false)
.map(([match, options]) => {
if (typeof options === 'string') {
options = {
suggest: options
};
}

return {
match,
regex: new RegExp(match, 'gu'),
fix: true,
...options
};
});
}

const create = context => {
const {patterns} = {
patterns: {},
...context.options[0]
};
const replacements = getReplacements(patterns);

if (replacements.length === 0) {
return {};
}

return {
'Literal, TemplateElement': node => {
const {type} = node;

let string;
if (type === 'Literal') {
string = node.value;
if (typeof string !== 'string') {
return;
}
} else {
string = node.value.raw;
}

if (!string) {
return;
}

const replacement = replacements.find(({regex}) => regex.test(string));

if (!replacement) {
return;
}

const {fix, message = defaultMessage, match, suggest} = replacement;
const problem = {
node,
message,
data: {
match,
suggest
}
};

if (!fix) {
context.report(problem);
return;
}

const fixed = string.replace(replacement.regex, suggest);
if (type === 'Literal') {
problem.fix = fixer => fixer.replaceText(
node,
quoteString(fixed, node.raw[0])
);
} else {
problem.fix = fixer => replaceTemplateElement(
fixer,
node,
escapeTemplateElementRaw(fixed)
);
}

context.report(problem);
}
};
};

const schema = [
{
type: 'object',
properties: {
patterns: {
type: 'object',
additionalProperties: {
anyOf: [
{
enum: [
false
]
},
{
type: 'string'
},
{
type: 'object',
required: [
'suggest'
],
properties: {
suggest: {
type: 'string'
},
fix: {
type: 'boolean'
// Default: true
},
message: {
type: 'string'
// Default: ''
}
},
additionalProperties: false
}
]
}}
},
additionalProperties: false
}
];

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
schema
}
};
6 changes: 6 additions & 0 deletions rules/utils/escape-template-element-raw.js
@@ -0,0 +1,6 @@
'use strict';

module.exports = string => string.replace(
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g,
'\\$<symbol>'
);
14 changes: 11 additions & 3 deletions rules/utils/quote-string.js
@@ -1,9 +1,17 @@
'use strict';

/**
Escape apostrophe and wrap the result in single quotes.
Escape string and wrap the result in quotes.
@param {string} string - The string to be quoted.
@returns {string} - The quoted string.
@param {string} quote - The quote character.
@returns {string} - The quoted and escaped string.
*/
module.exports = string => `'${string.replace(/'/g, '\\\'')}'`;
module.exports = (string, quote = '\'') => {
const escaped = string
.replace(/\\/g, '\\\\')
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(new RegExp(quote, 'g'), `\\${quote}`);
return quote + escaped + quote;
};
2 changes: 1 addition & 1 deletion test/prefer-replace-all.js
Expand Up @@ -86,7 +86,7 @@ ruleTester.run('prefer-replace-all', rule, {
},
{
code: 'foo.replace(/\\\\\\./g, bar)',
output: 'foo.replaceAll(\'\\.\', bar)',
output: 'foo.replaceAll(\'\\\\.\', bar)',
errors: [error]
}
]
Expand Down

0 comments on commit 0972a89

Please sign in to comment.