UNPKG

@typescript-eslint/eslint-plugin

Version:
473 lines • 22.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("@typescript-eslint/utils"); const tsutils = __importStar(require("ts-api-utils")); const ts = __importStar(require("typescript")); const util_1 = require("../util"); const getParentFunctionNode_1 = require("../util/getParentFunctionNode"); const shared_1 = require("./enum-utils/shared"); const assigningOperators = new Set([ '=', '&&=', '&=', '??=', '^=', '|=', '||=', ]); const bitwiseOperators = new Set(['&', '^', '|']); exports.default = (0, util_1.createRule)({ name: 'no-unsafe-enum-assignment', meta: { type: 'problem', docs: { description: 'Disallow assigning non-enum values to enum typed locations', recommended: 'strict', requiresTypeChecking: true, }, messages: { unsafeEnumAccess: 'The computed key used here does not have a shared enum type with the expected enum {{enumNames}}.', unsafeEnumArgument: 'The argument passed here does not have a shared enum type with the expected enum {{enumNames}}.', unsafeEnumAssertion: 'The value asserted here does not have a shared enum type with the expected enum {{enumNames}}.', unsafeEnumAssignment: 'The value assigned here does not have a shared enum type with the expected enum {{enumNames}}.', unsafeEnumMutation: 'This mutation can produce a value outside of the expected enum {{enumNames}}.', unsafeEnumReturn: 'The value returned here does not have a shared enum type with the expected enum {{enumNames}}.', }, schema: [], }, defaultOptions: [], create(context) { const services = (0, util_1.getParserServices)(context); const checker = services.program.getTypeChecker(); const checkedNodes = new WeakSet(); function report(node, messageId, receiverTypes) { context.report({ node, messageId, data: { enumNames: describeEnumTypes(checker, receiverTypes) }, }); } // Literals inside an already reported value would otherwise be reported // again by their own handlers: // // ```ts // const value: T = [Fruit.Apple, 1] as const; // ``` function markChecked(node) { checkedNodes.add(node); switch (node.type) { case utils_1.AST_NODE_TYPES.ArrayExpression: node.elements.forEach(element => { if (element) { markChecked(element); } }); break; case utils_1.AST_NODE_TYPES.ConditionalExpression: markChecked(node.consequent); markChecked(node.alternate); break; case utils_1.AST_NODE_TYPES.LogicalExpression: markChecked(node.left); markChecked(node.right); break; case utils_1.AST_NODE_TYPES.ObjectExpression: node.properties.forEach(markChecked); break; case utils_1.AST_NODE_TYPES.Property: markChecked(node.value); break; case utils_1.AST_NODE_TYPES.SpreadElement: markChecked(node.argument); break; case utils_1.AST_NODE_TYPES.TSAsExpression: case utils_1.AST_NODE_TYPES.TSNonNullExpression: case utils_1.AST_NODE_TYPES.TSSatisfiesExpression: case utils_1.AST_NODE_TYPES.TSTypeAssertion: markChecked(node.expression); break; } } function getContextualType(node) { return checker.getContextualType(services.esTreeNodeToTSNodeMap.get(node)); } // Bitwise combinations of an enum's own members are its bit flags: // // ```ts // const readWrite: Flags = Flags.Read | Flags.Write; // flags &= ~Flags.Write; // ``` function isSafeEnumBitwiseExpression(node, receiverType) { function isSafeOperand(operand) { return (isSafeEnumBitwiseExpression(operand, receiverType) || !isMismatchedEnumAssignmentTypes(checker, services.getTypeAtLocation(operand), receiverType)); } switch (node.type) { case utils_1.AST_NODE_TYPES.BinaryExpression: return (bitwiseOperators.has(node.operator) && isSafeOperand(node.left) && isSafeOperand(node.right)); case utils_1.AST_NODE_TYPES.UnaryExpression: return node.operator === '~' && isSafeOperand(node.argument); default: return false; } } function isUnsafeAssignment(senderNode, receiverType, senderType = services.getTypeAtLocation(senderNode)) { return (hasDeepEnumAssignmentMismatch(checker, senderType, receiverType) && !isSafeEnumBitwiseExpression(senderNode, receiverType)); } function checkAssignment(receiverType, senderNode, reportingNode, messageId = 'unsafeEnumAssignment', senderType) { // Object and array literals are reported per property and element by // their own handlers instead: // // ```ts // const box: { fruit: Fruit } = { fruit: 1 }; // ``` if (senderNode.type === utils_1.AST_NODE_TYPES.ArrayExpression || senderNode.type === utils_1.AST_NODE_TYPES.ObjectExpression || !isUnsafeAssignment(senderNode, receiverType, senderType)) { return; } report(reportingNode, messageId, [receiverType]); markChecked(senderNode); } function checkArguments(node, args) { const signature = util_1.FunctionSignature.create(checker, services.esTreeNodeToTSNodeMap.get(node)); if (node.type === utils_1.AST_NODE_TYPES.TaggedTemplateExpression) { // The first parameter receives the template's strings, not a value. signature.getNextParameterType(); } for (const argument of args) { if (argument.type === utils_1.AST_NODE_TYPES.SpreadElement) { const spreadType = services.getTypeAtLocation(argument.argument); // takesFruits(...[Fruit.Apple, 1]); if (checker.isTupleType(spreadType)) { const mismatchedParameterTypes = checker .getTypeArguments(spreadType) .flatMap(elementType => { const parameterType = signature.getNextParameterType(); return parameterType != null && hasDeepEnumAssignmentMismatch(checker, elementType, parameterType) ? [parameterType] : []; }); if (mismatchedParameterTypes.length > 0) { report(argument, 'unsafeEnumArgument', mismatchedParameterTypes); } if (spreadType.target.combinedFlags & ts.ElementFlags.Variable) { signature.consumeRemainingArguments(); } continue; } } // takesFruit(1); // takesFruits(...numbers); const parameterType = signature.getNextParameterType(); if (parameterType != null) { checkAssignment(parameterType, argument, argument, 'unsafeEnumArgument'); } } } function checkClassMember(node) { // Class members don't get contextually typed by the members they // implement or override, so those types are also checked explicitly: // // ```ts // class Basket implements HasFruit { // fruit = 1; // } // ``` const heritageMemberTypes = getHeritageMemberTypes(node); if (heritageMemberTypes.length > 0 && heritageMemberTypes.every(heritageMemberType => isUnsafeAssignment(node.value, heritageMemberType))) { report(node, 'unsafeEnumAssignment', heritageMemberTypes); markChecked(node.value); return; } checkAssignment(services.getTypeAtLocation(node), node.value, node); } function getHeritageMemberTypes(node) { const memberName = (0, util_1.getStaticMemberAccessValue)(node, context); if (typeof memberName !== 'string') { return []; } const classNode = services.esTreeNodeToTSNodeMap.get(node).parent; return (classNode.heritageClauses ?? []).flatMap(heritageClause => heritageClause.types.flatMap(heritageType => { const memberSymbol = checker .getTypeAtLocation(heritageType) .getProperty(memberName); return memberSymbol ? [checker.getTypeOfSymbol(memberSymbol)] : []; })); } function checkMutation(targetNode, reportingNode) { const targetType = services.getTypeAtLocation(targetNode); if ((0, shared_1.getEnumTypes)(checker, getConstraintType(checker, targetType)).length > 0) { report(reportingNode, 'unsafeEnumMutation', [targetType]); } } function checkReturn(returnNode, reportingNode) { const functionNode = (0, getParentFunctionNode_1.getParentFunctionNode)(returnNode); if (functionNode == null) { // return 1; return; } const signature = (0, util_1.nullThrows)(checker.getSignatureFromDeclaration(services.esTreeNodeToTSNodeMap.get(functionNode)), 'Expected the function to have a signature.'); let receiverType = signature.getReturnType(); let senderType = services.getTypeAtLocation(returnNode); // async function getFruit(): Promise<Fruit> { // return Promise.resolve(1); // } if (functionNode.async) { receiverType = checker.getAwaitedType(receiverType); senderType = checker.getAwaitedType(senderType); } if (receiverType && senderType) { checkAssignment(receiverType, returnNode, reportingNode, 'unsafeEnumReturn', senderType); } } function checkTypeAssertion(node) { checkAssignment(services.getTypeAtLocation(node.typeAnnotation), node.expression, node, 'unsafeEnumAssertion'); } return { 'AccessorProperty[value != null], PropertyDefinition[value != null]': checkClassMember, ArrayExpression(node) { if (checkedNodes.has(node)) { return; } // const fruits: Fruit[] = [1, ...numbers]; for (const element of node.elements) { if (element) { const receiverType = getContextualType(element); if (receiverType) { checkAssignment(receiverType, element, element); } } } }, 'ArrowFunctionExpression[body.type != "BlockStatement"]'(node) { checkReturn(node.body, node.body); }, AssignmentExpression(node) { if (assigningOperators.has(node.operator)) { checkAssignment(services.getTypeAtLocation(node.left), node.right, node); } else { checkMutation(node.left, node); } }, AssignmentPattern(node) { checkAssignment(services.getTypeAtLocation(node.left), node.right, node); }, 'CallExpression, NewExpression'(node) { checkArguments(node, node.arguments); }, 'JSXAttribute > JSXExpressionContainer > :not(JSXEmptyExpression)'(node) { const receiverType = getContextualType(node); if (receiverType) { checkAssignment(receiverType, node, node); } }, 'MemberExpression[computed = true]'(node) { const receiverTypes = checker .getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(node).expression) ?.declarations?.flatMap(declaration => getMappedKeyConstraintTypes(checker, declaration)) ?? []; if (receiverTypes.length === 0) { return; } // declare const foo: { [key in Fruit]: string }; // foo[0]; const senderType = services.getTypeAtLocation(node.property); if (receiverTypes.every(receiverType => hasDeepEnumAssignmentMismatch(checker, senderType, receiverType) || !checker.isTypeAssignableTo(senderType, receiverType))) { report(node.property, 'unsafeEnumAccess', receiverTypes); } }, ObjectExpression(node) { if (checkedNodes.has(node)) { return; } // const box: { fruit: Fruit } = { fruit: 1, ...source }; for (const property of node.properties) { const [receiverNode, senderNode] = property.type === utils_1.AST_NODE_TYPES.SpreadElement ? [node, property.argument] : [property.value, property.value]; const receiverType = getContextualType(receiverNode); if (receiverType) { checkAssignment(receiverType, senderNode, property); } } }, ReturnStatement(node) { if (node.argument) { checkReturn(node.argument, node); } }, TaggedTemplateExpression(node) { checkArguments(node, node.quasi.expressions); }, TSAsExpression: checkTypeAssertion, TSTypeAssertion: checkTypeAssertion, UpdateExpression(node) { checkMutation(node.argument, node); }, 'VariableDeclarator[init != null]'(node) { checkAssignment(services.getTypeAtLocation(node.id), node.init, node); }, }; }, }); function getConstraintType(checker, type) { return checker.getBaseConstraintOfType(type) ?? type; } function getTypeArguments(checker, type) { return tsutils.isTypeReference(type) ? checker.getTypeArguments(type) : []; } function hasDeepEnumAssignmentMismatch(checker, senderType, receiverType, visited = new WeakMap()) { const constrainedSenderType = getConstraintType(checker, senderType); const constrainedReceiverType = getConstraintType(checker, receiverType); // Recursive types would otherwise be visited endlessly. let visitedReceiverTypes = visited.get(constrainedSenderType); if (visitedReceiverTypes == null) { visitedReceiverTypes = new WeakSet(); visited.set(constrainedSenderType, visitedReceiverTypes); } else if (visitedReceiverTypes.has(constrainedReceiverType)) { return false; } visitedReceiverTypes.add(constrainedReceiverType); if (isMismatchedEnumAssignmentTypes(checker, constrainedSenderType, constrainedReceiverType)) { return true; } // Set<number> -> Set<Fruit> const senderTypeArguments = getTypeArguments(checker, constrainedSenderType); const receiverTypeArguments = getTypeArguments(checker, constrainedReceiverType); if (senderTypeArguments.length === receiverTypeArguments.length && senderTypeArguments.some((senderTypeArgument, index) => hasDeepEnumAssignmentMismatch(checker, senderTypeArgument, receiverTypeArguments[index], visited))) { return true; } // [number, Fruit] -> Fruit[] const senderElementType = constrainedSenderType.getNumberIndexType(); const receiverElementType = constrainedReceiverType.getNumberIndexType(); if (senderElementType && receiverElementType && hasDeepEnumAssignmentMismatch(checker, senderElementType, receiverElementType, visited)) { return true; } // { fruit: number } -> { fruit: Fruit } return constrainedReceiverType.getProperties().some(receiverProperty => { const senderProperty = constrainedSenderType.getProperty(receiverProperty.name); return (senderProperty != null && hasDeepEnumAssignmentMismatch(checker, checker.getTypeOfSymbol(senderProperty), checker.getTypeOfSymbol(receiverProperty), visited)); }); } function getMappedKeyConstraintTypes(checker, declaration) { if (!(ts.isGetAccessorDeclaration(declaration) || ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration) || ts.isVariableDeclaration(declaration)) || declaration.type == null) { return []; } const typeNode = declaration.type; // { [key in Fruit]: string } -> [Fruit] if (ts.isMappedTypeNode(typeNode)) { return [ checker.getTypeFromTypeNode((0, util_1.nullThrows)(typeNode.typeParameter.constraint, 'Expected the mapped type parameter to have a constraint.')), ]; } // { [Fruit.Apple]: string; [Vegetable.Asparagus]: string } -> [Fruit.Apple, Vegetable.Asparagus] if (ts.isTypeLiteralNode(typeNode)) { return typeNode.members.flatMap(member => { const name = ts.getNameOfDeclaration(member); return name && ts.isComputedPropertyName(name) ? [checker.getTypeAtLocation(name.expression)] : []; }); } return []; } // [Fruit, Set<Vegetable>] -> 'Fruit', 'Vegetable' function describeEnumTypes(checker, types) { const enumNames = new Set(); const visited = new Set(); function visit(type) { const constrainedType = getConstraintType(checker, type); if (visited.has(constrainedType)) { return; } visited.add(constrainedType); for (const enumType of (0, shared_1.getEnumTypes)(checker, constrainedType)) { enumNames.add(checker.typeToString(enumType)); } for (const typeArgument of getTypeArguments(checker, constrainedType)) { visit(typeArgument); } const elementType = constrainedType.getNumberIndexType(); if (elementType) { visit(elementType); } for (const property of constrainedType.getProperties()) { visit(checker.getTypeOfSymbol(property)); } } types.forEach(visit); return [...enumNames] .sort() .map(enumName => `'${enumName}'`) .join(', '); } function isMismatchedEnumAssignmentTypes(checker, senderType, receiverType) { const receiverEnumTypes = (0, shared_1.getEnumTypes)(checker, receiverType); const receiverTypeParts = tsutils.unionConstituents(receiverType); const receiverEnumValueTypes = new Set(receiverTypeParts.map(shared_1.getEnumValueType)); const receiverNonEnumParts = receiverTypeParts.filter(receiverTypePart => (0, shared_1.getEnumTypes)(checker, receiverTypePart).length === 0); return tsutils.unionConstituents(senderType).some(senderTypePart => // const fruit: Fruit = 1; ((receiverEnumValueTypes.has(ts.TypeFlags.Number) && (0, util_1.isNumberLike)(senderTypePart)) || (receiverEnumValueTypes.has(ts.TypeFlags.String) && (0, util_1.isStringLike)(senderTypePart))) && // const fruit: Fruit = Fruit.Apple; !hasSharedEnumType(checker, senderTypePart, receiverEnumTypes) && // const fruitOrNumber: Fruit | number = 1; !receiverNonEnumParts.some(receiverTypePart => checker.isTypeAssignableTo(senderTypePart, receiverTypePart))); } function hasSharedEnumType(checker, type, expectedEnumTypes) { const typeEnumTypes = new Set((0, shared_1.getEnumTypes)(checker, type)); return expectedEnumTypes.some(expectedEnumType => typeEnumTypes.has(expectedEnumType)); } //# sourceMappingURL=no-unsafe-enum-assignment.js.map