Skip to content

Commit

Permalink
feat(immutable-data): add option for ignoreNonConstDeclarations to …
Browse files Browse the repository at this point in the history
…`treatParametersAsConst` (#794)

fix #724
  • Loading branch information
RebeccaStevens authored Mar 25, 2024
1 parent f147c2e commit 059591a
Show file tree
Hide file tree
Showing 5 changed files with 398 additions and 36 deletions.
10 changes: 9 additions & 1 deletion docs/rules/immutable-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ This rule accepts an options object of the following type:
type Options = {
ignoreClasses: boolean | "fieldsOnly";
ignoreImmediateMutation: boolean;
ignoreNonConstDeclarations: boolean;
ignoreNonConstDeclarations:
| boolean
| {
treatParametersAsConst: boolean;
};
ignoreIdentifierPattern?: string[] | string;
ignoreAccessorPattern?: string[] | string;
};
Expand Down Expand Up @@ -110,6 +114,10 @@ Note: If a value is referenced by both a `let` and a `const` variable, the `let`
reference can be modified while the `const` one can't. The may lead to value of
the `const` variable unexpectedly changing when the `let` one is modified elsewhere.

#### `treatParametersAsConst`

If true, parameters won't be ignored, while other non-const variables will be.

### `ignoreClasses`

Ignore mutations inside classes.
Expand Down
139 changes: 107 additions & 32 deletions src/rules/immutable-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ type Options = [
IgnoreClassesOption &
IgnoreIdentifierPatternOption & {
ignoreImmediateMutation: boolean;
ignoreNonConstDeclarations: boolean;
ignoreNonConstDeclarations:
| boolean
| {
treatParametersAsConst: boolean;
};
},
];

Expand All @@ -80,7 +84,20 @@ const schema: JSONSchema4[] = [
type: "boolean",
},
ignoreNonConstDeclarations: {
type: "boolean",
oneOf: [
{
type: "boolean",
},
{
type: "object",
properties: {
treatParametersAsConst: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
} satisfies JSONSchema4ObjectSchema["properties"],
),
Expand Down Expand Up @@ -230,11 +247,23 @@ function checkAssignmentExpression(
};
}

if (ignoreNonConstDeclarations) {
if (ignoreNonConstDeclarations !== false) {
const rootIdentifier = findRootIdentifier(node.left.object);
if (
rootIdentifier !== undefined &&
isDefinedByMutableVariable(rootIdentifier, context)
isDefinedByMutableVariable(
rootIdentifier,
context,
(variableNode) =>
ignoreNonConstDeclarations === true ||
!ignoreNonConstDeclarations.treatParametersAsConst ||
shouldIgnorePattern(
variableNode,
context,
ignoreIdentifierPattern,
ignoreAccessorPattern,
),
)
) {
return {
context,
Expand Down Expand Up @@ -283,11 +312,23 @@ function checkUnaryExpression(
};
}

if (ignoreNonConstDeclarations) {
if (ignoreNonConstDeclarations !== false) {
const rootIdentifier = findRootIdentifier(node.argument.object);
if (
rootIdentifier !== undefined &&
isDefinedByMutableVariable(rootIdentifier, context)
isDefinedByMutableVariable(
rootIdentifier,
context,
(variableNode) =>
ignoreNonConstDeclarations === true ||
!ignoreNonConstDeclarations.treatParametersAsConst ||
shouldIgnorePattern(
variableNode,
context,
ignoreIdentifierPattern,
ignoreAccessorPattern,
),
)
) {
return {
context,
Expand Down Expand Up @@ -335,11 +376,23 @@ function checkUpdateExpression(
};
}

if (ignoreNonConstDeclarations) {
if (ignoreNonConstDeclarations !== false) {
const rootIdentifier = findRootIdentifier(node.argument.object);
if (
rootIdentifier !== undefined &&
isDefinedByMutableVariable(rootIdentifier, context)
isDefinedByMutableVariable(
rootIdentifier,
context,
(variableNode) =>
ignoreNonConstDeclarations === true ||
!ignoreNonConstDeclarations.treatParametersAsConst ||
shouldIgnorePattern(
variableNode,
context,
ignoreIdentifierPattern,
ignoreAccessorPattern,
),
)
) {
return {
context,
Expand Down Expand Up @@ -473,18 +526,29 @@ function checkCallExpression(
!isInChainCallAndFollowsNew(node.callee, context)) &&
isArrayType(getTypeOfNode(node.callee.object, context))
) {
if (ignoreNonConstDeclarations) {
const rootIdentifier = findRootIdentifier(node.callee.object);
if (
rootIdentifier === undefined ||
!isDefinedByMutableVariable(rootIdentifier, context)
) {
return {
context,
descriptors: [{ node, messageId: "array" }],
};
}
} else {
if (ignoreNonConstDeclarations === false) {
return {
context,
descriptors: [{ node, messageId: "array" }],
};
}
const rootIdentifier = findRootIdentifier(node.callee.object);
if (
rootIdentifier === undefined ||
!isDefinedByMutableVariable(
rootIdentifier,
context,
(variableNode) =>
ignoreNonConstDeclarations === true ||
!ignoreNonConstDeclarations.treatParametersAsConst ||
shouldIgnorePattern(
variableNode,
context,
ignoreIdentifierPattern,
ignoreAccessorPattern,
),
)
) {
return {
context,
descriptors: [{ node, messageId: "array" }],
Expand All @@ -507,18 +571,29 @@ function checkCallExpression(
) &&
isObjectConstructorType(getTypeOfNode(node.callee.object, context))
) {
if (ignoreNonConstDeclarations) {
const rootIdentifier = findRootIdentifier(node.callee.object);
if (
rootIdentifier === undefined ||
!isDefinedByMutableVariable(rootIdentifier, context)
) {
return {
context,
descriptors: [{ node, messageId: "object" }],
};
}
} else {
if (ignoreNonConstDeclarations === false) {
return {
context,
descriptors: [{ node, messageId: "object" }],
};
}
const rootIdentifier = findRootIdentifier(node.callee.object);
if (
rootIdentifier === undefined ||
!isDefinedByMutableVariable(
rootIdentifier,
context,
(variableNode) =>
ignoreNonConstDeclarations === true ||
!ignoreNonConstDeclarations.treatParametersAsConst ||
shouldIgnorePattern(
variableNode,
context,
ignoreIdentifierPattern,
ignoreAccessorPattern,
),
)
) {
return {
context,
descriptors: [{ node, messageId: "object" }],
Expand Down
31 changes: 28 additions & 3 deletions src/utils/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ export function isArgument(node: TSESTree.Node): boolean {
);
}

/**
* Is the given node a parameter?
*/
export function isParameter(node: TSESTree.Node): boolean {
return (
node.parent !== undefined &&
isFunctionLike(node.parent) &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
node.parent.params.includes(node as any)
);
}

/**
* Is the given node a getter function?
*/
Expand Down Expand Up @@ -273,14 +285,27 @@ export function getKeyOfValueInObjectExpression(
*/
export function isDefinedByMutableVariable<
Context extends RuleContext<string, BaseOptions>,
>(node: TSESTree.Identifier, context: Context) {
>(
node: TSESTree.Identifier,
context: Context,
treatParametersAsMutable: (node: TSESTree.Node) => boolean,
): boolean {
const services = getParserServices(context);
const symbol = services.getSymbolAtLocation(node);
const variableDeclaration = symbol?.valueDeclaration;

if (variableDeclaration === undefined) {
return true;
}
const variableDeclarationNode =
services.tsNodeToESTreeNodeMap.get(variableDeclaration);
if (
variableDeclaration === undefined ||
!typescript!.isVariableDeclaration(variableDeclaration)
variableDeclarationNode !== undefined &&
isParameter(variableDeclarationNode)
) {
return treatParametersAsMutable(variableDeclarationNode);
}
if (!typescript!.isVariableDeclaration(variableDeclaration)) {
return true;
}

Expand Down
Loading

0 comments on commit 059591a

Please sign in to comment.