diff --git a/docs/new-rule.md b/docs/new-rule.md index de48f32446..bb82966e9a 100644 --- a/docs/new-rule.md +++ b/docs/new-rule.md @@ -2,6 +2,7 @@ ## Prerequisite +- Use npm to install. - Ensure ESLint doesn't already have the [rule built-in](https://eslint.org/docs/rules/). - [Read the ESLint docs on creating a new rule.](https://eslint.org/docs/developer-guide/working-with-rules) - Look at the commit for how previous rules were added as inspiration. For example, the [`no-unused-properties` rule](https://github.com/sindresorhus/eslint-plugin-unicorn/commit/0179443f24326fb01342a0bf799f7ac66e0e2c23). @@ -17,7 +18,7 @@ Use the [`astexplorer` site](https://astexplorer.net) with the `espree` parser a - Open β€œrules/{RULE_ID}.js” and implement the rule logic. - Add the correct [`meta.type`](https://eslint.org/docs/developer-guide/working-with-rules#rule-basics) to the rule. - Open β€œdocs/rules/{RULE_ID}.js” and write some documentation. -- Double check `configs/recommended.js` and `readme.md`, make sure the new rule is correctly added. +- Double check `readme.md`, make sure the new rule is correctly added. - Run `npm test` to ensure the tests pass. - Run `npm run integration` to run the rules against real projects to ensure your rule does not fail on real-world code. - Open a pull request with a title in exactly the format `` Add `rule-name` rule ``, for example, `` Add `no-unused-properties` rule ``. diff --git a/docs/rules/no-array-fill-with-reference-type.md b/docs/rules/no-array-fill-with-reference-type.md new file mode 100644 index 0000000000..a4b2e977cb --- /dev/null +++ b/docs/rules/no-array-fill-with-reference-type.md @@ -0,0 +1,161 @@ +# Disallows using `Array.fill()` or `Array.from().fill()` with **reference types** to prevent unintended shared references across array elements + +πŸ’Ό This rule is enabled in the βœ… `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). + + + + +Disallows usingΒ `Array.fill()`Β or `Array.from().fill()` withΒ **reference types**Β (objects, arrays, functions, Maps, Sets, RegExp literals, etc.) to prevent unintended shared references across array elements. EncouragesΒ `Array.from()`Β or explicit iteration for creating independent instances. + +**Reason**: +`Array(len).fill(value)`Β fills all elements with theΒ **same reference**Β ifΒ `value`Β is non-primitive (e.g.,Β `fill([])`), leading to bugs when one element’s mutations affect others. + +**Key Features**: + +- CatchesΒ **all reference types**: Objects, Arrays, Functions,Β `new`Β expressions, RegExp literals, and variables referencing them. +- **Clear error messages**: Identifies the reference type (e.g.,Β `Object`,Β `RegExp`,Β `variable (name)`). + +**Note**: Primitive types (`number`,Β `string`,Β `boolean`,Β `null`,Β `undefined`,Β `symbol`,Β `bigint`) are always allowed. + +## Examples + +```js +// ❌ Object +new Array(3).fill({}); +Array(3).fill({}); +Array.from({ length: 3 }).fill({}); + +// βœ… +Array.from({ length: 3 }, () => ({})); +``` + +```js +// ❌ Array +new Array(3).fill([]); +Array(3).fill([]); +Array.from({ length: 3 }).fill([]); + +// βœ… +Array.from({ length: 3 }, () => []); +``` + +```js +// ❌ Map +new Array(3).fill(new Map()); +Array(3).fill(new Map()); +Array.from({ length: 3 }).fill(new Map()); + +// βœ… +Array.from({ length: 3 }, () => new Map()); +``` + +```js +// ❌ Date +new Array(3).fill(new Date()); +Array(3).fill(new Date()); +Array.from({ length: 3 }).fill(new Date()); + +// βœ… +Array.from({ length: 3 }, () => new Date()); +``` + +```js +// ❌ Class +class BarClass {}; +new Array(3).fill(new BarClass()); +Array(3).fill(new BarClass()); +Array.from({ length: 3 }).fill(new BarClass()); + +// βœ… +Array.from({ length: 3 }, () => new BarClass()); +``` + +```js +// ❌ Function +new Array(3).fill(function () {}) +Array(3).fill(function () {}) +Array.from({ length: 3 }).fill(function () {}); + +// βœ… +Array.from({ length: 3 }, () => function () {}); +``` + +```js +// ❌ RegExp literal +new Array(3).fill(/pattern/); +Array(3).fill(/pattern/); +Array.from({ length: 3 }).fill(/pattern/); + +// βœ… +Array.from({ length: 3 }, () => /pattern/); +``` + +```js +const box = [] + +// ❌ Shared reference +new Array(3).fill(box); +Array(3).fill(box); +Array.from({ length: 3 }).fill(box); + +// βœ… +Array.from({ length: 3 }, () => []); +``` + +## Options + +### allowFunctions + +Type: `boolean`\ +Default: `true` + +Should check function when filling an array? + +This would pass by default: + +```js +new Array(3).fill(function () {}) +``` + +```js +"unicorn/catch-error-name": [ + "error", + { + "allowFunctions": false + } +] +``` + +with `allowFunctions: false`, this would fail: + +```js +new Array(3).fill(function () {}) +``` + +### allowRegularExpressions + +Type: `boolean`\ +Default: `true` + +Should check function when filling an array? + +This would pass by default: + +```js +new Array(3).fill(/pattern/) +``` + +```js +"unicorn/catch-error-name": [ + "error", + { + "allowRegularExpressions": false + } +] +``` + +with `allowRegularExpressions: false`, this would fail: + +```js +new Array(3).fill(/pattern/) +``` diff --git a/package.json b/package.json index 1cb55bec97..28e0993b1d 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "eslint-remote-tester-repositories": "^2.0.1", "espree": "^10.4.0", "listr2": "^8.3.3", + "lodash-es": "^4.17.21", "markdownlint-cli": "^0.45.0", "memoize": "^10.1.0", "nano-spawn": "^1.0.2", diff --git a/readme.md b/readme.md index 7b9dd9c850..73b83ae09e 100644 --- a/readme.md +++ b/readme.md @@ -77,6 +77,7 @@ export default [ | [no-accessor-recursion](docs/rules/no-accessor-recursion.md) | Disallow recursive access to `this` within getters and setters. | βœ… | | | | [no-anonymous-default-export](docs/rules/no-anonymous-default-export.md) | Disallow anonymous functions and classes as the default export. | βœ… | | πŸ’‘ | | [no-array-callback-reference](docs/rules/no-array-callback-reference.md) | Prevent passing a function reference directly to iterator methods. | βœ… | | πŸ’‘ | +| [no-array-fill-with-reference-type](docs/rules/no-array-fill-with-reference-type.md) | Disallows using `Array.fill()` or `Array.from().fill()` with **reference types** to prevent unintended shared references across array elements. | βœ… | | | | [no-array-for-each](docs/rules/no-array-for-each.md) | Prefer `for…of` over the `forEach` method. | βœ… | πŸ”§ | πŸ’‘ | | [no-array-method-this-argument](docs/rules/no-array-method-this-argument.md) | Disallow using the `this` argument in array methods. | βœ… | πŸ”§ | πŸ’‘ | | [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | βœ… | | | diff --git a/rules/ast/is-member-expression.d.ts b/rules/ast/is-member-expression.d.ts new file mode 100644 index 0000000000..69994ea620 --- /dev/null +++ b/rules/ast/is-member-expression.d.ts @@ -0,0 +1,16 @@ +import type { Node, MemberExpression } from "estree"; + +export default function isMemberExpression( + node: Node, + options?: + | { + property?: string; + properties?: string[]; + object?: string; + objects?: string[]; + optional?: boolean; + computed?: boolean; + } + | string + | string[] +): node is MemberExpression; diff --git a/rules/ast/is-member-expression.js b/rules/ast/is-member-expression.js index 9507cfd13a..453ef7580a 100644 --- a/rules/ast/is-member-expression.js +++ b/rules/ast/is-member-expression.js @@ -1,5 +1,7 @@ +// @ts-check /* eslint-disable complexity */ /** +@param {ESTreeNode} node @param { { property?: string, @@ -10,7 +12,7 @@ computed?: boolean } | string | string[] } [options] -@returns {string} +@returns {boolean} */ export default function isMemberExpression(node, options) { if (node?.type !== 'MemberExpression') { @@ -96,3 +98,5 @@ export default function isMemberExpression(node, options) { return true; } + +/** @typedef {import('estree').Node} ESTreeNode */ diff --git a/rules/index.js b/rules/index.js index 272c04c9f1..89dab08df0 100644 --- a/rules/index.js +++ b/rules/index.js @@ -21,6 +21,7 @@ export {default as 'no-abusive-eslint-disable'} from './no-abusive-eslint-disabl export {default as 'no-accessor-recursion'} from './no-accessor-recursion.js'; export {default as 'no-anonymous-default-export'} from './no-anonymous-default-export.js'; export {default as 'no-array-callback-reference'} from './no-array-callback-reference.js'; +export {default as 'no-array-fill-with-reference-type'} from './no-array-fill-with-reference-type.js'; export {default as 'no-array-for-each'} from './no-array-for-each.js'; export {default as 'no-array-method-this-argument'} from './no-array-method-this-argument.js'; export {default as 'no-array-reduce'} from './no-array-reduce.js'; diff --git a/rules/no-array-fill-with-reference-type.js b/rules/no-array-fill-with-reference-type.js new file mode 100644 index 0000000000..a52e5ce575 --- /dev/null +++ b/rules/no-array-fill-with-reference-type.js @@ -0,0 +1,517 @@ +// @ts-check +import {findVariable} from '@eslint-community/eslint-utils'; +import { + isCallExpression, + isFunction, isMemberExpression, isMethodCall, isNewExpression, isRegexLiteral, +} from './ast/index.js'; + +const debugging = false; + +// @ts-check +const MESSAGE_ID_ERROR = 'no-array-fill-with-reference-type/error'; +const messages = { + [MESSAGE_ID_ERROR]: 'Avoid using `{{actual}}` with reference type{{type}}. Use `Array.from({ ... }, () => { return independent instance })` instead to ensure no reference shared.', +}; + +const DEFAULTS = { + // Not check for function expressions by default because it is rare to fill an array with a function and add properties to it. + allowFunctions: true, + // The same reason as above. + allowRegularExpressions: true, +}; + +const log = (...arguments_) => debugging && console.log(...arguments_); + +const RECURSION_LIMIT = 5; + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => ({ + CallExpression(node) { + // Check all `fill` method call even if the object is not Array because we don't know if its runtime type is array + // `arr.fill()` or `new Array().fill()` or `Array.from().fill()` + const isArrayFill = isMethodCall(node, {method: 'fill', argumentsLength: 1}); + + // `Array.from().map` or `Array.from(arrayLike, mapper)` + const isArrayFrom = isMethodCall(node, {object: 'Array', method: 'from'}); + + log('isArrayFill:', {isArrayFill, isArrayFrom}); + + if (!isArrayFill && !isArrayFrom) { + return; + } + + const fillArgument = isArrayFill ? node.arguments[0] : getArrayFromReturnNode(node, context); + + log('fillArgument:', fillArgument); + + const [is, resolvedNode] = isReferenceType(fillArgument, context); + + if (!is) { + return; + } + + const actual = isMethodCall(node.callee.object, {object: 'Array', method: 'from'}) ? 'Array.from().fill()' : 'Array.fill()'; + const type = getType(resolvedNode, context); + + return { + node: fillArgument, + messageId: MESSAGE_ID_ERROR, + data: { + actual, + type: type ? ` (${type})` : '', + }, + }; + }, +}); + +/** + + @param {import('estree').CallExpression} node + @param {RuleContext} context + @returns + */ +function getArrayFromReturnNode(node, context) { + const secondArgument = node.arguments[1]; + log('secondArgument:', secondArgument); + + // Array.from({ length: 10 }, () => { return sharedObject; }); + let result; + if (secondArgument && isFunction(secondArgument)) { + result = getReturnIdentifier(secondArgument, context); + // @ts-expect-error node always has a parent + } else if (isMemberExpression(node.parent, 'map')) { + // Array.from({ length: 10 }).map(() => { return sharedObject; }); + // @ts-expect-error node always has a parent + result = getReturnIdentifier(node.parent.parent.arguments[0], context); + } + + // Should not check reference type if the identifier is declared in the current function + if (result?.declaredInCurrentFunction) { + return; + } + + const fillArgument = result?.returnNode; + + return fillArgument; +} + +/** + + @param {import('estree').FunctionExpression | Node} node - callback for map + @returns {{ returnNode: Node, declaredInCurrentFunction: boolean }} + */ +function getReturnIdentifier(node, context) { + if (node.type === 'Identifier') { + const scope = context.sourceCode.getScope(node); + + const variable = findVariable(scope, node); + + if (!variable) { + // Not check if the identifier is not declared + return {returnNode: node, declaredInCurrentFunction: true}; + } + + // Must be ArrowFunctionExpression or FunctionExpression + const init = variable.defs[0]?.node?.init; + + // `init` will be undefined if the identifier is builtin globals like String + if (!init || !isFunction(init)) { + // Not check if the identifier is not a function + return {returnNode: node, declaredInCurrentFunction: true}; + } + + return getReturnIdentifier(init, context); + } + + // Console.log('node:', node); + + // @ts-expect-error node is FunctionExpression + const {body: nodeBody} = node; + + // No check member expression as callback `Array.from(element.querySelectorAll('ng2 li')).map(angular.element);` + if (!nodeBody) { + return {returnNode: node, declaredInCurrentFunction: true}; + } + + if (nodeBody.type === 'Identifier') { + return {returnNode: nodeBody, declaredInCurrentFunction: false}; + } + + // Array.from({ length: 3 }, () => (new Map)) + // Array.from({ length: 3 }, () => ({})) + // Array.from({ length: 3 }, () => {}) + if (!nodeBody.body) { + return {returnNode: nodeBody, declaredInCurrentFunction: true}; + } + + const returnStatement = nodeBody.body.find(node => node.type === 'ReturnStatement'); + const name = returnStatement?.argument?.name; + if (!name) { + return {returnNode: returnStatement?.argument, declaredInCurrentFunction: true}; + } + + const declaredInCurrentFunction = nodeBody.body.some(node => node.type === 'VariableDeclaration' && node.declarations.some(declaration => declaration.id.name === name)); + + return {returnNode: returnStatement?.argument, declaredInCurrentFunction}; +} + +/** + @param {*} fillArgument + @param {import('eslint').Rule.RuleContext} context + @returns {string} + */ +function getType(fillArgument, context) { + switch (fillArgument.type) { + case 'ObjectExpression': { + return 'Object'; + } + + case 'ArrayExpression': { + return 'Array'; + } + + case 'NewExpression': { + return getNewExpressionType(fillArgument, context); + } + + case 'FunctionExpression': + case 'ArrowFunctionExpression': { + return 'Function'; + } + + default: { + if (fillArgument.type === 'Literal' && fillArgument.regex) { + return 'RegExp'; + } + + if (fillArgument.type === 'Identifier') { + return `variable (${fillArgument.name})`; + } + } + } + + return ''; +} + +/** + + @param {*} fillArgument + @param {import('eslint').Rule.RuleContext} context + @returns {string} + */ +function getNewExpressionType(fillArgument, context) { + if (fillArgument.callee.name) { + return `new ${fillArgument.callee.name}()`; + } + + // NewExpression.callee not always have a name. + // new A.B() and new class {} + // Try the best to get the type from source code + const matches = context.sourceCode.getText(fillArgument.callee).split('\n')[0].match(/\S+/); + + if (matches) { + // Limit the length to avoid too long tips + return 'new ' + matches[0].slice(0, 32); + } + + return 'new ()'; +} + +/** + @param {Node} node + @param {import('eslint').Rule.RuleContext} context + @returns {[is: false] | [is: true, node: Node]} + */ +function isReferenceType(node, context) { + log('[isReferenceType]: ', node); + if (!node) { + return [false]; + } + + /** @type {typeof DEFAULTS} */ + const options = { + ...DEFAULTS, + ...context.options[0], + }; + + // For null, number, string, boolean. + if (node.type === 'Literal') { + // Exclude regular expression literals (e.g., `/pattern/`, which are objects despite being literals). + if (!options.allowRegularExpressions && isRegexLiteral(node)) { + return [true, node]; + } + + return [false]; + } + + // For template literals. + if (node.type === 'TemplateLiteral') { + return [false]; + } + + // For variable identifiers (recursively check its declaration). + if (node.type === 'Identifier') { + return isIdentifierReferenceType(node, context); + } + + if (isSymbol(node)) { + return [false]; + } + + if (options.allowFunctions && isFunction(node)) { + return [false]; + } + + if (options.allowRegularExpressions && isNewExpression(node, 'RegExp')) { + return [false]; + } + + if (isMemberExpression(node)) { + const propertyNode = getMemberExpressionLeafNode(node, context); + if (!propertyNode) { + return [false]; + } + + return isReferenceType(propertyNode, context); + } + + // Other cases: objects, arrays, new expressions, regular expressions, etc. + return [true, node]; +} + +/** + Get member expression leaf node + like get nested object property in plain object but in ESLint AST Node + @param {MemberExpression} node - The whole member expression node + @param {RuleContext} context - ESLint rule context + @returns {undefined | ESTreeNode} - The leaf node + @example + // pseudo code + const obj = { a: { b: { c: { list: [] } } } }; + obj.a.b.c.list // => [] + */ +function getMemberExpressionLeafNode(node, context) { + const chain = getPropertyAccessChain(node); + + if (!chain || chain.length === 0) { + return; + } + + // The chain names: [ 'obj', 'a', 'b', 'c', 'list' ] + // if the MemberExpression is `obj.a.b.c.list` + // @ts-ignore + log('chain names:', chain.map(node => node.name ?? node.property?.name)); + + // @ts-expect-error `chain[0].name` cannot be undefined because the previous check ensures + const variable = findVariableDefinition({node, variableName: chain[0].name, context}); + if (!variable || !variable.defs[0]?.node) { + return; + } + + /** @type {ESTreeNode | undefined} */ + let currentObject = variable.defs[0].node.init; + + for (let index = 1; index < chain.length; index++) { + const currentPropertyInChain = chain[index].property; + // .log(`#${index}`, 'currentPropertyInChain:', currentPropertyInChain?.type, currentPropertyInChain); + // .log(`#${index}`, 'currentObject:', currentObject); + if (!currentObject || currentObject.type !== 'ObjectExpression') { + return; + } + + const property = currentObject.properties.find( + // @ts-expect-error + p => p.key.type === 'Identifier' + // @ts-expect-error + && p.key.name === (currentPropertyInChain.type === 'Identifier' ? currentPropertyInChain.name : currentPropertyInChain.value), + ); + // .log(`#${index}`, 'property:', property); + + if (!property) { + return; + } + + // @ts-expect-error + currentObject = property.value; + } + + return currentObject; +} + +/** + Extracts the property access chain from a MemberExpression + @param {MemberExpression | Identifier} node - The node to analyze + @returns {PropertyAccessNode[] | undefined} - Array of access nodes or undefined if invalid + @example + return [ Node('obj'), Property('a'), Property('b'), Property('c'), Property('list') ] if node is MemberExpress `obj.a.b.c.list` + */ +function getPropertyAccessChain(node) { + /** @type {PropertyAccessNode[]} */ + const chain = []; + /** @type {MemberExpression | Identifier | null} */ + let current = node; + let times = 0; + + // We use `unshift` because `obj.a.b.c.list` loop order is `list` -> `c` -> `b` -> `a` -> `obj` + while (current) { + times += 1; + if (times > RECURSION_LIMIT) { + log('Skip deep-nested member checks for performance and to prevent potential infinite loops.'); + return; + } + + if (current.type === 'Identifier') { + chain.unshift({name: current.name}); + // `break` at end of chain. + break; + } + + if (current.type === 'MemberExpression') { + if (current.property.type === 'Identifier') { + chain.unshift({property: current.property}); + } else if (current.property.type === 'Literal') { + chain.unshift({property: current.property}); + } else { + // Unsupported property type + return; + } + + // @ts-expect-error + current = current.object; + } else { + // Unsupported node type + return; + } + } + + return chain.length > 0 ? chain : undefined; +} + +/** + @param {any} node + @returns {boolean} + */ +function isSymbol(node) { + const SYMBOL = 'Symbol'; + // Symbol (such as `Symbol('description')`) will not check + if (node.type === 'CallExpression' && node.callee.name === SYMBOL) { + return true; + } + + // Symbol (such as `Symbol.for('description')`) will not check + if (isCallExpression(node) && node.callee.object?.name === SYMBOL) { + return true; + } + + // Symbol (such as `Symbol.iterator`) will not check + if (isMemberExpression(node, {object: SYMBOL})) { + return true; + } + + return false; +} + +/** + Variable can be declared in its parent or grandparent scope so we need to check all the scopes up to the global scope. + @param {{variableName: string; node: any; context: import('eslint').Rule.RuleContext}} params + @returns + */ +function findVariableDefinition({variableName, node, context}) { + if (!node) { + return; + } + + const scope = context.sourceCode.getScope(node); + const {variables} = scope; + log('[findVariableDefinition] variables', variables); + const variable = variables.find(v => v.name === variableName); + + if (variable) { + return variable; + } + + return findVariableDefinition({variableName, node: node.parent, context}); +} + +/** + + @param {*} node + @param {import('eslint').Rule.RuleContext} context + @returns {[is: false] | [is: true, node: Node]} + */ +function isIdentifierReferenceType(node, context) { + const variable = findVariableDefinition({variableName: node.name, node, context}); + const definitionNode = variable?.defs[0]?.node; + + log({definitionNode}); + + if (!variable || !definitionNode) { + return [false]; + } + + // Check `const foo = []; Array(3).fill(foo);` + if (definitionNode.type === 'VariableDeclarator') { + // Not check `let` `let foo = []; Array(3).fill(foo);` + if (definitionNode.parent.kind === 'let') { + return [false]; + } + + return isReferenceType(definitionNode.init, context); + } + + return isReferenceType(definitionNode, context); +} + +const schema = [ + { + type: 'object', + additionalProperties: false, + properties: { + allowFunctions: { + type: 'boolean', + }, + allowRegularExpressions: { + type: 'boolean', + }, + }, + }, +]; + +/** @type {import('eslint').Rule.RuleModule} */ +const config = { + create, + meta: { + type: 'problem', + docs: { + description: 'Disallows using `Array.fill()` or `Array.from().fill()` with **reference types** to prevent unintended shared references across array elements.', + recommended: true, + }, + schema, + defaultOptions: [{}], + messages, + }, +}; + +export default config; + +/** + @typedef {ESTreeNode} Node + */ + +/** + @typedef {Object} PropertyAccessNode + @property {string} [name] - For identifiers (root object) + @property {import('estree').Identifier | import('estree').Literal} [property] - For property access + */ + +/** + @typedef {import('eslint').Rule.RuleContext} RuleContext + @typedef {import('estree').Node} ESTreeNode + @typedef {import('estree').MemberExpression} MemberExpression + @typedef {import('estree').Identifier} Identifier + @typedef {import('estree').Literal} Literal + @typedef {import('estree').VariableDeclarator} VariableDeclarator + @typedef {import('estree').ObjectExpression} ObjectExpression + @typedef {import('estree').Property} Property + @typedef {import('eslint-scope').Variable} ESLintVariable + */ diff --git a/test/catch-error-name.js b/test/catch-error-name.js index fdd2fdcba2..a8b2e996c1 100644 --- a/test/catch-error-name.js +++ b/test/catch-error-name.js @@ -496,6 +496,9 @@ test({ obj.then(err => {}, error => {}); obj.then(err => {}, error => {}); `, + + // It's safe due to no mutation to `errors` + // eslint-disable-next-line unicorn/no-array-fill-with-reference-type errors: Array.from({length: 4}).fill(generateError('err', 'error')), }, diff --git a/test/no-array-fill-with-reference-type.js b/test/no-array-fill-with-reference-type.js new file mode 100644 index 0000000000..92f9f8f1d0 --- /dev/null +++ b/test/no-array-fill-with-reference-type.js @@ -0,0 +1,409 @@ +import {getTester} from './utils/test.js'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + 'const foo = "πŸ¦„";', + 'new Array(3).fill(0); // βœ“ number (primitive) ', + 'new Array(3).fill(10n); // βœ“ bigint (primitive) ', + 'new Array(3).fill(null); // βœ“ null (primitive) ', + 'new Array(3).fill(undefined); // βœ“ undefined(primitive) ', + 'new Array(3).fill(\'foo\'); // βœ“ string (primitive) ', + 'new Array(3).fill(``); // βœ“ TemplateLiteral (primitive) ', + // eslint-disable-next-line no-template-curly-in-string + 'new Array(3).fill(`${10}`); // βœ“ TemplateLiteral (primitive)', + // eslint-disable-next-line no-template-curly-in-string + 'const foo = "foo"; new Array(3).fill(`Hi ${foo}`); // βœ“ TemplateLiteral (primitive)', + 'new Array(3).fill(false); // βœ“ boolean (primitive) ', + 'new Array(3).fill(Symbol(\'foo\')); // βœ“ Symbol(primitive) ', + + 'Array.from({ length: 3 }, () => ({})); // βœ“ Safe alternative', + 'Array.from({ length: 3 }, () => { return {} }); // βœ“ Safe alternative', + 'Array.from({ length: 3 }, () => (new Map)); // βœ“ Safe alternative', + 'Array.from({ length: 3 }, () => { return new Map }); // βœ“ Safe alternative', + 'Array.from({ length: 33 }, () => { return new Map() }); // βœ“ Safe alternative', + + 'Array(3).fill(0); // βœ“ number (primitive)', + + // Should be invalid but will pass. + // Due to the rule name it will not check other than `Array.fill` or `Array.from`. + // Too hard to implement exhaustive check. + ` + const map = new Map(); + const list = []; + for (let i = 0; i < 3; i++) { + list.push(map); + } + `, + + 'const foo = 0; new Array(8).fill(foo);', + + // Not check functions + // function expression + 'new Array(1).fill(() => 1);', + 'new Array(2).fill(() => {});', + `new Array(3).fill(() => { + return {} + });`, + 'new Array(4).fill(function () {});', + + // Set allowFunctions explicitly to true + { + code: 'new Array(41).fill(function () {});', + options: [{ + allowFunctions: true, + }], + }, + + // Function declaration + 'const foo = () => 0; new Array(5).fill(foo);', + 'const foo = function () {}; new Array(6).fill(foo);', + 'function foo() {}; new Array(7).fill(foo);', + + // RegExp is not check by default + 'new Array(3).fill(/pattern/);', + 'new Array(3).fill(new RegExp("pattern"));', + 'const p = /pattern/; new Array(3).fill(p);', + 'const p = new RegExp("pattern"); new Array(3).fill(p);', + + // [undefined, undefined, undefined] + 'Array.from({ length: 3 }, () => {});', + + `let a = [] + a = 2 + new Array(3).fill(a)`, + + // Not check `let` variables even if it is reference value but it can be reassigned + 'let foo = []; Array(3).fill(foo);', + + // Valid because it returns a new map every time + ` + const map = new Map(); + const list = Array.from({ length: 3 }, () => { + const map = new Map(); + return map + }); + `, + + // It should be invalid because it return reference to the same map `outerMap`, but we cannot check every corner case + ` + const outerMap = new Map(); + const list = Array.from({ length: 3 }, () => { + const map = outerMap; + return map + }); + `, + + ` + const error = { + messageId: 'prefer-negative-index', + }; + Array.from({length: 4}, () => { + return { ...error } + }); + `, + + // Case from integration failed test + // https://github.com/sindresorhus/eslint-plugin-unicorn/actions/runs/15963375449/job/45019476320?pr=2661#step:5:139 + ` + import { ref } from 'vue' + + let id = 0 + + const dataGenerator = () => ({ + id: \`random-id-$\{++id}\`, + name: 'Tom', + date: '2020-10-1', + }) + + const data = ref(Array.from({ length: 200 }).map(dataGenerator)) + `, + + // Not crash for variable `dataGenerator404` not found + (` + import { ref } from 'vue' + + let id = 0 + + const dataGenerator = () => ({ + id: \`random-id-$\{++id}\`, + name: 'Tom', + date: '2020-10-1', + }) + + const data = ref(Array.from({ length: 200 }).map(dataGenerator404)) + `), + + (` + import { ref } from 'vue' + + let id = 0 + + const dataGenerator = { + id: \`random-id-$\{++id}\`, + name: 'Tom', + date: '2020-10-1', + } + + const data = ref(Array.from({ length: 200 }).map(dataGenerator)) + `), + + // https://github.com/vercel/next.js/blob/canary/packages/next/src/build/turborepo-access-trace/result.ts#L33C11-L33C47 + 'Array.from(this.fsPaths).map(String)', + // https://github.com/angular/angular/blob/main/devtools/projects/ng-devtools-backend/src/lib/component-tree/component-tree.ts#L553 + ` + import { + buildDirectiveTree, + getLViewFromDirectiveOrElementInstance, + } from '../directive-forest/index'; + + export const buildDirectiveForest = () => { + const roots = getRoots(); + return Array.prototype.concat.apply([], Array.from(roots).map(buildDirectiveTree)); + };`, + + // https://github.com/microsoft/vscode/blob/main/src/vs/base/test/common/map.test.ts#L527 + 'assert.deepStrictEqual(Array.from(map.keys()).map(String), [fileA].map(String));', + + // Will not check this even if sharedObj is a reference type + ` + const foo = 1, sharedObj = { + name: 'Tom', + date: '2020-10-1', + }; + + let dataGenerator = () => 1; // because findVariable only find the first variable + + dataGenerator = () => (sharedObj); + + const data = Array.from({ length: 200 }).map(dataGenerator) + `, + + // This is valid since sharedObj is overwritten to a primitive value. + ` + let sharedObj = { + name: 'Tom', + date: '2020-10-1', + }; + + sharedObj = 1; + + let dataGenerator = () => sharedObj; + + const data = Array.from({ length: 200 }).map(dataGenerator) + `, + + // This should be invalid since sharedObj is overwritten to a reference value. + // but we will not check this corner case. + ` + let sharedObj = 1; + + sharedObj = { + name: 'Tom', + date: '2020-10-1', + }; + + // let dataGenerator = () => sharedObj; + + const data = Array.from({ length: 200 }).map(() => sharedObj); + `, + + // https://github.com/angular/angular/blob/main/packages/upgrade/src/dynamic/test/upgrade_spec.ts#L800 + ` + const ng2Descendants = Array.from(element.querySelectorAll('ng2 li')).map( + angular.element, + );`, + + 'const arr = new Array(3); arr.fill(1)', + 'const arr = new Array(3).fill(Symbol.for("description"))', + 'const arr = new Array(3).fill(Symbol.iterator)', + + 'const obj = { primitive: 1 }; const arr = new Array(3).fill(obj.primitive)', + 'const obj = { a: { b: { c: { primitive: 1 } } } }; const arr = new Array(3).fill(obj.a.b.c.primitive)', + // `undefined` is not a reference type + 'const obj = { primitive: 1 }; const arr = new Array(3).fill(obj.list)', + // `undefined` is not a reference type + 'const obj = { a: { b: { c: { } } } }; const arr = new Array(3).fill(obj.a.b.c.list)', + // `undefined` is not a reference type + 'const obj = {}; const arr = new Array(3).fill(obj.a.b.c.list)', + // Will not check too deep (> 5) even if `list` is a reference type. + 'const obj = { a: { b: { c: { d: { list: [] } } } } }; const arr = new Array(3).fill(obj.a.b.c.d.list)', + 'const obj = { a: { b: { c: { d: { e: { list: [] } } } } } }; const arr = new Array(3).fill(obj.a.b.c.d.e.list)', + + ` + const obj2 = { a: { b: { c: { list: [] } } } }; + const obj = { a: { b1: { c: { list: [] } } , b: { c1: { list: 0 }, c: { list1: [], list: [] } } } }; + + const arr = new Array(3).fill(obj.a.b.c1.list); + `, + ` + const obj2 = { a: { b: { c: { list: 0 } } } }; + const obj = { a: { b1: { c: { list: [] } } , b: { c1: { list: 0 }, c: { list1: [], list: [] } } } }; + + const arr = new Array(3).fill(obj2.a.b.c.list); + `, + + // Not check computed property for simplicity. + 'const prop = "list"; const obj = { list: [] }; const arr = new Array(3).fill(obj[prop])', + + // Will not check too deep even if its return value is a reference type. + ` + const createError = (match, suggest) => [ + { + message: 'temp', + suggestions: undefined, + }, + ]; + + const obj = { + errors: Array.from({length: 3}).fill(createError("no", "yes")[0]), + }; + `, + ], + invalid: [ + 'new Array(3).fill([]);', // βœ— Array + 'new Array(3).fill(Array());', // βœ— Array + 'new Array(3).fill(new Array());', // βœ— Array + 'new Array(3).fill({}); // βœ— Object ', + 'new Array(3).fill(new Map()); // βœ— Map', + 'new Array(3).fill(new Set()); // βœ— Set', + + { + code: 'new Array(3).fill(/pattern/); // βœ— RegExp', + options: [{ + allowRegularExpressions: false, + }], + }, + { + code: 'new Array(3).fill(new RegExp("pattern")); // βœ— RegExp', + options: [{ + allowRegularExpressions: false, + }], + }, + { + code: 'const p = /pattern/; new Array(3).fill(p); // βœ— RegExp', + options: [{ + allowRegularExpressions: false, + }], + }, + { + code: 'const p = new RegExp("pattern"); new Array(3).fill(p); // βœ— RegExp', + options: [{ + allowRegularExpressions: false, + }], + }, + + 'new Array(3).fill(new String(\'fff\')); // βœ— new String', + + 'new Array(3).fill(new Foo(\'fff\')); // βœ— new Class', + 'class BarClass {}; new Array(3).fill(BarClass); // βœ— Class', + 'class BarClass {}; new Array(3).fill(new BarClass()); // βœ— Class instance', + + 'const map = new Map(); new Array(3).fill(map); // βœ— Variable (map)', + + 'Array(3).fill({}); // βœ— Object ', + // βœ— Object + 'Array.from({ length: 3 }).fill({});', + + 'new Array(3).fill(new Date())', + 'Array.from({ length: 3 }).fill(new Date())', + + 'const initialArray = []; new Array(3).fill(initialArray); // βœ— Variable (array)', + + // Should not fill with function + { + code: 'new Array(3).fill(() => 1);', + options: [{ + allowFunctions: false, + }], + }, + { + code: 'new Array(3).fill(() => {});', + options: [{ + allowFunctions: false, + }], + }, + { + code: 'new Array(3).fill(() => { return {} });', + options: [{ + allowFunctions: false, + }], + }, + { + code: 'new Array(3).fill(function () {});', + options: [{ + allowFunctions: false, + }], + }, + + 'new Array(3).fill(new class {});', + 'new Array(3).fill(new A.B());', + 'const cls = new class {}; new Array(3).fill(cls);', + + 'const obj = {}; Array.from({ length: 3 }).fill(obj);', + + // Variable declared in parent scope + 'const map = new Map({ foo: "bar" }); Array.from({ length: 3 }, () => map);', + + // Variable declared in its grand parent scope + 'const map = new Map({ foo: "bar" }); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }', + + 'function getMap() { return new Map({ foo: "bar" }); } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }', + + // `initialArray` is filled with no reference type (literal string) but will be treated as such because it is a function calling + // we will not dive deep into the function body to check if it returns a reference type + 'function getMap() { return "literal string" } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }', + + ` + const object = {} + Array.from({length: 3}, () => object) + `, + + 'const object = {}; Array.from({length: 31}).map(() => object);', + + // Case from integration failed test + // https://github.com/sindresorhus/eslint-plugin-unicorn/actions/runs/15963375449/job/45019476320?pr=2661#step:5:139 + ` + import { ref } from 'vue' + + let id = 0 + + const sharedObj = { + id: \`random-id-$\{++id}\`, + name: 'Tom', + date: '2020-10-1', + } + + const dataGenerator = () => (sharedObj) + + const data = ref(Array.from({ length: 200 }).map(dataGenerator)) + `, + + 'const arr = new Array(3); arr.fill([])', + 'new Foo(3).fill({});', // Check all fill method call even if the object is not Array + 'Foo(3).fill({});', // Check all fill method call even if the object is not Array + + 'const obj = { arr: [] }; const arr = new Array(3).fill(obj.arr)', + ` + const obj = { a: { b: { c: { list: [] } } } }; + const arr = new Array(3).fill(obj.a.b.c.list); + `, + ` + const obj2 = { a: { b: { c: { list: [] } } } }; + const obj = { a: { b: { c: { list: [] } } } }; + const arr = new Array(3).fill(obj.a.b.c.list); + `, + ` + const obj2 = { a: { b: { c: { list: [] } } } }; + const obj = { a: { b1: { c: { list: [] } } , b: { c1: { list: [] }, c: { list1: [], list: [] } } } }; + const arr = new Array(3).fill(obj.a.b.c.list); + `, + + 'const obj = { list: [] }; const arr = new Array(3).fill(obj["list"])', + ` + const obj = { a: { b: { c: { list: [] } } } }; + const arr = new Array(3).fill(obj['a']['b']['c']['list']); + `, + ], +}); diff --git a/test/prefer-negative-index.js b/test/prefer-negative-index.js index 205d5e9507..19727c63d0 100644 --- a/test/prefer-negative-index.js +++ b/test/prefer-negative-index.js @@ -166,6 +166,8 @@ test({ [NOT_EMPTY].slice.call(foo, [foo.length - 1, foo.length - 2, foo.length - 3]); [NOT_EMPTY].splice.call(foo, [foo.length - 1, foo.length - 2, foo.length - 3]); `, + // It's safe due to no mutation to `errors` + // eslint-disable-next-line unicorn/no-array-fill-with-reference-type errors: Array.from({length: 4}, () => error), output: outdent` [].slice.call(foo, - 1, - 2, foo.length - 3); @@ -190,6 +192,7 @@ test({ 'NOT_EMPTY'.slice.apply(foo, [foo.length - 1, foo.length - 2, foo.length - 3]); 'NOT_EMPTY'.splice.apply(foo, [foo.length - 1, foo.length - 2, foo.length - 3]); `, + // eslint-disable-next-line unicorn/no-array-fill-with-reference-type errors: Array.from({length: 2}, () => error), output: outdent` ''.slice.call(foo, - 1, - 2, foo.length - 3); @@ -239,6 +242,7 @@ test({ NOT_SUPPORTED.prototype.slice.call(foo, foo.length - 1, foo.length - 2, foo.length - 3); NOT_SUPPORTED.prototype.splice.call(foo, foo.length - 1, foo.length - 2, foo.length - 3); `, + // eslint-disable-next-line unicorn/no-array-fill-with-reference-type errors: Array.from({length: 16}, () => error), output: outdent` Array.prototype.slice.call(foo, - 1, - 2, foo.length - 3); @@ -312,6 +316,7 @@ test({ NOT_SUPPORTED.prototype.slice.apply(foo, [foo.length - 1, foo.length - 2, foo.length - 3]); NOT_SUPPORTED.prototype.splice.apply(foo, [foo.length - 1, foo.length - 2, foo.length - 3]); `, + // eslint-disable-next-line unicorn/no-array-fill-with-reference-type errors: Array.from({length: 16}, () => error), output: outdent` Array.prototype.slice.apply(foo, [- 1, - 2, foo.length - 3]); diff --git a/test/snapshots/no-array-fill-with-reference-type.js.md b/test/snapshots/no-array-fill-with-reference-type.js.md new file mode 100644 index 0000000000..09ec6a07bf --- /dev/null +++ b/test/snapshots/no-array-fill-with-reference-type.js.md @@ -0,0 +1,807 @@ +# Snapshot report for `test/no-array-fill-with-reference-type.js` + +The actual snapshot is saved in `no-array-fill-with-reference-type.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## invalid(1): new Array(3).fill([]); + +> Input + + `␊ + 1 | new Array(3).fill([]);␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill([]);␊ + | ^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(2): new Array(3).fill(Array()); + +> Input + + `␊ + 1 | new Array(3).fill(Array());␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(Array());␊ + | ^^^^^^^ Avoid using \`Array.fill()\` with reference type. Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(3): new Array(3).fill(new Array()); + +> Input + + `␊ + 1 | new Array(3).fill(new Array());␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new Array());␊ + | ^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new Array()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(4): new Array(3).fill({}); // βœ— Object + +> Input + + `␊ + 1 | new Array(3).fill({}); // βœ— Object ␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill({}); // βœ— Object ␊ + | ^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(5): new Array(3).fill(new Map()); // βœ— Map + +> Input + + `␊ + 1 | new Array(3).fill(new Map()); // βœ— Map␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new Map()); // βœ— Map␊ + | ^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new Map()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(6): new Array(3).fill(new Set()); // βœ— Set + +> Input + + `␊ + 1 | new Array(3).fill(new Set()); // βœ— Set␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new Set()); // βœ— Set␊ + | ^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new Set()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(7): new Array(3).fill(/pattern/); // βœ— RegExp + +> Input + + `␊ + 1 | new Array(3).fill(/pattern/); // βœ— RegExp␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowRegularExpressions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(/pattern/); // βœ— RegExp␊ + | ^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (RegExp). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(8): new Array(3).fill(new RegExp("pattern")); // βœ— RegExp + +> Input + + `␊ + 1 | new Array(3).fill(new RegExp("pattern")); // βœ— RegExp␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowRegularExpressions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new RegExp("pattern")); // βœ— RegExp␊ + | ^^^^^^^^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new RegExp()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(9): const p = /pattern/; new Array(3).fill(p); // βœ— RegExp + +> Input + + `␊ + 1 | const p = /pattern/; new Array(3).fill(p); // βœ— RegExp␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowRegularExpressions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | const p = /pattern/; new Array(3).fill(p); // βœ— RegExp␊ + | ^ Avoid using \`Array.fill()\` with reference type (RegExp). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(10): const p = new RegExp("pattern"); new Array(3).fill(p); // βœ— RegExp + +> Input + + `␊ + 1 | const p = new RegExp("pattern"); new Array(3).fill(p); // βœ— RegExp␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowRegularExpressions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | const p = new RegExp("pattern"); new Array(3).fill(p); // βœ— RegExp␊ + | ^ Avoid using \`Array.fill()\` with reference type (new RegExp()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(11): new Array(3).fill(new String('fff')); // βœ— new String + +> Input + + `␊ + 1 | new Array(3).fill(new String('fff')); // βœ— new String␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new String('fff')); // βœ— new String␊ + | ^^^^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new String()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(12): new Array(3).fill(new Foo('fff')); // βœ— new Class + +> Input + + `␊ + 1 | new Array(3).fill(new Foo('fff')); // βœ— new Class␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new Foo('fff')); // βœ— new Class␊ + | ^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new Foo()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(13): class BarClass {}; new Array(3).fill(BarClass); // βœ— Class + +> Input + + `␊ + 1 | class BarClass {}; new Array(3).fill(BarClass); // βœ— Class␊ + ` + +> Error 1/1 + + `␊ + > 1 | class BarClass {}; new Array(3).fill(BarClass); // βœ— Class␊ + | ^^^^^^^^ Avoid using \`Array.fill()\` with reference type. Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(14): class BarClass {}; new Array(3).fill(new BarClass()); // βœ— Class instance + +> Input + + `␊ + 1 | class BarClass {}; new Array(3).fill(new BarClass()); // βœ— Class instance␊ + ` + +> Error 1/1 + + `␊ + > 1 | class BarClass {}; new Array(3).fill(new BarClass()); // βœ— Class instance␊ + | ^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new BarClass()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(15): const map = new Map(); new Array(3).fill(map); // βœ— Variable (map) + +> Input + + `␊ + 1 | const map = new Map(); new Array(3).fill(map); // βœ— Variable (map)␊ + ` + +> Error 1/1 + + `␊ + > 1 | const map = new Map(); new Array(3).fill(map); // βœ— Variable (map)␊ + | ^^^ Avoid using \`Array.fill()\` with reference type (new Map()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(16): Array(3).fill({}); // βœ— Object + +> Input + + `␊ + 1 | Array(3).fill({}); // βœ— Object ␊ + ` + +> Error 1/1 + + `␊ + > 1 | Array(3).fill({}); // βœ— Object ␊ + | ^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(17): Array.from({ length: 3 }).fill({}); + +> Input + + `␊ + 1 | Array.from({ length: 3 }).fill({});␊ + ` + +> Error 1/1 + + `␊ + > 1 | Array.from({ length: 3 }).fill({});␊ + | ^^ Avoid using \`Array.from().fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(18): new Array(3).fill(new Date()) + +> Input + + `␊ + 1 | new Array(3).fill(new Date())␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new Date())␊ + | ^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new Date()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(19): Array.from({ length: 3 }).fill(new Date()) + +> Input + + `␊ + 1 | Array.from({ length: 3 }).fill(new Date())␊ + ` + +> Error 1/1 + + `␊ + > 1 | Array.from({ length: 3 }).fill(new Date())␊ + | ^^^^^^^^^^ Avoid using \`Array.from().fill()\` with reference type (new Date()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(20): const initialArray = []; new Array(3).fill(initialArray); // βœ— Variable (array) + +> Input + + `␊ + 1 | const initialArray = []; new Array(3).fill(initialArray); // βœ— Variable (array)␊ + ` + +> Error 1/1 + + `␊ + > 1 | const initialArray = []; new Array(3).fill(initialArray); // βœ— Variable (array)␊ + | ^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(21): new Array(3).fill(() => 1); + +> Input + + `␊ + 1 | new Array(3).fill(() => 1);␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowFunctions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(() => 1);␊ + | ^^^^^^^ Avoid using \`Array.fill()\` with reference type (Function). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(22): new Array(3).fill(() => {}); + +> Input + + `␊ + 1 | new Array(3).fill(() => {});␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowFunctions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(() => {});␊ + | ^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Function). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(23): new Array(3).fill(() => { return {} }); + +> Input + + `␊ + 1 | new Array(3).fill(() => { return {} });␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowFunctions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(() => { return {} });␊ + | ^^^^^^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Function). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(24): new Array(3).fill(function () {}); + +> Input + + `␊ + 1 | new Array(3).fill(function () {});␊ + ` + +> Options + + `␊ + [␊ + {␊ + "allowFunctions": false␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(function () {});␊ + | ^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Function). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(25): new Array(3).fill(new class {}); + +> Input + + `␊ + 1 | new Array(3).fill(new class {});␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new class {});␊ + | ^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new class). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(26): new Array(3).fill(new A.B()); + +> Input + + `␊ + 1 | new Array(3).fill(new A.B());␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Array(3).fill(new A.B());␊ + | ^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (new A.B). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(27): const cls = new class {}; new Array(3).fill(cls); + +> Input + + `␊ + 1 | const cls = new class {}; new Array(3).fill(cls);␊ + ` + +> Error 1/1 + + `␊ + > 1 | const cls = new class {}; new Array(3).fill(cls);␊ + | ^^^ Avoid using \`Array.fill()\` with reference type (new class). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(28): const obj = {}; Array.from({ length: 3 }).fill(obj); + +> Input + + `␊ + 1 | const obj = {}; Array.from({ length: 3 }).fill(obj);␊ + ` + +> Error 1/1 + + `␊ + > 1 | const obj = {}; Array.from({ length: 3 }).fill(obj);␊ + | ^^^ Avoid using \`Array.from().fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(29): const map = new Map({ foo: "bar" }); Array.from({ length: 3 }, () => map); + +> Input + + `␊ + 1 | const map = new Map({ foo: "bar" }); Array.from({ length: 3 }, () => map);␊ + ` + +> Error 1/1 + + `␊ + > 1 | const map = new Map({ foo: "bar" }); Array.from({ length: 3 }, () => map);␊ + | ^^^ Avoid using \`Array.fill()\` with reference type (new Map()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(30): const map = new Map({ foo: "bar" }); if (true) { const initialArray = Array.from({ length: 3 }, () => map); } + +> Input + + `␊ + 1 | const map = new Map({ foo: "bar" }); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }␊ + ` + +> Error 1/1 + + `␊ + > 1 | const map = new Map({ foo: "bar" }); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }␊ + | ^^^ Avoid using \`Array.fill()\` with reference type (new Map()). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(31): function getMap() { return new Map({ foo: "bar" }); } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); } + +> Input + + `␊ + 1 | function getMap() { return new Map({ foo: "bar" }); } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }␊ + ` + +> Error 1/1 + + `␊ + > 1 | function getMap() { return new Map({ foo: "bar" }); } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }␊ + | ^^^ Avoid using \`Array.fill()\` with reference type. Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(32): function getMap() { return "literal string" } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); } + +> Input + + `␊ + 1 | function getMap() { return "literal string" } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }␊ + ` + +> Error 1/1 + + `␊ + > 1 | function getMap() { return "literal string" } const map = getMap(); if (true) { const initialArray = Array.from({ length: 3 }, () => map); }␊ + | ^^^ Avoid using \`Array.fill()\` with reference type. Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(33): const object = {} Array.from({length: 3}, () => object) + +> Input + + `␊ + 1 |␊ + 2 | const object = {}␊ + 3 | Array.from({length: 3}, () => object)␊ + 4 | ␊ + ` + +> Error 1/1 + + `␊ + 1 |␊ + 2 | const object = {}␊ + > 3 | Array.from({length: 3}, () => object)␊ + | ^^^^^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + 4 | ␊ + ` + +## invalid(34): const object = {}; Array.from({length: 31}).map(() => object); + +> Input + + `␊ + 1 | const object = {}; Array.from({length: 31}).map(() => object);␊ + ` + +> Error 1/1 + + `␊ + > 1 | const object = {}; Array.from({length: 31}).map(() => object);␊ + | ^^^^^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(35): import { ref } from 'vue' let id = 0 const sharedObj = { id: `random-id-${++id}`, name: 'Tom', date: '2020-10-1', } const dataGenerator = () => (sharedObj) const data = ref(Array.from({ length: 200 }).map(dataGenerator)) + +> Input + + `␊ + 1 |␊ + 2 | import { ref } from 'vue'␊ + 3 |␊ + 4 | let id = 0␊ + 5 |␊ + 6 | const sharedObj = {␊ + 7 | id: \`random-id-${++id}\`,␊ + 8 | name: 'Tom',␊ + 9 | date: '2020-10-1',␊ + 10 | }␊ + 11 |␊ + 12 | const dataGenerator = () => (sharedObj)␊ + 13 |␊ + 14 | const data = ref(Array.from({ length: 200 }).map(dataGenerator))␊ + 15 | ␊ + ` + +> Error 1/1 + + `␊ + 1 |␊ + 2 | import { ref } from 'vue'␊ + 3 |␊ + 4 | let id = 0␊ + 5 |␊ + 6 | const sharedObj = {␊ + 7 | id: \`random-id-${++id}\`,␊ + 8 | name: 'Tom',␊ + 9 | date: '2020-10-1',␊ + 10 | }␊ + 11 |␊ + > 12 | const dataGenerator = () => (sharedObj)␊ + | ^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + 13 |␊ + 14 | const data = ref(Array.from({ length: 200 }).map(dataGenerator))␊ + 15 | ␊ + ` + +## invalid(36): const arr = new Array(3); arr.fill([]) + +> Input + + `␊ + 1 | const arr = new Array(3); arr.fill([])␊ + ` + +> Error 1/1 + + `␊ + > 1 | const arr = new Array(3); arr.fill([])␊ + | ^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(37): new Foo(3).fill({}); + +> Input + + `␊ + 1 | new Foo(3).fill({});␊ + ` + +> Error 1/1 + + `␊ + > 1 | new Foo(3).fill({});␊ + | ^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(38): Foo(3).fill({}); + +> Input + + `␊ + 1 | Foo(3).fill({});␊ + ` + +> Error 1/1 + + `␊ + > 1 | Foo(3).fill({});␊ + | ^^ Avoid using \`Array.fill()\` with reference type (Object). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(39): const obj = { arr: [] }; const arr = new Array(3).fill(obj.arr) + +> Input + + `␊ + 1 | const obj = { arr: [] }; const arr = new Array(3).fill(obj.arr)␊ + ` + +> Error 1/1 + + `␊ + > 1 | const obj = { arr: [] }; const arr = new Array(3).fill(obj.arr)␊ + | ^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(40): const obj = { a: { b: { c: { list: [] } } } }; const arr = new Array(3).fill(obj.a.b.c.list); + +> Input + + `␊ + 1 |␊ + 2 | const obj = { a: { b: { c: { list: [] } } } };␊ + 3 | const arr = new Array(3).fill(obj.a.b.c.list);␊ + 4 | ␊ + ` + +> Error 1/1 + + `␊ + 1 |␊ + 2 | const obj = { a: { b: { c: { list: [] } } } };␊ + > 3 | const arr = new Array(3).fill(obj.a.b.c.list);␊ + | ^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + 4 | ␊ + ` + +## invalid(41): const obj2 = { a: { b: { c: { list: [] } } } }; const obj = { a: { b: { c: { list: [] } } } }; const arr = new Array(3).fill(obj.a.b.c.list); + +> Input + + `␊ + 1 |␊ + 2 | const obj2 = { a: { b: { c: { list: [] } } } };␊ + 3 | const obj = { a: { b: { c: { list: [] } } } };␊ + 4 | const arr = new Array(3).fill(obj.a.b.c.list);␊ + 5 | ␊ + ` + +> Error 1/1 + + `␊ + 1 |␊ + 2 | const obj2 = { a: { b: { c: { list: [] } } } };␊ + 3 | const obj = { a: { b: { c: { list: [] } } } };␊ + > 4 | const arr = new Array(3).fill(obj.a.b.c.list);␊ + | ^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + 5 | ␊ + ` + +## invalid(42): const obj2 = { a: { b: { c: { list: [] } } } }; const obj = { a: { b1: { c: { list: [] } } , b: { c1: { list: [] }, c: { list1: [], list: [] } } } }; const arr = new Array(3).fill(obj.a.b.c.list); + +> Input + + `␊ + 1 |␊ + 2 | const obj2 = { a: { b: { c: { list: [] } } } };␊ + 3 | const obj = { a: { b1: { c: { list: [] } } , b: { c1: { list: [] }, c: { list1: [], list: [] } } } };␊ + 4 | const arr = new Array(3).fill(obj.a.b.c.list);␊ + 5 | ␊ + ` + +> Error 1/1 + + `␊ + 1 |␊ + 2 | const obj2 = { a: { b: { c: { list: [] } } } };␊ + 3 | const obj = { a: { b1: { c: { list: [] } } , b: { c1: { list: [] }, c: { list1: [], list: [] } } } };␊ + > 4 | const arr = new Array(3).fill(obj.a.b.c.list);␊ + | ^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + 5 | ␊ + ` + +## invalid(43): const obj = { list: [] }; const arr = new Array(3).fill(obj["list"]) + +> Input + + `␊ + 1 | const obj = { list: [] }; const arr = new Array(3).fill(obj["list"])␊ + ` + +> Error 1/1 + + `␊ + > 1 | const obj = { list: [] }; const arr = new Array(3).fill(obj["list"])␊ + | ^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + ` + +## invalid(44): const obj = { a: { b: { c: { list: [] } } } }; const arr = new Array(3).fill(obj['a']['b']['c']['list']); + +> Input + + `␊ + 1 |␊ + 2 | const obj = { a: { b: { c: { list: [] } } } };␊ + 3 | const arr = new Array(3).fill(obj['a']['b']['c']['list']);␊ + 4 | ␊ + ` + +> Error 1/1 + + `␊ + 1 |␊ + 2 | const obj = { a: { b: { c: { list: [] } } } };␊ + > 3 | const arr = new Array(3).fill(obj['a']['b']['c']['list']);␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid using \`Array.fill()\` with reference type (Array). Use \`Array.from({ ... }, () => { return independent instance })\` instead to ensure no reference shared.␊ + 4 | ␊ + ` diff --git a/test/snapshots/no-array-fill-with-reference-type.js.snap b/test/snapshots/no-array-fill-with-reference-type.js.snap new file mode 100644 index 0000000000..98468a25b5 Binary files /dev/null and b/test/snapshots/no-array-fill-with-reference-type.js.snap differ diff --git a/test/string-content.js b/test/string-content.js index d36a06da66..8b409e9dff 100644 --- a/test/string-content.js +++ b/test/string-content.js @@ -223,6 +223,7 @@ test({ code: 'const foo = `no${foo}no${foo}no`', output: 'const foo = `yes${foo}yes${foo}yes`', options: [{patterns: noToYesPattern}], + errors: Array.from({length: 3}).fill(createError('no', 'yes')[0]), }, // Escape