pierrejeambrun commented on code in PR #73437:
URL: https://github.com/apache/airflow/pull/73437#discussion_r4093594938
##########
ts-sdk/src/sdk/dag.ts:
##########
@@ -560,6 +632,54 @@ function spreadArgs(fn: TaskFunction): TaskFunction {
handler(...Object.values(args ?? {}))) as unknown as TaskFunction;
}
+// What Airflow accepts as a task id, and so what a name taken from a handler
+// has to look like.
+const TASK_ID_CHARACTERS = /^[\p{L}\p{N}_.-]+$/u;
+
+// A parameter list of plain identifiers, which is the one shape whose names
can
+// be read off a function without parsing it.
+const PLAIN_PARAMETERS = /^[A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*$/;
+
+/**
+ * A handler's own function name, or undefined when it has none.
+ *
+ * Where a defaulted task id comes from. `airflow-ts-pack` bundles with
esbuild's
+ * `keepNames`, so the name survives minification; a bundler that drops names
+ * leaves the empty string, which is why the empty string is not an id.
+ */
+function readFunctionName(handler: unknown): string | undefined {
+ if (typeof handler !== "function") return undefined;
+ const { name } = handler as { name?: unknown };
+ return typeof name === "string" && name.length > 0 ? name : undefined;
+}
+
+/**
+ * The handler's parameter names, when it declares plain ones.
+ *
+ * All-or-nothing: a default, a destructuring pattern, a rest element or a
+ * comment leaves the labels `arg0`, `arg1`, which bind exactly the same way.
+ * `airflow-ts-pack` leaves identifiers alone, so these are the names the
author
+ * wrote even in a packed bundle.
+ */
+function inferArgBindings(handler: unknown): readonly string[] | undefined {
+ if (typeof handler !== "function") return undefined;
+ const source = Function.prototype.toString.call(handler);
+ // `async rows => ...`: one parameter, and no parentheses to slice.
+ const bare = /^(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/.exec(source);
+ if (bare) return Object.freeze([bare[1]!]);
+ const open = source.indexOf("(");
+ const close = source.indexOf(")");
+ const arrow = source.indexOf("=>");
+ // A "(" past the arrow belongs to the body, not to a parameter list.
+ if (open === -1 || close < open || (arrow !== -1 && open > arrow)) return
undefined;
+ const parameters = source.slice(open + 1, close).trim();
+ if (parameters === "" || !PLAIN_PARAMETERS.test(parameters)) return
undefined;
+ const names = parameters.split(",").map((name) => name.trim());
+ // `length` stops at the first default or rest element, so it matches only a
+ // plain list — and not a slice that a ")" inside a default value cut short.
+ return names.length === handler.length ? Object.freeze(names) : undefined;
+}
Review Comment:
Yes this isn't great. Trying to consider only allowing object destructuring
arguments and not allow positional ones.
That's very common in js/ts and would remove a lot of complexity. This
wasn't kept in original ADR because it was assumed that positional is more
natural for typescript author but I'm pushing back on this:
```
- useQuery({ queryKey, queryFn, staleTime, enabled }) — TanStack Query.
- prisma.user.create({ data, include, select }) — Prisma.
- page.click(selector, { button, force, delay }) — Playwright (one
positional + destructured options).
- fetch(url, { method, headers, body }) — WHATWG.
- axios({ url, method, data, headers }) — Axios (either form; the options
form is what's documented).
- Router.push({ pathname, query }) — Next.js.
- z.object({ … }) — Zod.
- sql.insert(table).values({ … }) — Drizzle/TypeORM builders.
The pattern is consistent: one required positional + destructured
options-object for anything configurable, and fully
destructured for anything that reads more like a form (React props, most
builder inputs).
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]