134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
// https://github.com/typescript-eslint/typescript-eslint/blob/75c128856b1ce05a4fec799bfa6de03b3dab03d0/packages/eslint-plugin/src/rules/restrict-template-expressions.ts
|
|
import {
|
|
getConstrainedTypeAtLocation,
|
|
getTypeName,
|
|
isTypeAnyType,
|
|
isTypeFlagSet,
|
|
isTypeNeverType,
|
|
} from '@typescript-eslint/type-utils';
|
|
import {
|
|
AST_NODE_TYPES,
|
|
ESLintUtils,
|
|
ParserServicesWithTypeInformation,
|
|
type TSESTree,
|
|
} from '@typescript-eslint/utils';
|
|
import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
|
|
import * as ts from 'typescript';
|
|
|
|
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',
|
|
},
|
|
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]) {
|
|
let services: ParserServicesWithTypeInformation | undefined;
|
|
try {
|
|
services = getParserServices(context);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
|
|
if (!services?.program) return {};
|
|
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
});
|