Skip to content

Commit

Permalink
feat(eslint-plugin): [restrict-template-expressions] add support for …
Browse files Browse the repository at this point in the history
…intersection types (#1803)
  • Loading branch information
ulrichb committed Apr 14, 2020
1 parent 73675d1 commit cc70e4f
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 78 deletions.
Expand Up @@ -6,6 +6,9 @@ Examples of **correct** code:
const arg = 'foo';
const msg1 = `arg = ${arg}`;
const msg2 = `arg = ${arg || 'default'}`;

const stringWithKindProp: string & { _kind?: 'MyString' } = 'foo';
const msg3 = `stringWithKindProp = ${stringWithKindProp}`;
```

Examples of **incorrect** code:
Expand All @@ -28,6 +31,8 @@ type Options = {
allowNumber?: boolean;
// if true, also allow boolean type in template expressions
allowBoolean?: boolean;
// if true, also allow any in template expressions
allowAny?: boolean;
// if true, also allow null and undefined in template expressions
allowNullable?: boolean;
};
Expand Down
142 changes: 64 additions & 78 deletions packages/eslint-plugin/src/rules/restrict-template-expressions.ts
Expand Up @@ -7,10 +7,10 @@ import * as util from '../util';

type Options = [
{
allowNullable?: boolean;
allowNumber?: boolean;
allowBoolean?: boolean;
allowAny?: boolean;
allowNullable?: boolean;
},
];

Expand All @@ -33,10 +33,10 @@ export default util.createRule<Options, MessageId>({
{
type: 'object',
properties: {
allowAny: { type: 'boolean' },
allowNumber: { type: 'boolean' },
allowBoolean: { type: 'boolean' },
allowAny: { type: 'boolean' },
allowNullable: { type: 'boolean' },
allowNumber: { type: 'boolean' },
},
},
],
Expand All @@ -46,31 +46,40 @@ export default util.createRule<Options, MessageId>({
const service = util.getParserServices(context);
const typeChecker = service.program.getTypeChecker();

type BaseType =
| 'string'
| 'number'
| 'bigint'
| 'boolean'
| 'null'
| 'undefined'
| 'any'
| 'other';

const allowedTypes: BaseType[] = [
'string',
...(options.allowNumber ? (['number', 'bigint'] as const) : []),
...(options.allowBoolean ? (['boolean'] as const) : []),
...(options.allowNullable ? (['null', 'undefined'] as const) : []),
...(options.allowAny ? (['any'] as const) : []),
];

function isAllowedType(types: BaseType[]): boolean {
for (const type of types) {
if (!allowedTypes.includes(type)) {
return false;
}
function isUnderlyingTypePrimitive(type: ts.Type): boolean {
if (util.isTypeFlagSet(type, ts.TypeFlags.StringLike)) {
return true;
}

if (
options.allowNumber &&
util.isTypeFlagSet(
type,
ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
)
) {
return true;
}
return true;

if (
options.allowBoolean &&
util.isTypeFlagSet(type, ts.TypeFlags.BooleanLike)
) {
return true;
}

if (options.allowAny && util.isTypeFlagSet(type, ts.TypeFlags.Any)) {
return true;
}

if (
options.allowNullable &&
util.isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined)
) {
return true;
}

return false;
}

return {
Expand All @@ -80,70 +89,47 @@ export default util.createRule<Options, MessageId>({
return;
}

for (const expr of node.expressions) {
const type = getNodeType(expr);
if (!isAllowedType(type)) {
for (const expression of node.expressions) {
if (
!isUnderlyingExpressionTypeConfirmingTo(
expression,
isUnderlyingTypePrimitive,
)
) {
context.report({
node: expr,
node: expression,
messageId: 'invalidType',
});
}
}
},
};

/**
* Helper function to get base type of node
* @param node the node to be evaluated.
*/
function getNodeType(node: TSESTree.Expression): BaseType[] {
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
const type = util.getConstrainedTypeAtLocation(typeChecker, tsNode);
function isUnderlyingExpressionTypeConfirmingTo(
expression: TSESTree.Expression,
predicate: (underlyingType: ts.Type) => boolean,
): boolean {
return rec(getExpressionNodeType(expression));

return getBaseType(type);
}

function getBaseType(type: ts.Type): BaseType[] {
if (type.isStringLiteral()) {
return ['string'];
}
if (type.isNumberLiteral()) {
return ['number'];
}
if (type.flags & ts.TypeFlags.BigIntLiteral) {
return ['bigint'];
}
if (type.flags & ts.TypeFlags.BooleanLiteral) {
return ['boolean'];
}
if (type.flags & ts.TypeFlags.Null) {
return ['null'];
}
if (type.flags & ts.TypeFlags.Undefined) {
return ['undefined'];
}
if (type.flags & ts.TypeFlags.Any) {
return ['any'];
}
function rec(type: ts.Type): boolean {
if (type.isUnion()) {
return type.types.every(rec);
}

if (type.isUnion()) {
return type.types
.map(getBaseType)
.reduce((all, array) => [...all, ...array], []);
}
if (type.isIntersection()) {
return type.types.some(rec);
}

const stringType = typeChecker.typeToString(type);
if (
stringType === 'string' ||
stringType === 'number' ||
stringType === 'bigint' ||
stringType === 'boolean' ||
stringType === 'any'
) {
return [stringType];
return predicate(type);
}
}

return ['other'];
/**
* Helper function to extract the TS type of an TSESTree expression.
*/
function getExpressionNodeType(node: TSESTree.Expression): ts.Type {
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
return util.getConstrainedTypeAtLocation(typeChecker, tsNode);
}
},
});
Expand Up @@ -30,6 +30,12 @@ ruleTester.run('restrict-template-expressions', rule, {
return \`arg = \${arg}\`;
}
`,
// Base case - intersection type
`
function test<T extends string & { _kind: 'MyBrandedString' }>(arg: T) {
return \`arg = \${arg}\`;
}
`,
// Base case - don't check tagged templates
`
tag\`arg = \${null}\`;
Expand Down Expand Up @@ -68,6 +74,14 @@ ruleTester.run('restrict-template-expressions', rule, {
}
`,
},
{
options: [{ allowNumber: true }],
code: `
function test<T extends number & { _kind: 'MyBrandedNumber' }>(arg: T) {
return \`arg = \${arg}\`;
}
`,
},
{
options: [{ allowNumber: true }],
code: `
Expand Down Expand Up @@ -236,6 +250,13 @@ ruleTester.run('restrict-template-expressions', rule, {
`,
errors: [{ messageId: 'invalidType', line: 3, column: 30 }],
},
{
code: `
declare const arg: { a: string } & { b: string };
const msg = \`arg = \${arg}\`;
`,
errors: [{ messageId: 'invalidType', line: 3, column: 30 }],
},
{
options: [{ allowNumber: true, allowBoolean: true, allowNullable: true }],
code: `
Expand Down

0 comments on commit cc70e4f

Please sign in to comment.