Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

Commit

Permalink
fix: detect a wider range of scheduled functions
Browse files Browse the repository at this point in the history
scheduled functions which are reassigned or not directly exported are now detected
  • Loading branch information
danez committed Jun 7, 2022
1 parent 123e7e8 commit 37f5c2f
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 19 deletions.
6 changes: 4 additions & 2 deletions src/runtimes/node/in_source_config/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ArgumentPlaceholder, Expression, SpreadElement, JSXNamespacedName } from '@babel/types'

import { nonNullable } from '../../../utils/non_nullable.js'
import { createBindingsMethod } from '../parser/bindings.js'
import { getMainExport } from '../parser/exports.js'
import { getImports } from '../parser/imports.js'
import { safelyParseFile } from '../parser/index.js'
Expand All @@ -22,7 +23,8 @@ export const findISCDeclarationsInPath = async (sourcePath: string): Promise<ISC
}

const imports = ast.body.flatMap((node) => getImports(node, IN_SOURCE_CONFIG_MODULE))
const mainExports = getMainExport(ast.body)
const getAllBindings = createBindingsMethod(ast.body)
const mainExports = getMainExport(ast.body, getAllBindings)
const iscExports = mainExports
.map(({ args, local: exportName }) => {
const matchingImport = imports.find(({ local: importName }) => importName === exportName)
Expand All @@ -33,7 +35,7 @@ export const findISCDeclarationsInPath = async (sourcePath: string): Promise<ISC

switch (matchingImport.imported) {
case 'schedule':
return parseSchedule({ args })
return parseSchedule({ args }, getAllBindings)

default:
// no-op
Expand Down
13 changes: 11 additions & 2 deletions src/runtimes/node/in_source_config/properties/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type { BindingMethod } from '../../parser/bindings.js'
import type { ISCHandlerArg } from '../index.js'

export const parse = ({ args }: { args: ISCHandlerArg[] }) => {
const [expression] = args
export const parse = ({ args }: { args: ISCHandlerArg[] }, getAllBindings: BindingMethod) => {
let [expression] = args

if (expression.type === 'Identifier') {
const binding = getAllBindings().get(expression.name)
if (binding) {
expression = binding
}
}

const schedule = expression.type === 'StringLiteral' ? expression.value : undefined

return {
Expand Down
55 changes: 55 additions & 0 deletions src/runtimes/node/parser/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Expression, Statement, VariableDeclaration } from '@babel/types'

type Bindings = Map<string, Expression>

const getBindingFromVariableDeclaration = function (node: VariableDeclaration, bindings: Bindings): void {
node.declarations.forEach((declaration) => {
if (declaration.id.type === 'Identifier' && declaration.init) {
bindings.set(declaration.id.name, declaration.init)
}
})
}

// eslint-disable-next-line complexity
const getBindingsFromNode = function (node: Statement, bindings: Bindings): void {
if (node.type === 'VariableDeclaration') {
// A variable was created, so create it and store the potential value
getBindingFromVariableDeclaration(node, bindings)
} else if (
node.type === 'ExpressionStatement' &&
node.expression.type === 'AssignmentExpression' &&
node.expression.left.type === 'Identifier'
) {
// The variable was reassigned, so lets store the new value
bindings.set(node.expression.left.name, node.expression.right)
} else if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
// A `export const|let ...` creates a binding that can later be referenced again
getBindingFromVariableDeclaration(node.declaration, bindings)
}
}

/**
* Goes through all relevant nodes and creates a map from binding name to assigned value/expression
*/
const getAllBindings = function (nodes: Statement[]): Bindings {
const bindings: Bindings = new Map()

nodes.forEach((node) => {
getBindingsFromNode(node, bindings)
})

return bindings
}

export type BindingMethod = () => Bindings

export const createBindingsMethod = function (nodes: Statement[]): BindingMethod {
// memoize the result for these nodes
let result: Bindings | null = null
return () => {
if (!result) {
result = getAllBindings(nodes)
}
return result
}
}
54 changes: 40 additions & 14 deletions src/runtimes/node/parser/exports.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { CallExpression, Statement } from '@babel/types'
import type {
ExportDefaultSpecifier,
ExportNamespaceSpecifier,
ExportSpecifier,
Expression,
Statement,
} from '@babel/types'

import type { ISCExport } from '../in_source_config/index.js'

import type { BindingMethod } from './bindings.js'
import { isModuleExports } from './helpers.js'

// Finds the main handler export in an AST.
export const getMainExport = (nodes: Statement[]) => {
export const getMainExport = (nodes: Statement[], getAllBindings: BindingMethod) => {
let handlerExport: ISCExport[] = []

nodes.find((node) => {
const esmExports = getMainExportFromESM(node)
const esmExports = getMainExportFromESM(node, getAllBindings)

if (esmExports.length !== 0) {
handlerExport = esmExports
Expand Down Expand Up @@ -39,24 +46,27 @@ const getMainExportFromCJS = (node: Statement) => {
]

return handlerPaths.flatMap((handlerPath) => {
if (!isModuleExports(node, handlerPath) || node.expression.right.type !== 'CallExpression') {
if (!isModuleExports(node, handlerPath)) {
return []
}

return getExportsFromCallExpression(node.expression.right)
return getExportsFromExpression(node.expression.right)
})
}

// Finds the main handler export in an ESM AST.
// eslint-disable-next-line complexity
const getMainExportFromESM = (node: Statement) => {
const getMainExportFromESM = (node: Statement, getAllBindings: BindingMethod) => {
if (node.type !== 'ExportNamedDeclaration' || node.exportKind !== 'value') {
return []
}

const { declaration } = node
const { declaration, specifiers } = node

if (!declaration || declaration.type !== 'VariableDeclaration') {
if (specifiers?.length > 0) {
return getExportsFromBindings(specifiers, getAllBindings)
}

if (declaration?.type !== 'VariableDeclaration') {
return []
}

Expand All @@ -66,16 +76,32 @@ const getMainExportFromESM = (node: Statement) => {
return type === 'VariableDeclarator' && id.type === 'Identifier' && id.name === 'handler'
})

if (handlerDeclaration?.init?.type !== 'CallExpression') {
return []
}
const exports = getExportsFromExpression(handlerDeclaration?.init)

return exports
}

const getExportsFromBindings = (
specifiers: (ExportSpecifier | ExportDefaultSpecifier | ExportNamespaceSpecifier)[],
getAllBindings: BindingMethod,
) => {
const specifier = specifiers.find(
({ type, exported }) =>
type === 'ExportSpecifier' &&
((exported.type === 'Identifier' && exported.name === 'handler') ||
(exported.type === 'StringLiteral' && exported.value === 'handler')),
) as ExportSpecifier

const exports = getExportsFromCallExpression(handlerDeclaration.init)
const binding = getAllBindings().get(specifier.local.name)
const exports = getExportsFromExpression(binding)

return exports
}

const getExportsFromCallExpression = (node: CallExpression) => {
const getExportsFromExpression = (node: Expression | undefined | null) => {
if (node?.type !== 'CallExpression') {
return []
}
const { arguments: args, callee } = node

if (callee.type !== 'Identifier') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { schedule } from '@netlify/functions'

const handler = schedule('@daily', async () => {
// function handler
})

export { handler }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { schedule } from '@netlify/functions'

const _handler = schedule('@daily', async () => {
// function handler
})
export { _handler as handler }
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { schedule } from '@netlify/functions'

const handler = async () => {
// function handler
}

const _handler = schedule('@daily', handler)
export { _handler as handler }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { schedule } from '@netlify/functions'

const SCHEDULE = '@daily'

export const handler = schedule(SCHEDULE, async () => {
// function handler
})
2 changes: 1 addition & 1 deletion tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2662,7 +2662,7 @@ testMany(
'Finds in-source config declarations using the `schedule` helper',
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
async (options, t) => {
const FUNCTIONS_COUNT = 7
const FUNCTIONS_COUNT = 11
const { files } = await zipFixture(t, join('in-source-config', 'functions'), {
opts: options,
length: FUNCTIONS_COUNT,
Expand Down

0 comments on commit 37f5c2f

Please sign in to comment.