@@ -274,10 +274,67 @@ function functionContainsExternalRoute(fn: SyntaxNode): boolean {
274274 return found
275275}
276276
277- function containsParamsReference ( expression : SyntaxNode ) : boolean {
277+ function collectBindingIdentifiers ( pattern : SyntaxNode , bindings : Set < string > ) : void {
278+ const current = unwrapExpression ( pattern )
279+ if ( current . type === 'Identifier' && typeof current . name === 'string' ) {
280+ bindings . add ( current . name )
281+ return
282+ }
283+ if ( current . type === 'AssignmentPattern' && isSyntaxNode ( current . left ) ) {
284+ collectBindingIdentifiers ( current . left , bindings )
285+ return
286+ }
287+ if ( current . type === 'RestElement' && isSyntaxNode ( current . argument ) ) {
288+ collectBindingIdentifiers ( current . argument , bindings )
289+ return
290+ }
291+ if ( current . type === 'TSParameterProperty' && isSyntaxNode ( current . parameter ) ) {
292+ collectBindingIdentifiers ( current . parameter , bindings )
293+ return
294+ }
295+ if ( current . type === 'ObjectPattern' && Array . isArray ( current . properties ) ) {
296+ for ( const property of current . properties ) {
297+ if ( ! isSyntaxNode ( property ) ) continue
298+ if ( property . type === 'RestElement' && isSyntaxNode ( property . argument ) ) {
299+ collectBindingIdentifiers ( property . argument , bindings )
300+ } else if ( property . type === 'ObjectProperty' && isSyntaxNode ( property . value ) ) {
301+ collectBindingIdentifiers ( property . value , bindings )
302+ }
303+ }
304+ return
305+ }
306+ if ( current . type === 'ArrayPattern' && Array . isArray ( current . elements ) ) {
307+ for ( const element of current . elements ) {
308+ if ( isSyntaxNode ( element ) ) collectBindingIdentifiers ( element , bindings )
309+ }
310+ }
311+ }
312+
313+ function getFunctionParameterBindings ( fn : SyntaxNode ) : Set < string > {
314+ const bindings = new Set < string > ( )
315+ const current = unwrapExpression ( fn )
316+ if ( ! Array . isArray ( current . params ) ) return bindings
317+ for ( const param of current . params ) {
318+ if ( isSyntaxNode ( param ) ) collectBindingIdentifiers ( param , bindings )
319+ }
320+ return bindings
321+ }
322+
323+ function containsParameterReference (
324+ expression : SyntaxNode ,
325+ parameterBindings : ReadonlySet < string >
326+ ) : boolean {
278327 const current = unwrapExpression ( expression )
279- if ( current . type === 'Identifier' && current . name === 'params' ) return true
280- return getChildNodes ( current ) . some ( ( child ) => containsParamsReference ( child ) )
328+ if (
329+ current . type === 'Identifier' &&
330+ typeof current . name === 'string' &&
331+ parameterBindings . has ( current . name )
332+ ) {
333+ return true
334+ }
335+ return getChildNodes ( current ) . some ( ( child ) =>
336+ containsParameterReference ( child , parameterBindings )
337+ )
281338}
282339
283340function isEncodedPathExpression ( expression : SyntaxNode ) : boolean {
@@ -303,8 +360,48 @@ function getConcatenationParts(expression: SyntaxNode): SyntaxNode[] {
303360 return [ ...getConcatenationParts ( current . left ) , ...getConcatenationParts ( current . right ) ]
304361}
305362
363+ function templateElementContainsQuery ( element : unknown ) : boolean {
364+ if ( ! isSyntaxNode ( element ) ) return false
365+ const value = element . value
366+ return (
367+ typeof value === 'object' &&
368+ value !== null &&
369+ ( ( 'cooked' in value && typeof value . cooked === 'string' && value . cooked . includes ( '?' ) ) ||
370+ ( 'raw' in value && typeof value . raw === 'string' && value . raw . includes ( '?' ) ) )
371+ )
372+ }
373+
374+ function inspectTemplatePathExpressions (
375+ template : SyntaxNode ,
376+ parameterBindings : ReadonlySet < string > ,
377+ initialQueryStarted = false
378+ ) : { queryStarted : boolean ; unsafe : boolean } {
379+ if ( ! Array . isArray ( template . quasis ) || ! Array . isArray ( template . expressions ) ) {
380+ return { queryStarted : initialQueryStarted , unsafe : false }
381+ }
382+
383+ let queryStarted = initialQueryStarted
384+ for ( let index = 0 ; index < template . expressions . length ; index ++ ) {
385+ if ( templateElementContainsQuery ( template . quasis [ index ] ) ) queryStarted = true
386+ const expression = template . expressions [ index ]
387+ if (
388+ ! queryStarted &&
389+ isSyntaxNode ( expression ) &&
390+ containsParameterReference ( expression , parameterBindings ) &&
391+ ! isEncodedPathExpression ( expression )
392+ ) {
393+ return { queryStarted, unsafe : true }
394+ }
395+ }
396+ if ( templateElementContainsQuery ( template . quasis [ template . expressions . length ] ) ) {
397+ queryStarted = true
398+ }
399+ return { queryStarted, unsafe : false }
400+ }
401+
306402function functionContainsUnsafeInternalPathInterpolation ( fn : SyntaxNode ) : boolean {
307403 const current = unwrapExpression ( fn )
404+ const parameterBindings = getFunctionParameterBindings ( current )
308405 let found = false
309406
310407 const visit = ( node : SyntaxNode ) => {
@@ -316,12 +413,25 @@ function functionContainsUnsafeInternalPathInterpolation(fn: SyntaxNode): boolea
316413 ) {
317414 let queryStarted = false
318415 for ( const part of getConcatenationParts ( node ) ) {
416+ const currentPart = unwrapExpression ( part )
417+ if ( currentPart . type === 'TemplateLiteral' ) {
418+ const inspected = inspectTemplatePathExpressions (
419+ currentPart ,
420+ parameterBindings ,
421+ queryStarted
422+ )
423+ if ( inspected . unsafe ) {
424+ found = true
425+ return
426+ }
427+ queryStarted = inspected . queryStarted
428+ continue
429+ }
319430 const prefix = getStringPrefix ( part )
320431 if ( prefix ?. includes ( '?' ) ) queryStarted = true
321432 if (
322433 ! queryStarted &&
323- unwrapExpression ( part ) . type !== 'TemplateLiteral' &&
324- containsParamsReference ( part ) &&
434+ containsParameterReference ( part , parameterBindings ) &&
325435 ! isEncodedPathExpression ( part )
326436 ) {
327437 found = true
@@ -337,32 +447,9 @@ function functionContainsUnsafeInternalPathInterpolation(fn: SyntaxNode): boolea
337447 isSyntaxNode ( node . quasis [ 0 ] ) &&
338448 getStringPrefix ( node ) ?. startsWith ( '/api/' )
339449 ) {
340- let queryStarted = false
341- for ( let index = 0 ; index < node . expressions . length ; index ++ ) {
342- const quasi = node . quasis [ index ]
343- if ( isSyntaxNode ( quasi ) ) {
344- const value = quasi . value
345- if (
346- typeof value === 'object' &&
347- value !== null &&
348- ( ( 'cooked' in value &&
349- typeof value . cooked === 'string' &&
350- value . cooked . includes ( '?' ) ) ||
351- ( 'raw' in value && typeof value . raw === 'string' && value . raw . includes ( '?' ) ) )
352- ) {
353- queryStarted = true
354- }
355- }
356- const expression = node . expressions [ index ]
357- if (
358- ! queryStarted &&
359- isSyntaxNode ( expression ) &&
360- containsParamsReference ( expression ) &&
361- ! isEncodedPathExpression ( expression )
362- ) {
363- found = true
364- return
365- }
450+ if ( inspectTemplatePathExpressions ( node , parameterBindings ) . unsafe ) {
451+ found = true
452+ return
366453 }
367454 }
368455 for ( const child of getChildNodes ( node ) ) visit ( child )
0 commit comments