core/quality/eslint/no-mutable-parameter.eslint.rule.ts
core/quality/eslint/no-mutable-parameter.eslint.rule.ts is a file in GovLab Quality. 116 lines of code and 27 definitions.
import type { Rule, Scope } from "eslint";
import { govlabMeta } from "#core/factories/eslint.factory";
import type ts from "typescript";
const MUTATORS = new Set<string>([
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
"fill",
"copyWithin",
]);
interface MemberNode {
type: string;
computed?: boolean;
object?: MemberNode;
property?: { name?: string };
name?: string;
}
const isMember = function isMember(value: unknown): value is MemberNode {
return value !== null && typeof value === "object" && "type" in value;
};
const asMember = function asMember(value: unknown): MemberNode | null {
return isMember(value) ? value : null;
};
const rootIdentifier = function rootIdentifier(node: MemberNode | null): MemberNode | null {
let current = node;
while (current?.type === "MemberExpression") {
current = current.object ?? null;
}
return current?.type === "Identifier" ? current : null;
};
const resolvesToParameter = function resolvesToParameter(scope: Scope.Scope | null, name: string): boolean {
let current: Scope.Scope | null = scope;
while (current) {
const variable = current.variables.find((entry) => entry.name === name);
if (variable) {
return variable.defs.some((def) => def.type === "Parameter");
}
current = current.upper;
}
return false;
};
interface TypedParserServices {
esTreeNodeToTSNodeMap: { get: (node: unknown) => ts.Node | undefined };
program: ts.Program;
}
const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
};
const isTypedServices = function isTypedServices(value: unknown): value is TypedParserServices {
if (!isRecord(value)) {
return false;
}
const { esTreeNodeToTSNodeMap: nodeMap, program } = value;
const mapsNodes = isRecord(nodeMap) && typeof nodeMap["get"] === "function";
return mapsNodes && isRecord(program) && typeof program["getTypeChecker"] === "function";
};
const isArrayLikeReceiver = function isArrayLikeReceiver(context: Rule.RuleContext, receiver: unknown): boolean {
const services: unknown = context.sourceCode.parserServices;
if (!isTypedServices(services)) {
return true;
}
const tsNode = services.esTreeNodeToTSNodeMap.get(receiver);
if (tsNode === undefined) {
return true;
}
const checker = services.program.getTypeChecker();
const type: ts.Type = checker.getTypeAtLocation(tsNode);
const parts: ts.Type[] = type.isUnion() ? type.types : [type];
return parts.some((part) => checker.isArrayType(part) || checker.isTupleType(part));
};
const mutatedParam = function mutatedParam(
context: Rule.RuleContext,
node: Rule.Node,
): { method: string; name: string } | null {
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed) {
return null;
}
const { callee } = node;
const method = callee.property.type === "Identifier" ? callee.property.name : "";
if (!MUTATORS.has(method)) {
return null;
}
if (!isArrayLikeReceiver(context, callee.object)) {
return null;
}
const root = rootIdentifier(asMember(callee.object));
if (root === null || typeof root.name !== "string") {
return null;
}
return resolvesToParameter(context.sourceCode.getScope(node), root.name) ? { method, name: root.name } : null;
};
export default {
create(context: Rule.RuleContext): Rule.RuleListener {
const onCall = (node: Rule.Node): void => {
const hit = mutatedParam(context, node);
if (hit !== null) {
context.report({ data: hit, messageId: "mutableParam", node });
}
};
const handlers: [string, (node: Rule.Node) => void][] = [["CallExpression", onCall]];
return Object.fromEntries(handlers);
},
meta: govlabMeta({
canonical: ["immutability"],
description: "Disallow mutating a function parameter (or its members) in place — mandate returning new data",
messages: {
mutableParam:
"This mutates the parameter `{{name}}` in place via `.{{method}}()` — a caller's data is changed as a side effect and the transform is not reproducible. Build and return a new value ({ ...input, items: [...input.items, added] }) and type the parameter readonly.",
},
ruleId: "no_mutable_parameter",
}),
} satisfies Rule.RuleModule;