Skip to content

Rule proposal: Effect.useSpan with an unused span parameter loses child-span nesting that Effect.withSpan provides #663

Description

@mattiamanzati

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions