Skip to content

Add no-async-from-sync rule#1

Merged
cm2435 merged 1 commit into
mainfrom
no-async-from-sync-rule
Apr 21, 2026
Merged

Add no-async-from-sync rule#1
cm2435 merged 1 commit into
mainfrom
no-async-from-sync-rule

Conversation

@cm2435

@cm2435 cm2435 commented Apr 21, 2026

Copy link
Copy Markdown
Owner

Summary

New rule no-async-from-sync that flags async-dispatch patterns inside sync functions — the shape LLMs love to produce when they've lost track of whether the surrounding function is async def.

Flagged patterns (inside a non-async def):

  • asyncio.get_event_loop() — deprecated, auto-creates a fresh loop
  • asyncio.new_event_loop()
  • asyncio.run(...) — reentrancy bomb if a loop is already running
  • anything.run_until_complete(...) — catches loop.run_until_complete, aliased event_loop.run_until_complete, etc.
  • asyncio.ensure_future(...)
  • asyncio.create_task(...)

Intentionally not flagged:

  • Module-level asyncio.run(main()) (the canonical if __name__ == "__main__": entrypoint)
  • Any of the above inside async def — caller has await, asyncio.get_running_loop() available
  • app.run(...), thread.run(), scheduler.create_task(...)run and create_task only match when the object is literally asyncio, since bare .run / .create_task are common on unrelated objects (Flask, threading, custom schedulers)

Implementation notes

  • Built on the same ancestor-walk pattern as no-nested-try and no-redundant-none-check: find nearest enclosing function_definition, check whether its first child is the async token
  • Unambiguous asyncio-only attribute names (get_event_loop, new_event_loop, run_until_complete, ensure_future) match on attribute alone — resilient to import asyncio as aio
  • Ambiguous names (run, create_task) require the object identifier to be asyncio
  • Legitimate sync/async bridges (nest_asyncio, Jupyter, Django ORM adapters) can suppress with # slopcop: ignore[no-async-from-sync] — called out in the help text

Test plan

  • cargo test --test no_async_from_sync — 15 new tests, all pass
  • cargo test — full suite, no regressions
  • cargo build — clean
  • cargo clippy — no new warnings from this change
  • Dogfood on a real codebase to gauge false-positive rate before cutting a release

🤖 Generated with Claude Code

…ions

Flags `asyncio.get_event_loop()`, `asyncio.run()`, `.run_until_complete()`,
`asyncio.ensure_future()`, and `asyncio.create_task()` when called inside
a non-async `def`. These patterns either deadlock against an already-running
loop, silently create a new loop whose tasks never run, or raise RuntimeError.

Module-level calls (the `if __name__ == "__main__": asyncio.run(main())`
entrypoint) and calls inside `async def` are intentionally not flagged.
`run` and `create_task` only match when the object is literally `asyncio`
to avoid false positives on `app.run`, `thread.run`, `scheduler.create_task`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cm2435
cm2435 merged commit bc10d1a into main Apr 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant