Skip to content

Source Linter

The source linter runs deterministic AST-based checks against your Effect code and emits findings with rule codes, severities, source spans, docs URLs, and copy-pasteable Bad → Good examples. It is independent of IR analysis — it operates on the TypeScript AST directly so it is fast enough to run on every commit.

Terminal window
effect-analyze ./src --lint-source

Fifteen rules covering the most common Effect anti-patterns surfaced in EffectPatterns, effect-ts/examples, and real-world reviews:

Code Severity Catches
detached-fiber-in-test warning Effect.runFork(Effect.never) in tests — fiber outlives test scope
empty-effect-all info Effect.all([]) — dead or placeholder code
forEach-without-concurrency info Effect.forEach(items, f) without an explicit concurrency option
identity-catch warning Effect.catch(e, (e) => Effect.fail(e)) — no-op handler
live-layer-in-test warning Live layer references in test files where test layers are expected
mutable-in-concurrent warning let/var mutation inside parallel / fork / race contexts
nondeterministic-test-api warning Date.now() / new Date() / Math.random() in test code
promise-api-in-gen warning Promise.all / Promise.race inside Effect.gen
runPromise-then-chain info .then / .catch after Effect.runPromise
runSync-on-async error Effect.runSync on an async-tainted effect — throws at runtime
runSyncExit-on-async error Effect.runSyncExit on an async-tainted effect
schedule-unbounded warning Schedule.forever and friends without visible bounds
sleep-without-testclock info Real-time Effect.sleep in tests without TestClock
unsafe-api-usage warning Effect.unsafe* APIs bypassing runtime safety
untagged-throw warning throw new Error(...) inside an Effect context

Every finding carries:

  • rule code + severity
  • source span (file, line, column, end line, end column)
  • docs.effect.website link for the rule
  • Bad → Good example snippet pulled from the rule registry

These rules are AST-based and deliberately narrow. For the ~95 type-aware Effect rules — floating effects, missing context/error channels, Effect-native replacements for fetch / console / Date / process.env, Schema checks — use the official @effect/tsgo language service. effect-analyzer does not reimplement them; it merges them in:

Terminal window
effect-analyze ./src --lint-source --tsgo=./tsconfig.json

Bare --tsgo uses tsconfig.json. When options come before the source path, use --tsgo=path/to/tsconfig.json to provide an explicit project without ambiguity.

@effect/tsgo is a required effect-analyzer dependency, so the bridge and its platform binary are installed automatically. It resolves the target project’s typescript package and selects the native artifact matching that exact version (which is why the project needs TypeScript 7+). The CLI therefore uses the same compiler generation and Effect language-service rules as the editor.

Terminal window
effect-analyze --explain-rule runSync-on-async
Terminal window
effect-analyze --list-rules # full table
effect-analyze --search-rules concurrency # text search across codes/titles/descriptions
effect-analyze --index-rules # searchable index entries (one per line)

Emit SARIF 2.1.0 — the standard format consumed by GitHub Code Scanning, Azure DevOps, SonarQube, and most static analysis tools.

Terminal window
effect-analyze ./src --lint-source --sarif -o findings.sarif

Upload to GitHub Code Scanning in a workflow:

- name: Lint Effect source
run: pnpm exec effect-analyze ./src --lint-source --sarif -o findings.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: findings.sarif

To adopt the linter on a non-greenfield codebase, snapshot current findings as a baseline and fail CI only on new findings.

Terminal window
# 1. Snapshot once
effect-analyze ./src --lint-source -o .cache/effect-lint-baseline.json
# 2. In CI, fail only on findings not in the baseline
effect-analyze ./src --lint-source \
--baseline .cache/effect-lint-baseline.json \
--fail-on-new

This is the recommended adoption path: get the workflow into CI today, then drive the baseline to zero over time.

A compact {file → counts by severity} report for tracking lint debt by area:

Terminal window
effect-analyze ./src --lint-source --scorecard

Suppress a single line with an effect-analyzer-disable-next-line comment:

// effect-analyzer-disable-next-line runSync-on-async — initial bootstrap only
Effect.runSync(initEffect)

Tighten the policy in CI:

Flag Effect
--require-suppression-reason Suppressions must include a — reason after the rule code
--fail-on-stale-suppressions Suppressions that no longer suppress anything fail the build

Produce a deterministic artifact directory that contains diagnostics, SARIF, summary, the rule registry snapshot, and the session envelope:

Terminal window
effect-analyze ./src --lint-source --bundle-output ./.artifacts/effect-lint

Useful for build attestation, debugging CI regressions, or feeding the bundle into a downstream agent.

Switch the active rule set with --profile:

Profile Use
strict All rules, including info-level
ci Errors + warnings only
migration Rules tuned for codebases being migrated to Effect
docs Rules surfaced in published documentation
Terminal window
effect-analyze ./src --lint-source --profile ci --fail-on-new --baseline .cache/baseline.json