Skip to content

Commit 1a402e8

Browse files
committed
fix(audits): classify the selected member of a local handler object
1 parent 2d0bbdc commit 1a402e8

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

‎scripts/check-source-conventions.test.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,20 @@ describe('bare-route-handler', () => {
250250
'const loop = () => other()',
251251
'const other = () => loop()',
252252
'const wrapped = withRouteHandler(handler)',
253+
'const routes = { PATCH: async () => new Response(), HEAD() { return new Response() } }',
253254
'export const GET = alias',
254255
'export const PUT = loop',
256+
'export const PATCH = routes.PATCH',
257+
'export const HEAD = routes.HEAD',
255258
'export { handler as POST, wrapped as DELETE }',
256259
].join('\n')
257-
expect(hits(ROUTE, source, 'bare-route-handler')).toEqual(['GET', 'PUT', 'POST'])
260+
expect(hits(ROUTE, source, 'bare-route-handler')).toEqual([
261+
'GET',
262+
'PUT',
263+
'PATCH',
264+
'HEAD',
265+
'POST',
266+
])
258267
})
259268

260269
it('allows wrapped results, their aliases and members, imports, and delegates to them', () => {
@@ -271,7 +280,14 @@ describe('bare-route-handler', () => {
271280
'export const OPTIONS = flag ? handler : handlers.OPTIONS',
272281
'export const dynamic = "force-dynamic"',
273282
].join('\n')
283+
const objectMembers = [
284+
'const handler = defineInternalJsonRoute({ useCase })',
285+
'const routes = { GET: withRouteHandler(async () => new Response()), PUT: handler }',
286+
'export const GET = routes.GET',
287+
'export const PUT = routes.PUT',
288+
].join('\n')
274289
expect(hits(ROUTE, source, 'bare-route-handler')).toEqual([])
290+
expect(hits(ROUTE, objectMembers, 'bare-route-handler')).toEqual([])
275291
})
276292

277293
it('only checks route files', () => {

‎scripts/check-source-conventions.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,12 @@ function classifyHandler(
412412
? 'wrapped'
413413
: 'opaque'
414414
}
415-
case 'MemberExpression':
416-
return classifyHandler(node.object as Node, bindings, seen)
415+
case 'MemberExpression': {
416+
const member = objectLiteralMember(node, bindings)
417+
if (member === undefined) return classifyHandler(node.object as Node, bindings, seen)
418+
if (member.type === 'ObjectMethod') return 'bare'
419+
return classifyHandler(member.value as Node, bindings, seen)
420+
}
417421
case 'Identifier': {
418422
const name = node.name as string
419423
const binding = bindings.get(name)
@@ -427,6 +431,26 @@ function classifyHandler(
427431
}
428432
}
429433

434+
/**
435+
* The property `handlers.GET` selects when `handlers` is a local object literal, so the selected
436+
* value is classified rather than the object. Undefined when the object is anything else.
437+
*/
438+
function objectLiteralMember(member: Node, bindings: Map<string, Node>): Node | undefined {
439+
const object = unwrap(member.object as Node)
440+
const property = member.property as Node
441+
if (object?.type !== 'Identifier' || member.computed || property.type !== 'Identifier') {
442+
return undefined
443+
}
444+
const binding = bindings.get(object.name as string)
445+
const literal = binding?.type === 'VariableDeclarator' ? unwrap(binding.init as Node) : undefined
446+
if (literal?.type !== 'ObjectExpression') return undefined
447+
return (literal.properties as Node[]).find(
448+
(entry) =>
449+
(entry.type === 'ObjectProperty' || entry.type === 'ObjectMethod') &&
450+
propertyKeyName(entry) === property.name
451+
)
452+
}
453+
430454
/** Whether an arrow's expression body calls through a local binding that is a wrapped handler. */
431455
function delegatesToWrapped(body: Node, bindings: Map<string, Node>, seen: Set<string>): boolean {
432456
let delegates = false

0 commit comments

Comments
 (0)