📄 unbound-method¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 10 |
| 📦 Imports | 7 |
| 📊 Variables & Constants | 3 |
| 📐 Interfaces | 2 |
| 📑 Type Aliases | 2 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/unbound-method.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'unbound-method' |
meta.type |
'problem' |
meta.docs.description |
'Enforce unbound methods are called with their expected scope' |
meta.docs.recommended |
'recommended' |
meta.docs.requiresTypeChecking |
true |
meta.messages.unbound |
BASE_MESSAGE |
meta.messages.unboundWithoutThisAnnotation |
${BASE_MESSAGE}\nIf a function does not access \this`, it can be annotated with `this: void`.` |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { ignoreStatic: { type: 'boolean', description: 'Whether... |
defaultOptions |
[ { ignoreStatic: false, }, ] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
TSESTree |
@typescript-eslint/utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
createRule |
../util |
getModifiers |
../util |
getParserServices |
../util |
isBuiltinSymbolLike |
../util |
isSymbolFromDefaultLibrary |
../util |
Variables & Constants¶
| Name | Type | Kind | Value | Exported |
|---|---|---|---|---|
SUPPORTED_GLOBALS |
readonly ["Number", "Object", "String... |
const | [ 'Number', 'Object', 'String', // eslint-disable-line @typescript-eslint/int... |
✗ |
nativelyBoundMembers |
Set<string> |
const | new Set( SUPPORTED_GLOBALS.flatMap(namespace => { if (!(namespace in global))... |
✗ |
SUPPORTED_GLOBAL_TYPES |
string[] |
const | [ 'NumberConstructor', 'ObjectConstructor', 'StringConstructor', 'SymbolConst... |
✗ |
Functions¶
create(context: any, [{ ignoreStatic }]: any): { MemberExpression(node: TSESTree.MemberExpression): void; …¶
Parameters:
contextany[{ ignoreStatic }]any
Returns: { MemberExpression(node: TSESTree.MemberExpression): void; ObjectPattern(node: any): void; }
Calls:
getParserServices (from ../util)services.program.getSourceFilecheckIfMethodcontext.reporttsutils .unionConstituents(type) .flatMaptsutils.intersectionConstituentscheckIfMethodAndReportintersectionPart.getPropertytsutils .unionConstituents(services.getTypeAtLocation(node.property)) .flatMappart.isStringLiteralpart.isNumberLiteralpart.value.toStringservices.getSymbolAtLocationisNotImportednativelyBoundMembers.hasisBuiltinSymbolLike (from ../util)services.getTypeAtLocationisSymbolFromDefaultLibrary (from ../util)services.getTypeAtLocation(property).getSymbolisSafeUseisNativelyBoundgetAccessedPropertyNamescheckUnionConstituentsAndReportisNodeInsideTypeDeclarationservices .getTypeAtLocation(initNode) .getProperty
Internal Comments:
// We can't rely entirely on the type-level checks made at the end of this
// function, because sometimes type declarations don't come from the
// default library, but come from, for example, "@types/node". And we can't
// tell if a method is unbound just by looking at its signature declared in
// the interface.
//
// See related discussion https://github.com/typescript-eslint/typescript-eslint/pull/8952#discussion_r1576543310
// if `${object.name}.${property.name}` doesn't match any of
// the nativelyBoundMembers, then we fallback to type-level checks
Code
create(context, [{ ignoreStatic }]) {
const services = getParserServices(context);
const currentSourceFile = services.program.getSourceFile(context.filename);
function checkIfMethodAndReport(
node: TSESTree.Node,
symbol: ts.Symbol | undefined,
): boolean {
if (!symbol) {
return false;
}
const { dangerous, firstParamIsThis } = checkIfMethod(
symbol,
ignoreStatic,
);
if (dangerous) {
context.report({
node,
messageId:
firstParamIsThis === false
? 'unboundWithoutThisAnnotation'
: 'unbound',
});
return true;
}
return false;
}
function checkUnionConstituentsAndReport(
reportNode: TSESTree.Node,
propertyName: string,
type: ts.Type,
): boolean {
for (const intersectionPart of tsutils
.unionConstituents(type)
.flatMap(unionPart => tsutils.intersectionConstituents(unionPart))) {
const reported = checkIfMethodAndReport(
reportNode,
intersectionPart.getProperty(propertyName),
);
if (reported) {
return true;
}
}
return false;
}
function getAccessedPropertyNames(
node: TSESTree.MemberExpression,
): string[] {
if (!node.computed) {
return node.property.type === AST_NODE_TYPES.Identifier
? [node.property.name]
: [];
}
return tsutils
.unionConstituents(services.getTypeAtLocation(node.property))
.flatMap(part => {
return part.isStringLiteral() || part.isNumberLiteral()
? [part.value.toString()]
: [];
});
}
function isNativelyBound(
object: TSESTree.Node,
property: TSESTree.Node,
): boolean {
// We can't rely entirely on the type-level checks made at the end of this
// function, because sometimes type declarations don't come from the
// default library, but come from, for example, "@types/node". And we can't
// tell if a method is unbound just by looking at its signature declared in
// the interface.
//
// See related discussion https://github.com/typescript-eslint/typescript-eslint/pull/8952#discussion_r1576543310
if (
object.type === AST_NODE_TYPES.Identifier &&
property.type === AST_NODE_TYPES.Identifier
) {
const objectSymbol = services.getSymbolAtLocation(object);
const notImported =
objectSymbol != null &&
isNotImported(objectSymbol, currentSourceFile);
if (
notImported &&
nativelyBoundMembers.has(`${object.name}.${property.name}`)
) {
return true;
}
}
// if `${object.name}.${property.name}` doesn't match any of
// the nativelyBoundMembers, then we fallback to type-level checks
return (
isBuiltinSymbolLike(
services.program,
services.getTypeAtLocation(object),
SUPPORTED_GLOBAL_TYPES,
) &&
isSymbolFromDefaultLibrary(
services.program,
services.getTypeAtLocation(property).getSymbol(),
)
);
}
return {
MemberExpression(node: TSESTree.MemberExpression): void {
if (isSafeUse(node) || isNativelyBound(node.object, node.property)) {
return;
}
const propertyNames = getAccessedPropertyNames(node);
if (propertyNames.length === 0) {
return;
}
const objectType = services.getTypeAtLocation(node.object);
for (const propertyName of propertyNames) {
if (checkUnionConstituentsAndReport(node, propertyName, objectType)) {
break;
}
}
},
ObjectPattern(node): void {
if (isNodeInsideTypeDeclaration(node)) {
return;
}
let initNode: TSESTree.Node | null = null;
if (node.parent.type === AST_NODE_TYPES.VariableDeclarator) {
initNode = node.parent.init;
} else if (
node.parent.type === AST_NODE_TYPES.AssignmentPattern ||
node.parent.type === AST_NODE_TYPES.AssignmentExpression
) {
initNode = node.parent.right;
}
for (const property of node.properties) {
if (
property.type !== AST_NODE_TYPES.Property ||
property.key.type !== AST_NODE_TYPES.Identifier
) {
continue;
}
if (initNode) {
if (!isNativelyBound(initNode, property.key)) {
const reported = checkIfMethodAndReport(
property.key,
services
.getTypeAtLocation(initNode)
.getProperty(property.key.name),
);
if (reported) {
continue;
}
// In assignment patterns, we should also check the type of
// Foo's nativelyBound method because initNode might be used as
// default value:
// function ({ nativelyBound }: Foo = NativeObject) {}
} else if (node.parent.type !== AST_NODE_TYPES.AssignmentPattern) {
continue;
}
}
checkUnionConstituentsAndReport(
property.key,
property.key.name,
services.getTypeAtLocation(node),
);
}
},
};
}
isNotImported(symbol: ts.Symbol, currentSourceFile: ts.SourceFile | undefined): boolean¶
Parameters:
symbolts.SymbolcurrentSourceFilets.SourceFile | undefined
Returns: boolean
Calls:
valueDeclaration.getSourceFile
Internal Comments:
Code
(
symbol: ts.Symbol,
currentSourceFile: ts.SourceFile | undefined,
): boolean => {
const { valueDeclaration } = symbol;
if (!valueDeclaration) {
// working around https://github.com/microsoft/TypeScript/issues/31294
return false;
}
return (
!!currentSourceFile &&
currentSourceFile !== valueDeclaration.getSourceFile()
);
}
isNodeInsideTypeDeclaration(node: TSESTree.Node): boolean¶
Parameters:
nodeTSESTree.Node
Returns: boolean
Code
function isNodeInsideTypeDeclaration(node: TSESTree.Node): boolean {
let parent: TSESTree.Node | undefined = node;
while ((parent = parent.parent)) {
if (
(parent.type === AST_NODE_TYPES.ClassDeclaration && parent.declare) ||
parent.type === AST_NODE_TYPES.TSAbstractMethodDefinition ||
parent.type === AST_NODE_TYPES.TSDeclareFunction ||
parent.type === AST_NODE_TYPES.TSFunctionType ||
parent.type === AST_NODE_TYPES.TSInterfaceDeclaration ||
parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||
(parent.type === AST_NODE_TYPES.VariableDeclaration && parent.declare)
) {
return true;
}
}
return false;
}
checkIfMethod(symbol: ts.Symbol, ignoreStatic: boolean): CheckMethodResult¶
Parameters:
symbolts.SymbolignoreStaticboolean
Returns: CheckMethodResult
Calls:
checkMethod
Internal Comments:
Code
function checkIfMethod(
symbol: ts.Symbol,
ignoreStatic: boolean,
): CheckMethodResult {
const { valueDeclaration } = symbol;
if (!valueDeclaration) {
// working around https://github.com/microsoft/TypeScript/issues/31294
return { dangerous: false };
}
switch (valueDeclaration.kind) {
case ts.SyntaxKind.PropertyDeclaration:
return {
dangerous:
(valueDeclaration as ts.PropertyDeclaration).initializer?.kind ===
ts.SyntaxKind.FunctionExpression,
};
case ts.SyntaxKind.PropertyAssignment: {
const assignee = (valueDeclaration as ts.PropertyAssignment).initializer;
if (assignee.kind !== ts.SyntaxKind.FunctionExpression) {
return {
dangerous: false,
};
}
return checkMethod(assignee as ts.FunctionExpression, ignoreStatic);
}
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature: {
return checkMethod(
valueDeclaration as ts.MethodDeclaration | ts.MethodSignature,
ignoreStatic,
);
}
}
return { dangerous: false };
}
checkMethod(valueDeclaration: ts.FunctionExpression | ts.MethodDeclar…, ignoreStatic: boolean): CheckMethodResult¶
Parameters:
valueDeclarationts.FunctionExpression | ts.MethodDeclaration | ts.MethodSignatureignoreStaticboolean
Returns: CheckMethodResult
Calls:
valueDeclaration.parameters.attsutils.includesModifiergetModifiers (from ../util)
Internal Comments:
Code
function checkMethod(
valueDeclaration:
ts.FunctionExpression | ts.MethodDeclaration | ts.MethodSignature,
ignoreStatic: boolean,
): CheckMethodResult {
const firstParam = valueDeclaration.parameters.at(0);
const firstParamIsThis =
firstParam?.name.kind === ts.SyntaxKind.Identifier &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
firstParam.name.escapedText === 'this';
const thisArgIsVoid =
firstParamIsThis && firstParam.type?.kind === ts.SyntaxKind.VoidKeyword;
return {
dangerous:
!thisArgIsVoid &&
!(
ignoreStatic &&
tsutils.includesModifier(
getModifiers(valueDeclaration),
ts.SyntaxKind.StaticKeyword,
)
),
firstParamIsThis,
};
}
isSafeUse(node: TSESTree.Node): boolean¶
Parameters:
nodeTSESTree.Node
Returns: boolean
Calls:
['!', 'delete', 'typeof', 'void'].includes['!=', '!==', '==', '===', 'instanceof'].includesisSafeUse
Internal Comments:
// the first case is safe for obvious
// reasons. The second one is also fine
// since we're returning something falsy
// this is safe, as && will return the left if and only if it's falsy
// in all other cases, it's likely the logical expression will return the method ref
// so make sure the parent is a safe usage
Code
function isSafeUse(node: TSESTree.Node): boolean {
const parent = node.parent;
switch (parent?.type) {
case AST_NODE_TYPES.IfStatement:
case AST_NODE_TYPES.ForStatement:
case AST_NODE_TYPES.MemberExpression:
case AST_NODE_TYPES.SwitchStatement:
case AST_NODE_TYPES.UpdateExpression:
case AST_NODE_TYPES.WhileStatement:
return true;
case AST_NODE_TYPES.CallExpression:
return parent.callee === node;
case AST_NODE_TYPES.ConditionalExpression:
return parent.test === node;
case AST_NODE_TYPES.TaggedTemplateExpression:
return parent.tag === node;
case AST_NODE_TYPES.UnaryExpression:
// the first case is safe for obvious
// reasons. The second one is also fine
// since we're returning something falsy
return ['!', 'delete', 'typeof', 'void'].includes(parent.operator);
case AST_NODE_TYPES.BinaryExpression:
return ['!=', '!==', '==', '===', 'instanceof'].includes(parent.operator);
case AST_NODE_TYPES.AssignmentExpression:
return (
parent.operator === '=' &&
(node === parent.left ||
(node.type === AST_NODE_TYPES.MemberExpression &&
node.object.type === AST_NODE_TYPES.Super &&
parent.left.type === AST_NODE_TYPES.MemberExpression &&
parent.left.object.type === AST_NODE_TYPES.ThisExpression))
);
case AST_NODE_TYPES.ChainExpression:
case AST_NODE_TYPES.TSNonNullExpression:
case AST_NODE_TYPES.TSAsExpression:
case AST_NODE_TYPES.TSTypeAssertion:
return isSafeUse(parent);
case AST_NODE_TYPES.LogicalExpression:
if (parent.operator === '&&' && parent.left === node) {
// this is safe, as && will return the left if and only if it's falsy
return true;
}
// in all other cases, it's likely the logical expression will return the method ref
// so make sure the parent is a safe usage
return isSafeUse(parent);
}
return false;
}
Internal helpers¶
Declared inside another function in this file.
checkIfMethodAndReport(node: TSESTree.Node, symbol: ts.Symbol | undefined): boolean¶
Parameters:
nodeTSESTree.Nodesymbolts.Symbol | undefined
Returns: boolean
Calls:
checkIfMethodcontext.report
Code
function checkIfMethodAndReport(
node: TSESTree.Node,
symbol: ts.Symbol | undefined,
): boolean {
if (!symbol) {
return false;
}
const { dangerous, firstParamIsThis } = checkIfMethod(
symbol,
ignoreStatic,
);
if (dangerous) {
context.report({
node,
messageId:
firstParamIsThis === false
? 'unboundWithoutThisAnnotation'
: 'unbound',
});
return true;
}
return false;
}
checkUnionConstituentsAndReport(reportNode: TSESTree.Node, propertyName: string, type: ts.Type): boolean¶
Parameters:
reportNodeTSESTree.NodepropertyNamestringtypets.Type
Returns: boolean
Calls:
tsutils .unionConstituents(type) .flatMaptsutils.intersectionConstituentscheckIfMethodAndReportintersectionPart.getProperty
Code
function checkUnionConstituentsAndReport(
reportNode: TSESTree.Node,
propertyName: string,
type: ts.Type,
): boolean {
for (const intersectionPart of tsutils
.unionConstituents(type)
.flatMap(unionPart => tsutils.intersectionConstituents(unionPart))) {
const reported = checkIfMethodAndReport(
reportNode,
intersectionPart.getProperty(propertyName),
);
if (reported) {
return true;
}
}
return false;
}
getAccessedPropertyNames(node: TSESTree.MemberExpression): string[]¶
Parameters:
nodeTSESTree.MemberExpression
Returns: string[]
Calls:
tsutils .unionConstituents(services.getTypeAtLocation(node.property)) .flatMappart.isStringLiteralpart.isNumberLiteralpart.value.toString
Code
function getAccessedPropertyNames(
node: TSESTree.MemberExpression,
): string[] {
if (!node.computed) {
return node.property.type === AST_NODE_TYPES.Identifier
? [node.property.name]
: [];
}
return tsutils
.unionConstituents(services.getTypeAtLocation(node.property))
.flatMap(part => {
return part.isStringLiteral() || part.isNumberLiteral()
? [part.value.toString()]
: [];
});
}
isNativelyBound(object: TSESTree.Node, property: TSESTree.Node): boolean¶
Parameters:
objectTSESTree.NodepropertyTSESTree.Node
Returns: boolean
Calls:
services.getSymbolAtLocationisNotImportednativelyBoundMembers.hasisBuiltinSymbolLike (from ../util)services.getTypeAtLocationisSymbolFromDefaultLibrary (from ../util)services.getTypeAtLocation(property).getSymbol
Internal Comments:
// We can't rely entirely on the type-level checks made at the end of this
// function, because sometimes type declarations don't come from the
// default library, but come from, for example, "@types/node". And we can't
// tell if a method is unbound just by looking at its signature declared in
// the interface.
//
// See related discussion https://github.com/typescript-eslint/typescript-eslint/pull/8952#discussion_r1576543310
// if `${object.name}.${property.name}` doesn't match any of
// the nativelyBoundMembers, then we fallback to type-level checks
Code
function isNativelyBound(
object: TSESTree.Node,
property: TSESTree.Node,
): boolean {
// We can't rely entirely on the type-level checks made at the end of this
// function, because sometimes type declarations don't come from the
// default library, but come from, for example, "@types/node". And we can't
// tell if a method is unbound just by looking at its signature declared in
// the interface.
//
// See related discussion https://github.com/typescript-eslint/typescript-eslint/pull/8952#discussion_r1576543310
if (
object.type === AST_NODE_TYPES.Identifier &&
property.type === AST_NODE_TYPES.Identifier
) {
const objectSymbol = services.getSymbolAtLocation(object);
const notImported =
objectSymbol != null &&
isNotImported(objectSymbol, currentSourceFile);
if (
notImported &&
nativelyBoundMembers.has(`${object.name}.${property.name}`)
) {
return true;
}
}
// if `${object.name}.${property.name}` doesn't match any of
// the nativelyBoundMembers, then we fallback to type-level checks
return (
isBuiltinSymbolLike(
services.program,
services.getTypeAtLocation(object),
SUPPORTED_GLOBAL_TYPES,
) &&
isSymbolFromDefaultLibrary(
services.program,
services.getTypeAtLocation(property).getSymbol(),
)
);
}
Interfaces¶
Config¶
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
ignoreStatic |
boolean |
✗ | not shown |
CheckMethodResult¶
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
dangerous |
boolean |
✗ | not shown |
firstParamIsThis |
boolean |
✓ | not shown |
Type Aliases¶
Options¶
MessageIds¶
Generated by Syntax Scribe