New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(eslint-plugin): [restrict-template-expressions] add support for intersection types #1803
Changes from 1 commit
054e62e
27ed61f
df0ad14
f2fab58
9ebb323
8d74671
8df74f4
dfe2c64
e46717c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -44,29 +44,36 @@ export default util.createRule<Options, MessageId>({ | |||||||||
const service = util.getParserServices(context); | ||||||||||
const typeChecker = service.program.getTypeChecker(); | ||||||||||
|
||||||||||
type BaseType = | ||||||||||
| 'string' | ||||||||||
| 'number' | ||||||||||
| 'bigint' | ||||||||||
| 'boolean' | ||||||||||
| 'null' | ||||||||||
| 'undefined' | ||||||||||
| 'other'; | ||||||||||
|
||||||||||
const allowedTypes: BaseType[] = [ | ||||||||||
'string', | ||||||||||
...(options.allowNumber ? (['number', 'bigint'] as const) : []), | ||||||||||
...(options.allowBoolean ? (['boolean'] as const) : []), | ||||||||||
...(options.allowNullable ? (['null', 'undefined'] 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 ( | ||||||||||
util.isTypeFlagSet( | ||||||||||
type, | ||||||||||
ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike, | ||||||||||
) && | ||||||||||
options.allowNumber | ||||||||||
) { | ||||||||||
return true; | ||||||||||
} | ||||||||||
return true; | ||||||||||
|
||||||||||
if ( | ||||||||||
util.isTypeFlagSet(type, ts.TypeFlags.BooleanLike) && | ||||||||||
options.allowBoolean | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto
Suggested change
|
||||||||||
) { | ||||||||||
return true; | ||||||||||
} | ||||||||||
|
||||||||||
if ( | ||||||||||
util.isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined) && | ||||||||||
options.allowNullable | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto
Suggested change
|
||||||||||
) { | ||||||||||
return true; | ||||||||||
} | ||||||||||
|
||||||||||
return false; | ||||||||||
} | ||||||||||
|
||||||||||
return { | ||||||||||
|
@@ -76,75 +83,52 @@ 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 = typeChecker.getTypeAtLocation(tsNode); | ||||||||||
function isUnderlyingExpressionTypeConfirmingTo( | ||||||||||
expression: TSESTree.Expression, | ||||||||||
predicate: (underlyingType: ts.Type) => boolean, | ||||||||||
): boolean { | ||||||||||
const expressionType = getExpressionNodeType(expression); | ||||||||||
|
||||||||||
return getBaseType(type); | ||||||||||
} | ||||||||||
return rec( | ||||||||||
// "Extracts" generic constraint, indexed access and conditional types: | ||||||||||
typeChecker.getBaseConstraintOfType(expressionType) ?? expressionType, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've got a utility function for this!
Suggested change
|
||||||||||
); | ||||||||||
|
||||||||||
function getBaseType(type: ts.Type): BaseType[] { | ||||||||||
const constraint = type.getConstraint(); | ||||||||||
if ( | ||||||||||
constraint && | ||||||||||
// for generic types with union constraints, it will return itself | ||||||||||
constraint !== type | ||||||||||
) { | ||||||||||
return getBaseType(constraint); | ||||||||||
} | ||||||||||
|
||||||||||
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']; | ||||||||||
} | ||||||||||
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' | ||||||||||
) { | ||||||||||
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 typeChecker.getTypeAtLocation(tsNode); | ||||||||||
} | ||||||||||
}, | ||||||||||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - do the option check first to save some cycles