📄 unbound-method.ts
¶
📊 Analysis Summary¶
Metric | Count |
---|---|
🔧 Functions | 7 |
📦 Imports | 7 |
📊 Variables & Constants | 12 |
📐 Interfaces | 2 |
📑 Type Aliases | 2 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/unbound-method.ts
📦 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", "RegExp", "Symbol", "Array", "Proxy", "Date", "Atomics", "Reflect", "console", "Math", "JSON", "Intl"] |
const | `[ | |
'Number', | ||||
'Object', | ||||
'String', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum | ||||
'RegExp', | ||||
'Symbol', | ||||
'Array', | ||||
'Proxy', | ||||
'Date', | ||||
'Atomics', | ||||
'Reflect', | ||||
'console', | ||||
'Math', | ||||
'JSON', | ||||
'Intl', | ||||
] as const` | ✗ | |||
object |
SymbolConstructor | Console | ObjectConstructor | RegExpConstructor | ArrayConstructor | ... 8 more ... | JSON |
const | global[namespace] |
✗ |
nativelyBoundMembers |
Set<string> |
const | `new Set( | |
SUPPORTED_GLOBALS.flatMap(namespace => { | ||||
if (!(namespace in global)) { | ||||
// node.js might not have namespaces like Intl depending on compilation options | ||||
// https://nodejs.org/api/intl.html#intl_options_for_building_node_js | ||||
return []; | ||||
} | ||||
const object = global[namespace]; | ||||
return Object.getOwnPropertyNames(object) | ||||
.filter( | ||||
name => | ||||
!name.startsWith('_') && | ||||
typeof (object as Record |
||||
) | ||||
.map(name => ${namespace}.${name} ); |
||||
}), | ||||
)` | ✗ | |||
SUPPORTED_GLOBAL_TYPES |
string[] |
const | `[ | |
'NumberConstructor', | ||||
'ObjectConstructor', | ||||
'StringConstructor', | ||||
'SymbolConstructor', | ||||
'ArrayConstructor', | ||||
'Array', | ||||
'ProxyConstructor', | ||||
'Console', | ||||
'DateConstructor', | ||||
'Atomics', | ||||
'Math', | ||||
'JSON', | ||||
]` | ✗ | |||
BASE_MESSAGE |
"Avoid referencing unbound methods which may cause unintentional scoping of this." |
const | 'Avoid referencing unbound methods which may cause unintentional scoping of this.' |
✗ |
notImported |
boolean |
const | `objectSymbol != null && | |
isNotImported(objectSymbol, currentSourceFile)` | ✗ | |||
initNode |
TSESTree.Node | null |
let/var | null |
✗ |
parent |
TSESTree.Node | undefined |
let/var | node |
✗ |
assignee |
any |
const | (valueDeclaration as ts.PropertyAssignment).initializer |
✗ |
firstParamIsThis |
boolean |
const | `firstParam?.name.kind === ts.SyntaxKind.Identifier && | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison | ||||
firstParam.name.escapedText === 'this'` | ✗ | |||
thisArgIsVoid |
boolean |
const | firstParamIsThis && firstParam.type?.kind === ts.SyntaxKind.VoidKeyword |
✗ |
parent |
any |
const | node.parent |
✗ |
Functions¶
isNotImported(symbol: ts.Symbol, currentSourceFile: ts.SourceFile | undefined): boolean
¶
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()
);
}
- Parameters:
symbol: ts.Symbol
currentSourceFile: ts.SourceFile | undefined
- Return Type:
boolean
- Calls:
valueDeclaration.getSourceFile
- Internal Comments:
checkIfMethodAndReport(node: TSESTree.Node, symbol: ts.Symbol | undefined): boolean
¶
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;
}
- Parameters:
node: TSESTree.Node
symbol: ts.Symbol | undefined
- Return Type:
boolean
- Calls:
checkIfMethod
context.report
isNativelyBound(object: TSESTree.Node, property: TSESTree.Node): boolean
¶
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(),
)
);
}
- Parameters:
object: TSESTree.Node
property: TSESTree.Node
- Return Type:
boolean
- Calls:
services.getSymbolAtLocation
isNotImported
nativelyBoundMembers.has
isBuiltinSymbolLike (from ../util)
services.getTypeAtLocation
isSymbolFromDefaultLibrary (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
isNodeInsideTypeDeclaration(node: TSESTree.Node): 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;
}
- Parameters:
node: TSESTree.Node
- Return Type:
boolean
checkIfMethod(symbol: ts.Symbol, ignoreStatic: boolean): CheckMethodResult
¶
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 };
}
- Parameters:
symbol: ts.Symbol
ignoreStatic: boolean
- Return Type:
CheckMethodResult
- Calls:
checkMethod
- Internal Comments:
`checkMethod(valueDeclaration: | ts.FunctionExpression¶
| ts.MethodDeclaration
| ts.MethodSignature, ignoreStatic: boolean): CheckMethodResult`
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,
};
}
- Parameters:
valueDeclaration: | ts.FunctionExpression | ts.MethodDeclaration | ts.MethodSignature
ignoreStatic: boolean
- Return Type:
CheckMethodResult
- Calls:
valueDeclaration.parameters.at
tsutils.includesModifier
getModifiers (from ../util)
- Internal Comments:
isSafeUse(node: TSESTree.Node): boolean
¶
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;
}
- Parameters:
node: TSESTree.Node
- Return Type:
boolean
- Calls:
['!', 'delete', 'typeof', 'void'].includes
['!=', '!==', '==', '===', 'instanceof'].includes
isSafeUse
- 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
Interfaces¶
Config
¶
Properties¶
Name | Type | Optional | Description |
---|---|---|---|
ignoreStatic |
boolean |
✗ |
CheckMethodResult
¶
Properties¶
Name | Type | Optional | Description |
---|---|---|---|
dangerous |
boolean |
✗ | |
firstParamIsThis |
boolean |
✓ |