function _extract_paths(paths, inserts, param, expression, update_expression, has_default_value) {
	switch (param.type) {
		case 'Identifier':
		case 'MemberExpression':
			paths.push({
				node: param,
				is_rest: false,
				has_default_value,
				expression,
				update_expression
			});
			break;

		case 'ObjectPattern':
			for (const prop of param.properties) {
				if (prop.type === 'RestElement') {
					/** @type {ESTree.Expression[]} */
					const props = [];

					for (const p of param.properties) {
						if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
							if (p.key.type === 'Identifier' && !p.computed) {
								props.push(b.literal(p.key.name));
							} else if (p.key.type === 'Literal') {
								props.push(b.literal(String(p.key.value)));
							} else {
								props.push(b.call('String', p.key));
							}
						}
					}

					const rest_expression = b.call('$.exclude_from_object', expression, b.array(props));

					if (prop.argument.type === 'Identifier') {
						paths.push({
							node: prop.argument,
							is_rest: true,
							has_default_value,
							expression: rest_expression,
							update_expression: rest_expression
						});
					} else {
						_extract_paths(
							paths,
							inserts,
							prop.argument,
							rest_expression,
							rest_expression,
							has_default_value
						);
					}
				} else {
					const object_expression = b.member(
						expression,
						prop.key,
						prop.computed || prop.key.type !== 'Identifier'
					);

					_extract_paths(
						paths,
						inserts,
						prop.value,
						object_expression,
						object_expression,
						has_default_value
					);
				}
			}

			break;

		case 'ArrayPattern': {
			// we create an intermediate declaration to convert iterables to arrays if necessary.
			// the consumer is responsible for setting the name of the identifier
			const id = b.id('#');

			const value = b.call(
				'$.to_array',
				expression,
				param.elements.at(-1)?.type === 'RestElement' ? undefined : b.literal(param.elements.length)
			);

			inserts.push({ id, value });

			for (let i = 0; i < param.elements.length; i += 1) {
				const element = param.elements[i];
				if (element) {
					if (element.type === 'RestElement') {
						const rest_expression = b.call(b.member(id, 'slice'), b.literal(i));

						if (element.argument.type === 'Identifier') {
							paths.push({
								node: element.argument,
								is_rest: true,
								has_default_value,
								expression: rest_expression,
								update_expression: rest_expression
							});
						} else {
							_extract_paths(
								paths,
								inserts,
								element.argument,
								rest_expression,
								rest_expression,
								has_default_value
							);
						}
					} else {
						const array_expression = b.member(id, b.literal(i), true);

						_extract_paths(
							paths,
							inserts,
							element,
							array_expression,
							array_expression,
							has_default_value
						);
					}
				}
			}

			break;
		}

		case 'AssignmentPattern': {
			const fallback_expression = build_fallback(expression, param.right);

			if (param.left.type === 'Identifier') {
				paths.push({
					node: param.left,
					is_rest: false,
					has_default_value: true,
					expression: fallback_expression,
					update_expression
				});
			} else {
				_extract_paths(paths, inserts, param.left, fallback_expression, update_expression, true);
			}

			break;
		}
	}

	return paths;
}
@param {DestructuredAssignment[]} paths
@param {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>} inserts
@param {ESTree.Node} param
@param {ESTree.Expression} expression
@param {ESTree.Expression} update_expression
@param {boolean} has_default_value
@returns {DestructuredAssignment[]}