Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 0 additions & 74 deletions .eslintrc.json

This file was deleted.

15 changes: 15 additions & 0 deletions eslint-plugin-pure-functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const noUnusedPureCalls = require('./rules/no-unused-pure-calls');

module.exports = {
rules: {
'no-unused-pure-calls': noUnusedPureCalls,
},
configs: {
recommended: {
plugins: ['pure-functions'],
rules: {
'pure-functions/no-unused-pure-calls': 'error',
},
},
},
};
173 changes: 173 additions & 0 deletions eslint-plugin-pure-functions/rules/no-unused-pure-calls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow calling pure functions without using their return value',
category: 'Possible Errors',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
schema: [
{
type: 'object',
properties: {
pureMethods: {
type: 'array',
items: { type: 'string' },
default: [],
},
},
additionalProperties: false,
},
],
messages: {
unusedPureCall: "Pure method '{{method}}' called without using return value. Did you mean to assign the result?",
},
},

create(context) {
// Default pure methods that don't mutate the original object/array
const defaultPureMethods = [
// Array methods that return new arrays
'concat',
'slice',
'map',
'filter',
'reduce',
'reduceRight',
'find',
'findIndex',
'some',
'every',
'includes',
'indexOf',
'lastIndexOf',
'join',
'toString',
'toLocaleString',
'flatMap',
'flat',
'with',
'toReversed',
'toSorted',
'toSpliced',

// String methods that return new strings
'substring',
'substr',
'toLowerCase',
'toUpperCase',
'trim',
'trimStart',
'trimEnd',
'replace',
'replaceAll',
'split',
'padStart',
'padEnd',
'repeat',
'charAt',
'charCodeAt',
'slice',
'substr',
'substring',

// Object methods that return new objects/values
'assign',
'keys',
'values',
'entries',
'freeze',
'seal',
'getOwnPropertyNames',
'getOwnPropertyDescriptors',
'sign',
]
Comment on lines +77 to +86

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The defaultPureMethods array includes 'assign', which is not a pure function (Object.assign mutates the first argument). Remove 'assign' to avoid false positives where mutating methods are flagged as pure. [possible issue, importance: 9]

Suggested change
'assign',
'keys',
'values',
'entries',
'freeze',
'seal',
'getOwnPropertyNames',
'getOwnPropertyDescriptors',
'sign',
]
'keys',
'values',
'entries',
'freeze',
'seal',
'getOwnPropertyNames',
'getOwnPropertyDescriptors',
'sign',
]


const options = context.options[0] || {}
const pureMethods = [...defaultPureMethods, ...(options.pureMethods || [])]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The pureMethods array may contain duplicate method names, especially since some methods like 'slice', 'substr', and 'substring' appear multiple times in defaultPureMethods. This can cause unnecessary repeated checks and potential confusion. Remove duplicates from the pureMethods array to ensure each method is only checked once. [general, importance: 6]

Suggested change
const pureMethods = [...defaultPureMethods, ...(options.pureMethods || [])]
const pureMethods = Array.from(new Set([...defaultPureMethods, ...(options.pureMethods || [])]))


function isPureMethodCall(node) {
if (node.type !== 'CallExpression') return false
if (node.callee.type !== 'MemberExpression') return false
if (node.callee.property.type !== 'Identifier') return false

return pureMethods.includes(node.callee.property.name)
}

function isResultUsed(node) {
const parent = node.parent

// Check if return value is used in meaningful way
switch (parent.type) {
case 'AssignmentExpression':
return parent.right === node
case 'VariableDeclarator':
return parent.init === node
case 'ReturnStatement':
return true
case 'CallExpression':
return parent.arguments.includes(node)
case 'BinaryExpression':
case 'LogicalExpression':
case 'UnaryExpression':
return true
case 'ConditionalExpression':
return parent.test === node || parent.consequent === node || parent.alternate === node
case 'ArrayExpression':
return parent.elements.includes(node)
case 'ObjectExpression':
return parent.properties.some((prop) => prop.value === node)
case 'Property':
return parent.value === node
case 'IfStatement':
return parent.test === node
case 'WhileStatement':
case 'ForStatement':
return parent.test === node
case 'ExpressionStatement':
return false // This is the problem case - standalone expression
case 'AwaitExpression':
return parent.argument === node
default:
return true // Assume used in other contexts
}
}

return {
ExpressionStatement(node) {
const expression = node.expression

if (isPureMethodCall(expression) && !isResultUsed(expression)) {
const methodName = expression.callee.property.name

context.report({
node: expression,
messageId: 'unusedPureCall',
data: {
method: methodName,
},
suggest: [
{
desc: 'Assign result to variable',
fix(fixer) {
const sourceCode = context.getSourceCode()
const objectText = sourceCode.getText(expression.callee.object)
const methodCall = sourceCode.getText(expression)

// Suggest assignment back to the same variable if possible
if (expression.callee.object.type === 'Identifier') {
return fixer.replaceText(node, `${objectText} = ${methodCall};`)
} else {
return fixer.replaceText(node, `const result = ${methodCall};`)
}
},
},
],
})
}
},
}
},
}
Loading
Loading