diff --git a/docs/rules/catch-error-name.md b/docs/rules/catch-error-name.md index c415ac9391..35decc0c16 100644 --- a/docs/rules/catch-error-name.md +++ b/docs/rules/catch-error-name.md @@ -1,12 +1,11 @@ # Enforce a specific parameter name in catch clauses -Applies to both `try/catch` clauses and `promise.catch(...)` handlers. +Applies to both `try/catch` clauses and `promise.catch(…)` handlers. The desired name is configurable, but defaults to `error`. This rule is fixable unless the reported code was destructuring an error. - ## Fail ```js @@ -21,7 +20,6 @@ try { somePromise.catch(e => {}) ``` - ## Pass ```js @@ -74,7 +72,6 @@ somePromise.catch(_ => { }); ``` - ## Options ### name @@ -101,7 +98,7 @@ You can set the `name` option like this: ] ``` -This option lets you specify a regex pattern for matches to ignore. The default is `^_$`. +This option lets you specify a regex pattern for matches to ignore. The default allows `_` and descriptive names like `networkError`. With `^unicorn$`, this would fail: @@ -122,3 +119,7 @@ try { // … } ``` + +## Tip + +In order to avoid shadowing in nested catch clauses, the auto-fix rule appends underscores to the identifier name. Since this might be hard to read, the default setting for `caughtErrorsIgnorePattern` allows the use of descriptive names instead, for example, `fsError` or `authError`. diff --git a/docs/rules/string-content.md b/docs/rules/string-content.md new file mode 100644 index 0000000000..fd3e0909c0 --- /dev/null +++ b/docs/rules/string-content.md @@ -0,0 +1,85 @@ +# 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.* + +This rule ignores the following tagged template literals as they're known to contain code: + +- ``gql`…` `` +- ``html`…` `` +- ``svg`…` `` +- ``styled.*`…` `` + +## 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 +{ + "'": "’" +} +``` diff --git a/index.js b/index.js index dd0c36d826..b6b74f212d 100644 --- a/index.js +++ b/index.js @@ -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' } } diff --git a/package.json b/package.json index 1ebb1b8a6f..711e70f60e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-unicorn", - "version": "16.1.1", + "version": "17.1.0", "description": "Various awesome ESLint rules", "license": "MIT", "repository": "sindresorhus/eslint-plugin-unicorn", diff --git a/readme.md b/readme.md index 7cc0a5a0ab..775c5a1014 100644 --- a/readme.md +++ b/readme.md @@ -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" } } @@ -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 diff --git a/rules/better-regex.js b/rules/better-regex.js index 2b4fbfd9ad..22b635ce6b 100644 --- a/rules/better-regex.js +++ b/rules/better-regex.js @@ -62,9 +62,6 @@ const create = context => { const newPattern = cleanRegexp(oldPattern, flags); if (oldPattern !== newPattern) { - // Escape backslash - const fixed = quoteString(newPattern.replace(/\\/g, '\\\\')); - context.report({ node, message, @@ -72,7 +69,10 @@ const create = context => { original: oldPattern, optimized: newPattern }, - fix: fixer => fixer.replaceText(patternNode, fixed) + fix: fixer => fixer.replaceText( + patternNode, + quoteString(newPattern) + ) }); } } diff --git a/rules/catch-error-name.js b/rules/catch-error-name.js index 0dc12ef6d2..89e6263625 100644 --- a/rules/catch-error-name.js +++ b/rules/catch-error-name.js @@ -34,7 +34,7 @@ const create = context => { const options = { name: 'error', - caughtErrorsIgnorePattern: '^_$', + caughtErrorsIgnorePattern: /^_$|^[\dA-Za-z]+(e|E)rror$/.source, ...context.options[0] }; diff --git a/rules/consistent-function-scoping.js b/rules/consistent-function-scoping.js index af18ab45dc..a8992a40e8 100644 --- a/rules/consistent-function-scoping.js +++ b/rules/consistent-function-scoping.js @@ -1,8 +1,8 @@ 'use strict'; const getDocumentationUrl = require('./utils/get-documentation-url'); -const MESSAGE_ID_ARROW = 'ArrowFunctionExpression'; -const MESSAGE_ID_FUNCTION = 'FunctionDeclaration'; +const MESSAGE_ID_NAMED = 'named'; +const MESSAGE_ID_ANONYMOUS = 'anonymous'; const getReferences = scope => scope.references.concat( ...scope.childScopes.map(scope => getReferences(scope)) @@ -12,7 +12,22 @@ const isSameScope = (scope1, scope2) => scope1 && scope2 && (scope1 === scope2 || scope1.block === scope2.block); function checkReferences(scope, parent, scopeManager) { - const hitReference = references => references.some(reference => isSameScope(parent, reference.from)); + const hitReference = references => references.some(reference => { + if (isSameScope(parent, reference.from)) { + return true; + } + + const {resolved} = reference; + const [definition] = resolved.defs; + + // Skip recursive function name + if (definition && definition.type === 'FunctionName' && resolved.name === definition.name.name) { + return false; + } + + return isSameScope(parent, resolved.scope); + }); + const hitDefinitions = definitions => definitions.some(definition => { const scope = scopeManager.acquire(definition.node); return isSameScope(parent, scope); @@ -110,9 +125,26 @@ const create = context => { }, ':matches(ArrowFunctionExpression, FunctionDeclaration):exit': node => { if (!hasJsx && !checkNode(node, scopeManager)) { + const functionType = node.type === 'ArrowFunctionExpression' ? 'arrow function' : 'function'; + let functionName = ''; + if (node.id) { + functionName = node.id.name; + } else if ( + node.parent && + node.parent.type === 'VariableDeclarator' && + node.parent.id && + node.parent.id.type === 'Identifier' + ) { + functionName = node.parent.id.name; + } + context.report({ node, - messageId: node.type + messageId: functionName ? MESSAGE_ID_NAMED : MESSAGE_ID_ANONYMOUS, + data: { + functionType, + functionName + } }); } @@ -132,8 +164,8 @@ module.exports = { url: getDocumentationUrl(__filename) }, messages: { - [MESSAGE_ID_ARROW]: 'Move arrow function to the outer scope.', - [MESSAGE_ID_FUNCTION]: 'Move function to the outer scope.' + [MESSAGE_ID_NAMED]: 'Move {{functionType}} `{{functionName}}` to the outer scope.', + [MESSAGE_ID_ANONYMOUS]: 'Move {{functionType}} to the outer scope.' } } }; diff --git a/rules/prefer-exponentiation-operator.js b/rules/prefer-exponentiation-operator.js index 42f15aef6e..7567daa04a 100644 --- a/rules/prefer-exponentiation-operator.js +++ b/rules/prefer-exponentiation-operator.js @@ -1,7 +1,10 @@ 'use strict'; const getDocumentationUrl = require('./utils/get-documentation-url'); +const create = () => ({}); + module.exports = { + create, meta: { type: 'suggestion', docs: { diff --git a/rules/prefer-spread.js b/rules/prefer-spread.js index 1809b97b00..e00d63ab11 100644 --- a/rules/prefer-spread.js +++ b/rules/prefer-spread.js @@ -14,7 +14,32 @@ const selector = [ ].join(''); const create = context => { - const getSource = node => context.getSourceCode().getText(node); + const sourceCode = context.getSourceCode(); + const getSource = node => sourceCode.getText(node); + + const needsSemicolon = node => { + const tokenBefore = sourceCode.getTokenBefore(node); + + if (tokenBefore) { + const {type, value} = tokenBefore; + if (type === 'Punctuator') { + if (value === ';') { + return false; + } + + if (value === ']' || value === ')') { + return true; + } + } + + const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]); + if (lastBlockNode && lastBlockNode.type === 'ObjectExpression') { + return true; + } + } + + return false; + }; return { [selector](node) { @@ -23,7 +48,7 @@ const create = context => { message: 'Prefer the spread operator over `Array.from()`.', fix: fixer => { const [arrayLikeArgument, mapFn, thisArgument] = node.arguments.map(getSource); - let replacement = `[...${arrayLikeArgument}]`; + let replacement = `${needsSemicolon(node) ? ';' : ''}[...${arrayLikeArgument}]`; if (mapFn) { const mapArguments = [mapFn, thisArgument].filter(Boolean); diff --git a/rules/regex-shorthand.js b/rules/regex-shorthand.js index 8229c0da7c..9ccb9232a0 100644 --- a/rules/regex-shorthand.js +++ b/rules/regex-shorthand.js @@ -1,7 +1,10 @@ 'use strict'; const getDocumentationUrl = require('./utils/get-documentation-url'); +const create = () => ({}); + module.exports = { + create, meta: { type: 'suggestion', docs: { diff --git a/rules/string-content.js b/rules/string-content.js new file mode 100644 index 0000000000..64ea86ce4d --- /dev/null +++ b/rules/string-content.js @@ -0,0 +1,191 @@ +'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 ignoredIdentifier = new Set([ + 'gql', + 'html', + 'svg' +]); + +const ignoredMemberExpressionObject = new Set([ + 'styled' +]); + +const isIgnoredTag = node => { + if (!node.parent || !node.parent.parent || !node.parent.parent.tag) { + return false; + } + + const {tag} = node.parent.parent; + + if (tag.type === 'Identifier' && ignoredIdentifier.has(tag.name)) { + return true; + } + + if (tag.type === 'MemberExpression') { + const {object} = tag; + if ( + object.type === 'Identifier' && + ignoredMemberExpressionObject.has(object.name) + ) { + return true; + } + } + + return false; +}; + +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 if (!isIgnoredTag(node)) { + 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 + } +}; diff --git a/rules/utils/escape-template-element-raw.js b/rules/utils/escape-template-element-raw.js new file mode 100644 index 0000000000..2f9c7e8a39 --- /dev/null +++ b/rules/utils/escape-template-element-raw.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = string => string.replace( + /(?<=(?:^|[^\\])(?:\\\\)*)(?(?:`|\$(?={)))/g, + '\\$' +); diff --git a/rules/utils/quote-string.js b/rules/utils/quote-string.js index eee76eb6f3..40186423e6 100644 --- a/rules/utils/quote-string.js +++ b/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; +}; diff --git a/test/catch-error-name.js b/test/catch-error-name.js index b67c1003ad..170a853590 100644 --- a/test/catch-error-name.js +++ b/test/catch-error-name.js @@ -34,7 +34,8 @@ ruleTester.run('catch-error-name', rule, { } } `), - testCase(outdent` + testCase( + outdent` const handleError = err => { try { doSomething(); @@ -42,7 +43,9 @@ ruleTester.run('catch-error-name', rule, { console.log(err_); } } - `, 'err'), + `, + 'err' + ), testCase(outdent` const handleError = error => { const error_ = new Error('🦄'); @@ -60,11 +63,14 @@ ruleTester.run('catch-error-name', rule, { obj.catch(error_ => { }); } `), - testCase(outdent` + testCase( + outdent` const handleError = err => { obj.catch(err_ => { }); } - `, 'err'), + `, + 'err' + ), testCase(outdent` const handleError = error => { const error_ = new Error('foo bar'); @@ -91,11 +97,15 @@ ruleTester.run('catch-error-name', rule, { testCase('obj.catch((_) => {})'), testCase('obj.catch((_) => { console.log(foo); })'), testCase('obj.catch(err => {})', 'err'), - testCase('obj.catch(outerError => { return obj2.catch(innerError => {}) })'), + testCase( + 'obj.catch(outerError => { return obj2.catch(innerError => {}) })' + ), testCase('obj.catch(function (error) {})'), testCase('obj.catch(function () {})'), testCase('obj.catch(function (err) {})', 'err'), - testCase('obj.catch(function (outerError) { return obj2.catch(function (innerError) {}) })'), + testCase( + 'obj.catch(function (outerError) { return obj2.catch(function (innerError) {}) })' + ), testCase('obj.catch()'), testCase('obj.catch(_ => { console.log(_); })'), testCase('obj.catch(function (_) { console.log(_); })'), @@ -129,21 +139,76 @@ ruleTester.run('catch-error-name', rule, { } catch { console.log('failed'); } - `) + `), + testCase('try {} catch (descriptiveError) {}'), + testCase('try {} catch (descriptiveerror) {}') ], invalid: [ - testCase('try {} catch (err) { console.log(err) }', null, true, 'try {} catch (error) { console.log(error) }'), - testCase('try {} catch (error) { console.log(error) }', 'err', true, 'try {} catch (err) { console.log(err) }'), + testCase( + 'try {} catch (err) { console.log(err) }', + null, + true, + 'try {} catch (error) { console.log(error) }' + ), + testCase( + 'try {} catch (error) { console.log(error) }', + 'err', + true, + 'try {} catch (err) { console.log(err) }' + ), testCase('try {} catch ({message}) {}', null, true), - testCase('try {} catch (outerError) {}', null, true, 'try {} catch (error) {}'), - testCase('try {} catch (innerError) {}', null, true, 'try {} catch (error) {}'), + { + code: 'try {} catch (outerError) {}', + output: 'try {} catch (error) {}', + errors: [ + { + ruleId: 'catch-error-message', + message: 'The catch parameter should be named `error`.' + } + ], + options: [ + { + caughtErrorsIgnorePattern: '^_$' + } + ] + }, + { + code: 'try {} catch (innerError) {}', + output: 'try {} catch (error) {}', + errors: [ + { + ruleId: 'catch-error-name', + message: 'The catch parameter should be named `error`.' + } + ], + options: [ + { + caughtErrorsIgnorePattern: '^_$' + } + ] + }, testCase('obj.catch(err => err)', null, true, 'obj.catch(error => error)'), - testCase('obj.catch(error => error.stack)', 'err', true, 'obj.catch(err => err.stack)'), + testCase( + 'obj.catch(error => error.stack)', + 'err', + true, + 'obj.catch(err => err.stack)' + ), testCase('obj.catch(({message}) => {})', null, true), - testCase('obj.catch(function (err) { console.log(err) })', null, true, 'obj.catch(function (error) { console.log(error) })'), + testCase( + 'obj.catch(function (err) { console.log(err) })', + null, + true, + 'obj.catch(function (error) { console.log(error) })' + ), testCase('obj.catch(function ({message}) {})', null, true), - testCase('obj.catch(function (error) { console.log(error) })', 'err', true, 'obj.catch(function (err) { console.log(err) })'), + testCase( + 'obj.catch(function (error) { console.log(error) })', + 'err', + true, + 'obj.catch(function (err) { console.log(err) })' + ), // Failing tests for #107 // testCase(outdent` // foo.then(() => { @@ -205,6 +270,11 @@ ruleTester.run('catch-error-name', rule, { ruleId: 'catch-error-name', message: 'The catch parameter should be named `error_`.' } + ], + options: [ + { + caughtErrorsIgnorePattern: '^_$' + } ] }, { @@ -295,10 +365,7 @@ ruleTester.run('catch-error-name', rule, { obj.catch(error => {}); obj.catch(error => {}); `, - errors: [ - {ruleId: 'catch-error-name'}, - {ruleId: 'catch-error-name'} - ] + errors: [{ruleId: 'catch-error-name'}, {ruleId: 'catch-error-name'}] }, { code: 'try {} catch (_error) {}', @@ -333,6 +400,11 @@ ruleTester.run('catch-error-name', rule, { ruleId: 'catch-error-message', message: 'The catch parameter should be named `error`.' } + ], + options: [ + { + caughtErrorsIgnorePattern: '^_$' + } ] } ] diff --git a/test/consistent-function-scoping.js b/test/consistent-function-scoping.js index 49a00a557a..dbe565d7eb 100644 --- a/test/consistent-function-scoping.js +++ b/test/consistent-function-scoping.js @@ -17,15 +17,18 @@ const typescriptRuleTester = avaRuleTester(test, { parser: require.resolve('@typescript-eslint/parser') }); -const arrowError = { - ruleId: 'consistent-function-scoping', - messageId: 'ArrowFunctionExpression' -}; +const ruleId = 'consistent-function-scoping'; +const MESSAGE_ID_NAMED = 'named'; +const MESSAGE_ID_ANONYMOUS = 'anonymous'; -const functionError = { - ruleId: 'consistent-function-scoping', - messageId: 'FunctionDeclaration' -}; +const createError = ({name, arrow}) => ({ + ruleId, + messageId: name ? MESSAGE_ID_NAMED : MESSAGE_ID_ANONYMOUS, + data: { + functionType: arrow ? 'arrow function' : 'function', + functionName: name + } +}); ruleTester.run('consistent-function-scoping', rule, { valid: [ @@ -244,6 +247,18 @@ ruleTester.run('consistent-function-scoping', rule, { eventEmitter.once('error', onError2); }; + `, + // #586 + outdent` + function outer(stream) { + let content; + + function inner() { + process.stdout.write(content); + } + + inner(); + } ` ], invalid: [ @@ -256,7 +271,7 @@ ruleTester.run('consistent-function-scoping', rule, { return foo; } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -268,7 +283,7 @@ ruleTester.run('consistent-function-scoping', rule, { return foo; } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -278,7 +293,7 @@ ruleTester.run('consistent-function-scoping', rule, { } } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -288,13 +303,13 @@ ruleTester.run('consistent-function-scoping', rule, { } } `, - errors: [arrowError] + errors: [createError({name: 'doBar', arrow: true})] }, { code: outdent` const doFoo = () => bar => bar; `, - errors: [arrowError] + errors: [createError({arrow: true})] }, { code: outdent` @@ -305,7 +320,7 @@ ruleTester.run('consistent-function-scoping', rule, { return foo; } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -316,7 +331,7 @@ ruleTester.run('consistent-function-scoping', rule, { return doBar; } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -324,7 +339,7 @@ ruleTester.run('consistent-function-scoping', rule, { function doBar() {} } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -339,7 +354,7 @@ ruleTester.run('consistent-function-scoping', rule, { return foo; } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -351,7 +366,7 @@ ruleTester.run('consistent-function-scoping', rule, { } } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] }, { code: outdent` @@ -361,7 +376,7 @@ ruleTester.run('consistent-function-scoping', rule, { } } `, - errors: [functionError] + errors: [createError({name: 'doBar'})] } ] }); diff --git a/test/integration/test.js b/test/integration/test.js index 0477d2b09a..58bb0354f0 100755 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -100,9 +100,7 @@ const projects = [ 'https://github.com/sindresorhus/capture-website', 'https://github.com/sindresorhus/file-type', 'https://github.com/sindresorhus/slugify', - // TODO: add this project when #254 got fixed - // https://github.com/gatsbyjs/gatsby/blob/e720d8efe58eba0f6fae9f26ec8879128967d0b5/packages/gatsby/src/bootstrap/page-hot-reloader.js#L30 - // 'https://github.com/gatsbyjs/gatsby', + 'https://github.com/gatsbyjs/gatsby', { repository: 'https://github.com/puppeteer/puppeteer', path: 'lib' diff --git a/test/prefer-replace-all.js b/test/prefer-replace-all.js index 21b4e11fdd..93c748fc62 100644 --- a/test/prefer-replace-all.js +++ b/test/prefer-replace-all.js @@ -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] } ] diff --git a/test/prefer-spread.js b/test/prefer-spread.js index ec37c8cf2c..2c8b05ffa5 100644 --- a/test/prefer-spread.js +++ b/test/prefer-spread.js @@ -123,6 +123,54 @@ ruleTester.run('prefer-spread', rule, { } ], output: '[...document.querySelectorAll("*")].map(() => {});' + }, + // #254 + { + code: ` + const foo = [] + Array.from(arrayLike).forEach(doSomething) + `, + errors: [ + { + message: 'Prefer the spread operator over `Array.from()`.' + } + ], + output: ` + const foo = [] + ;[...arrayLike].forEach(doSomething) + ` + }, + // https://github.com/gatsbyjs/gatsby/blob/e720d8efe58eba0f6fae9f26ec8879128967d0b5/packages/gatsby/src/bootstrap/page-hot-reloader.js#L30 + { + code: ` + foo() + Array.from(arrayLike).forEach(doSomething) + `, + errors: [ + { + message: 'Prefer the spread operator over `Array.from()`.' + } + ], + output: ` + foo() + ;[...arrayLike].forEach(doSomething) + ` + }, + // https://github.com/gatsbyjs/gatsby/blob/4ab3f194cf5d6dcafcb2a75d9604aac79d963554/packages/gatsby/src/redux/__tests__/nodes.js#L277 + { + code: ` + const foo = {} + Array.from(arrayLike).forEach(doSomething) + `, + errors: [ + { + message: 'Prefer the spread operator over `Array.from()`.' + } + ], + output: ` + const foo = {} + ;[...arrayLike].forEach(doSomething) + ` } ] }); diff --git a/test/string-content.js b/test/string-content.js new file mode 100644 index 0000000000..4b603427ed --- /dev/null +++ b/test/string-content.js @@ -0,0 +1,231 @@ +import test from 'ava'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import rule from '../rules/string-content'; + +const ruleTester = avaRuleTester(test, { + env: { + es6: true + } +}); + +const patterns = { + unicorn: { + suggest: '🦄' + }, + awesome: { + suggest: '😎' + }, + quote: {suggest: '\'"'} +}; + +const createError = (match, suggest) => [ + { + message: `Prefer \`${suggest}\` over \`${match}\`.` + } +]; + +ruleTester.run('string-content', rule, { + valid: [ + // `Literal` string + 'const foo = \'🦄\';', + // Not `a string` + 'const foo = 0;', + // Not `Literal` + 'const foo = bar;', + // Disable default patterns + { + code: 'const foo = \'\\\'\'', + options: [{patterns: {'\'': false}}] + }, + /* eslint-disable no-template-curly-in-string */ + // `TemplateLiteral` + 'const foo = `🦄`', + // Should not escape + 'const foo = `\\`\\${1}`', + // Ignored + ` + const foo = gql\`{ + field(input: '...') + }\`; + `, + ` + const foo = styled.div\` + background: url('...') + \`; + `, + ` + const foo = html\` +
...
+ \`; + `, + ` + const foo = svg\` + + \`; + ` + /* eslint-enable no-template-curly-in-string */ + ], + invalid: [ + // `Literal` string + { + code: 'const foo = \'\\\'\'', + output: 'const foo = \'’\'', + errors: createError('\'', '’') + }, + // Custom patterns + { + code: 'const foo = \'unicorn\'', + output: 'const foo = \'🦄\'', + options: [{patterns}], + errors: createError('unicorn', '🦄') + }, + // Custom patterns should not override default patterns + { + code: 'const foo = \'\\\'\'', + output: 'const foo = \'’\'', + options: [{patterns}], + errors: createError('\'', '’') + }, + // Escape single quote + { + code: 'const foo = \'quote\'', + output: 'const foo = \'\\\'"\'', + options: [{patterns}], + errors: createError('quote', '\'"') + }, + { + code: 'const foo = \'\\\\quote\\\\\'', + output: 'const foo = \'\\\\\\\'"\\\\\'', + options: [{patterns}], + errors: createError('quote', '\'"') + }, + // Escape double quote + { + code: 'const foo = "quote"', + output: 'const foo = "\'\\""', + options: [{patterns}], + errors: createError('quote', '\'"') + }, + { + code: 'const foo = "\\\\quote\\\\"', + output: 'const foo = "\\\\\'\\"\\\\"', + options: [{patterns}], + errors: createError('quote', '\'"') + }, + // Not fix + { + code: 'const foo = "unicorn"', + options: [{patterns: {unicorn: {...patterns.unicorn, fix: false}}}], + errors: createError('unicorn', '🦄') + }, + // Conflict patterns + { + code: 'const foo = "a"', + output: 'const foo = "A"', + options: [{patterns: {a: 'A', A: 'a'}}], + errors: createError('a', 'A') + }, + { + code: 'const foo = "A"', + output: 'const foo = "a"', + options: [{patterns: {a: 'A', A: 'a'}}], + errors: createError('A', 'a') + }, + { + code: 'const foo = "aA"', + output: 'const foo = "AA"', + options: [{patterns: {a: 'A', A: 'a'}}], + errors: createError('a', 'A') + }, + { + code: 'const foo = "aA"', + output: 'const foo = "aa"', + options: [{patterns: {A: 'a', a: 'A'}}], + errors: createError('A', 'a') + }, + + // Escaped pattern + { + code: 'const foo = "foo.bar"', + output: 'const foo = "_______"', + options: [{patterns: {'.': '_'}}], // <- not escaped + errors: createError('.', '_') + }, + { + code: 'const foo = "foo.bar"', + output: 'const foo = "foo_bar"', + options: [{patterns: {'\\.': '_'}}], // <- escaped + errors: createError('\\.', '_') + }, + + // Custom message + { + code: 'const foo = "foo"', + output: 'const foo = "bar"', + options: [{patterns: {foo: {suggest: 'bar', message: '`bar` is better than `foo`.'}}}], + errors: [{message: '`bar` is better than `foo`.'}] + }, + + // Should not crash on multiline string + // https://github.com/avajs/ava/blob/7f99aef61f3aed2389ca9407115ad4c9aecada92/test/assert.js#L1477 + { + code: 'const foo = "\'\\n"', + output: 'const foo = "’\\n"', + options: [{patterns}], + errors: createError('\'', '’') + }, + // https://github.com/sindresorhus/execa/blob/df08cfb2d849adb31dc764ca3ab5f29e5b191d50/test/error.js#L20 + { + code: 'const foo = "\'\\r"', + output: 'const foo = "’\\r"', + options: [{patterns}], + errors: createError('\'', '’') + }, + + /* eslint-disable no-template-curly-in-string */ + // `TemplateLiteral` + { + code: 'const foo = `\'`', + output: 'const foo = `’`', + errors: createError('\'', '’') + }, + // `TemplateElement` position + { + code: 'const foo = `\'${foo}\'${foo}\'`', + output: 'const foo = `’${foo}’${foo}’`', + errors: Array.from({length: 3}).fill(createError('\'', '’')) + }, + // Escape + { + code: 'const foo = `foo_foo`', + output: 'const foo = `bar\\`bar_bar\\`bar`', + options: [{patterns: {foo: 'bar`bar'}}], + errors: createError('foo', 'bar`bar') + }, + { + code: 'const foo = `foo_foo`', + output: 'const foo = `\\${bar}_\\${bar}`', + options: [{patterns: {foo: '${bar}'}}], + errors: createError('foo', '${bar}') + }, + { + code: 'const foo = `$foo`', // <-- not escaped $ + output: 'const foo = `\\${bar}`', + options: [{patterns: {foo: '{bar}'}}], + errors: createError('foo', '{bar}') + }, + { + code: 'const foo = `\\\\$foo`', // <-- escaped $ + output: 'const foo = `\\\\\\${bar}`', + options: [{patterns: {foo: '{bar}'}}], + errors: createError('foo', '{bar}') + }, + // Not ignored tag + { + code: 'const foo = notIgnoredTag`\'`', + output: 'const foo = notIgnoredTag`’`', + errors: createError('\'', '’') + } + /* eslint-enable no-template-curly-in-string */ + ] +});