diff --git a/docs/rules/prefer-nullish-coalescing.md b/docs/rules/prefer-nullish-coalescing.md new file mode 100644 index 0000000000..4a157eacab --- /dev/null +++ b/docs/rules/prefer-nullish-coalescing.md @@ -0,0 +1,27 @@ +# Prefer the nullish coalescing operator(`??`) over the logical OR operator(`||`) + +The [nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) only coalesces when the value is `null` or `undefined`, it is safer than [logical OR operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR) which coalesces on any [falsy value](https://developer.mozilla.org/en-US/docs/Glossary/Falsy). + +## Fail + +```js +const foo = bar || value; +``` + +```js +foo ||= value; +``` + +```js +if (foo || value) {} +``` + +## Pass + +```js +const foo = bar ?? value; +``` + +```js +foo ??= value; +``` diff --git a/index.js b/index.js index ec977e5188..9bacd38ad4 100644 --- a/index.js +++ b/index.js @@ -102,6 +102,8 @@ module.exports = { 'unicorn/prefer-module': 'error', 'unicorn/prefer-negative-index': 'error', 'unicorn/prefer-node-protocol': 'error', + // TODO: Enable this by default when targeting Node.js 14. + 'unicorn/prefer-nullish-coalescing': 'off', 'unicorn/prefer-number-properties': 'error', // TODO: Enable this by default when targeting a Node.js version that supports `Object.hasOwn`. 'unicorn/prefer-object-has-own': 'off', diff --git a/readme.md b/readme.md index 6dcfdee20e..a986972b1a 100644 --- a/readme.md +++ b/readme.md @@ -98,6 +98,7 @@ Configure it in `package.json`. "unicorn/prefer-module": "error", "unicorn/prefer-negative-index": "error", "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-nullish-coalescing": "off", "unicorn/prefer-number-properties": "error", "unicorn/prefer-object-has-own": "off", "unicorn/prefer-optional-catch-binding": "error", @@ -202,6 +203,7 @@ Each rule has emojis denoting: | [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ | 🔧 | 💡 | | [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`. | ✅ | 🔧 | | | [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | ✅ | 🔧 | | +| [prefer-nullish-coalescing](docs/rules/prefer-nullish-coalescing.md) | Prefer the nullish coalescing operator(`??`) over the logical OR operator(`||`). | | | 💡 | | [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ | 🔧 | 💡 | | [prefer-object-has-own](docs/rules/prefer-object-has-own.md) | Prefer `Object.hasOwn(…)` over `Object.prototype.hasOwnProperty.call(…)`. | | 🔧 | | | [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 | | diff --git a/rules/better-regex.js b/rules/better-regex.js index 92acb6a081..13286a6bfc 100644 --- a/rules/better-regex.js +++ b/rules/better-regex.js @@ -15,7 +15,7 @@ const newRegExp = [ ].join(''); const create = context => { - const {sortCharacterClasses} = context.options[0] || {}; + const {sortCharacterClasses} = context.options[0] ?? {}; const ignoreList = []; diff --git a/rules/custom-error-definition.js b/rules/custom-error-definition.js index e376457c0f..40dee14785 100644 --- a/rules/custom-error-definition.js +++ b/rules/custom-error-definition.js @@ -133,7 +133,7 @@ function * customErrorDefinition(context, node) { yield fixer.insertTextAfterRange([ superExpression.range[0], superExpression.range[0] + 6 - ], rhs.raw || rhs.name); + ], rhs.raw ?? rhs.name); } yield fixer.removeRange([ diff --git a/rules/expiring-todo-comments.js b/rules/expiring-todo-comments.js index 7d7a423169..517005ca3e 100644 --- a/rules/expiring-todo-comments.js +++ b/rules/expiring-todo-comments.js @@ -87,7 +87,7 @@ function parseTodoWithArguments(string, {terms}) { function createArgumentGroup(arguments_) { const groups = {}; for (const {value, type} of arguments_) { - groups[type] = groups[type] || []; + groups[type] = groups[type] ?? []; groups[type].push(value); } @@ -226,7 +226,7 @@ function tryToCoerceVersion(rawVersion) { try { // Try to semver.parse a perfect match while semver.coerce tries to fix errors // But coerce can't parse pre-releases. - return semver.parse(version) || semver.coerce(version); + return semver.parse(version) ?? semver.coerce(version); } catch { /* istanbul ignore next: We don't have this `package.json` to test */ return false; @@ -420,7 +420,7 @@ const create = context => { } } - const packageEngines = packageJson.engines || {}; + const packageEngines = packageJson.engines ?? {}; for (const engine of engines) { uses++; diff --git a/rules/filename-case.js b/rules/filename-case.js index 06196a4140..a7375c2f02 100644 --- a/rules/filename-case.js +++ b/rules/filename-case.js @@ -98,7 +98,7 @@ function fixFilename(words, caseFunctions, {leading, extension}) { const leadingUnderscoresRegex = /^(?_+)(?.*)$/; function splitFilename(filename) { - const result = leadingUnderscoresRegex.exec(filename) || {groups: {}}; + const result = leadingUnderscoresRegex.exec(filename) ?? {groups: {}}; const {leading = '', tailing = filename} = result.groups; const words = []; @@ -143,9 +143,9 @@ function englishishJoinWords(words) { } const create = context => { - const options = context.options[0] || {}; + const options = context.options[0] ?? {}; const chosenCases = getChosenCases(options); - const ignore = (options.ignore || []).map(item => { + const ignore = (options.ignore ?? []).map(item => { if (item instanceof RegExp) { return item; } diff --git a/rules/fix/remove-argument.js b/rules/fix/remove-argument.js index 90e90eb0c9..1b384b055f 100644 --- a/rules/fix/remove-argument.js +++ b/rules/fix/remove-argument.js @@ -6,8 +6,8 @@ function removeArgument(fixer, node, sourceCode) { const callExpression = node.parent; const index = callExpression.arguments.indexOf(node); const parentheses = getParentheses(node, sourceCode); - const firstToken = parentheses[0] || node; - const lastToken = parentheses[parentheses.length - 1] || node; + const firstToken = parentheses[0] ?? node; + const lastToken = parentheses[parentheses.length - 1] ?? node; let [start] = firstToken.range; let [, end] = lastToken.range; diff --git a/rules/import-index.js b/rules/import-index.js index e663626155..2c8d5800f2 100644 --- a/rules/import-index.js +++ b/rules/import-index.js @@ -21,7 +21,7 @@ const importIndex = (context, node, argument) => { }; const create = context => { - const options = context.options[0] || {}; + const options = context.options[0] ?? {}; const rules = { [STATIC_REQUIRE_SELECTOR]: node => importIndex(context, node, node.arguments[0]) diff --git a/rules/no-array-push-push.js b/rules/no-array-push-push.js index 4ed0088f11..6a53e6c1da 100644 --- a/rules/no-array-push-push.js +++ b/rules/no-array-push-push.js @@ -21,7 +21,7 @@ const selector = `${arrayPushExpressionStatement} + ${arrayPushExpressionStateme function getFirstExpression(node, sourceCode) { const {parent} = node; - const visitorKeys = sourceCode.visitorKeys[parent.type] || Object.keys(parent); + const visitorKeys = sourceCode.visitorKeys[parent.type] ?? Object.keys(parent); for (const property of visitorKeys) { const value = parent[property]; diff --git a/rules/no-for-loop.js b/rules/no-for-loop.js index 133714d0af..01dd65ded4 100644 --- a/rules/no-for-loop.js +++ b/rules/no-for-loop.js @@ -255,7 +255,7 @@ const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) => { const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) => { return variables.some(variable => { return !variable.references.every(reference => { - return scopeContains(forScope, reference.from) || + return scopeContains(forScope, reference.from) ?? nodeContains(forStatement, reference.identifier); }); }); @@ -354,8 +354,8 @@ const create = context => { const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName); const index = indexIdentifierName; - const element = elementIdentifierName || - avoidCapture(singular(arrayIdentifierName) || defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); + const element = elementIdentifierName ?? + avoidCapture(singular(arrayIdentifierName) ?? defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); const array = arrayIdentifierName; let declarationElement = element; diff --git a/rules/no-keyword-prefix.js b/rules/no-keyword-prefix.js index 2cd3a5dac9..0d227643d7 100644 --- a/rules/no-keyword-prefix.js +++ b/rules/no-keyword-prefix.js @@ -11,7 +11,7 @@ const prepareOptions = ({ onlyCamelCase = true } = {}) => { return { - disallowedPrefixes: (disallowedPrefixes || [ + disallowedPrefixes: (disallowedPrefixes ?? [ 'new', 'class' ]), diff --git a/rules/no-unused-properties.js b/rules/no-unused-properties.js index 56544551f1..2d4fa45865 100644 --- a/rules/no-unused-properties.js +++ b/rules/no-unused-properties.js @@ -6,7 +6,7 @@ const messages = { }; const getDeclaratorOrPropertyValue = declaratorOrProperty => - declaratorOrProperty.init || + declaratorOrProperty.init ?? declaratorOrProperty.value; const isMemberExpressionCall = memberExpression => diff --git a/rules/prefer-add-event-listener.js b/rules/prefer-add-event-listener.js index 2a56202c1c..720db309e7 100644 --- a/rules/prefer-add-event-listener.js +++ b/rules/prefer-add-event-listener.js @@ -61,8 +61,8 @@ const isClearing = node => { }; const create = context => { - const options = context.options[0] || {}; - const excludedPackages = new Set(options.excludedPackages || ['koa', 'sax']); + const options = context.options[0] ?? {}; + const excludedPackages = new Set(options.excludedPackages ?? ['koa', 'sax']); let isDisabled; const nodeReturnsSomething = new WeakMap(); @@ -95,7 +95,7 @@ const create = context => { }, ReturnStatement(node) { - codePathInfo.returnsSomething = codePathInfo.returnsSomething || Boolean(node.argument); + codePathInfo.returnsSomething = codePathInfo.returnsSomething ?? Boolean(node.argument); }, 'AssignmentExpression:exit'(node) { diff --git a/rules/prefer-keyboard-event-key.js b/rules/prefer-keyboard-event-key.js index 65ef172ed0..4129764e55 100644 --- a/rules/prefer-keyboard-event-key.js +++ b/rules/prefer-keyboard-event-key.js @@ -84,7 +84,7 @@ const fix = node => fixer => { const isTestingEquality = operator === '==' || operator === '==='; const isRightValid = isTestingEquality && right.type === 'Literal' && typeof right.value === 'number'; // Either a meta key or a printable character - const keyCode = translateToKey[right.value] || String.fromCharCode(right.value); + const keyCode = translateToKey[right.value] ?? String.fromCharCode(right.value); // And if we recognize the `.keyCode` if (!isRightValid || !keyCode) { return; diff --git a/rules/prefer-nullish-coalescing.js b/rules/prefer-nullish-coalescing.js new file mode 100644 index 0000000000..66645ddac5 --- /dev/null +++ b/rules/prefer-nullish-coalescing.js @@ -0,0 +1,74 @@ +'use strict'; +const {getStaticValue} = require('eslint-utils'); +const {matches} = require('./selectors/index.js'); +const {isBooleanNode} = require('./utils/boolean.js'); + +const ERROR = 'error'; +const SUGGESTION = 'suggestion'; +const messages = { + [ERROR]: 'Prefer `{{replacement}}` over `{{original}}`.', + [SUGGESTION]: 'Use `{{replacement}}`' +}; + +const selector = matches([ + 'LogicalExpression[operator="||"]', + 'AssignmentExpression[operator="||="]' +]); + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + return { + [selector](node) { + if ( + [node.parent, node.left, node.right].some(node => node.type === 'LogicalExpression') || + isBooleanNode(node) || + isBooleanNode(node.left) + ) { + return; + } + + const {left} = node; + + const staticValue = getStaticValue(left, context.getScope()); + if (staticValue) { + const {value} = staticValue; + if (!(typeof value === 'undefined' || value === null)) { + return; + } + } + + const isAssignment = node.type === 'AssignmentExpression'; + const originalOperator = isAssignment ? '||=' : '||'; + const replacementOperator = isAssignment ? '??=' : '??'; + const operatorToken = context.getSourceCode() + .getTokenAfter( + node.left, + token => token.type === 'Punctuator' && token.value === originalOperator + ); + + const messageData = { + original: originalOperator, + replacement: replacementOperator + }; + return { + node: operatorToken, + messageId: ERROR, + data: messageData, + fix: fixer => fixer.replaceText(operatorToken, replacementOperator) + }; + } + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer the nullish coalescing operator(`??`) over the logical OR operator(`||`).' + }, + messages, + hasSuggestions: true, + fixable: 'code' + } +}; diff --git a/rules/prefer-reflect-apply.js b/rules/prefer-reflect-apply.js index 46f1558b50..c4b3c75503 100644 --- a/rules/prefer-reflect-apply.js +++ b/rules/prefer-reflect-apply.js @@ -68,7 +68,7 @@ const create = context => { return { [selector]: node => { const sourceCode = context.getSourceCode(); - const fix = fixDirectApplyCall(node, sourceCode) || fixFunctionPrototypeCall(node, sourceCode); + const fix = fixDirectApplyCall(node, sourceCode) ?? fixFunctionPrototypeCall(node, sourceCode); if (fix) { return { node, diff --git a/rules/prefer-spread.js b/rules/prefer-spread.js index 9d329dfab3..7f2469e151 100644 --- a/rules/prefer-spread.js +++ b/rules/prefer-spread.js @@ -134,7 +134,7 @@ function fixConcat(node, sourceCode, fixableArguments) { text = `...${text}`; } - return text || ' '; + return text ?? ' '; }) .join(', '); @@ -388,7 +388,7 @@ const create = context => { fix: fixConcat( node, sourceCode, - node.arguments.map(node => getConcatArgumentSpreadable(node, scope) || {node, isSpreadable: true}) + node.arguments.map(node => getConcatArgumentSpreadable(node, scope) ?? {node, isSpreadable: true}) ) }); } diff --git a/rules/prefer-string-slice.js b/rules/prefer-string-slice.js index 459f5bf7e2..d111f5fc75 100644 --- a/rules/prefer-string-slice.js +++ b/rules/prefer-string-slice.js @@ -37,7 +37,7 @@ const isLengthProperty = node => ( node.property.name === 'length' ); -const isLikelyNumeric = node => isLiteralNumber(node) || isLengthProperty(node); +const isLikelyNumeric = node => isLiteralNumber(node) ?? isLengthProperty(node); const create = context => { const sourceCode = context.getSourceCode(); diff --git a/rules/prefer-switch.js b/rules/prefer-switch.js index d9432c3054..a58853dea5 100644 --- a/rules/prefer-switch.js +++ b/rules/prefer-switch.js @@ -33,7 +33,7 @@ function getEqualityComparisons(node) { function getCommonReferences(expressions, candidates) { for (const {left, right} of expressions) { - candidates = candidates.filter(node => isSame(node, left) || isSame(node, right)); + candidates = candidates.filter(node => isSame(node, left) ?? isSame(node, right)); if (candidates.length === 0) { break; diff --git a/rules/utils/avoid-capture.js b/rules/utils/avoid-capture.js index 9df03e93bd..4a62df45f0 100644 --- a/rules/utils/avoid-capture.js +++ b/rules/utils/avoid-capture.js @@ -26,7 +26,7 @@ const nameCollidesWithArgumentsSpecial = (name, scopes, isStrict) => { return false; } - return isStrict || scopes.some(scope => scopeHasArgumentsSpecial(scope)); + return isStrict ?? scopes.some(scope => scopeHasArgumentsSpecial(scope)); }; /* @@ -47,7 +47,7 @@ function unicorn() { ``` */ const isUnresolvedName = (name, scopes) => scopes.some(scope => - scope.references.some(reference => reference.identifier && reference.identifier.name === name && !reference.resolved) || + scope.references.some(reference => reference.identifier && reference.identifier.name === name && !reference.resolved) ?? isUnresolvedName(name, scope.childScopes) ); diff --git a/rules/utils/boolean.js b/rules/utils/boolean.js index 82bf5e1d0c..cd70ceba58 100644 --- a/rules/utils/boolean.js +++ b/rules/utils/boolean.js @@ -3,7 +3,6 @@ const isLogicalExpression = require('./is-logical-expression.js'); const isLogicNot = node => - node && node.type === 'UnaryExpression' && node.operator === '!'; const isLogicNotArgument = node => @@ -13,14 +12,22 @@ const isBooleanCallArgument = node => isBooleanCall(node.parent) && node.parent.arguments[0] === node; const isBooleanCall = node => - node && node.type === 'CallExpression' && - node.callee && + !node.optional && node.callee.type === 'Identifier' && node.callee.name === 'Boolean' && node.arguments.length === 1; +const isObjectIsCall = node => + node.type === 'CallExpression' && + !node.optional && + node.callee.type === 'MemberExpression' && + !node.callee.computed && + !node.callee.optional && + node.callee.object.type === 'Identifier' && + node.callee.object.name === 'Object' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'is'; const isVueBooleanAttributeValue = node => - node && node.type === 'VExpressionContainer' && node.parent.type === 'VAttribute' && node.parent.directive && @@ -32,6 +39,25 @@ const isVueBooleanAttributeValue = node => node.parent.key.name.rawName === 'else-if' || node.parent.key.name.rawName === 'show' ); +const isBooleanLiteral = node => + node.type === 'Literal' && + typeof node.value === 'boolean'; +// https://github.com/estree/estree/blob/master/es5.md#binaryoperator +const comparisonOperators = new Set([ + '==', + '!=', + '===', + '!==', + '<', + '<=', + '>', + '>=', + 'in', + 'instanceof' +]); +const isComparison = node => + node.type === 'BinaryExpression' && + comparisonOperators.has(node.operator); /** Check if the value of node is a `boolean`. @@ -40,11 +66,18 @@ Check if the value of node is a `boolean`. @returns {boolean} */ function isBooleanNode(node) { + if (!node) { + return false; + } + if ( isLogicNot(node) || isLogicNotArgument(node) || isBooleanCall(node) || - isBooleanCallArgument(node) + isBooleanCallArgument(node) || + isBooleanLiteral(node) || + isComparison(node) || + isObjectIsCall(node) ) { return true; } diff --git a/rules/utils/is-function-self-used-inside.js b/rules/utils/is-function-self-used-inside.js index 12cd26b55c..4e733ce270 100644 --- a/rules/utils/is-function-self-used-inside.js +++ b/rules/utils/is-function-self-used-inside.js @@ -2,7 +2,7 @@ const {findVariable} = require('eslint-utils'); const getReferences = (scope, nodeOrName) => { - const {references = []} = findVariable(scope, nodeOrName) || {}; + const {references = []} = findVariable(scope, nodeOrName) ?? {}; return references; }; diff --git a/rules/utils/numeric.js b/rules/utils/numeric.js index 6d698755b8..5628299970 100644 --- a/rules/utils/numeric.js +++ b/rules/utils/numeric.js @@ -8,7 +8,7 @@ const isDecimalIntegerNode = node => isNumber(node) && isDecimalInteger(node.raw const isNumber = node => typeof node.value === 'number'; const isBigInt = node => Boolean(node.bigint); -const isNumeric = node => isNumber(node) || isBigInt(node); +const isNumeric = node => isNumber(node) ?? isBigInt(node); const isLegacyOctal = node => isNumber(node) && /^0\d+$/.test(node.raw); function getPrefix(text) { diff --git a/rules/utils/parentheses.js b/rules/utils/parentheses.js index 8ceaf42eaa..041c53819e 100644 --- a/rules/utils/parentheses.js +++ b/rules/utils/parentheses.js @@ -52,8 +52,8 @@ Get the parenthesized range of the node. */ function getParenthesizedRange(node, sourceCode) { const parentheses = getParentheses(node, sourceCode); - const [start] = (parentheses[0] || node).range; - const [, end] = (parentheses[parentheses.length - 1] || node).range; + const [start] = (parentheses[0] ?? node).range; + const [, end] = (parentheses[parentheses.length - 1] ?? node).range; return [start, end]; } diff --git a/test/catch-error-name.mjs b/test/catch-error-name.mjs index d70364f1cb..a3153b8af3 100644 --- a/test/catch-error-name.mjs +++ b/test/catch-error-name.mjs @@ -21,7 +21,7 @@ function invalidTestCase(options) { const {code, name, output, errors} = options; return { code, - output: output || code, + output: output ?? code, options: name ? [{name}] : [], errors }; diff --git a/test/integration/test.mjs b/test/integration/test.mjs index 99837c1ced..1f56775988 100644 --- a/test/integration/test.mjs +++ b/test/integration/test.mjs @@ -28,7 +28,7 @@ const enrichErrors = (packageName, cliArguments, f) => async (...arguments_) => const makeEslintTask = (project, destination) => { const arguments_ = [ 'eslint', - project.path || '.', + project.path ?? '.', '--fix-dry-run', '--no-eslintrc', '--ext', @@ -90,7 +90,7 @@ const makeEslintTask = (project, destination) => { const getBranch = mem(async dirname => (await execa('git', ['branch', '--show-current'], {cwd: dirname})).stdout); const execute = project => { - const destination = project.location || path.join(dirname, 'fixtures', project.name); + const destination = project.location ?? path.join(dirname, 'fixtures', project.name); return new Listr([ { diff --git a/test/prefer-nullish-coalescing.mjs b/test/prefer-nullish-coalescing.mjs new file mode 100644 index 0000000000..bcad0c943b --- /dev/null +++ b/test/prefer-nullish-coalescing.mjs @@ -0,0 +1,46 @@ +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + 'const a = 0; const foo = a || 1;', + 'const a = {b: false}; const foo = a.b || 1;', + 'const foo = 0n || 1;', + 'const foo = "" || 1;', + 'const foo = `` || 1;', + 'const foo = NaN || 1;', + // Boolean + 'const foo = !(a || b)', + 'const foo = Boolean(a || b)', + 'if (a || b);', + 'const foo = (a || b) ? c : d;', + 'while (a || b);', + 'do {} while (a || b);', + 'for (;a || b;);', + // Left is boolean + 'const foo = false || a', + 'const foo = !bar || a', + 'const foo = a == 1 || bar', + 'const foo = a != 1 || bar', + 'const foo = a === b || a === c', + 'const foo = a !== b || bar', + 'const foo = a < 1 || bar', + 'const foo = a <= 1 || bar', + 'const foo = a > 1 || bar', + 'const foo = a >= 1 || bar', + 'const foo = Object.is(a, -0) || a < 0', + 'const foo = ("key" in object) || bar', + 'const foo = (object instanceof Foo) || Array.isArray(object)', + // Mixed + 'const foo = a || (b && c)', + 'const foo = (a || b) && c', + 'const foo = a ?? (b || c)', + 'const foo = (a ?? b) || c' + ], + invalid: [ + 'const foo = a || b', + 'foo ||= b', + 'const foo = (( a )) || b' + ] +}); diff --git a/test/run-rules-on-codebase/lint.mjs b/test/run-rules-on-codebase/lint.mjs index 278bddedb6..49b4257592 100644 --- a/test/run-rules-on-codebase/lint.mjs +++ b/test/run-rules-on-codebase/lint.mjs @@ -3,7 +3,7 @@ import {ESLint} from 'eslint'; import unicorn from '../../index.js'; const {recommended} = unicorn.configs; -const files = [process.argv[2] || '.']; +const files = [process.argv[2] ?? '.']; const fix = process.argv.includes('--fix'); const enableAllRules = Object.fromEntries( @@ -94,7 +94,7 @@ const sum = (collection, fieldName) => { const fixableErrorCount = sum(results, 'fixableErrorCount'); const fixableWarningCount = sum(results, 'fixableWarningCount'); - const hasFixable = fixableErrorCount || fixableWarningCount; + const hasFixable = fixableErrorCount ?? fixableWarningCount; if (errorCount || warningCount) { const {format} = await eslint.loadFormatter(); diff --git a/test/snapshots/prefer-nullish-coalescing.mjs.md b/test/snapshots/prefer-nullish-coalescing.mjs.md new file mode 100644 index 0000000000..1698ec7fee --- /dev/null +++ b/test/snapshots/prefer-nullish-coalescing.mjs.md @@ -0,0 +1,47 @@ +# Snapshot report for `test/prefer-nullish-coalescing.mjs` + +The actual snapshot is saved in `prefer-nullish-coalescing.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## Invalid #1 + 1 | const foo = a || b + +> Error 1/1 + + `␊ + > 1 | const foo = a || b␊ + | ^^ Prefer \`??\` over \`||\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Use \`??\`␊ + 1 | const foo = a ?? b␊ + ` + +## Invalid #2 + 1 | foo ||= b + +> Error 1/1 + + `␊ + > 1 | foo ||= b␊ + | ^^^ Prefer \`??=\` over \`||=\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Use \`??=\`␊ + 1 | foo ??= b␊ + ` + +## Invalid #3 + 1 | const foo = (( a )) || b + +> Error 1/1 + + `␊ + > 1 | const foo = (( a )) || b␊ + | ^^ Prefer \`??\` over \`||\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Use \`??\`␊ + 1 | const foo = (( a )) ?? b␊ + ` diff --git a/test/snapshots/prefer-nullish-coalescing.mjs.snap b/test/snapshots/prefer-nullish-coalescing.mjs.snap new file mode 100644 index 0000000000..396adaafc1 Binary files /dev/null and b/test/snapshots/prefer-nullish-coalescing.mjs.snap differ diff --git a/test/utils/test.mjs b/test/utils/test.mjs index 1ee55e8b3e..cffc1da54f 100644 --- a/test/utils/test.mjs +++ b/test/utils/test.mjs @@ -72,7 +72,7 @@ class Tester { typescript(tests) { const {testerOptions = {}} = tests; - testerOptions.parserOptions = testerOptions.parserOptions || {}; + testerOptions.parserOptions = testerOptions.parserOptions ?? {}; return this.runTest({ ...tests, @@ -89,10 +89,10 @@ class Tester { babel(tests) { const {testerOptions = {}} = tests; - testerOptions.parserOptions = testerOptions.parserOptions || {}; - testerOptions.parserOptions.babelOptions = testerOptions.parserOptions.babelOptions || {}; - testerOptions.parserOptions.babelOptions.parserOpts = testerOptions.parserOptions.babelOptions.parserOpts || {}; - let babelPlugins = testerOptions.parserOptions.babelOptions.parserOpts.plugins || []; + testerOptions.parserOptions = testerOptions.parserOptions ?? {}; + testerOptions.parserOptions.babelOptions = testerOptions.parserOptions.babelOptions ?? {}; + testerOptions.parserOptions.babelOptions.parserOpts = testerOptions.parserOptions.babelOptions.parserOpts ?? {}; + let babelPlugins = testerOptions.parserOptions.babelOptions.parserOpts.plugins ?? []; babelPlugins = [ ['estree', {classFeatures: true}], 'jsx',