Update
This commit is contained in:
122
src/custom/restrict-template-expressions.ts
Normal file
122
src/custom/restrict-template-expressions.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user