Problem
Detects Effect.useSpan(name, evaluate) calls whose evaluate callback never touches the Span it receives — the callback declares no parameter, an underscore-prefixed parameter, or a parameter with zero references in the body. The only reason to reach for Effect.useSpan over Effect.withSpan is programmatic access to the Span handle (setting attributes, reading trace/span ids, passing it onward). When the handle is ignored, the author almost certainly wanted Effect.withSpan — and there is a real behavioral difference, not just style: per the v4 docs, the span created by useSpan "is not added to the current span stack, so no child spans will be created for it." Any spans created inside the callback silently fail to nest under it, fragmenting the trace tree. Effect.withSpan installs the span as the current parent, so children nest correctly — which is almost always what the user wants — and it additionally cleans ParentSpan out of the effect's requirements.
The bad code compiles cleanly and even traces "correctly" when viewed one span at a time; the damage only shows up as orphaned child spans in the trace viewer, so a static suggestion is the cheapest place to catch it.
Bad — compiles cleanly, the rule should flag this
// RULE: useSpanUnusedSpanToWithSpan
// BAD: the span handle from Effect.useSpan is never used, so useSpan buys
// nothing over withSpan — and it costs something: useSpan does NOT install
// the span on the current span stack, so any span created inside fetchUser
// will not nest under "fetch-user". The trace tree silently fragments.
import { Effect } from "effect"
declare const fetchUser: (id: string) => Effect.Effect<{ name: string }>
const getUser = (id: string) =>
Effect.useSpan("fetch-user", (_span) => fetchUser(id))
void getUser
Good
// RULE: useSpanUnusedSpanToWithSpan
// GOOD: Effect.withSpan wraps the effect in a child span that is installed
// as the current parent, so spans created inside fetchUser nest under
// "fetch-user" as expected — and ParentSpan is removed from R.
import { Effect } from "effect"
declare const fetchUser: (id: string) => Effect.Effect<{ name: string }>
const getUser = (id: string) =>
fetchUser(id).pipe(Effect.withSpan("fetch-user"))
void getUser
Proposed rule behavior
- Match a
CallExpression whose callee resolves (via the type checker) to Effect.useSpan, in both arities: useSpan(name, evaluate) and useSpan(name, options, evaluate).
- Inspect the
evaluate callback (arrow function or function expression written inline): flag when it declares no parameter, an underscore-prefixed parameter (_, _span), or a parameter whose symbol has zero resolved references in the callback body — symbol-reference counting only, no data-flow analysis.
- Do not flag when the callback is not an inline function literal (an identifier or other expression passed as
evaluate) — parameter usage can't be established syntactically.
- Do not flag when the parameter is referenced anywhere, including only being passed onward to another function (as in every real
useSpan call site in Effect-TS/effect: attributes, trace/span ids, or forwarding the span).
- Suggested fix: rewrite to
<body>.pipe(Effect.withSpan(name[, options])), carrying the options argument across when present.
- Severity: suggestion, and the message should state the behavioral difference (withSpan installs the span as the parent for child spans; useSpan does not) — deliberately keeping a span off the stack is theoretically valid, so this must not be an error.
Where this came up
No true-positive occurrences found in Effect-TS/effect@c3c7647 or anomalyco/opencode@550d1ff — proposed from the API sweep; both reference codebases are expert-written, so absence there is weak negative signal. Every Effect.useSpan call site inspected in the effect repo consumes its span parameter (e.g. packages/effect/src/unstable/sql/Statement.ts passes it into withConnectionSpan), confirming the rule stays silent on deliberate uses.
Mined from a per-export sweep of the Effect module (v4): for each exported function, asking what manual pattern it replaces and whether that pattern is statically detectable; grounded against Effect-TS/effect and anomalyco/opencode; deduplicated against implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
useSpanUnusedSpanToWithSpan
Problem
Detects
Effect.useSpan(name, evaluate)calls whoseevaluatecallback never touches theSpanit receives — the callback declares no parameter, an underscore-prefixed parameter, or a parameter with zero references in the body. The only reason to reach forEffect.useSpanoverEffect.withSpanis programmatic access to theSpanhandle (setting attributes, reading trace/span ids, passing it onward). When the handle is ignored, the author almost certainly wantedEffect.withSpan— and there is a real behavioral difference, not just style: per the v4 docs, the span created byuseSpan"is not added to the current span stack, so no child spans will be created for it." Any spans created inside the callback silently fail to nest under it, fragmenting the trace tree.Effect.withSpaninstalls the span as the current parent, so children nest correctly — which is almost always what the user wants — and it additionally cleansParentSpanout of the effect's requirements.The bad code compiles cleanly and even traces "correctly" when viewed one span at a time; the damage only shows up as orphaned child spans in the trace viewer, so a static suggestion is the cheapest place to catch it.
Bad — compiles cleanly, the rule should flag this
Good
Proposed rule behavior
CallExpressionwhose callee resolves (via the type checker) toEffect.useSpan, in both arities:useSpan(name, evaluate)anduseSpan(name, options, evaluate).evaluatecallback (arrow function or function expression written inline): flag when it declares no parameter, an underscore-prefixed parameter (_,_span), or a parameter whose symbol has zero resolved references in the callback body — symbol-reference counting only, no data-flow analysis.evaluate) — parameter usage can't be established syntactically.useSpancall site in Effect-TS/effect: attributes, trace/span ids, or forwarding the span).<body>.pipe(Effect.withSpan(name[, options])), carrying the options argument across when present.Where this came up
No true-positive occurrences found in Effect-TS/effect@c3c7647 or anomalyco/opencode@550d1ff — proposed from the API sweep; both reference codebases are expert-written, so absence there is weak negative signal. Every
Effect.useSpancall site inspected in the effect repo consumes its span parameter (e.g.packages/effect/src/unstable/sql/Statement.tspasses it intowithConnectionSpan), confirming the rule stays silent on deliberate uses.Mined from a per-export sweep of the Effect module (v4): for each exported function, asking what manual pattern it replaces and whether that pattern is statically detectable; grounded against Effect-TS/effect and anomalyco/opencode; deduplicated against implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
useSpanUnusedSpanToWithSpan