Skip to content
Open
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
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v24.7.0
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',
]

const options = context.options[0] || {}
const pureMethods = [...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};`)
}
},
},
],
})
}
},
}
},
}
125 changes: 125 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import js from '@eslint/js'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import security from 'eslint-plugin-security'
import pureFunctions from './eslint-plugin-pure-functions/index.js'
import prettierConfig from 'eslint-config-prettier'
import globals from 'globals'

export default [
// Base JavaScript recommended rules
js.configs.recommended,

// Global ignores
{
ignores: [
'**/dist/**',
'**/build/**',
'**/node_modules/**',
'**/*.js',
'**/*.cjs',
'**/*.mjs',
// Specific files to ignore from the old config
],
},

// TypeScript configuration
{
files: ['**/*.ts', '**/*.tsx'],

languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
globals: {
...globals.node,
},
},

plugins: {
'@typescript-eslint': typescriptEslint,
security,
'pure-functions': pureFunctions,
},

rules: {
// Turn off base rules that conflict with TypeScript versions
'no-unused-vars': 'off',
'no-unused-expressions': 'off',
'no-use-before-define': 'off',

// Basic ESLint rules
'no-empty': [
'warn',
{
allowEmptyCatch: true,
},
],

//ban types replacement
'@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',

// TypeScript recommended rules (equivalent to plugin:@typescript-eslint/recommended)
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-duplicate-enum-values': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-extra-non-null-assertion': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-unused-vars': ['warn', { caughtErrors: 'none' }],
'@typescript-eslint/no-wrapper-object-types': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/triple-slash-reference': 'error',

// Type-aware rules (require parserOptions.project)
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',

// The important rule for catching unused expressions like arr.concat()
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],

// Function return type rule from original config
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true,
},
],

// Security rules
'security/detect-object-injection': 'warn',

// Custom rule to catch only pure function calls without assignment
'pure-functions/no-unused-pure-calls': 'error',
},
},

// Prettier config - must come last to disable conflicting formatting rules
prettierConfig,
]
Loading
Loading