@@ -5,7 +5,10 @@ import {
55} from "@typescript-eslint/utils/json-schema" ;
66import { type RuleContext } from "@typescript-eslint/utils/ts-eslint" ;
77import { deepmerge } from "deepmerge-ts" ;
8+ import { isNodeFlagSet } from "ts-api-utils" ;
9+ import type ts from "typescript" ;
810
11+ import typescript from "#eslint-plugin-functional/conditional-imports/typescript" ;
912import {
1013 type IgnoreAccessorPatternOption ,
1114 type IgnoreIdentifierPatternOption ,
@@ -52,6 +55,7 @@ type Options = [
5255 IgnoreClassesOption &
5356 IgnoreIdentifierPatternOption & {
5457 ignoreImmediateMutation : boolean ;
58+ ignoreNonConstDeclarations : boolean ;
5559 } ,
5660] ;
5761
@@ -69,6 +73,9 @@ const schema: JSONSchema4[] = [
6973 ignoreImmediateMutation : {
7074 type : "boolean" ,
7175 } ,
76+ ignoreNonConstDeclarations : {
77+ type : "boolean" ,
78+ } ,
7279 } satisfies JSONSchema4ObjectSchema [ "properties" ] ,
7380 ) ,
7481 additionalProperties : false ,
@@ -82,6 +89,7 @@ const defaultOptions: Options = [
8289 {
8390 ignoreClasses : false ,
8491 ignoreImmediateMutation : true ,
92+ ignoreNonConstDeclarations : false ,
8593 } ,
8694] ;
8795
@@ -158,6 +166,39 @@ const objectConstructorMutatorFunctions = new Set([
158166 "setPrototypeOf" ,
159167] ) ;
160168
169+ /**
170+ * Is the given identifier defined by a mutable variable (let or var)?
171+ */
172+ function isDefinedByMutableVaraible (
173+ node : TSESTree . Identifier ,
174+ context : Readonly < RuleContext < keyof typeof errorMessages , Options > > ,
175+ ) {
176+ if ( typescript === undefined ) {
177+ return true ;
178+ }
179+
180+ const tsNode = context . parserServices ?. esTreeNodeToTSNodeMap . get ( node ) ;
181+ const variableDeclaration =
182+ tsNode !== undefined &&
183+ "flowNode" in tsNode &&
184+ typeof tsNode . flowNode === "object" &&
185+ tsNode . flowNode !== null &&
186+ "node" in tsNode . flowNode &&
187+ typeof tsNode . flowNode . node === "object" &&
188+ tsNode . flowNode . node !== null &&
189+ typescript . isVariableDeclaration ( tsNode . flowNode . node as ts . Node )
190+ ? ( tsNode . flowNode . node as ts . VariableDeclaration )
191+ : undefined ;
192+
193+ const variableDeclarationList = variableDeclaration ?. parent ;
194+
195+ return (
196+ variableDeclarationList === undefined ||
197+ ! typescript . isVariableDeclarationList ( variableDeclarationList ) ||
198+ ! isNodeFlagSet ( variableDeclarationList , typescript . NodeFlags . Const )
199+ ) ;
200+ }
201+
161202/**
162203 * Check if the given assignment expression violates this rule.
163204 */
@@ -167,8 +208,12 @@ function checkAssignmentExpression(
167208 options : Readonly < Options > ,
168209) : RuleResult < keyof typeof errorMessages , Options > {
169210 const [ optionsObject ] = options ;
170- const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreClasses } =
171- optionsObject ;
211+ const {
212+ ignoreIdentifierPattern,
213+ ignoreAccessorPattern,
214+ ignoreNonConstDeclarations,
215+ ignoreClasses,
216+ } = optionsObject ;
172217
173218 if (
174219 ! isMemberExpression ( node . left ) ||
@@ -186,6 +231,17 @@ function checkAssignmentExpression(
186231 } ;
187232 }
188233
234+ if (
235+ ignoreNonConstDeclarations &&
236+ isIdentifier ( node . left . object ) &&
237+ isDefinedByMutableVaraible ( node . left . object , context )
238+ ) {
239+ return {
240+ context,
241+ descriptors : [ ] ,
242+ } ;
243+ }
244+
189245 return {
190246 context,
191247 descriptors :
@@ -203,8 +259,12 @@ function checkUnaryExpression(
203259 options : Readonly < Options > ,
204260) : RuleResult < keyof typeof errorMessages , Options > {
205261 const [ optionsObject ] = options ;
206- const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreClasses } =
207- optionsObject ;
262+ const {
263+ ignoreIdentifierPattern,
264+ ignoreAccessorPattern,
265+ ignoreNonConstDeclarations,
266+ ignoreClasses,
267+ } = optionsObject ;
208268
209269 if (
210270 ! isMemberExpression ( node . argument ) ||
@@ -222,6 +282,17 @@ function checkUnaryExpression(
222282 } ;
223283 }
224284
285+ if (
286+ ignoreNonConstDeclarations &&
287+ isIdentifier ( node . argument . object ) &&
288+ isDefinedByMutableVaraible ( node . argument . object , context )
289+ ) {
290+ return {
291+ context,
292+ descriptors : [ ] ,
293+ } ;
294+ }
295+
225296 return {
226297 context,
227298 descriptors :
@@ -238,8 +309,12 @@ function checkUpdateExpression(
238309 options : Readonly < Options > ,
239310) : RuleResult < keyof typeof errorMessages , Options > {
240311 const [ optionsObject ] = options ;
241- const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreClasses } =
242- optionsObject ;
312+ const {
313+ ignoreIdentifierPattern,
314+ ignoreAccessorPattern,
315+ ignoreNonConstDeclarations,
316+ ignoreClasses,
317+ } = optionsObject ;
243318
244319 if (
245320 ! isMemberExpression ( node . argument ) ||
@@ -257,6 +332,17 @@ function checkUpdateExpression(
257332 } ;
258333 }
259334
335+ if (
336+ ignoreNonConstDeclarations &&
337+ isIdentifier ( node . argument . object ) &&
338+ isDefinedByMutableVaraible ( node . argument . object , context )
339+ ) {
340+ return {
341+ context,
342+ descriptors : [ ] ,
343+ } ;
344+ }
345+
260346 return {
261347 context,
262348 descriptors : [ { node, messageId : "generic" } ] ,
@@ -306,8 +392,12 @@ function checkCallExpression(
306392 options : Readonly < Options > ,
307393) : RuleResult < keyof typeof errorMessages , Options > {
308394 const [ optionsObject ] = options ;
309- const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreClasses } =
310- optionsObject ;
395+ const {
396+ ignoreIdentifierPattern,
397+ ignoreAccessorPattern,
398+ ignoreNonConstDeclarations,
399+ ignoreClasses,
400+ } = optionsObject ;
311401
312402 // Not potential object mutation?
313403 if (
@@ -334,7 +424,10 @@ function checkCallExpression(
334424 arrayMutatorMethods . has ( node . callee . property . name ) &&
335425 ( ! ignoreImmediateMutation ||
336426 ! isInChainCallAndFollowsNew ( node . callee , context ) ) &&
337- isArrayType ( getTypeOfNode ( node . callee . object , context ) )
427+ isArrayType ( getTypeOfNode ( node . callee . object , context ) ) &&
428+ ( ! ignoreNonConstDeclarations ||
429+ ! isIdentifier ( node . callee . object ) ||
430+ ! isDefinedByMutableVaraible ( node . callee . object , context ) )
338431 ) {
339432 return {
340433 context,
@@ -355,7 +448,10 @@ function checkCallExpression(
355448 ignoreIdentifierPattern ,
356449 ignoreAccessorPattern ,
357450 ) &&
358- isObjectConstructorType ( getTypeOfNode ( node . callee . object , context ) )
451+ isObjectConstructorType ( getTypeOfNode ( node . callee . object , context ) ) &&
452+ ( ! ignoreNonConstDeclarations ||
453+ ! isIdentifier ( node . callee . object ) ||
454+ ! isDefinedByMutableVaraible ( node . callee . object , context ) )
359455 ) {
360456 return {
361457 context,
0 commit comments