1- /* eslint-disable @typescript-eslint/internal/prefer-ast-types-enum */
21import type { TSESTree } from '@typescript-eslint/utils' ;
32
43import { AST_NODE_TYPES } from '@typescript-eslint/utils' ;
@@ -69,7 +68,7 @@ export default createRule<Options, MessageIds>({
6968 const checker = services . program . getTypeChecker ( ) ;
7069 const ignoredTypeNames = option . ignoredTypeNames ?? [ ] ;
7170
72- function checkExpression ( node : TSESTree . Node , type ?: ts . Type ) : void {
71+ function checkExpression ( node : TSESTree . Expression , type ?: ts . Type ) : void {
7372 if ( node . type === AST_NODE_TYPES . Literal ) {
7473 return ;
7574 }
@@ -176,15 +175,17 @@ export default createRule<Options, MessageIds>({
176175 }
177176
178177 function collectToStringCertainty ( type : ts . Type ) : Usefulness {
179- const toString =
180- checker . getPropertyOfType ( type , 'toString' ) ??
181- checker . getPropertyOfType ( type , 'toLocaleString' ) ;
182- const declarations = toString ?. getDeclarations ( ) ;
183- if ( ! toString || ! declarations || declarations . length === 0 ) {
178+ // https://github.com/JoshuaKGoldberg/ts-api-utils/issues/382
179+ if ( ( tsutils . isTypeParameter as ( t : ts . Type ) => boolean ) ( type ) ) {
180+ const constraint = type . getConstraint ( ) ;
181+ if ( constraint ) {
182+ return collectToStringCertainty ( constraint ) ;
183+ }
184+ // unconstrained generic means `unknown`
184185 return Usefulness . Always ;
185186 }
186187
187- // Patch for old version TypeScript, the Boolean type definition missing toString()
188+ // the Boolean type definition missing toString()
188189 if (
189190 type . flags & ts . TypeFlags . Boolean ||
190191 type . flags & ts . TypeFlags . BooleanLiteral
@@ -196,32 +197,49 @@ export default createRule<Options, MessageIds>({
196197 return Usefulness . Always ;
197198 }
198199
199- if (
200- declarations . every (
201- ( { parent } ) =>
202- ! ts . isInterfaceDeclaration ( parent ) || parent . name . text !== 'Object' ,
203- )
204- ) {
205- return Usefulness . Always ;
206- }
207-
208200 if ( type . isIntersection ( ) ) {
209201 return collectIntersectionTypeCertainty ( type , collectToStringCertainty ) ;
210202 }
211203
212- if ( ! type . isUnion ( ) ) {
213- return Usefulness . Never ;
204+ if ( type . isUnion ( ) ) {
205+ return collectUnionTypeCertainty ( type , collectToStringCertainty ) ;
206+ }
207+
208+ const toString =
209+ checker . getPropertyOfType ( type , 'toString' ) ??
210+ checker . getPropertyOfType ( type , 'toLocaleString' ) ;
211+ if ( ! toString ) {
212+ // e.g. any/unknown
213+ return Usefulness . Always ;
214214 }
215- return collectUnionTypeCertainty ( type , collectToStringCertainty ) ;
215+
216+ const declarations = toString . getDeclarations ( ) ;
217+
218+ if ( declarations == null || declarations . length !== 1 ) {
219+ // If there are multiple declarations, at least one of them must not be
220+ // the default object toString.
221+ //
222+ // This may only matter for older versions of TS
223+ // see https://github.com/typescript-eslint/typescript-eslint/issues/8585
224+ return Usefulness . Always ;
225+ }
226+
227+ const declaration = declarations [ 0 ] ;
228+ const isBaseToString =
229+ ts . isInterfaceDeclaration ( declaration . parent ) &&
230+ declaration . parent . name . text === 'Object' ;
231+ return isBaseToString ? Usefulness . Never : Usefulness . Always ;
216232 }
217233
218234 function isBuiltInStringCall ( node : TSESTree . CallExpression ) : boolean {
219235 if (
220236 node . callee . type === AST_NODE_TYPES . Identifier &&
237+ // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
221238 node . callee . name === 'String' &&
222239 node . arguments [ 0 ]
223240 ) {
224241 const scope = context . sourceCode . getScope ( node ) ;
242+ // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
225243 const variable = scope . set . get ( 'String' ) ;
226244 return ! variable ?. defs . length ;
227245 }
@@ -245,7 +263,10 @@ export default createRule<Options, MessageIds>({
245263 }
246264 } ,
247265 CallExpression ( node : TSESTree . CallExpression ) : void {
248- if ( isBuiltInStringCall ( node ) ) {
266+ if (
267+ isBuiltInStringCall ( node ) &&
268+ node . arguments [ 0 ] . type !== AST_NODE_TYPES . SpreadElement
269+ ) {
249270 checkExpression ( node . arguments [ 0 ] ) ;
250271 }
251272 } ,
0 commit comments