UNPKG

true-myth

Version:

A library for safe functional programming in JavaScript, with first-class support for TypeScript

203 lines (202 loc) 7.05 kB
import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, Resolver, TRUE_MYTH_MUST_AWAIT_TYPES, TRUE_MYTH_MUST_USE_TYPES, getTypedParserServices, mustUseTypesFrom, } from './true-myth-support.js'; const MESSAGE_ID = 'unusedMustUseValue'; const DEFAULT_OPTIONS = { additionalTypes: [], allowVoid: true, }; const mustUseTypeSchema = { type: 'object', additionalProperties: false, properties: { export: { anyOf: [ { type: 'object', additionalProperties: false, properties: { kind: { enum: ['default'], type: 'string' }, }, required: ['kind'], }, { type: 'object', additionalProperties: false, properties: { kind: { enum: ['named'], type: 'string' }, name: { type: 'string' }, }, required: ['kind', 'name'], }, ], }, module: { type: 'string' }, }, required: ['module', 'export'], }; /** Requires callers to use True Myth `Result` and `Task` values. @see https://true-myth.js.org/eslint-plugin/must-use */ export const mustUse = createRule({ name: 'must-use', meta: { type: 'problem', docs: { description: 'Require callers to use values whose type must not be discarded.', }, messages: { [MESSAGE_ID]: '{{typeName}} values should be used or explicitly discarded with `void`.', }, schema: [ { type: 'object', additionalProperties: false, properties: { allowVoid: { anyOf: [ { type: 'boolean', }, { type: 'array', items: { ...mustUseTypeSchema, }, uniqueItems: true, }, ], }, additionalTypes: { type: 'array', items: { ...mustUseTypeSchema, }, }, }, }, ], }, create(context) { const services = getTypedParserServices(context); const checker = services.program.getTypeChecker(); const options = optionsFrom(context.options[0]); const mustAwaitTypes = [...TRUE_MYTH_MUST_AWAIT_TYPES, ...options.additionalTypes]; const resolver = new Resolver(services.program, checker, [ ...TRUE_MYTH_MUST_USE_TYPES, ...options.additionalTypes, ]); return { ExpressionStatement(node) { const candidate = unwrapExpressionStatement(node.expression); const expression = candidate.expression; if (!isMustUseCandidate(expression)) { return; } const obligation = obligationForExpression(expression); if (obligation.isJust) { if (candidate.explicitlyDiscarded && allowVoidFor(obligation.value, options.allowVoid)) { return; } context.report({ node: expression, messageId: 'unusedMustUseValue', data: { typeName: obligation.value.label, }, }); } }, }; function obligationForExpression(expression) { if (expression.type === AST_NODE_TYPES.AwaitExpression) { let obligation = resolver.obligationFor(services.getTypeAtLocation(expression.argument)); if (obligation.isJust && isAwaitObligation(obligation.value, mustAwaitTypes)) { return obligation; } } return resolver.obligationFor(services.getTypeAtLocation(expression)); } }, }); function unwrapExpressionStatement(expression) { let current = expression; while (true) { if (current.type === AST_NODE_TYPES.UnaryExpression && current.operator === 'void') { return { explicitlyDiscarded: true, expression: unwrapSyntax(current.argument), }; } let next = unwrapSyntaxOnce(current); if (next === current) { return { explicitlyDiscarded: false, expression: current, }; } current = next; } } function unwrapSyntax(expression) { let current = expression; while (true) { let next = unwrapSyntaxOnce(current); if (next === current) { return current; } current = next; } } function unwrapSyntaxOnce(expression) { switch (expression.type) { case AST_NODE_TYPES.ChainExpression: return expression.expression; case AST_NODE_TYPES.TSAsExpression: case AST_NODE_TYPES.TSNonNullExpression: case AST_NODE_TYPES.TSTypeAssertion: return expression.expression; default: return expression; } } function isMustUseCandidate(expression) { return (expression.type === AST_NODE_TYPES.AwaitExpression || expression.type === AST_NODE_TYPES.CallExpression || expression.type === AST_NODE_TYPES.NewExpression); } function allowVoidFor(obligation, allowVoid) { return (allowVoid === true || (Array.isArray(allowVoid) && allowVoid.some((allowedType) => obligation.matches(allowedType)))); } function optionsFrom(value) { if (value === undefined) { return DEFAULT_OPTIONS; } if (!isRecord(value)) { throw new TypeError('Expected must-use options to be an object.'); } return { additionalTypes: mustUseTypesFrom(value.additionalTypes, 'additionalTypes'), allowVoid: allowVoidFrom(value.allowVoid), }; } function allowVoidFrom(value) { if (value === undefined) { return DEFAULT_OPTIONS.allowVoid; } if (typeof value === 'boolean') { return value; } if (Array.isArray(value)) { return mustUseTypesFrom(value, 'allowVoid'); } throw new TypeError('Expected allowVoid to be a boolean or an array of must-use types.'); } function isAwaitObligation(obligation, mustAwaitTypes) { return mustAwaitTypes.some((type) => obligation.matches(type)); } function isRecord(value) { return typeof value === 'object' && value !== null; } //# sourceMappingURL=must-use.js.map