Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 44 additions & 25 deletions src/rules/prefer-array-fill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,26 @@ ruleTester.run('prefer-array-fill', preferArrayFill, {

// Different patterns
'const arr = new Array(5)',
'const arr = Array(5)'
'const arr = Array(5)',

// Non-constant callback - function call that could return different values each time
'const arr = Array.from({length: 5}, () => faker.lorem.sentences(3))',
Comment thread
43081j marked this conversation as resolved.
'const arr = Array.from({length: 5}, () => Math.random())',
'const arr = Array.from({length: 3}, function() { return faker.lorem.sentences(3) })',
'const arr = Array.from({length: 5}, () => new Date())',
'const arr = Array.from({length: 5}, () => [])',
'const arr = [...Array(5)].map(() => faker.lorem.sentences(3))',
'const arr = [...Array(5)].map(() => Math.random())',
'const arr = [...Array(3)].map(function() { return Math.random() })',
'const arr = [...Array(5)].map(() => new Foo())',

// Non-constant callback - object/array literal creates new value each time
'const arr = Array.from({length: 5}, () => ({}))',
'const arr = [...Array(5)].map(() => [])',

// Non-constant callback - logical/conditional containing a call
'const arr = Array.from({length: 5}, () => a || getVal())',
'const arr = [...Array(5)].map(() => a ? getVal() : b)'
],

invalid: [
Expand Down Expand Up @@ -75,18 +94,6 @@ ruleTester.run('prefer-array-fill', preferArrayFill, {
]
},

// Array.from with object value
{
code: 'const arr = Array.from({length: 5}, () => ({}))',
output: 'const arr = Array.from({length: 5}).fill({})',
errors: [
{
messageId: 'preferFillArrayFrom',
data: {length: '5', value: '{}'}
}
]
},

// Array.from with regular function expression
{
code: 'const arr = Array.from({length: 5}, function() { return 0 })',
Expand Down Expand Up @@ -123,18 +130,6 @@ ruleTester.run('prefer-array-fill', preferArrayFill, {
]
},

// Spread Array with map and expression
{
code: 'const arr = [...Array(10)].map(() => [])',
output: 'const arr = Array(10).fill([])',
errors: [
{
messageId: 'preferFillSpreadMap',
data: {length: '10', value: '[]'}
}
]
},

// Spread Array with map and function expression
{
code: 'const arr = [...Array(5)].map(function() { return 1 })',
Expand Down Expand Up @@ -211,6 +206,30 @@ const arr2 = Array(3).fill("test");`,
data: {length: '5', value: '1 + 2'}
}
]
},

// Logical expression with all-constant operands
{
code: 'const arr = Array.from({length: 5}, () => a ?? b)',
output: 'const arr = Array.from({length: 5}).fill(a ?? b)',
errors: [
{
messageId: 'preferFillArrayFrom',
data: {length: '5', value: 'a ?? b'}
}
]
},

// Conditional expression with all-constant branches
{
code: 'const arr = [...Array(5)].map(() => a ? b : c)',
output: 'const arr = Array(5).fill(a ? b : c)',
errors: [
{
messageId: 'preferFillSpreadMap',
data: {length: '5', value: 'a ? b : c'}
}
]
}
]
});
79 changes: 64 additions & 15 deletions src/rules/prefer-array-fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,81 @@ import type {Rule, SourceCode} from 'eslint';
import type {
CallExpression,
ArrowFunctionExpression,
FunctionExpression
FunctionExpression,
Expression
} from 'estree';

function isConstantExpression(node: Expression): boolean {
switch (node.type) {
case 'Literal':
case 'Identifier':
return true;
case 'CallExpression':
case 'NewExpression':
case 'ObjectExpression':
case 'ArrayExpression':
return false;
case 'MemberExpression':
return (
node.object.type !== 'Super' &&
isConstantExpression(node.object) &&
(!node.computed || isConstantExpression(node.property as Expression))
);
case 'UnaryExpression':
return isConstantExpression(node.argument);
case 'BinaryExpression':
case 'LogicalExpression':
return (
node.left.type !== 'PrivateIdentifier' &&
isConstantExpression(node.left) &&
isConstantExpression(node.right)
);
case 'ConditionalExpression':
return (
isConstantExpression(node.test) &&
isConstantExpression(node.consequent) &&
isConstantExpression(node.alternate)
);
case 'TemplateLiteral':
return node.expressions.every((expr) => isConstantExpression(expr));
default:
return false;
}
}

function getCallbackValueNode(
func: ArrowFunctionExpression | FunctionExpression
): Expression | undefined {
if (func.body.type === 'BlockStatement') {
if (func.body.body.length !== 1) return undefined;
const returnStmt = func.body.body[0];
if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
return returnStmt.argument;
}
return undefined;
}
return func.body;
}

function isConstantCallback(
func: ArrowFunctionExpression | FunctionExpression
): boolean {
return (
func.params.length === 0 &&
(func.body.type !== 'BlockStatement' ||
(func.body.body.length === 1 &&
func.body.body[0]?.type === 'ReturnStatement'))
);
if (func.params.length !== 0) {
return false;
}
const valueNode = getCallbackValueNode(func);
if (!valueNode) {
return false;
}
return isConstantExpression(valueNode);
}

function getCallbackValueText(
func: ArrowFunctionExpression | FunctionExpression,
sourceCode: SourceCode
): string | undefined {
if (func.body.type === 'BlockStatement') {
const returnStmt = func.body.body[0];
if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
return sourceCode.getText(returnStmt.argument);
}
return undefined;
}
return sourceCode.getText(func.body);
const valueNode = getCallbackValueNode(func);
return valueNode ? sourceCode.getText(valueNode) : undefined;
}

export const preferArrayFill: Rule.RuleModule = {
Expand Down