📄 no-misused-spread¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 15 |
| 📦 Imports | 14 |
| 📑 Type Aliases | 2 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/no-misused-spread.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'no-misused-spread' |
meta.type |
'problem' |
meta.docs.description |
'Disallow using the spread operator when it might cause unexpected behavior' |
meta.docs.recommended |
'strict' |
meta.docs.requiresTypeChecking |
true |
meta.hasSuggestions |
true |
meta.messages.addAwait |
'Add await operator.' |
meta.messages.noArraySpreadInObject |
'Using the spread operator on an array in an object will result in a list of indices.' |
meta.messages.noClassDeclarationSpreadInObject |
'Using the spread operator on class declarations will spread only their static properties, and will lose their class ... |
meta.messages.noClassInstanceSpreadInObject |
'Using the spread operator on class instances will lose their class prototype.' |
meta.messages.noFunctionSpreadInObject |
'Using the spread operator on a function without additional properties can cause unexpected behavior. Did you forget ... |
meta.messages.noIterableSpreadInObject |
'Using the spread operator on an Iterable in an object can cause unexpected behavior.' |
meta.messages.noMapSpreadInObject |
'Using the spread operator on a Map in an object will result in an empty object. Did you mean to use `Object.fromEntr... |
meta.messages.noPromiseSpreadInObject |
'Using the spread operator on Promise in an object can cause unexpected behavior. Did you forget to await the promise?' |
meta.messages.noStringSpread |
[ 'Using the spread operator on a string can mishandle special characters, as can .split("").', '- ... produces U... |
meta.messages.replaceMapSpreadInObject |
'Replace map spread in object with Object.fromEntries()' |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { allow: { ...readonlynessOptionsSchema.properties.allow... |
defaultOptions |
[ { allow: [], }, ] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
TypeOrValueSpecifier |
../util |
createRule |
../util |
getConstrainedTypeAtLocation |
../util |
getParserServices |
../util |
getWrappingFixer |
../util |
isBuiltinSymbolLike |
../util |
isPromiseLike |
../util |
isTypeFlagSet |
../util |
readonlynessOptionsSchema |
../util |
typeMatchesSomeSpecifier |
../util |
isHigherPrecedenceThanAwait |
../util |
Functions¶
create(context: any, [options]: any): { 'ArrayExpression > SpreadElement': (node: TSESTree.Spread…¶
Parameters:
contextany[options]any
Returns: { 'ArrayExpression > SpreadElement': (node: TSESTree.SpreadElement) => void; 'CallExpression > SpreadElement': (node: TSESTree.SpreadElement) => void; JSXSpreadAttribute: (node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement) => void; 'ObjectExpression > SpreadElement': (node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement) => void; }
Calls:
getParserServices (from ../util)services.program.getTypeCheckergetConstrainedTypeAtLocation (from ../util)typeMatchesSomeSpecifier (from ../util)isStringcontext.reporttsutils.unionConstituentstypes.someisMapgetWrappingFixer (from ../util)isHigherPrecedenceThanAwait (from ../util)services.esTreeNodeToTSNodeMap.getfixer.insertTextBeforefixer.insertTextAfterisPromisegetPromiseSpreadSuggestionsisFunctionWithoutPropsgetMapSpreadSuggestionsisArrayisIterableisClassInstanceisClassDeclaration
Internal Comments:
Code
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
function checkArrayOrCallSpread(node: TSESTree.SpreadElement): void {
const type = getConstrainedTypeAtLocation(services, node.argument);
if (
!typeMatchesSomeSpecifier(type, options.allow, services.program) &&
isString(type)
) {
context.report({
node,
messageId: 'noStringSpread',
});
}
}
function getMapSpreadSuggestions(
node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
type: ts.Type,
): TSESLint.ReportSuggestionArray<MessageIds> | null {
const types = tsutils.unionConstituents(type);
if (types.some(t => !isMap(services.program, t))) {
return null;
}
if (
node.parent.type === AST_NODE_TYPES.ObjectExpression &&
node.parent.properties.length === 1
) {
return [
{
messageId: 'replaceMapSpreadInObject',
fix: getWrappingFixer({
node: node.parent,
innerNode: node.argument,
sourceCode: context.sourceCode,
wrap: code => `Object.fromEntries(${code})`,
}),
},
];
}
return [
{
messageId: 'replaceMapSpreadInObject',
fix: getWrappingFixer({
node: node.argument,
sourceCode: context.sourceCode,
wrap: code => `Object.fromEntries(${code})`,
}),
},
];
}
function getPromiseSpreadSuggestions(
node: TSESTree.Expression,
): TSESLint.ReportSuggestionArray<MessageIds> {
const isHighPrecedence = isHigherPrecedenceThanAwait(
services.esTreeNodeToTSNodeMap.get(node),
);
return [
{
messageId: 'addAwait',
fix: fixer =>
isHighPrecedence
? fixer.insertTextBefore(node, 'await ')
: [
fixer.insertTextBefore(node, 'await ('),
fixer.insertTextAfter(node, ')'),
],
},
];
}
function checkObjectSpread(
node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
): void {
const type = getConstrainedTypeAtLocation(services, node.argument);
if (typeMatchesSomeSpecifier(type, options.allow, services.program)) {
return;
}
if (isPromise(services.program, type)) {
context.report({
node,
messageId: 'noPromiseSpreadInObject',
suggest: getPromiseSpreadSuggestions(node.argument),
});
return;
}
if (isFunctionWithoutProps(type)) {
context.report({
node,
messageId: 'noFunctionSpreadInObject',
});
return;
}
if (isMap(services.program, type)) {
context.report({
node,
messageId: 'noMapSpreadInObject',
suggest: getMapSpreadSuggestions(node, type),
});
return;
}
if (isArray(checker, type)) {
context.report({
node,
messageId: 'noArraySpreadInObject',
});
return;
}
if (
isIterable(type, checker) &&
// Don't report when the type is string, since TS will flag it already
!isString(type)
) {
context.report({
node,
messageId: 'noIterableSpreadInObject',
});
return;
}
if (isClassInstance(checker, type)) {
context.report({
node,
messageId: 'noClassInstanceSpreadInObject',
});
return;
}
if (isClassDeclaration(type)) {
context.report({
node,
messageId: 'noClassDeclarationSpreadInObject',
});
}
}
return {
'ArrayExpression > SpreadElement': checkArrayOrCallSpread,
'CallExpression > SpreadElement': checkArrayOrCallSpread,
JSXSpreadAttribute: checkObjectSpread,
'ObjectExpression > SpreadElement': checkObjectSpread,
};
}
isIterable(type: ts.Type, checker: ts.TypeChecker): boolean¶
Parameters:
typets.Typecheckerts.TypeChecker
Returns: boolean
Calls:
tsutils .typeConstituents(type) .sometsutils.getWellKnownSymbolPropertyOfType
Code
isArray(checker: ts.TypeChecker, type: ts.Type): boolean¶
Parameters:
checkerts.TypeCheckertypets.Type
Returns: boolean
Calls:
isTypeRecurserchecker.isArrayTypechecker.isTupleType
Code
isString(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
isTypeRecurserisTypeFlagSet (from ../util)
Code
isFunctionWithoutProps(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
isTypeRecursert.getCallSignaturest.getProperties
Code
isPromise(program: ts.Program, type: ts.Type): boolean¶
Parameters:
programts.Programtypets.Type
Returns: boolean
Calls:
isTypeRecurserisPromiseLike (from ../util)
Code
isClassInstance(checker: ts.TypeChecker, type: ts.Type): boolean¶
Parameters:
checkerts.TypeCheckertypets.Type
Returns: boolean
Calls:
isTypeRecursert.getConstructSignaturest.getSymbolsymbol ?.getDeclarations() ?.somechecker .getTypeOfSymbolAtLocation(symbol, declaration) .getConstructSignatures
Internal Comments:
// If the type itself has a construct signature, it's a class(-like)
// If the type's symbol has a construct signature, the type is an instance
Code
function isClassInstance(checker: ts.TypeChecker, type: ts.Type): boolean {
return isTypeRecurser(type, t => {
// If the type itself has a construct signature, it's a class(-like)
if (t.getConstructSignatures().length) {
return false;
}
const symbol = t.getSymbol();
// If the type's symbol has a construct signature, the type is an instance
return !!symbol
?.getDeclarations()
?.some(
declaration =>
checker
.getTypeOfSymbolAtLocation(symbol, declaration)
.getConstructSignatures().length,
);
});
}
isClassDeclaration(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
isTypeRecursertsutils.isObjectTypetsutils.isObjectFlagSett.getSymbol
Code
function isClassDeclaration(type: ts.Type): boolean {
return isTypeRecurser(type, t => {
if (
tsutils.isObjectType(t) &&
tsutils.isObjectFlagSet(t, ts.ObjectFlags.InstantiationExpressionType)
) {
return true;
}
const kind = t.getSymbol()?.valueDeclaration?.kind;
return (
kind === ts.SyntaxKind.ClassDeclaration ||
kind === ts.SyntaxKind.ClassExpression
);
});
}
isMap(program: ts.Program, type: ts.Type): boolean¶
Parameters:
programts.Programtypets.Type
Returns: boolean
Calls:
isTypeRecurserisBuiltinSymbolLike (from ../util)
Code
isTypeRecurser(type: ts.Type, predicate: (t: ts.Type) => boolean): boolean¶
Parameters:
typets.Typepredicate(t: ts.Type) => boolean
Returns: boolean
Calls:
type.isUnionOrIntersectiontype.types.someisTypeRecurserpredicate
Code
Internal helpers¶
Declared inside another function in this file.
checkArrayOrCallSpread(node: TSESTree.SpreadElement): void¶
Parameters:
nodeTSESTree.SpreadElement
Returns: void
Calls:
getConstrainedTypeAtLocation (from ../util)typeMatchesSomeSpecifier (from ../util)isStringcontext.report
Code
getMapSpreadSuggestions(node: TSESTree.JSXSpreadAttribute | TSESTree.…, type: ts.Type): TSESLint.ReportSuggestionArray<MessageIds> | null¶
Parameters:
nodeTSESTree.JSXSpreadAttribute | TSESTree.SpreadElementtypets.Type
Returns: TSESLint.ReportSuggestionArray<MessageIds> | null
Calls:
tsutils.unionConstituentstypes.someisMapgetWrappingFixer (from ../util)
Code
function getMapSpreadSuggestions(
node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
type: ts.Type,
): TSESLint.ReportSuggestionArray<MessageIds> | null {
const types = tsutils.unionConstituents(type);
if (types.some(t => !isMap(services.program, t))) {
return null;
}
if (
node.parent.type === AST_NODE_TYPES.ObjectExpression &&
node.parent.properties.length === 1
) {
return [
{
messageId: 'replaceMapSpreadInObject',
fix: getWrappingFixer({
node: node.parent,
innerNode: node.argument,
sourceCode: context.sourceCode,
wrap: code => `Object.fromEntries(${code})`,
}),
},
];
}
return [
{
messageId: 'replaceMapSpreadInObject',
fix: getWrappingFixer({
node: node.argument,
sourceCode: context.sourceCode,
wrap: code => `Object.fromEntries(${code})`,
}),
},
];
}
getPromiseSpreadSuggestions(node: TSESTree.Expression): TSESLint.ReportSuggestionArray<MessageIds>¶
Parameters:
nodeTSESTree.Expression
Returns: TSESLint.ReportSuggestionArray<MessageIds>
Calls:
isHigherPrecedenceThanAwait (from ../util)services.esTreeNodeToTSNodeMap.getfixer.insertTextBeforefixer.insertTextAfter
Code
function getPromiseSpreadSuggestions(
node: TSESTree.Expression,
): TSESLint.ReportSuggestionArray<MessageIds> {
const isHighPrecedence = isHigherPrecedenceThanAwait(
services.esTreeNodeToTSNodeMap.get(node),
);
return [
{
messageId: 'addAwait',
fix: fixer =>
isHighPrecedence
? fixer.insertTextBefore(node, 'await ')
: [
fixer.insertTextBefore(node, 'await ('),
fixer.insertTextAfter(node, ')'),
],
},
];
}
fix(fixer: any): any¶
Parameters:
fixerany
Returns: any
Code
checkObjectSpread(node: TSESTree.JSXSpreadAttribute | TSESTree.…): void¶
Parameters:
nodeTSESTree.JSXSpreadAttribute | TSESTree.SpreadElement
Returns: void
Calls:
getConstrainedTypeAtLocation (from ../util)typeMatchesSomeSpecifier (from ../util)isPromisecontext.reportgetPromiseSpreadSuggestionsisFunctionWithoutPropsisMapgetMapSpreadSuggestionsisArrayisIterableisStringisClassInstanceisClassDeclaration
Internal Comments:
Code
function checkObjectSpread(
node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
): void {
const type = getConstrainedTypeAtLocation(services, node.argument);
if (typeMatchesSomeSpecifier(type, options.allow, services.program)) {
return;
}
if (isPromise(services.program, type)) {
context.report({
node,
messageId: 'noPromiseSpreadInObject',
suggest: getPromiseSpreadSuggestions(node.argument),
});
return;
}
if (isFunctionWithoutProps(type)) {
context.report({
node,
messageId: 'noFunctionSpreadInObject',
});
return;
}
if (isMap(services.program, type)) {
context.report({
node,
messageId: 'noMapSpreadInObject',
suggest: getMapSpreadSuggestions(node, type),
});
return;
}
if (isArray(checker, type)) {
context.report({
node,
messageId: 'noArraySpreadInObject',
});
return;
}
if (
isIterable(type, checker) &&
// Don't report when the type is string, since TS will flag it already
!isString(type)
) {
context.report({
node,
messageId: 'noIterableSpreadInObject',
});
return;
}
if (isClassInstance(checker, type)) {
context.report({
node,
messageId: 'noClassInstanceSpreadInObject',
});
return;
}
if (isClassDeclaration(type)) {
context.report({
node,
messageId: 'noClassDeclarationSpreadInObject',
});
}
}
Type Aliases¶
Options¶
MessageIds¶
type MessageIds = | 'addAwait'
| 'noArraySpreadInObject'
| 'noClassDeclarationSpreadInObject'
| 'noClassInstanceSpreadInObject'
| 'noFunctionSpreadInObject'
| 'noIterableSpreadInObject'
| 'noMapSpreadInObject'
| 'noPromiseSpreadInObject'
| 'noStringSpread'
| 'replaceMapSpreadInObject';
Generated by Syntax Scribe