function get_hoisted_params(node, context) {
	const scope = context.state.scope;

	/** @type {Identifier[]} */
	const params = [];

	/**
	 * We only want to push if it's not already present to avoid name clashing
	 * @param {Identifier} id
	 */
	function push_unique(id) {
		if (!params.find((param) => param.name === id.name)) {
			params.push(id);
		}
	}

	for (const [reference] of scope.references) {
		let binding = scope.get(reference);

		if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
			if (binding.kind === 'store_sub') {
				// We need both the subscription for getting the value and the store for updating
				push_unique(b.id(binding.node.name));
				binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
			}

			let expression = context.state.transform[reference]?.read(b.id(binding.node.name));

			if (
				// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
				// TODO this code is bad, we need to kill it
				expression != null &&
				typeof expression !== 'function' &&
				expression.type === 'MemberExpression' &&
				expression.object.type === 'CallExpression' &&
				expression.object.callee.type === 'Identifier' &&
				expression.object.callee.name === '$.get' &&
				expression.object.arguments[0].type === 'Identifier'
			) {
				push_unique(b.id(expression.object.arguments[0].name));
			} else if (
				// If we are referencing a simple $$props value, then we need to reference the object property instead
				(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
				!is_prop_source(binding, context.state)
			) {
				push_unique(b.id('$$props'));
			} else if (
				// imports don't need to be hoisted
				binding.declaration_kind !== 'import'
			) {
				// create a copy to remove start/end tags which would mess up source maps
				push_unique(b.id(binding.node.name));
				// rest props are often accessed through the $$props object for optimization reasons,
				// but we can't know if the delegated event handler will use it, so we need to add both as params
				if (binding.kind === 'rest_prop' && context.state.analysis.runes) {
					push_unique(b.id('$$props'));
				}
			}
		}
	}

	if (dev) {
		// this is a little hacky, but necessary for ownership validation
		// to work inside hoisted event handlers

		/**
		 * @param {AssignmentExpression | UpdateExpression} node
		 * @param {{ next: () => void, stop: () => void }} context
		 */
		function visit(node, { next, stop }) {
			if (validate_mutation(node, /** @type {any} */ (context), node) !== node) {
				params.push(b.id('$$ownership_validator'));
				stop();
			} else {
				next();
			}
		}

		walk(/** @type {Node} */ (node), null, {
			AssignmentExpression: visit,
			UpdateExpression: visit
		});
	}

	return params;
}
@param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
@param {ComponentContext} context
@returns {Pattern[]}