This commit is contained in:
Alex
2024-08-11 17:32:54 -04:00
parent afb93c4971
commit 191848fdca
15 changed files with 151 additions and 84 deletions

View File

@ -0,0 +1,122 @@
// https://github.com/typescript-eslint/typescript-eslint/blob/75c128856b1ce05a4fec799bfa6de03b3dab03d0/packages/eslint-plugin/src/rules/restrict-template-expressions.ts
import * as ts from 'typescript';
import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
import {
getConstrainedTypeAtLocation,
getTypeName,
isTypeAnyType,
isTypeFlagSet,
isTypeNeverType,
} from '@typescript-eslint/type-utils';
import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://typescript-eslint.io/rules/${name}`,
);
interface Option {
allow: string[];
}
const defaultOption: Option = {
allow: ['any', 'boolean', 'null', 'undefined', 'number', 'RegExp', 'URLSearchParams'],
};
type MessageId = 'invalidType';
export default createRule<Option[], MessageId>({
name: 'restrict-template-expressions',
meta: {
type: 'problem',
docs: {
description: 'Enforce template literal expressions to be of `string` type',
recommended: 'recommended',
requiresTypeChecking: true,
},
messages: {
invalidType: 'Invalid type "{{type}}" of template literal expression.',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
allow: {
type: 'array',
items: {
type: 'string',
},
description: 'Allow specific types',
uniqueItems: true,
},
},
},
],
},
defaultOptions: [defaultOption],
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
const allowed = new Set(options.allow);
const { StringLike, NumberLike, BigIntLike, BooleanLike, Null, Undefined } =
ts.TypeFlags;
function isUnderlyingTypePrimitive(type: ts.Type): boolean {
return (
isTypeFlagSet(type, StringLike) ||
(allowed.has('number') && isTypeFlagSet(type, NumberLike | BigIntLike)) ||
(allowed.has('boolean') && isTypeFlagSet(type, BooleanLike)) ||
(allowed.has('any') && isTypeAnyType(type)) ||
allowed.has(getTypeName(checker, type)) ||
(allowed.has('null') && isTypeFlagSet(type, Null)) ||
(allowed.has('undefined') && isTypeFlagSet(type, Undefined)) ||
(allowed.has('never') && isTypeNeverType(type))
);
}
return {
TemplateLiteral(node: TSESTree.TemplateLiteral): void {
// don't check tagged template literals
if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
return;
}
for (const expression of node.expressions) {
const expressionType = getConstrainedTypeAtLocation(services, expression);
if (
!isInnerUnionOrIntersectionConformingTo(
expressionType,
isUnderlyingTypePrimitive,
)
) {
context.report({
node: expression,
messageId: 'invalidType',
data: { type: checker.typeToString(expressionType) },
});
}
}
},
};
function isInnerUnionOrIntersectionConformingTo(
type: ts.Type,
predicate: (underlyingType: ts.Type) => boolean,
): boolean {
return rec(type);
function rec(innerType: ts.Type): boolean {
if (innerType.isUnion()) {
return innerType.types.every(rec);
}
if (innerType.isIntersection()) {
return innerType.types.some(rec);
}
return predicate(innerType);
}
}
},
});