{"id":50974906,"url":"https://github.com/agusmdev/evloop-lint","last_synced_at":"2026-06-19T06:33:03.828Z","repository":{"id":363180267,"uuid":"1261568633","full_name":"agusmdev/evloop-lint","owner":"agusmdev","description":"Detect sync event-loop-blocking calls reachable from async code in FastAPI — including deeply nested, interprocedural cases ruff's flat ASYNC rules miss.","archived":false,"fork":false,"pushed_at":"2026-06-07T18:43:17.000Z","size":69,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-07T20:19:57.551Z","etag":null,"topics":["asyncio","event-loop","fastapi","linter","python","static-analysis"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/agusmdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-06T21:40:19.000Z","updated_at":"2026-06-07T18:43:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/agusmdev/evloop-lint","commit_stats":null,"previous_names":["agusmdev/evloop-lint"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/agusmdev/evloop-lint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fevloop-lint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fevloop-lint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fevloop-lint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fevloop-lint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agusmdev","download_url":"https://codeload.github.com/agusmdev/evloop-lint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fevloop-lint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34520431,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","event-loop","fastapi","linter","python","static-analysis"],"created_at":"2026-06-19T06:33:03.512Z","updated_at":"2026-06-19T06:33:03.822Z","avatar_url":"https://github.com/agusmdev.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# evloop-lint\n\n[![CI](https://github.com/agusmdev/evloop-lint/actions/workflows/ci.yml/badge.svg)](https://github.com/agusmdev/evloop-lint/actions/workflows/ci.yml)\n[![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n\nDetect synchronous, **event-loop-blocking** calls reachable from `async` code in\nFastAPI / async-Python projects — including the **deeply nested, interprocedural,\ncross-file** cases that ruff's flat `ASYNC` rules cannot follow.\n\n```\napp/main.py:4:5 EVL001 [definite] time.sleep blocks the event loop\n  main.deep_handler [app/main.py:3] (async entry)\n  -\u003e service.fetch [app/service.py:7] (calls)\n  -\u003e db.query [app/db.py:4] (calls)\n  -\u003e time.sleep [app/db.py:4] (blocks via)\n  fix: consider asyncio.sleep\n```\n\nruff catches `time.sleep` only when you write it *directly* inside an `async def`.\nevloop-lint follows the call across files — router → service → repository → driver\n— and reports the whole chain. It is pure-stdlib (no runtime dependencies) and\nfast (hundreds of files in well under a second).\n\n## Quick start\n\nThe fastest way, with [uv](https://docs.astral.sh/uv/) (no install, runs straight\nfrom this repo):\n\n```bash\nuvx --from git+https://github.com/agusmdev/evloop-lint evloop-lint path/to/your/app\n```\n\nOr clone and run:\n\n```bash\ngit clone https://github.com/agusmdev/evloop-lint\ncd evloop-lint\nuv run --with pytest pytest          # run the test suite (41 tests)\nuv run python -m evloop_lint.cli path/to/your/app\n```\n\nOr install into your environment:\n\n```bash\npip install git+https://github.com/agusmdev/evloop-lint\nevloop-lint path/to/your/app\n```\n\n### Try it on a sample in 10 seconds\n\n```bash\nmkdir -p demo/app\ncat \u003e demo/app/db.py      \u003c\u003c'EOF'\nimport time\ndef query():\n    time.sleep(1)          # the real blocker, hidden 3 hops deep\nEOF\ncat \u003e demo/app/service.py \u003c\u003c'EOF'\nfrom app.db import query\ndef fetch():\n    return query()\nEOF\ncat \u003e demo/app/main.py    \u003c\u003c'EOF'\nfrom app.service import fetch\nasync def handler():\n    fetch()                # ruff sees nothing here — evloop-lint follows the chain\nEOF\n\nuvx --from git+https://github.com/agusmdev/evloop-lint evloop-lint demo\n```\n\nYou should see a single `EVL001` finding with the full\n`handler -\u003e fetch -\u003e query -\u003e time.sleep` chain, and a non-zero exit code.\n\n## What it finds\n\n`evloop-lint` builds a project-wide call graph and propagates \"reaches a blocking\ncall\" taint backward from every `async def` entry point, carrying an on-loop /\noff-loop context so that **correctly-offloaded** work (`asyncio.to_thread`,\n`loop.run_in_executor`, `anyio.to_thread.run_sync`, `run_in_threadpool`, …) is\n*not* flagged. It understands schedulers (`call_soon`, `create_task`), re-entry\n(`anyio.from_thread.run`), `functools.partial`, constructor `__init__` bodies,\n`@property` getters, and FastAPI's threadpool semantics for plain `def` endpoints.\n\n## Rule codes\n\n| Code | Meaning | Tier(s) |\n|------|---------|---------|\n| `EVL001` | blocking I/O call on the loop | definite / probable |\n| `EVL002` | CPU-heavy call on the loop | definite / probable |\n| `EVL003` | unbounded loop, no yield point | possible |\n| `EVL004` | coroutine never awaited | definite |\n| `EVL005` | potential blocker past `--max-depth` | possible |\n| `EVL006` | ambiguous / dynamic dispatch | possible |\n| `EVL011` | blocking DB driver call | definite / probable |\n\n## Confidence tiers\n\nFindings are emitted at a tier matching *how* the chain was resolved:\n\n- **`definite`** — resolved through real definitions to a known blocker. Shown by\n  default; fails CI.\n- **`probable`** — confident heuristic method match (e.g. `self.repo.find()`).\n  Opt-in: `--confidence=probable`.\n- **`possible`** — structural / weak / partial resolution. Opt-in:\n  `--confidence=possible`.\n\nThe tool is **optimistic**: a call it cannot resolve is assumed safe, keeping the\nfalse-positive rate near zero so the default run stays trustworthy.\n\n## CLI flags\n\n```\n--max-depth N          max call hops to follow (default 4)\n--confidence TIER      minimum tier to report (definite|probable|possible)\n--format FMT           text | json | ndjson | sarif | github\n--select CODES         only these rule codes (comma-separated)\n--ignore CODES         exclude these rule codes\n--exclude GLOBS        path globs to skip\n--statistics           coverage + depth-truncation stats\n--no-framework-detect  treat every async def as on-loop (max recall)\n--strict               parse errors cause a non-zero exit\n--exit-zero            always exit 0 (report only)\n```\n\nExit codes: `0` no findings at/above the floor · `1` findings found · `2` usage error.\n\nSuppress a line with `# noqa` or `# noqa: EVL001`.\n\n## Configuration\n\nVia `pyproject.toml`:\n\n```toml\n[tool.evloop-lint]\nmax-depth = 4\nconfidence = \"definite\"\nignore = [\"EVL003\"]\nexclude = [\"tests/*\", \"migrations/*\"]\n```\n\n## CI integration\n\n```yaml\n- name: Check for event-loop blockers\n  run: uvx --from git+https://github.com/agusmdev/evloop-lint evloop-lint app/\n```\n\nSARIF output (`--format sarif`) uploads to GitHub code scanning; `--format github`\nemits inline PR annotations.\n\n## How it works / design\n\nThe detector is deliberately **generic**: every specific identifier (blocking\nprimitives, offload primitives, framework registration shapes, wrappers) lives in\na data registry (`src/evloop_lint/registry.py`), never in traversal logic. New\nlibraries are data rows, not code changes.\n\nIt was developed through an **adversarial loop**: breaker agents generate realistic\nFastAPI code that tries to evade detection, a judge labels true escapes, and each\nescape is fixed *generically* and added as a permanent regression test\n(`tests/test_adversarial.py`). See [`docs/DESIGN.md`](docs/DESIGN.md) for the full\nalgorithm (D1–D10) and [`docs/adr/`](docs/adr/) for the key decisions.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagusmdev%2Fevloop-lint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagusmdev%2Fevloop-lint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagusmdev%2Fevloop-lint/lists"}