UNPKG

@typescript-eslint/eslint-plugin

Version:
267 lines (266 loc) • 11.8 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"); var Usefulness; (function (Usefulness) { Usefulness["Always"] = "always"; Usefulness["Never"] = "will"; Usefulness["Sometimes"] = "may"; })(Usefulness || (Usefulness = {})); exports.default = (0, util_1.createRule)({ name: 'no-base-to-string', meta: { type: 'suggestion', docs: { description: 'Require `.toString()` and `.toLocaleString()` to only be called on objects which provide useful information when stringified', recommended: 'recommended', requiresTypeChecking: true, }, messages: { baseArrayJoin: "Using `join()` for {{name}} {{certainty}} use Object's default stringification format ('[object Object]') when stringified.", baseToString: "'{{name}}' {{certainty}} use Object's default stringification format ('[object Object]') when stringified.", }, schema: [ { type: 'object', additionalProperties: false, properties: { ignoredTypeNames: { type: 'array', description: 'Stringified regular expressions of type names to ignore.', items: { type: 'string', }, }, }, }, ], }, defaultOptions: [ { ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams'], }, ], create(context, [option]) { const services = (0, util_1.getParserServices)(context); const checker = services.program.getTypeChecker(); const ignoredTypeNames = option.ignoredTypeNames ?? []; function checkExpression(node, type) { if (node.type === utils_1.AST_NODE_TYPES.Literal) { return; } const certainty = collectToStringCertainty(type ?? services.getTypeAtLocation(node), new Set()); if (certainty === Usefulness.Always) { return; } context.report({ node, messageId: 'baseToString', data: { name: context.sourceCode.getText(node), certainty, }, }); } function checkExpressionForArrayJoin(node, type) { const certainty = collectJoinCertainty(type, new Set()); if (certainty === Usefulness.Always) { return; } context.report({ node, messageId: 'baseArrayJoin', data: { name: context.sourceCode.getText(node), certainty, }, }); } function collectUnionTypeCertainty(type, collectSubTypeCertainty) { const certainties = type.types.map(t => collectSubTypeCertainty(t)); if (certainties.every(certainty => certainty === Usefulness.Never)) { return Usefulness.Never; } if (certainties.every(certainty => certainty === Usefulness.Always)) { return Usefulness.Always; } return Usefulness.Sometimes; } function collectIntersectionTypeCertainty(type, collectSubTypeCertainty) { for (const subType of type.types) { const subtypeUsefulness = collectSubTypeCertainty(subType); if (subtypeUsefulness === Usefulness.Always) { return Usefulness.Always; } } return Usefulness.Never; } function collectTupleCertainty(type, visited) { const typeArgs = checker.getTypeArguments(type); const certainties = typeArgs.map(t => collectToStringCertainty(t, visited)); if (certainties.some(certainty => certainty === Usefulness.Never)) { return Usefulness.Never; } if (certainties.some(certainty => certainty === Usefulness.Sometimes)) { return Usefulness.Sometimes; } return Usefulness.Always; } function collectArrayCertainty(type, visited) { const elemType = (0, util_1.nullThrows)(type.getNumberIndexType(), 'array should have number index type'); return collectToStringCertainty(elemType, visited); } function collectJoinCertainty(type, visited) { if (tsutils.isUnionType(type)) { return collectUnionTypeCertainty(type, t => collectJoinCertainty(t, visited)); } if (tsutils.isIntersectionType(type)) { return collectIntersectionTypeCertainty(type, t => collectJoinCertainty(t, visited)); } if (checker.isTupleType(type)) { return collectTupleCertainty(type, visited); } if (checker.isArrayType(type)) { return collectArrayCertainty(type, visited); } return Usefulness.Always; } function collectToStringCertainty(type, visited) { if (visited.has(type)) { // don't report if this is a self referencing array or tuple type return Usefulness.Always; } if (tsutils.isTypeParameter(type)) { const constraint = type.getConstraint(); if (constraint) { return collectToStringCertainty(constraint, visited); } // unconstrained generic means `unknown` return Usefulness.Always; } // the Boolean type definition missing toString() if (type.flags & ts.TypeFlags.Boolean || type.flags & ts.TypeFlags.BooleanLiteral) { return Usefulness.Always; } if (ignoredTypeNames.includes((0, util_1.getTypeName)(checker, type))) { return Usefulness.Always; } if (type.isIntersection()) { return collectIntersectionTypeCertainty(type, t => collectToStringCertainty(t, visited)); } if (type.isUnion()) { return collectUnionTypeCertainty(type, t => collectToStringCertainty(t, visited)); } if (checker.isTupleType(type)) { return collectTupleCertainty(type, new Set([...visited, type])); } if (checker.isArrayType(type)) { return collectArrayCertainty(type, new Set([...visited, type])); } const toString = checker.getPropertyOfType(type, 'toString') ?? checker.getPropertyOfType(type, 'toLocaleString'); if (!toString) { // e.g. any/unknown return Usefulness.Always; } const declarations = toString.getDeclarations(); if (declarations == null || declarations.length !== 1) { // If there are multiple declarations, at least one of them must not be // the default object toString. // // This may only matter for older versions of TS // see https://github.com/typescript-eslint/typescript-eslint/issues/8585 return Usefulness.Always; } const declaration = declarations[0]; const isBaseToString = ts.isInterfaceDeclaration(declaration.parent) && declaration.parent.name.text === 'Object'; return isBaseToString ? Usefulness.Never : Usefulness.Always; } function isBuiltInStringCall(node) { if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier && // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum node.callee.name === 'String' && node.arguments[0]) { const scope = context.sourceCode.getScope(node); // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum const variable = scope.set.get('String'); return !variable?.defs.length; } return false; } return { 'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'(node) { const leftType = services.getTypeAtLocation(node.left); const rightType = services.getTypeAtLocation(node.right); if ((0, util_1.getTypeName)(checker, leftType) === 'string') { checkExpression(node.right, rightType); } else if ((0, util_1.getTypeName)(checker, rightType) === 'string' && node.left.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier) { checkExpression(node.left, leftType); } }, CallExpression(node) { if (isBuiltInStringCall(node) && node.arguments[0].type !== utils_1.AST_NODE_TYPES.SpreadElement) { checkExpression(node.arguments[0]); } }, 'CallExpression > MemberExpression.callee > Identifier[name = "join"].property'(node) { const memberExpr = node.parent; const type = (0, util_1.getConstrainedTypeAtLocation)(services, memberExpr.object); checkExpressionForArrayJoin(memberExpr.object, type); }, 'CallExpression > MemberExpression.callee > Identifier[name = /^(toLocaleString|toString)$/].property'(node) { const memberExpr = node.parent; checkExpression(memberExpr.object); }, TemplateLiteral(node) { if (node.parent.type === utils_1.AST_NODE_TYPES.TaggedTemplateExpression) { return; } for (const expression of node.expressions) { checkExpression(expression); } }, }; }, });