Repository: python-trio/flake8-async (language: python)

Issue:
Line endings
in https://github.com/Zac-HD/flake8-trio/commit/490a77585f40c19b0fb06d494d4317a6e505f444 you converted all files to CRLF line endings. Was this intentional?

It's not a huge problem for me, but it makes diffs on my local machine have ugly `^M`'s and apparently the remotes are stored with LF endings so whenever I try to diff or rebase against a remote branch it's a mess. So unless you don't mind I'd like to convert all files back to LF and add a `.gitattributes` file to hopefully stop it from happening in the future. But I'll wait with it until there's no open PR's to avoid merging nightmares.
If it was intentional I'll add a .gitattributes file indicating CRLF is preferred, and see if I can get git to play nicely with that on my local machine.


Discussion / hints:
Definitely an accident 😅, fix would be appreciated!

Fix patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fff342e
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Set the default behaviour in case people don't have core.autocrlf set
+# Lets the git installation on the users machine keep files with whatever line endings
+# they want, but normalizes them to LF upon being committed.
+* text=auto
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6eb5cb4..094281f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,60 +1,60 @@
-name: flake8-trio CI
-
-on:
-  push:
-    branches: [ main ]
-  pull_request:
-    branches: [ main ]
-
-jobs:
-  check:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python 3.10
-      uses: actions/setup-python@v3
-      with:
-        python-version: "3.10"
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip setuptools tox
-        python -m tox --notest --recreate -e check
-    - name: Run checks
-      run: |
-        python -m tox -e check
-        git diff --exit-code
-
-  test:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: ["3.8", "3.9", "3.10"]
-      fail-fast: false
-    steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v3
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip setuptools tox
-        python -m tox --notest --recreate -e test
-    - name: Run tests
-      run: python -m tox -e test
-
-  release:
-    runs-on: ubuntu-latest
-    needs: [check, test]
-    if: github.repository == 'Zac-HD/flake8-trio' &&  github.ref == 'refs/heads/main'
-    steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python 3
-      uses: actions/setup-python@v3
-    - name: Install tools
-      run: python -m pip install --upgrade build pip setuptools wheel twine
-    - name: Upload new release
-      env:
-        TWINE_USERNAME: '__token__'
-        TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
-      run: python -m build && twine upload --skip-existing dist/*
+name: flake8-trio CI
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python 3.10
+      uses: actions/setup-python@v3
+      with:
+        python-version: "3.10"
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip setuptools tox
+        python -m tox --notest --recreate -e check
+    - name: Run checks
+      run: |
+        python -m tox -e check
+        git diff --exit-code
+
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.8", "3.9", "3.10"]
+      fail-fast: false
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v3
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip setuptools tox
+        python -m tox --notest --recreate -e test
+    - name: Run tests
+      run: python -m tox -e test
+
+  release:
+    runs-on: ubuntu-latest
+    needs: [check, test]
+    if: github.repository == 'Zac-HD/flake8-trio' &&  github.ref == 'refs/heads/main'
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python 3
+      uses: actions/setup-python@v3
+    - name: Install tools
+      run: python -m pip install --upgrade build pip setuptools wheel twine
+    - name: Upload new release
+      env:
+        TWINE_USERNAME: '__token__'
+        TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
+      run: python -m build && twine upload --skip-existing dist/*
diff --git a/.gitignore b/.gitignore
index 25ff4b0..b6e4761 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,129 +1,129 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# pipenv
-#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-#   However, in case of collaboration, if having platform-specific dependencies or dependencies
-#   having no cross-platform support, pipenv may install dependencies that don't work, or not
-#   install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1bc581..e354ba4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,72 +1,72 @@
-# Changelog
-*[CalVer, YY.month.patch](https://calver.org/)*
-
-## 22.11.3
-- Add TRIO113, prefer `await nursery.start(...)` to `nursery.start_soon()` for compatible functions when opening a context manager
-
-## 22.11.2
-- TRIO105 now also checks that you `await`ed `nursery.start()`.
-
-## 22.11.1
-- TRIO102 is no longer skipped in (async) context managers, since it's not a missing-checkpoint warning.
-
-## 22.9.2
-- Fix a crash on nontrivial decorator expressions (calls, PEP-614) and document behavior.
-
-## 22.9.1
-- Add `--no-checkpoint-warning-decorators` option, to disable missing-checkpoint warnings for certain decorated functions.
-
-## 22.8.8
-- Fix false alarm on TRIO107 with checkpointing `try` and empty `finally`
-- Fix false alarm on TRIO107&108 with infinite loops
-
-## 22.8.7
-- TRIO107+108 now ignores `asynccontextmanager`s, since both `__aenter__` and `__aexit__` should checkpoint. `async with` is also treated as checkpointing on both enter and exit.
-- TRIO107 now completely ignores any function whose body consists solely of ellipsis, pass, or string constants.
-- TRIO103, 107 and 108 now inspects `while` conditions and `for` iterables to avoid false alarms on a couple cases where the loop body is guaranteed to run at least once.
-
-## 22.8.6
-- TRIO103 now correctly handles raises in loops, i.e. `raise` in else is guaranteed to run unless there's a `break` in the body.
-
-## 22.8.5
-- Add TRIO111: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accesed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager.
-- Add TRIO112: this single-task nursery could be replaced by awaiting the function call directly.
-
-## 22.8.4
-- Fix TRIO108 raising errors on yields in some sync code.
-- TRIO109 now skips all decorated functions to avoid false alarms
-
-## 22.8.3
-- TRIO108 now gives multiple error messages; one for each path lacking a guaranteed checkpoint
-
-## 22.8.2
-- Merged TRIO108 into TRIO107
-- TRIO108 now handles checkpointing in async iterators
-
-## 22.8.1
-- Added TRIO109: Async definitions should not have a `timeout` parameter. Use `trio.[fail/move_on]_[at/after]`
-- Added TRIO110: `while <condition>: await trio.sleep()` should be replaced by a `trio.Event`.
-
-## 22.7.6
-- Extend TRIO102 to also check inside `except BaseException` and `except trio.Cancelled`
-- Extend TRIO104 to also check for `yield`
-- Update error messages on TRIO102 and TRIO103
-
-## 22.7.5
-- Add TRIO103: `except BaseException` or `except trio.Cancelled` with a code path that doesn't re-raise
-- Add TRIO104: "Cancelled and BaseException must be re-raised" if user tries to return or raise a different exception.
-- Added TRIO107: Async functions must have at least one checkpoint on every code path, unless an exception is raised
-- Added TRIO108: Early return from async function must have at least one checkpoint on every code path before it.
-
-## 22.7.4
-- Added TRIO105 check for not immediately `await`ing async trio functions.
-- Added TRIO106 check that trio is imported in a form that the plugin can easily parse.
-
-## 22.7.3
-- Added TRIO102 check for unsafe checkpoints inside `finally:` blocks
-
-## 22.7.2
-- Avoid `TRIO100` false-alarms on cancel scopes containing `async for` or `async with`.
-
-## 22.7.1
-- Initial release with TRIO100 and TRIO101
+# Changelog
+*[CalVer, YY.month.patch](https://calver.org/)*
+
+## 22.11.3
+- Add TRIO113, prefer `await nursery.start(...)` to `nursery.start_soon()` for compatible functions when opening a context manager
+
+## 22.11.2
+- TRIO105 now also checks that you `await`ed `nursery.start()`.
+
+## 22.11.1
+- TRIO102 is no longer skipped in (async) context managers, since it's not a missing-checkpoint warning.
+
+## 22.9.2
+- Fix a crash on nontrivial decorator expressions (calls, PEP-614) and document behavior.
+
+## 22.9.1
+- Add `--no-checkpoint-warning-decorators` option, to disable missing-checkpoint warnings for certain decorated functions.
+
+## 22.8.8
+- Fix false alarm on TRIO107 with checkpointing `try` and empty `finally`
+- Fix false alarm on TRIO107&108 with infinite loops
+
+## 22.8.7
+- TRIO107+108 now ignores `asynccontextmanager`s, since both `__aenter__` and `__aexit__` should checkpoint. `async with` is also treated as checkpointing on both enter and exit.
+- TRIO107 now completely ignores any function whose body consists solely of ellipsis, pass, or string constants.
+- TRIO103, 107 and 108 now inspects `while` conditions and `for` iterables to avoid false alarms on a couple cases where the loop body is guaranteed to run at least once.
+
+## 22.8.6
+- TRIO103 now correctly handles raises in loops, i.e. `raise` in else is guaranteed to run unless there's a `break` in the body.
+
+## 22.8.5
+- Add TRIO111: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accesed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager.
+- Add TRIO112: this single-task nursery could be replaced by awaiting the function call directly.
+
+## 22.8.4
+- Fix TRIO108 raising errors on yields in some sync code.
+- TRIO109 now skips all decorated functions to avoid false alarms
+
+## 22.8.3
+- TRIO108 now gives multiple error messages; one for each path lacking a guaranteed checkpoint
+
+## 22.8.2
+- Merged TRIO108 into TRIO107
+- TRIO108 now handles checkpointing in async iterators
+
+## 22.8.1
+- Added TRIO109: Async definitions should not have a `timeout` parameter. Use `trio.[fail/move_on]_[at/after]`
+- Added TRIO110: `while <condition>: await trio.sleep()` should be replaced by a `trio.Event`.
+
+## 22.7.6
+- Extend TRIO102 to also check inside `except BaseException` and `except trio.Cancelled`
+- Extend TRIO104 to also check for `yield`
+- Update error messages on TRIO102 and TRIO103
+
+## 22.7.5
+- Add TRIO103: `except BaseException` or `except trio.Cancelled` with a code path that doesn't re-raise
+- Add TRIO104: "Cancelled and BaseException must be re-raised" if user tries to return or raise a different exception.
+- Added TRIO107: Async functions must have at least one checkpoint on every code path, unless an exception is raised
+- Added TRIO108: Early return from async function must have at least one checkpoint on every code path before it.
+
+## 22.7.4
+- Added TRIO105 check for not immediately `await`ing async trio functions.
+- Added TRIO106 check that trio is imported in a form that the plugin can easily parse.
+
+## 22.7.3
+- Added TRIO102 check for unsafe checkpoints inside `finally:` blocks
+
+## 22.7.2
+- Avoid `TRIO100` false-alarms on cancel scopes containing `async for` or `async with`.
+
+## 22.7.1
+- Initial release with TRIO100 and TRIO101
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 64fb8a0..e8eeeaa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,57 +1,57 @@
-# Contributor Guide
-
-Contributions welcome!  We'll expand this guide as we go.
-
-
-## Development
-
-When you wish to add a check to `flake8-trio` please ensure the following:
-
-- `README.md` gets a one line about your new warning
-- Add a CHANGELOG entry (see 'Releasing a new version' below)
-- Unittests are added showing the check highlight where it should and shouldn't
-  (see flake8-bugbear for examples of good linter tests)
-
-To run our test suite please use tox.
-
-```console
-# Formatting and Linting
-tox -e check
-# Test Running
-tox -e test
-# Run checks and tests against Python 3.8, 3.9 and 3.10
-tox -e ALL
-# Tip: Use -p (run tests in parallel) and --develop (install package with -e) to save time when modifying and rerunning
-tox -e ALL -p --develop
-```
-
-## Meta-tests
-To check that all codes are tested and documented there's a test that error codes mentioned in `README.md`, `CHANGELOG.md` (matching `TRIO\d\d\d`), the keys in `flake8_trio.Error_codes` and codes parsed from filenames matching `tests/trio*.py`, are all equal.
-
-## Test generator
-Tests are automatically generated for files named `trio*.py` in the `tests/` directory, with the code that it's testing interpreted from the file name. The file extension is split off, if there's a match for for `_py\d*` it strips that off and uses it to determine if there's a minimum python version for which the test should only run.
-
-Lines containing `error:` are parsed as expecting an error of the code matching the file name, with everything on the line after the colon `eval`'d and passed as arguments to `flake8_trio.Error_codes[<error_code>].str_format`. The `globals` argument to `eval` contains a `lineno` variable assigned the current line number, and the `flake8_trio.Statement` namedtuple. The first element after `error:` *must* be an integer containing the column where the error on that line originates.
-
-Test files by default filter out all errors not matching the file name, but if there's a line `#INCLUDE TRIO\d\d\d TRIO\d\d\d` those additional error codes are not filtered out and will be an error if encountered.
-
-
-## Style Guide
-
-**Code style:** code review should focus on correctness, performance, and readability.
-Low-level nitpicks are handled *exclusively* by our formatters and linters, so if
-`tox` passes there's nothing else to say.
-
-**Terminology:** use "false/missed alarm" rather than "true/false positive", or the
-even worse "type I/II error".  "False alarm" or "missed alarm" have obvious meanings
-which do not rely on confusing conventions (is noticing an error positive or negative?)
-or rote memorization of an arbitrary convention.
-
-
-## Releasing a new version
-We want to ship bigfixes or new features as soon as they're ready,
-so our release process is automated:
-
-1. Increment `__version__` in `src/flake8_trio.py`
-2. Ensure there's a corresponding entry in `CHANGELOG.md` with same version
-3. Merge to master, and CI will do the rest!
+# Contributor Guide
+
+Contributions welcome!  We'll expand this guide as we go.
+
+
+## Development
+
+When you wish to add a check to `flake8-trio` please ensure the following:
+
+- `README.md` gets a one line about your new warning
+- Add a CHANGELOG entry (see 'Releasing a new version' below)
+- Unittests are added showing the check highlight where it should and shouldn't
+  (see flake8-bugbear for examples of good linter tests)
+
+To run our test suite please use tox.
+
+```console
+# Formatting and Linting
+tox -e check
+# Test Running
+tox -e test
+# Run checks and tests against Python 3.8, 3.9 and 3.10
+tox -e ALL
+# Tip: Use -p (run tests in parallel) and --develop (install package with -e) to save time when modifying and rerunning
+tox -e ALL -p --develop
+```
+
+## Meta-tests
+To check that all codes are tested and documented there's a test that error codes mentioned in `README.md`, `CHANGELOG.md` (matching `TRIO\d\d\d`), the keys in `flake8_trio.Error_codes` and codes parsed from filenames matching `tests/trio*.py`, are all equal.
+
+## Test generator
+Tests are automatically generated for files named `trio*.py` in the `tests/` directory, with the code that it's testing interpreted from the file name. The file extension is split off, if there's a match for for `_py\d*` it strips that off and uses it to determine if there's a minimum python version for which the test should only run.
+
+Lines containing `error:` are parsed as expecting an error of the code matching the file name, with everything on the line after the colon `eval`'d and passed as arguments to `flake8_trio.Error_codes[<error_code>].str_format`. The `globals` argument to `eval` contains a `lineno` variable assigned the current line number, and the `flake8_trio.Statement` namedtuple. The first element after `error:` *must* be an integer containing the column where the error on that line originates.
+
+Test files by default filter out all errors not matching the file name, but if there's a line `#INCLUDE TRIO\d\d\d TRIO\d\d\d` those additional error codes are not filtered out and will be an error if encountered.
+
+
+## Style Guide
+
+**Code style:** code review should focus on correctness, performance, and readability.
+Low-level nitpicks are handled *exclusively* by our formatters and linters, so if
+`tox` passes there's nothing else to say.
+
+**Terminology:** use "false/missed alarm" rather than "true/false positive", or the
+even worse "type I/II error".  "False alarm" or "missed alarm" have obvious meanings
+which do not rely on confusing conventions (is noticing an error positive or negative?)
+or rote memorization of an arbitrary convention.
+
+
+## Releasing a new version
+We want to ship bigfixes or new features as soon as they're ready,
+so our release process is automated:
+
+1. Increment `__version__` in `src/flake8_trio.py`
+2. Ensure there's a corresponding entry in `CHANGELOG.md` with same version
+3. Merge to master, and CI will do the rest!
diff --git a/LICENSE b/LICENSE
index 2d87db1..9e9738f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,21 @@
-MIT License
-
-Copyright (c) 2022 Zac Hatfield-Dodds
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+MIT License
+
+Copyright (c) 2022 Zac Hatfield-Dodds
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
index 828a06b..13bfea6 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,4 @@
-include *.md
-include LICENSE
-include tox.ini
-recursive-include tests *.py
+include *.md
+include LICENSE
+include tox.ini
+recursive-include tests *.py
diff --git a/README.md b/README.md
index e42f25f..051d7c4 100644
--- a/README.md
+++ b/README.md
@@ -1,68 +1,68 @@
-# flake8-trio
-
-A highly opinionated flake8 plugin for Trio-related problems.
-
-This can include anything from outright bugs, to pointless/dead code,
-to likely performance issues, to minor points of idiom that might signal
-a misunderstanding.
-
-It may well be too noisy for anyone with different opinions, that's OK.
-
-Pairs well with flake8-async and flake8-bugbear.
-
-## Installation
-
-```console
-pip install flake8-trio
-```
-
-## List of warnings
-
-- **TRIO100**: a `with trio.fail_after(...):` or `with trio.move_on_after(...):`
-  context does not contain any `await` statements.  This makes it pointless, as
-  the timeout can only be triggered by a checkpoint.
-- **TRIO101**: `yield` inside a nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling.
-- **TRIO102**: it's unsafe to await inside `finally:` or `except BaseException/trio.Cancelled` unless you use a shielded
-  cancel scope with a timeout.
-- **TRIO103**: `except BaseException` and `except trio.Cancelled` with a code path that doesn't re-raise.
-- **TRIO104**: `Cancelled` and `BaseException` must be re-raised - when a user tries to `return` or `raise` a different exception.
-- **TRIO105**: Calling a trio async function without immediately `await`ing it.
-- **TRIO106**: trio must be imported with `import trio` for the linter to work.
-- **TRIO107**: exit or `return` from async function with no guaranteed checkpoint or exception since function definition.
-- **TRIO108**: exit, yield or return from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition)
-  Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit).
-- **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead
-- **TRIO110**: `while <condition>: await trio.sleep()` should be replaced by a `trio.Event`.
-- **TRIO111**: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accesed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager.
-- **TRIO112**: nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call.
-- **TRIO113**: using `nursery.start_soon` in `__aenter__` doesn't wait for the task to begin. Consider replacing with `nursery.start`.
-
-
-## Configuration
-### `no-checkpoint-warning-decorators`
-Specify a list of decorators to disable checkpointing checks for, turning off TRIO107 and TRIO108 warnings for functions decorated with any decorator matching any in the list. Matching is done with [fnmatch](https://docs.python.org/3/library/fnmatch.html). Defaults to disabling for `asynccontextmanager`.
-
-Decorators-to-match must be identifiers or dotted names only (not PEP-614 expressions), and will match against the name only - e.g. `foo.bar` matches `foo.bar`, `foo.bar()`, and `foo.bar(args, here)`, etc.
-
-[You can configure `flake8` with command-line options](https://flake8.pycqa.org/en/latest/user/configuration.html),
-but we prefer using a config file. For example:
-```
-[flake8]
-no-checkpoint-warning-decorators =
-  mydecorator,
-  mydecoratorpackage.checkpointing_decorators.*,
-  ign*,
-  *.ignore,
-```
-
-
-### `startable-in-context-manager`
-Comma-separated list of methods which should be used with `.start()` when opening a context manager,
-in addition to the default `trio.run_process`, `trio.serve_tcp`, `trio.serve_ssl_over_tcp`, and
-`trio.serve_listeners`.  Uses fnmatch, like `no-checkpoint-warning-decorators`. For example:
-```
-[flake8]
-startable-methods-in-context-manager =
-  mylib.myfun,
-  myfun2,mypackage.myfunlib.*,
-```
+# flake8-trio
+
+A highly opinionated flake8 plugin for Trio-related problems.
+
+This can include anything from outright bugs, to pointless/dead code,
+to likely performance issues, to minor points of idiom that might signal
+a misunderstanding.
+
+It may well be too noisy for anyone with different opinions, that's OK.
+
+Pairs well with flake8-async and flake8-bugbear.
+
+## Installation
+
+```console
+pip install flake8-trio
+```
+
+## List of warnings
+
+- **TRIO100**: a `with trio.fail_after(...):` or `with trio.move_on_after(...):`
+  context does not contain any `await` statements.  This makes it pointless, as
+  the timeout can only be triggered by a checkpoint.
+- **TRIO101**: `yield` inside a nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling.
+- **TRIO102**: it's unsafe to await inside `finally:` or `except BaseException/trio.Cancelled` unless you use a shielded
+  cancel scope with a timeout.
+- **TRIO103**: `except BaseException` and `except trio.Cancelled` with a code path that doesn't re-raise.
+- **TRIO104**: `Cancelled` and `BaseException` must be re-raised - when a user tries to `return` or `raise` a different exception.
+- **TRIO105**: Calling a trio async function without immediately `await`ing it.
+- **TRIO106**: trio must be imported with `import trio` for the linter to work.
+- **TRIO107**: exit or `return` from async function with no guaranteed checkpoint or exception since function definition.
+- **TRIO108**: exit, yield or return from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition)
+  Checkpoints are `await`, `async for`, and `async with` (on one of enter/exit).
+- **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead
+- **TRIO110**: `while <condition>: await trio.sleep()` should be replaced by a `trio.Event`.
+- **TRIO111**: Variable, from context manager opened inside nursery, passed to `start[_soon]` might be invalidly accesed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager.
+- **TRIO112**: nursery body with only a call to `nursery.start[_soon]` and not passing itself as a parameter can be replaced with a regular function call.
+- **TRIO113**: using `nursery.start_soon` in `__aenter__` doesn't wait for the task to begin. Consider replacing with `nursery.start`.
+
+
+## Configuration
+### `no-checkpoint-warning-decorators`
+Specify a list of decorators to disable checkpointing checks for, turning off TRIO107 and TRIO108 warnings for functions decorated with any decorator matching any in the list. Matching is done with [fnmatch](https://docs.python.org/3/library/fnmatch.html). Defaults to disabling for `asynccontextmanager`.
+
+Decorators-to-match must be identifiers or dotted names only (not PEP-614 expressions), and will match against the name only - e.g. `foo.bar` matches `foo.bar`, `foo.bar()`, and `foo.bar(args, here)`, etc.
+
+[You can configure `flake8` with command-line options](https://flake8.pycqa.org/en/latest/user/configuration.html),
+but we prefer using a config file. For example:
+```
+[flake8]
+no-checkpoint-warning-decorators =
+  mydecorator,
+  mydecoratorpackage.checkpointing_decorators.*,
+  ign*,
+  *.ignore,
+```
+
+
+### `startable-in-context-manager`
+Comma-separated list of methods which should be used with `.start()` when opening a context manager,
+in addition to the default `trio.run_process`, `trio.serve_tcp`, `trio.serve_ssl_over_tcp`, and
+`trio.serve_listeners`.  Uses fnmatch, like `no-checkpoint-warning-decorators`. For example:
+```
+[flake8]
+startable-methods-in-context-manager =
+  mylib.myfun,
+  myfun2,mypackage.myfunlib.*,
+```
diff --git a/flake8_trio.py b/flake8_trio.py
index 20209a6..7a6bfe3 100644
--- a/flake8_trio.py
+++ b/flake8_trio.py
@@ -1,1272 +1,1272 @@
-"""A highly opinionated flake8 plugin for Trio-related problems.
-
-This can include anything from outright bugs, to pointless/dead code,
-to likely performance issues, to minor points of idiom that might signal
-a misunderstanding.
-
-It may well be too noisy for anyone with different opinions, that's OK.
-
-Pairs well with flake8-async and flake8-bugbear.
-"""
-
-import ast
-import tokenize
-from argparse import Namespace
-from fnmatch import fnmatch
-from typing import (
-    Any,
-    Dict,
-    Iterable,
-    List,
-    NamedTuple,
-    Optional,
-    Set,
-    Tuple,
-    Union,
-    cast,
-)
-
-from flake8.options.manager import OptionManager
-
-# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
-__version__ = "22.11.3"
-
-
-Error_codes = {
-    "TRIO100": "{} context contains no checkpoints, add `await trio.sleep(0)`",
-    "TRIO101": (
-        "yield inside a nursery or cancel scope is only safe when implementing "
-        "a context manager - otherwise, it breaks exception handling"
-    ),
-    "TRIO102": (
-        "await inside {0.name} on line {0.lineno} must have shielded cancel "
-        "scope with a timeout"
-    ),
-    "TRIO103": "{} block with a code path that doesn't re-raise the error",
-    "TRIO104": "Cancelled (and therefore BaseException) must be re-raised",
-    "TRIO105": "trio async function {} must be immediately awaited",
-    "TRIO106": "trio must be imported with `import trio` for the linter to work",
-    "TRIO107": (
-        "{0} from async function with no guaranteed checkpoint or exception "
-        "since function definition on line {1.lineno}"
-    ),
-    "TRIO108": (
-        "{0} from async iterable with no guaranteed checkpoint since {1.name} "
-        "on line {1.lineno}"
-    ),
-    "TRIO109": (
-        "Async function definition with a `timeout` parameter - use "
-        "`trio.[fail/move_on]_[after/at]` instead"
-    ),
-    "TRIO110": "`while <condition>: await trio.sleep()` should be replaced by a `trio.Event`.",
-    "TRIO111": (
-        "variable {2} is usable within the context manager on line {0}, but that "
-        "will close before nursery opened on line {1} - this is usually a bug.  "
-        "Nurseries should generally be the inner-most context manager."
-    ),
-    "TRIO112": "Redundant nursery {}, consider replacing with directly awaiting the function call",
-    "TRIO113": "Dangerous `.start_soon()`, process might not run before `__aenter__` exits. Consider replacing with `.start()`.",
-}
-
-
-class Statement(NamedTuple):
-    name: str
-    lineno: int
-    col_offset: int = -1
-
-    def __eq__(self, other: Any) -> bool:
-        return (
-            isinstance(other, Statement)
-            and self[:2] == other[:2]
-            and (
-                self.col_offset == other.col_offset
-                or -1 in (self.col_offset, other.col_offset)
-            )
-        )
-
-
-HasLineCol = Union[ast.expr, ast.stmt, ast.arg, ast.excepthandler, Statement]
-
-
-def get_matching_call(
-    node: ast.AST, *names: str, base: str = "trio"
-) -> Optional[Tuple[ast.Call, str]]:
-    if (
-        isinstance(node, ast.Call)
-        and isinstance(node.func, ast.Attribute)
-        and isinstance(node.func.value, ast.Name)
-        and node.func.value.id == base
-        and node.func.attr in names
-    ):
-        return node, node.func.attr
-    return None
-
-
-class Error:
-    def __init__(self, error_code: str, lineno: int, col: int, *args: object):
-        self.line = lineno
-        self.col = col
-        self.code = error_code
-        self.args = args
-
-    # for yielding to flake8
-    def __iter__(self):
-        yield self.line
-        yield self.col
-        yield f"{self.code}: " + Error_codes[self.code].format(*self.args)
-        yield type(Plugin)
-
-    def cmp(self):
-        return self.line, self.col, self.code, self.args
-
-    # for sorting in tests
-    def __lt__(self, other: Any) -> bool:
-        assert isinstance(other, Error)
-        return self.cmp() < other.cmp()
-
-    def __eq__(self, other: Any) -> bool:
-        return isinstance(other, Error) and self.cmp() == other.cmp()
-
-    def __repr__(self) -> str:
-        trailer = "".join(f", {x!r}" for x in self.args)
-        return f"<{self.code} error at {self.line}:{self.col}{trailer}>"
-
-
-checkpoint_node_types = (ast.Await, ast.AsyncFor, ast.AsyncWith)
-cancel_scope_names = (
-    "fail_after",
-    "fail_at",
-    "move_on_after",
-    "move_on_at",
-    "CancelScope",
-)
-
-
-class Flake8TrioVisitor(ast.NodeVisitor):
-    def __init__(self, options: Namespace):
-        super().__init__()
-        self._problems: List[Error] = []
-        self.suppress_errors = False
-        self.options = options
-
-    @classmethod
-    def run(cls, tree: ast.AST, options: Namespace) -> Iterable[Error]:
-        visitor = cls(options)
-        visitor.visit(tree)
-        yield from visitor._problems
-
-    def visit_nodes(
-        self, *nodes: Union[ast.AST, Iterable[ast.AST]], generic: bool = False
-    ):
-        if generic:
-            visit = self.generic_visit
-        else:
-            visit = self.visit
-        for arg in nodes:
-            if isinstance(arg, ast.AST):
-                visit(arg)
-            else:
-                for node in arg:
-                    visit(node)
-
-    def error(self, error: str, node: HasLineCol, *args: object):
-        if not self.suppress_errors:
-            self._problems.append(Error(error, node.lineno, node.col_offset, *args))
-
-    def get_state(self, *attrs: str, copy: bool = False) -> Dict[str, Any]:
-        if not attrs:
-            attrs = tuple(self.__dict__.keys())
-        res: Dict[str, Any] = {}
-        for attr in attrs:
-            if attr == "_problems":
-                continue
-            value = getattr(self, attr)
-            if copy and hasattr(value, "copy"):
-                value = value.copy()
-            res[attr] = value
-        return res
-
-    def set_state(self, attrs: Dict[str, Any], copy: bool = False):
-        for attr, value in attrs.items():
-            if copy and hasattr(value, "copy"):
-                value = value.copy()
-            setattr(self, attr, value)
-
-    def walk(self, *body: ast.AST) -> Iterable[ast.AST]:
-        for b in body:
-            yield from ast.walk(b)
-
-
-# ignores module and only checks the unqualified name of the decorator
-def has_decorator(decorator_list: List[ast.expr], *names: str):
-    for dec in decorator_list:
-        if (isinstance(dec, ast.Name) and dec.id in names) or (
-            isinstance(dec, ast.Attribute) and dec.attr in names
-        ):
-            return True
-    return False
-
-
-# matches the fully qualified name against fnmatch pattern
-# used to match decorators and methods to user-supplied patterns
-def fnmatch_qualified_name(name_list: List[ast.expr], *patterns: str):
-    def construct_name(expr: ast.expr) -> Optional[str]:
-        if isinstance(expr, ast.Call):
-            expr = expr.func
-        if isinstance(expr, ast.Name):
-            return expr.id
-        elif isinstance(expr, ast.Attribute):
-            attr = construct_name(expr.value)
-            assert attr is not None
-            return attr + "." + expr.attr
-        # See https://peps.python.org/pep-0614/ - we don't handle everything
-        return None  # pragma: no cover  # impossible on Python 3.8
-
-    for name in name_list:
-        qualified_name = construct_name(name)
-        if qualified_name is None:
-            continue  # pragma: no cover  # impossible on Python 3.8
-        for pattern in patterns:
-            # strip leading "@"s for when we're working with decorators
-            if fnmatch(qualified_name, pattern.lstrip("@")):
-                return True
-    return False
-
-
-# handles 100, 101, 106, 109, 110, 111, 112
-class VisitorMiscChecks(Flake8TrioVisitor):
-    class NurseryCall(NamedTuple):
-        stack_index: int
-        name: str
-
-    class TrioContextManager(NamedTuple):
-        lineno: int
-        name: str
-        is_nursery: bool
-
-    def __init__(self, *args: Any, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-
-        # 101
-        self._yield_is_error = False
-        self._safe_decorator = False
-
-        # 111
-        self._context_managers: List[VisitorMiscChecks.TrioContextManager] = []
-        self._nursery_call: Optional[VisitorMiscChecks.NurseryCall] = None
-
-        self.defaults = self.get_state(copy=True)
-
-    # ---- 100, 101, 111, 112 ----
-    def visit_With(self, node: Union[ast.With, ast.AsyncWith]):
-        self.check_for_trio100(node)
-        self.check_for_trio112(node)
-
-        outer = self.get_state("_yield_is_error", "_context_managers", copy=True)
-
-        for item in node.items:
-            # 101
-            # if there's no safe decorator,
-            # and it's not yet been determined that yield is error
-            # and this withitem opens a cancelscope:
-            # then yielding is unsafe
-            if (
-                not self._safe_decorator
-                and not self._yield_is_error
-                and get_matching_call(
-                    item.context_expr, "open_nursery", *cancel_scope_names
-                )
-                is not None
-            ):
-                self._yield_is_error = True
-
-            # 111
-            # if a withitem is saved in a variable,
-            # push its line, variable, and whether it's a trio nursery
-            # to the _context_managers stack,
-            if isinstance(item.optional_vars, ast.Name):
-                self._context_managers.append(
-                    self.TrioContextManager(
-                        item.context_expr.lineno,
-                        item.optional_vars.id,
-                        get_matching_call(item.context_expr, "open_nursery")
-                        is not None,
-                    )
-                )
-
-        self.generic_visit(node)
-        self.set_state(outer)
-
-    visit_AsyncWith = visit_With
-
-    # ---- 100 ----
-    def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]):
-        # Context manager with no `await trio.X` call within
-        for item in (i.context_expr for i in node.items):
-            call = get_matching_call(item, *cancel_scope_names)
-            if call and not any(
-                isinstance(x, checkpoint_node_types) and x != node
-                for x in ast.walk(node)
-            ):
-                self.error("TRIO100", item, f"trio.{call[1]}")
-
-    # ---- 101 ----
-    def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
-        outer = self.get_state()
-        self.set_state(self.defaults, copy=True)
-
-        if has_decorator(node.decorator_list, "contextmanager", "asynccontextmanager"):
-            self._safe_decorator = True
-
-        self.generic_visit(node)
-
-        self.set_state(outer)
-
-    # ---- 101, 109 ----
-    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
-        self.check_for_trio109(node)
-        self.visit_FunctionDef(node)
-
-    def visit_Lambda(self, node: ast.Lambda):
-        outer = self.get_state()
-        self.set_state(self.defaults, copy=True)
-        self.generic_visit(node)
-        self.set_state(outer)
-
-    # ---- 101 ----
-    def visit_Yield(self, node: ast.Yield):
-        if self._yield_is_error:
-            self.error("TRIO101", node)
-
-        self.generic_visit(node)
-
-    # ---- 109 ----
-    def check_for_trio109(self, node: ast.AsyncFunctionDef):
-        # pending configuration or a more sophisticated check, ignore
-        # all functions with a decorator
-        if node.decorator_list:
-            return
-
-        args = node.args
-        for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs):
-            if arg.arg == "timeout":
-                self.error("TRIO109", arg)
-
-    # ---- 106 ----
-    def visit_ImportFrom(self, node: ast.ImportFrom):
-        if node.module == "trio":
-            self.error("TRIO106", node)
-        self.generic_visit(node)
-
-    def visit_Import(self, node: ast.Import):
-        for name in node.names:
-            if name.name == "trio" and name.asname is not None:
-                self.error("TRIO106", node)
-        self.generic_visit(node)
-
-    # ---- 110 ----
-    def visit_While(self, node: ast.While):
-        self.check_for_trio110(node)
-        self.generic_visit(node)
-
-    def check_for_trio110(self, node: ast.While):
-        if (
-            len(node.body) == 1
-            and isinstance(node.body[0], ast.Expr)
-            and isinstance(node.body[0].value, ast.Await)
-            and get_matching_call(node.body[0].value.value, "sleep", "sleep_until")
-        ):
-            self.error("TRIO110", node)
-
-    # ---- 111 ----
-    # if it's a <X>.start[_soon] call
-    # and <X> is a nursery listed in self._context_managers:
-    # Save <X>'s index in self._context_managers to guard against cm's higher in the
-    # stack being passed as parameters to it. (and save <X> for the error message)
-    def visit_Call(self, node: ast.Call):
-        outer = self.get_state("_nursery_call")
-
-        if (
-            isinstance(node.func, ast.Attribute)
-            and isinstance(node.func.value, ast.Name)
-            and node.func.attr in ("start", "start_soon")
-        ):
-            self._nursery_call = None
-            for i, cm in enumerate(self._context_managers):
-                if node.func.value.id == cm.name:
-                    # don't break upon finding a nursery in case there's multiple cm's
-                    # on the stack with the same name
-                    if cm.is_nursery:
-                        self._nursery_call = self.NurseryCall(i, node.func.attr)
-                    else:
-                        self._nursery_call = None
-
-        self.generic_visit(node)
-        self.set_state(outer)
-
-    # If we're inside a <X>.start[_soon] call (where <X> is a nursery),
-    # and we're accessing a variable cm that's on the self._context_managers stack,
-    # with a higher index than <X>:
-    #   Raise error since the scope of cm may close before the function passed to the
-    # nursery finishes.
-    def visit_Name(self, node: ast.Name):
-        self.generic_visit(node)
-        if self._nursery_call is None:
-            return
-
-        for i, cm in enumerate(self._context_managers):
-            if cm.name == node.id and i > self._nursery_call.stack_index:
-                self.error(
-                    "TRIO111",
-                    node,
-                    cm.lineno,
-                    self._context_managers[self._nursery_call.stack_index].lineno,
-                    node.id,
-                    self._nursery_call.name,
-                )
-
-    # if with has a withitem `trio.open_nursery() as <X>`,
-    # and the body is only a single expression <X>.start[_soon](),
-    # and does not pass <X> as a parameter to the expression
-    def check_for_trio112(self, node: Union[ast.With, ast.AsyncWith]):
-        # body is single expression
-        if len(node.body) != 1 or not isinstance(node.body[0], ast.Expr):
-            return
-        for item in node.items:
-            # get variable name <X>
-            if not isinstance(item.optional_vars, ast.Name):
-                continue
-            var_name = item.optional_vars.id
-
-            # check for trio.open_nursery
-            nursery = get_matching_call(item.context_expr, "open_nursery")
-
-            # isinstance(..., ast.Call) is done in get_matching_call
-            body_call = cast(ast.Call, node.body[0].value)
-
-            if (
-                nursery is not None
-                and get_matching_call(body_call, "start", "start_soon", base=var_name)
-                # check for presence of <X> as parameter
-                and not any(
-                    (isinstance(n, ast.Name) and n.id == var_name)
-                    for n in self.walk(*body_call.args, *body_call.keywords)
-                )
-            ):
-                self.error("TRIO112", item.context_expr, var_name)
-
-
-# used in 102, 103 and 104
-def critical_except(node: ast.ExceptHandler) -> Optional[Statement]:
-    def has_exception(node: Optional[ast.expr]) -> str:
-        if isinstance(node, ast.Name) and node.id == "BaseException":
-            return "BaseException"
-        if (
-            isinstance(node, ast.Attribute)
-            and isinstance(node.value, ast.Name)
-            and node.value.id == "trio"
-            and node.attr == "Cancelled"
-        ):
-            return "trio.Cancelled"
-        return ""
-
-    # bare except
-    if node.type is None:
-        return Statement("bare except", node.lineno, node.col_offset)
-    # several exceptions
-    if isinstance(node.type, ast.Tuple):
-        for element in node.type.elts:
-            name = has_exception(element)
-            if name:
-                return Statement(name, element.lineno, element.col_offset)
-    # single exception, either a Name or an Attribute
-    name = has_exception(node.type)
-    if name:
-        return Statement(name, node.type.lineno, node.type.col_offset)
-    return None
-
-
-class Visitor102(Flake8TrioVisitor):
-    class TrioScope:
-        def __init__(self, node: ast.Call, funcname: str):
-            self.node = node
-            self.funcname = funcname
-            self.variable_name: Optional[str] = None
-            self.shielded: bool = False
-            self.has_timeout: bool = True
-
-            # scope.shielded is assigned to in visit_Assign
-
-            if self.funcname == "CancelScope":
-                self.has_timeout = False
-                for kw in node.keywords:
-                    # Only accepts constant values
-                    if kw.arg == "shield" and isinstance(kw.value, ast.Constant):
-                        self.shielded = kw.value.value
-                    # sets to True even if timeout is explicitly set to inf
-                    if kw.arg == "deadline":
-                        self.has_timeout = True
-
-    def __init__(self, *args: Any, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-        self._critical_scope: Optional[Statement] = None
-        self._trio_context_managers: List[Visitor102.TrioScope] = []
-
-    # if we're inside a finally, and we're not inside a scope that doesn't have
-    # both a timeout and shield
-    def visit_Await(
-        self,
-        node: Union[ast.Await, ast.AsyncFor, ast.AsyncWith],
-        visit_children: bool = True,
-    ):
-        if self._critical_scope is not None and not any(
-            cm.has_timeout and cm.shielded for cm in self._trio_context_managers
-        ):
-            self.error("TRIO102", node, self._critical_scope)
-        if visit_children:
-            self.generic_visit(node)
-
-    visit_AsyncFor = visit_Await
-
-    def visit_With(self, node: Union[ast.With, ast.AsyncWith]):
-        has_context_manager = False
-
-        # Check for a `with trio.<scope_creater>`
-        for item in node.items:
-            call = get_matching_call(
-                item.context_expr, "open_nursery", *cancel_scope_names
-            )
-            if call is None:
-                continue
-
-            trio_scope = self.TrioScope(*call)
-            # check if it's saved in a variable
-            if isinstance(item.optional_vars, ast.Name):
-                trio_scope.variable_name = item.optional_vars.id
-
-            self._trio_context_managers.append(trio_scope)
-            has_context_manager = True
-            break
-
-        self.generic_visit(node)
-
-        if has_context_manager:
-            self._trio_context_managers.pop()
-
-    def visit_AsyncWith(self, node: ast.AsyncWith):
-        self.visit_Await(node, visit_children=False)
-        self.visit_With(node)
-
-    def critical_visit(
-        self,
-        node: Union[ast.ExceptHandler, Iterable[ast.AST]],
-        block: Statement,
-        generic: bool = False,
-    ):
-        outer = self.get_state("_critical_scope", "_trio_context_managers")
-
-        self._trio_context_managers = []
-        self._critical_scope = block
-
-        self.visit_nodes(node, generic=generic)
-        self.set_state(outer)
-
-    def visit_Try(self, node: ast.Try):
-        # There's no visit_Finally, so we need to manually visit the Try fields.
-        self.visit_nodes(node.body, node.handlers, node.orelse)
-        self.critical_visit(
-            node.finalbody, Statement("try/finally", node.lineno, node.col_offset)
-        )
-
-    def visit_ExceptHandler(self, node: ast.ExceptHandler):
-        res = critical_except(node)
-        if res is None:
-            self.generic_visit(node)
-        else:
-            self.critical_visit(node, res, generic=True)
-
-    def visit_Assign(self, node: ast.Assign):
-        # checks for <scopename>.shield = [True/False]
-        if self._trio_context_managers and len(node.targets) == 1:
-            last_scope = self._trio_context_managers[-1]
-            target = node.targets[0]
-            if (
-                last_scope.variable_name is not None
-                and isinstance(target, ast.Attribute)
-                and isinstance(target.value, ast.Name)
-                and target.value.id == last_scope.variable_name
-                and target.attr == "shield"
-                and isinstance(node.value, ast.Constant)
-            ):
-                last_scope.shielded = node.value.value
-        self.generic_visit(node)
-
-
-# Never have an except Cancelled or except BaseException block with a code path that
-# doesn't re-raise the error
-class Visitor103_104(Flake8TrioVisitor):
-    def __init__(self, *args: Any, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-        self.except_name: Optional[str] = ""
-        self.unraised: bool = False
-        self.unraised_break: bool = False
-        self.unraised_continue: bool = False
-        self.loop_depth = 0
-
-    # If an `except` is bare, catches `BaseException`, or `trio.Cancelled`
-    # set self.unraised, and if it's still set after visiting child nodes
-    # then there might be a code path that doesn't re-raise.
-    def visit_ExceptHandler(self, node: ast.ExceptHandler):
-
-        outer = self.get_state()
-        marker = critical_except(node)
-
-        # we need to *not* unset self.unraised if this is non-critical, to still
-        # warn about `return`s
-
-        if marker is not None:
-            # save name from `as <except_name>`
-            self.except_name = node.name
-
-            self.loop_depth = 0
-            self.unraised = True
-
-        # visit child nodes. Will unset self.unraised if all code paths `raise`
-        self.generic_visit(node)
-
-        if self.unraised and marker is not None:
-            self.error("TRIO103", marker, marker.name)
-
-        self.set_state(outer)
-
-    def visit_Raise(self, node: ast.Raise):
-        # if there's an unraised critical exception, the raise isn't bare,
-        # and the name doesn't match, signal a problem.
-        if (
-            self.unraised
-            and node.exc is not None
-            and not (isinstance(node.exc, ast.Name) and node.exc.id == self.except_name)
-        ):
-            self.error("TRIO104", node)
-
-        # treat it as safe regardless, to avoid unnecessary error messages.
-        self.unraised = False
-
-        self.generic_visit(node)
-
-    def visit_Return(self, node: Union[ast.Return, ast.Yield]):
-        if self.unraised:
-            # Error: must re-raise
-            self.error("TRIO104", node)
-        self.generic_visit(node)
-
-    visit_Yield = visit_Return
-
-    # Treat Try's as fully covering only if `finally` always raises.
-    def visit_Try(self, node: ast.Try):
-        if not self.unraised:
-            self.generic_visit(node)
-            return
-
-        # in theory it's okay if the try and all excepts re-raise,
-        # and there is a bare except
-        # but is a pain to parse and would require a special case for bare raises in
-        # nested excepts.
-        for n in (*node.body, *node.handlers, *node.orelse):
-            self.visit(n)
-            # re-set unraised to warn about returns in each block
-            self.unraised = True
-
-        # but it's fine if we raise in finally
-        self.visit_nodes(node.finalbody)
-
-    # Treat if's as fully covering if both `if` and `else` raise.
-    # `elif` is parsed by the ast as a new if statement inside the else.
-    def visit_If(self, node: ast.If):
-        if not self.unraised:
-            self.generic_visit(node)
-            return
-
-        body_raised = False
-        self.visit_nodes(node.body)
-
-        # does body always raise correctly
-        body_raised = not self.unraised
-
-        self.unraised = True
-        self.visit_nodes(node.orelse)
-
-        # if body didn't raise, or it's unraised after else, set unraise
-        self.unraised = not body_raised or self.unraised
-
-    # A loop is guaranteed to raise if:
-    # condition always raises, or
-    #   else always raises, and
-    #   always raise before break
-    # or body always raises (before break) and is guaranteed to run at least once
-    def visit_For(self, node: Union[ast.For, ast.While]):
-        if not self.unraised:
-            self.generic_visit(node)
-            return
-
-        infinite_loop = False
-        if isinstance(node, ast.While):
-            try:
-                infinite_loop = body_guaranteed_once = bool(ast.literal_eval(node.test))
-            except Exception:
-                body_guaranteed_once = False
-            self.visit_nodes(node.test)
-        else:
-            body_guaranteed_once = iter_guaranteed_once(node.iter)
-            self.visit_nodes(node.target)
-            self.visit_nodes(node.iter)
-
-        prebody = self.get_state("unraised_break", "unraised_continue")
-        self.unraised_break = False
-        self.unraised_continue = False
-
-        self.loop_depth += 1
-        self.visit_nodes(node.body)
-        self.loop_depth -= 1
-
-        # if body is not guaranteed to run, or can continue at unraised, reset
-        if not (infinite_loop or (body_guaranteed_once and not self.unraised_continue)):
-            self.unraised = True
-        self.visit_nodes(node.orelse)
-
-        # if we might break at an unraised point, set unraised
-        self.unraised |= self.unraised_break
-        self.set_state(prebody)
-
-    visit_While = visit_For
-
-    def visit_Break(self, node: ast.Break):
-        if self.unraised and self.loop_depth == 0:
-            self.error("TRIO104", node)
-        self.unraised_break |= self.unraised
-        self.generic_visit(node)
-
-    def visit_Continue(self, node: ast.Continue):
-        if self.unraised and self.loop_depth == 0:
-            self.error("TRIO104", node)
-        self.unraised_continue |= self.unraised
-        self.generic_visit(node)
-
-
-def iter_guaranteed_once(iterable: ast.expr) -> bool:
-    # static container with an "elts" attribute
-    if hasattr(iterable, "elts"):
-        elts: Iterable[ast.expr] = iterable.elts  # type: ignore
-        for elt in elts:
-            assert isinstance(elt, ast.expr)
-            # recurse starred expression
-            if isinstance(elt, ast.Starred):
-                if iter_guaranteed_once(elt.value):
-                    return True
-            else:
-                return True
-        return False
-    if isinstance(iterable, ast.Constant):
-        try:
-            return len(iterable.value) > 0
-        except Exception:
-            return False
-    if isinstance(iterable, ast.Dict):
-        for key, val in zip(iterable.keys, iterable.values):
-            # {**{...}, **{<...>}} is parsed as {None: {...}, None: {<...>}}
-            if key is None and isinstance(val, ast.Dict):
-                if iter_guaranteed_once(val):
-                    return True
-            else:
-                return True
-    # check for range() with literal parameters
-    if (
-        isinstance(iterable, ast.Call)
-        and isinstance(iterable.func, ast.Name)
-        and iterable.func.id == "range"
-    ):
-        try:
-            return len(range(*[ast.literal_eval(a) for a in iterable.args])) > 0
-        except Exception:
-            return False
-    return False
-
-
-trio_async_funcs = (
-    "aclose_forcefully",
-    "open_file",
-    "open_ssl_over_tcp_listeners",
-    "open_ssl_over_tcp_stream",
-    "open_tcp_listeners",
-    "open_tcp_stream",
-    "open_unix_socket",
-    "run_process",
-    "serve_listeners",
-    "serve_ssl_over_tcp",
-    "serve_tcp",
-    "sleep",
-    "sleep_forever",
-    "sleep_until",
-)
-
-
-def is_nursery_call(node: ast.AST, name: str) -> bool:  # pragma: no cover
-    assert name in ("start", "start_soon")
-    if isinstance(node, ast.Attribute):
-        if isinstance(node.value, ast.Name):
-            return node.attr == name and node.value.id.endswith("nursery")
-        if isinstance(node.value, ast.Attribute):
-            return node.attr == name and node.value.attr.endswith("nursery")
-    return False
-
-
-class Visitor105(Flake8TrioVisitor):
-    def __init__(self, *args: Any, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-        # keep a node stack so we can check whether calls are awaited
-        self.node_stack: List[ast.AST] = []
-
-    def visit(self, node: ast.AST):
-        self.node_stack.append(node)
-        super().visit(node)
-        self.node_stack.pop()
-
-    def visit_Call(self, node: ast.Call):
-        if (
-            isinstance(node.func, ast.Attribute)
-            and isinstance(node.func.value, ast.Name)
-            and (
-                (node.func.value.id == "trio" and node.func.attr in trio_async_funcs)
-                or is_nursery_call(node.func, "start")
-            )
-            and (
-                len(self.node_stack) < 2
-                or not isinstance(self.node_stack[-2], ast.Await)
-            )
-        ):
-            assert isinstance(node.func, ast.Attribute)
-            self.error("TRIO105", node, node.func.attr)
-        self.generic_visit(node)
-
-
-def empty_body(body: List[ast.stmt]) -> bool:
-    # Does the function body consist solely of `pass`, `...`, and (doc)string literals?
-    return all(
-        isinstance(stmt, ast.Pass)
-        or (
-            isinstance(stmt, ast.Expr)
-            and isinstance(stmt.value, ast.Constant)
-            and (stmt.value.value is Ellipsis or isinstance(stmt.value.value, str))
-        )
-        for stmt in body
-    )
-
-
-class Visitor107_108(Flake8TrioVisitor):
-    def __init__(self, *args: Any, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-        self.has_yield = False
-        self.safe_decorator = False
-        self.async_function = False
-
-        self.uncheckpointed_statements: Set[Statement] = set()
-        self.uncheckpointed_before_continue: Set[Statement] = set()
-        self.uncheckpointed_before_break: Set[Statement] = set()
-
-        self.default = self.get_state()
-
-    def visit(self, node: ast.AST):
-        if not self.async_function and not isinstance(node, ast.AsyncFunctionDef):
-            self.generic_visit(node)
-        else:
-            super().visit(node)
-
-    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
-        # don't lint functions whose bodies solely consist of pass or ellipsis
-        if has_decorator(node.decorator_list, "overload") or empty_body(node.body):
-            # no reason to generic_visit an empty body
-            return
-
-        outer = self.get_state()
-        self.set_state(self.default, copy=True)
-
-        # disable checks in asynccontextmanagers by saying the function isn't async
-        self.async_function = not fnmatch_qualified_name(
-            node.decorator_list, *self.options.no_checkpoint_warning_decorators
-        )
-
-        if self.async_function:
-            self.uncheckpointed_statements = {
-                Statement("function definition", node.lineno, node.col_offset)
-            }
-
-        self.generic_visit(node)
-
-        if self.async_function:
-            self.check_function_exit(node)
-
-        self.set_state(outer)
-
-    # error if function exits or returns with uncheckpointed statements
-    def check_function_exit(self, node: Union[ast.Return, ast.AsyncFunctionDef]):
-        for statement in self.uncheckpointed_statements:
-            self.error(
-                "TRIO108" if self.has_yield else "TRIO107",
-                node,
-                "return" if isinstance(node, ast.Return) else "exit",
-                statement,
-            )
-
-    def visit_Return(self, node: ast.Return):
-        self.generic_visit(node)
-        self.check_function_exit(node)
-
-        # avoid duplicate error messages
-        self.uncheckpointed_statements = set()
-
-    # disregard checkpoints in nested function definitions
-    def visit_FunctionDef(self, node: ast.FunctionDef):
-        outer = self.get_state()
-        self.set_state(self.default, copy=True)
-        self.generic_visit(node)
-        self.set_state(outer)
-
-    # checkpoint functions
-    def visit_Await(self, node: Union[ast.Await, ast.Raise]):
-        # the expression being awaited is not checkpointed
-        # so only set checkpoint after the await node
-        self.generic_visit(node)
-
-        # all nodes are now checkpointed
-        self.uncheckpointed_statements = set()
-
-    # raising exception means we don't need to checkpoint so we can treat it as one
-    visit_Raise = visit_Await
-
-    # Async context managers can reasonably checkpoint on either or both of entry and
-    # exit.  Given that we can't tell which, we assume "both" to avoid raising a
-    # missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
-    def visit_AsyncWith(self, node: ast.AsyncWith):
-        self.visit_nodes(node.items)
-
-        self.uncheckpointed_statements = set()
-        self.visit_nodes(node.body)
-
-        self.uncheckpointed_statements = set()
-
-    # error if no checkpoint since earlier yield or function entry
-    def visit_Yield(self, node: ast.Yield):
-        self.has_yield = True
-        self.generic_visit(node)
-        for statement in self.uncheckpointed_statements:
-            self.error("TRIO108", node, "yield", statement)
-
-        # mark as requiring checkpoint after
-        self.uncheckpointed_statements = {
-            Statement("yield", node.lineno, node.col_offset)
-        }
-
-    # valid checkpoint if there's valid checkpoints (or raise) in:
-    # (try or else) and all excepts, or in finally
-    #
-    # try can jump into any except or into the finally* at any point during it's
-    # execution so we need to make sure except & finally can handle worst-case
-    # * unless there's a bare except / except BaseException - not implemented.
-    def visit_Try(self, node: ast.Try):
-        # except & finally guaranteed to enter with checkpoint if checkpointed
-        # before try and no yield in try body.
-        body_uncheckpointed_statements = self.uncheckpointed_statements.copy()
-        for inner_node in self.walk(*node.body):
-            if isinstance(inner_node, ast.Yield):
-                body_uncheckpointed_statements.add(
-                    Statement("yield", inner_node.lineno, inner_node.col_offset)
-                )
-
-        # check try body
-        self.visit_nodes(node.body)
-
-        # save state at end of try for entering else
-        try_checkpoint = self.uncheckpointed_statements
-
-        # check that all except handlers checkpoint (await or most likely raise)
-        except_uncheckpointed_statements: Set[Statement] = set()
-
-        for handler in node.handlers:
-            # enter with worst case of try
-            self.uncheckpointed_statements = body_uncheckpointed_statements.copy()
-
-            self.visit_nodes(handler)
-
-            except_uncheckpointed_statements.update(self.uncheckpointed_statements)
-
-        # check else
-        # if else runs it's after all of try, so restore state to back then
-        self.uncheckpointed_statements = try_checkpoint
-        self.visit_nodes(node.orelse)
-
-        # checkpoint if else checkpoints, and all excepts checkpoint
-        self.uncheckpointed_statements.update(except_uncheckpointed_statements)
-
-        if node.finalbody:
-            added = body_uncheckpointed_statements.difference(
-                self.uncheckpointed_statements
-            )
-            # if there's no bare except or except BaseException, we can jump into
-            # finally from any point in try. But the exception will be reraised after
-            # finally, so track what we add so it can be removed later.
-            # (This is for catching return or yield in the finally, which is usually
-            # very bad)
-            if not any(
-                h.type is None
-                or (isinstance(h.type, ast.Name) and h.type.id == "BaseException")
-                for h in node.handlers
-            ):
-                self.uncheckpointed_statements.update(added)
-
-            self.visit_nodes(node.finalbody)
-            self.uncheckpointed_statements.difference_update(added)
-
-    # valid checkpoint if both body and orelse checkpoint
-    def visit_If(self, node: Union[ast.If, ast.IfExp]):
-        # visit condition
-        self.visit_nodes(node.test)
-        outer = self.uncheckpointed_statements.copy()
-
-        # visit body
-        self.visit_nodes(node.body)
-        body_outer = self.uncheckpointed_statements
-
-        # reset to after condition and visit orelse
-        self.uncheckpointed_statements = outer
-        self.visit_nodes(node.orelse)
-
-        # union of both branches is the new set of unhandled entries
-        self.uncheckpointed_statements.update(body_outer)
-
-    # inline if
-    visit_IfExp = visit_If
-
-    # Check for yields w/o checkpoint inbetween due to entering loop body the first time,
-    # after completing all of loop body, and after any continues.
-    # yield in else have same requirement
-    # state after the loop same as above, and in addition the state at any break
-    def visit_loop(self, node: Union[ast.While, ast.For, ast.AsyncFor]):
-        # visit condition
-        infinite_loop = False
-        if isinstance(node, ast.While):
-            try:
-                infinite_loop = body_guaranteed_once = bool(ast.literal_eval(node.test))
-            except Exception:
-                body_guaranteed_once = False
-            self.visit_nodes(node.test)
-        else:
-            self.visit_nodes(node.target)
-            self.visit_nodes(node.iter)
-            body_guaranteed_once = iter_guaranteed_once(node.iter)
-
-        # save state in case of nested loops
-        outer = self.get_state(
-            "uncheckpointed_before_continue",
-            "uncheckpointed_before_break",
-            "suppress_errors",
-        )
-
-        self.uncheckpointed_before_continue = set()
-
-        # AsyncFor guaranteed checkpoint at every iteration
-        if isinstance(node, ast.AsyncFor):
-            self.uncheckpointed_statements = set()
-
-        pre_body_uncheckpointed_statements = self.uncheckpointed_statements
-
-        # check for all possible uncheckpointed statements before entering start of loop
-        # due to `continue` or multiple iterations
-        if not isinstance(node, ast.AsyncFor):
-            # reset uncheckpointed_statements to not clear it if awaited
-            self.uncheckpointed_statements = set()
-
-            # avoid duplicate errors
-            self.suppress_errors = True
-
-            # set self.uncheckpointed_before_continue and uncheckpointed at end of loop
-            self.visit_nodes(node.body)
-
-            self.suppress_errors = outer["suppress_errors"]
-
-            self.uncheckpointed_statements.update(self.uncheckpointed_before_continue)
-
-        # add uncheckpointed on first iter
-        self.uncheckpointed_statements.update(pre_body_uncheckpointed_statements)
-
-        # visit body
-        self.uncheckpointed_before_break = set()
-        self.visit_nodes(node.body)
-
-        # AsyncFor guarantees checkpoint on running out of iterable
-        # so reset checkpoint state at end of loop. (but not state at break)
-        if isinstance(node, ast.AsyncFor):
-            self.uncheckpointed_statements = set()
-        else:
-            # enter orelse with worst case:
-            # loop body might execute fully before entering orelse
-            # (current state of self.uncheckpointed_statements)
-            # or not at all
-            if not body_guaranteed_once:
-                self.uncheckpointed_statements.update(
-                    pre_body_uncheckpointed_statements
-                )
-            # or at a continue, unless it's an infinite loop
-            if not infinite_loop:
-                self.uncheckpointed_statements.update(
-                    self.uncheckpointed_before_continue
-                )
-
-        # visit orelse
-        self.visit_nodes(node.orelse)
-
-        # if this is an infinite loop, with no break in it, don't raise
-        # alarms about the state after it.
-        if infinite_loop and not any(
-            isinstance(n, ast.Break) for n in self.walk(*node.body)
-        ):
-            self.uncheckpointed_statements = set()
-        else:
-            # We may exit from:
-            # orelse (covering: no body, body until continue, and all body)
-            # break
-            self.uncheckpointed_statements.update(self.uncheckpointed_before_break)
-
-        # reset break & continue in case of nested loops
-        self.set_state(outer)
-
-    visit_While = visit_loop
-    visit_For = visit_loop
-    visit_AsyncFor = visit_loop
-
-    # save state in case of continue/break at a point not guaranteed to checkpoint
-    def visit_Continue(self, node: ast.Continue):
-        self.uncheckpointed_before_continue.update(self.uncheckpointed_statements)
-
-    def visit_Break(self, node: ast.Break):
-        self.uncheckpointed_before_break.update(self.uncheckpointed_statements)
-
-    # first node in a condition is always evaluated, but may shortcut at any point
-    # after that so we track worst-case checkpoint (i.e. after yield)
-    def visit_BoolOp(self, node: ast.BoolOp):
-        self.visit(node.op)
-
-        # first value always evaluated
-        self.visit(node.values[0])
-
-        worst_case_shortcut = self.uncheckpointed_statements.copy()
-
-        for value in node.values[1:]:
-            self.visit(value)
-            worst_case_shortcut.update(self.uncheckpointed_statements)
-
-        self.uncheckpointed_statements = worst_case_shortcut
-
-
-def _get_identifier(node: ast.expr) -> str:
-    if isinstance(node, ast.Name):
-        return node.id
-    if isinstance(node, ast.Attribute):
-        return node.attr
-    return ""  # pragma: no cover
-
-
-STARTABLE_CALLS = (
-    "trio.run_process",
-    "trio.serve_ssl_over_tcp",
-    "trio.serve_tcp",
-    "trio.serve_listeners",
-)
-
-
-class Visitor113(Flake8TrioVisitor):
-    def __init__(self, *args: Any, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-        self.async_function = False
-        self.asynccontextmanager = False
-        self.aenter = False
-
-    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
-        outer = self.aenter
-
-        self.aenter = node.name == "__aenter__" or any(
-            _get_identifier(d) == "asynccontextmanager" for d in node.decorator_list
-        )
-
-        self.generic_visit(node)
-        self.aenter = outer
-
-    def visit_Yield(self, node: ast.Yield):
-        self.aenter = False
-
-    def visit_Call(self, node: ast.Call) -> None:
-        if (
-            self.aenter
-            and is_nursery_call(node.func, "start_soon")
-            and len(node.args) > 0
-            and fnmatch_qualified_name(
-                node.args[:1],
-                *STARTABLE_CALLS,
-                *self.options.startable_in_context_manager,
-            )
-        ):
-            self.error("TRIO113", node)
-
-
-class Plugin:
-    name = __name__
-    version = __version__
-    options: Namespace = Namespace()
-
-    def __init__(self, tree: ast.AST):
-        self._tree = tree
-
-    @classmethod
-    def from_filename(cls, filename: str) -> "Plugin":
-        with tokenize.open(filename) as f:
-            source = f.read()
-        return cls(ast.parse(source))
-
-    def run(self) -> Iterable[Error]:
-        for v in Flake8TrioVisitor.__subclasses__():
-            yield from v.run(self._tree, self.options)
-
-    @staticmethod
-    def add_options(option_manager: OptionManager):
-        option_manager.add_option(
-            "--no-checkpoint-warning-decorators",
-            default="asynccontextmanager",
-            parse_from_config=True,
-            required=False,
-            comma_separated_list=True,
-            help=(
-                "Comma-separated list of decorators to disable TRIO107 & TRIO108"
-                " checkpoint warnings for."
-                " Decorators can be dotted or not, as well as support * as a wildcard."
-                " For example, ``--no-checkpoint-warning-decorators=app.route,"
-                "mydecorator,mypackage.mydecorators.*``"
-            ),
-        )
-        option_manager.add_option(
-            "--startable-in-context-manager",
-            default="",
-            parse_from_config=True,
-            required=False,
-            comma_separated_list=True,
-            help=(
-                "Comma-separated list of method calls to enable TRIO113"
-                " warnings for."
-                " Use if you want to override the default methods the check is enabled for."
-                " Methods can be dotted or not, as well as support * as a wildcard."
-                " For example, ``--startable-in-context-manager=worker_serve,"
-                "mypackage.sub.*``"
-            ),
-        )
-
-    @staticmethod
-    def parse_options(options: Namespace):
-        Plugin.options = options
+"""A highly opinionated flake8 plugin for Trio-related problems.
+
+This can include anything from outright bugs, to pointless/dead code,
+to likely performance issues, to minor points of idiom that might signal
+a misunderstanding.
+
+It may well be too noisy for anyone with different opinions, that's OK.
+
+Pairs well with flake8-async and flake8-bugbear.
+"""
+
+import ast
+import tokenize
+from argparse import Namespace
+from fnmatch import fnmatch
+from typing import (
+    Any,
+    Dict,
+    Iterable,
+    List,
+    NamedTuple,
+    Optional,
+    Set,
+    Tuple,
+    Union,
+    cast,
+)
+
+from flake8.options.manager import OptionManager
+
+# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
+__version__ = "22.11.3"
+
+
+Error_codes = {
+    "TRIO100": "{} context contains no checkpoints, add `await trio.sleep(0)`",
+    "TRIO101": (
+        "yield inside a nursery or cancel scope is only safe when implementing "
+        "a context manager - otherwise, it breaks exception handling"
+    ),
+    "TRIO102": (
+        "await inside {0.name} on line {0.lineno} must have shielded cancel "
+        "scope with a timeout"
+    ),
+    "TRIO103": "{} block with a code path that doesn't re-raise the error",
+    "TRIO104": "Cancelled (and therefore BaseException) must be re-raised",
+    "TRIO105": "trio async function {} must be immediately awaited",
+    "TRIO106": "trio must be imported with `import trio` for the linter to work",
+    "TRIO107": (
+        "{0} from async function with no guaranteed checkpoint or exception "
+        "since function definition on line {1.lineno}"
+    ),
+    "TRIO108": (
+        "{0} from async iterable with no guaranteed checkpoint since {1.name} "
+        "on line {1.lineno}"
+    ),
+    "TRIO109": (
+        "Async function definition with a `timeout` parameter - use "
+        "`trio.[fail/move_on]_[after/at]` instead"
+    ),
+    "TRIO110": "`while <condition>: await trio.sleep()` should be replaced by a `trio.Event`.",
+    "TRIO111": (
+        "variable {2} is usable within the context manager on line {0}, but that "
+        "will close before nursery opened on line {1} - this is usually a bug.  "
+        "Nurseries should generally be the inner-most context manager."
+    ),
+    "TRIO112": "Redundant nursery {}, consider replacing with directly awaiting the function call",
+    "TRIO113": "Dangerous `.start_soon()`, process might not run before `__aenter__` exits. Consider replacing with `.start()`.",
+}
+
+
+class Statement(NamedTuple):
+    name: str
+    lineno: int
+    col_offset: int = -1
+
+    def __eq__(self, other: Any) -> bool:
+        return (
+            isinstance(other, Statement)
+            and self[:2] == other[:2]
+            and (
+                self.col_offset == other.col_offset
+                or -1 in (self.col_offset, other.col_offset)
+            )
+        )
+
+
+HasLineCol = Union[ast.expr, ast.stmt, ast.arg, ast.excepthandler, Statement]
+
+
+def get_matching_call(
+    node: ast.AST, *names: str, base: str = "trio"
+) -> Optional[Tuple[ast.Call, str]]:
+    if (
+        isinstance(node, ast.Call)
+        and isinstance(node.func, ast.Attribute)
+        and isinstance(node.func.value, ast.Name)
+        and node.func.value.id == base
+        and node.func.attr in names
+    ):
+        return node, node.func.attr
+    return None
+
+
+class Error:
+    def __init__(self, error_code: str, lineno: int, col: int, *args: object):
+        self.line = lineno
+        self.col = col
+        self.code = error_code
+        self.args = args
+
+    # for yielding to flake8
+    def __iter__(self):
+        yield self.line
+        yield self.col
+        yield f"{self.code}: " + Error_codes[self.code].format(*self.args)
+        yield type(Plugin)
+
+    def cmp(self):
+        return self.line, self.col, self.code, self.args
+
+    # for sorting in tests
+    def __lt__(self, other: Any) -> bool:
+        assert isinstance(other, Error)
+        return self.cmp() < other.cmp()
+
+    def __eq__(self, other: Any) -> bool:
+        return isinstance(other, Error) and self.cmp() == other.cmp()
+
+    def __repr__(self) -> str:
+        trailer = "".join(f", {x!r}" for x in self.args)
+        return f"<{self.code} error at {self.line}:{self.col}{trailer}>"
+
+
+checkpoint_node_types = (ast.Await, ast.AsyncFor, ast.AsyncWith)
+cancel_scope_names = (
+    "fail_after",
+    "fail_at",
+    "move_on_after",
+    "move_on_at",
+    "CancelScope",
+)
+
+
+class Flake8TrioVisitor(ast.NodeVisitor):
+    def __init__(self, options: Namespace):
+        super().__init__()
+        self._problems: List[Error] = []
+        self.suppress_errors = False
+        self.options = options
+
+    @classmethod
+    def run(cls, tree: ast.AST, options: Namespace) -> Iterable[Error]:
+        visitor = cls(options)
+        visitor.visit(tree)
+        yield from visitor._problems
+
+    def visit_nodes(
+        self, *nodes: Union[ast.AST, Iterable[ast.AST]], generic: bool = False
+    ):
+        if generic:
+            visit = self.generic_visit
+        else:
+            visit = self.visit
+        for arg in nodes:
+            if isinstance(arg, ast.AST):
+                visit(arg)
+            else:
+                for node in arg:
+                    visit(node)
+
+    def error(self, error: str, node: HasLineCol, *args: object):
+        if not self.suppress_errors:
+            self._problems.append(Error(error, node.lineno, node.col_offset, *args))
+
+    def get_state(self, *attrs: str, copy: bool = False) -> Dict[str, Any]:
+        if not attrs:
+            attrs = tuple(self.__dict__.keys())
+        res: Dict[str, Any] = {}
+        for attr in attrs:
+            if attr == "_problems":
+                continue
+            value = getattr(self, attr)
+            if copy and hasattr(value, "copy"):
+                value = value.copy()
+            res[attr] = value
+        return res
+
+    def set_state(self, attrs: Dict[str, Any], copy: bool = False):
+        for attr, value in attrs.items():
+            if copy and hasattr(value, "copy"):
+                value = value.copy()
+            setattr(self, attr, value)
+
+    def walk(self, *body: ast.AST) -> Iterable[ast.AST]:
+        for b in body:
+            yield from ast.walk(b)
+
+
+# ignores module and only checks the unqualified name of the decorator
+def has_decorator(decorator_list: List[ast.expr], *names: str):
+    for dec in decorator_list:
+        if (isinstance(dec, ast.Name) and dec.id in names) or (
+            isinstance(dec, ast.Attribute) and dec.attr in names
+        ):
+            return True
+    return False
+
+
+# matches the fully qualified name against fnmatch pattern
+# used to match decorators and methods to user-supplied patterns
+def fnmatch_qualified_name(name_list: List[ast.expr], *patterns: str):
+    def construct_name(expr: ast.expr) -> Optional[str]:
+        if isinstance(expr, ast.Call):
+            expr = expr.func
+        if isinstance(expr, ast.Name):
+            return expr.id
+        elif isinstance(expr, ast.Attribute):
+            attr = construct_name(expr.value)
+            assert attr is not None
+            return attr + "." + expr.attr
+        # See https://peps.python.org/pep-0614/ - we don't handle everything
+        return None  # pragma: no cover  # impossible on Python 3.8
+
+    for name in name_list:
+        qualified_name = construct_name(name)
+        if qualified_name is None:
+            continue  # pragma: no cover  # impossible on Python 3.8
+        for pattern in patterns:
+            # strip leading "@"s for when we're working with decorators
+            if fnmatch(qualified_name, pattern.lstrip("@")):
+                return True
+    return False
+
+
+# handles 100, 101, 106, 109, 110, 111, 112
+class VisitorMiscChecks(Flake8TrioVisitor):
+    class NurseryCall(NamedTuple):
+        stack_index: int
+        name: str
+
+    class TrioContextManager(NamedTuple):
+        lineno: int
+        name: str
+        is_nursery: bool
+
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+
+        # 101
+        self._yield_is_error = False
+        self._safe_decorator = False
+
+        # 111
+        self._context_managers: List[VisitorMiscChecks.TrioContextManager] = []
+        self._nursery_call: Optional[VisitorMiscChecks.NurseryCall] = None
+
+        self.defaults = self.get_state(copy=True)
+
+    # ---- 100, 101, 111, 112 ----
+    def visit_With(self, node: Union[ast.With, ast.AsyncWith]):
+        self.check_for_trio100(node)
+        self.check_for_trio112(node)
+
+        outer = self.get_state("_yield_is_error", "_context_managers", copy=True)
+
+        for item in node.items:
+            # 101
+            # if there's no safe decorator,
+            # and it's not yet been determined that yield is error
+            # and this withitem opens a cancelscope:
+            # then yielding is unsafe
+            if (
+                not self._safe_decorator
+                and not self._yield_is_error
+                and get_matching_call(
+                    item.context_expr, "open_nursery", *cancel_scope_names
+                )
+                is not None
+            ):
+                self._yield_is_error = True
+
+            # 111
+            # if a withitem is saved in a variable,
+            # push its line, variable, and whether it's a trio nursery
+            # to the _context_managers stack,
+            if isinstance(item.optional_vars, ast.Name):
+                self._context_managers.append(
+                    self.TrioContextManager(
+                        item.context_expr.lineno,
+                        item.optional_vars.id,
+                        get_matching_call(item.context_expr, "open_nursery")
+                        is not None,
+                    )
+                )
+
+        self.generic_visit(node)
+        self.set_state(outer)
+
+    visit_AsyncWith = visit_With
+
+    # ---- 100 ----
+    def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]):
+        # Context manager with no `await trio.X` call within
+        for item in (i.context_expr for i in node.items):
+            call = get_matching_call(item, *cancel_scope_names)
+            if call and not any(
+                isinstance(x, checkpoint_node_types) and x != node
+                for x in ast.walk(node)
+            ):
+                self.error("TRIO100", item, f"trio.{call[1]}")
+
+    # ---- 101 ----
+    def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
+        outer = self.get_state()
+        self.set_state(self.defaults, copy=True)
+
+        if has_decorator(node.decorator_list, "contextmanager", "asynccontextmanager"):
+            self._safe_decorator = True
+
+        self.generic_visit(node)
+
+        self.set_state(outer)
+
+    # ---- 101, 109 ----
+    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
+        self.check_for_trio109(node)
+        self.visit_FunctionDef(node)
+
+    def visit_Lambda(self, node: ast.Lambda):
+        outer = self.get_state()
+        self.set_state(self.defaults, copy=True)
+        self.generic_visit(node)
+        self.set_state(outer)
+
+    # ---- 101 ----
+    def visit_Yield(self, node: ast.Yield):
+        if self._yield_is_error:
+            self.error("TRIO101", node)
+
+        self.generic_visit(node)
+
+    # ---- 109 ----
+    def check_for_trio109(self, node: ast.AsyncFunctionDef):
+        # pending configuration or a more sophisticated check, ignore
+        # all functions with a decorator
+        if node.decorator_list:
+            return
+
+        args = node.args
+        for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs):
+            if arg.arg == "timeout":
+                self.error("TRIO109", arg)
+
+    # ---- 106 ----
+    def visit_ImportFrom(self, node: ast.ImportFrom):
+        if node.module == "trio":
+            self.error("TRIO106", node)
+        self.generic_visit(node)
+
+    def visit_Import(self, node: ast.Import):
+        for name in node.names:
+            if name.name == "trio" and name.asname is not None:
+                self.error("TRIO106", node)
+        self.generic_visit(node)
+
+    # ---- 110 ----
+    def visit_While(self, node: ast.While):
+        self.check_for_trio110(node)
+        self.generic_visit(node)
+
+    def check_for_trio110(self, node: ast.While):
+        if (
+            len(node.body) == 1
+            and isinstance(node.body[0], ast.Expr)
+            and isinstance(node.body[0].value, ast.Await)
+            and get_matching_call(node.body[0].value.value, "sleep", "sleep_until")
+        ):
+            self.error("TRIO110", node)
+
+    # ---- 111 ----
+    # if it's a <X>.start[_soon] call
+    # and <X> is a nursery listed in self._context_managers:
+    # Save <X>'s index in self._context_managers to guard against cm's higher in the
+    # stack being passed as parameters to it. (and save <X> for the error message)
+    def visit_Call(self, node: ast.Call):
+        outer = self.get_state("_nursery_call")
+
+        if (
+            isinstance(node.func, ast.Attribute)
+            and isinstance(node.func.value, ast.Name)
+            and node.func.attr in ("start", "start_soon")
+        ):
+            self._nursery_call = None
+            for i, cm in enumerate(self._context_managers):
+                if node.func.value.id == cm.name:
+                    # don't break upon finding a nursery in case there's multiple cm's
+                    # on the stack with the same name
+                    if cm.is_nursery:
+                        self._nursery_call = self.NurseryCall(i, node.func.attr)
+                    else:
+                        self._nursery_call = None
+
+        self.generic_visit(node)
+        self.set_state(outer)
+
+    # If we're inside a <X>.start[_soon] call (where <X> is a nursery),
+    # and we're accessing a variable cm that's on the self._context_managers stack,
+    # with a higher index than <X>:
+    #   Raise error since the scope of cm may close before the function passed to the
+    # nursery finishes.
+    def visit_Name(self, node: ast.Name):
+        self.generic_visit(node)
+        if self._nursery_call is None:
+            return
+
+        for i, cm in enumerate(self._context_managers):
+            if cm.name == node.id and i > self._nursery_call.stack_index:
+                self.error(
+                    "TRIO111",
+                    node,
+                    cm.lineno,
+                    self._context_managers[self._nursery_call.stack_index].lineno,
+                    node.id,
+                    self._nursery_call.name,
+                )
+
+    # if with has a withitem `trio.open_nursery() as <X>`,
+    # and the body is only a single expression <X>.start[_soon](),
+    # and does not pass <X> as a parameter to the expression
+    def check_for_trio112(self, node: Union[ast.With, ast.AsyncWith]):
+        # body is single expression
+        if len(node.body) != 1 or not isinstance(node.body[0], ast.Expr):
+            return
+        for item in node.items:
+            # get variable name <X>
+            if not isinstance(item.optional_vars, ast.Name):
+                continue
+            var_name = item.optional_vars.id
+
+            # check for trio.open_nursery
+            nursery = get_matching_call(item.context_expr, "open_nursery")
+
+            # isinstance(..., ast.Call) is done in get_matching_call
+            body_call = cast(ast.Call, node.body[0].value)
+
+            if (
+                nursery is not None
+                and get_matching_call(body_call, "start", "start_soon", base=var_name)
+                # check for presence of <X> as parameter
+                and not any(
+                    (isinstance(n, ast.Name) and n.id == var_name)
+                    for n in self.walk(*body_call.args, *body_call.keywords)
+                )
+            ):
+                self.error("TRIO112", item.context_expr, var_name)
+
+
+# used in 102, 103 and 104
+def critical_except(node: ast.ExceptHandler) -> Optional[Statement]:
+    def has_exception(node: Optional[ast.expr]) -> str:
+        if isinstance(node, ast.Name) and node.id == "BaseException":
+            return "BaseException"
+        if (
+            isinstance(node, ast.Attribute)
+            and isinstance(node.value, ast.Name)
+            and node.value.id == "trio"
+            and node.attr == "Cancelled"
+        ):
+            return "trio.Cancelled"
+        return ""
+
+    # bare except
+    if node.type is None:
+        return Statement("bare except", node.lineno, node.col_offset)
+    # several exceptions
+    if isinstance(node.type, ast.Tuple):
+        for element in node.type.elts:
+            name = has_exception(element)
+            if name:
+                return Statement(name, element.lineno, element.col_offset)
+    # single exception, either a Name or an Attribute
+    name = has_exception(node.type)
+    if name:
+        return Statement(name, node.type.lineno, node.type.col_offset)
+    return None
+
+
+class Visitor102(Flake8TrioVisitor):
+    class TrioScope:
+        def __init__(self, node: ast.Call, funcname: str):
+            self.node = node
+            self.funcname = funcname
+            self.variable_name: Optional[str] = None
+            self.shielded: bool = False
+            self.has_timeout: bool = True
+
+            # scope.shielded is assigned to in visit_Assign
+
+            if self.funcname == "CancelScope":
+                self.has_timeout = False
+                for kw in node.keywords:
+                    # Only accepts constant values
+                    if kw.arg == "shield" and isinstance(kw.value, ast.Constant):
+                        self.shielded = kw.value.value
+                    # sets to True even if timeout is explicitly set to inf
+                    if kw.arg == "deadline":
+                        self.has_timeout = True
+
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        self._critical_scope: Optional[Statement] = None
+        self._trio_context_managers: List[Visitor102.TrioScope] = []
+
+    # if we're inside a finally, and we're not inside a scope that doesn't have
+    # both a timeout and shield
+    def visit_Await(
+        self,
+        node: Union[ast.Await, ast.AsyncFor, ast.AsyncWith],
+        visit_children: bool = True,
+    ):
+        if self._critical_scope is not None and not any(
+            cm.has_timeout and cm.shielded for cm in self._trio_context_managers
+        ):
+            self.error("TRIO102", node, self._critical_scope)
+        if visit_children:
+            self.generic_visit(node)
+
+    visit_AsyncFor = visit_Await
+
+    def visit_With(self, node: Union[ast.With, ast.AsyncWith]):
+        has_context_manager = False
+
+        # Check for a `with trio.<scope_creater>`
+        for item in node.items:
+            call = get_matching_call(
+                item.context_expr, "open_nursery", *cancel_scope_names
+            )
+            if call is None:
+                continue
+
+            trio_scope = self.TrioScope(*call)
+            # check if it's saved in a variable
+            if isinstance(item.optional_vars, ast.Name):
+                trio_scope.variable_name = item.optional_vars.id
+
+            self._trio_context_managers.append(trio_scope)
+            has_context_manager = True
+            break
+
+        self.generic_visit(node)
+
+        if has_context_manager:
+            self._trio_context_managers.pop()
+
+    def visit_AsyncWith(self, node: ast.AsyncWith):
+        self.visit_Await(node, visit_children=False)
+        self.visit_With(node)
+
+    def critical_visit(
+        self,
+        node: Union[ast.ExceptHandler, Iterable[ast.AST]],
+        block: Statement,
+        generic: bool = False,
+    ):
+        outer = self.get_state("_critical_scope", "_trio_context_managers")
+
+        self._trio_context_managers = []
+        self._critical_scope = block
+
+        self.visit_nodes(node, generic=generic)
+        self.set_state(outer)
+
+    def visit_Try(self, node: ast.Try):
+        # There's no visit_Finally, so we need to manually visit the Try fields.
+        self.visit_nodes(node.body, node.handlers, node.orelse)
+        self.critical_visit(
+            node.finalbody, Statement("try/finally", node.lineno, node.col_offset)
+        )
+
+    def visit_ExceptHandler(self, node: ast.ExceptHandler):
+        res = critical_except(node)
+        if res is None:
+            self.generic_visit(node)
+        else:
+            self.critical_visit(node, res, generic=True)
+
+    def visit_Assign(self, node: ast.Assign):
+        # checks for <scopename>.shield = [True/False]
+        if self._trio_context_managers and len(node.targets) == 1:
+            last_scope = self._trio_context_managers[-1]
+            target = node.targets[0]
+            if (
+                last_scope.variable_name is not None
+                and isinstance(target, ast.Attribute)
+                and isinstance(target.value, ast.Name)
+                and target.value.id == last_scope.variable_name
+                and target.attr == "shield"
+                and isinstance(node.value, ast.Constant)
+            ):
+                last_scope.shielded = node.value.value
+        self.generic_visit(node)
+
+
+# Never have an except Cancelled or except BaseException block with a code path that
+# doesn't re-raise the error
+class Visitor103_104(Flake8TrioVisitor):
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        self.except_name: Optional[str] = ""
+        self.unraised: bool = False
+        self.unraised_break: bool = False
+        self.unraised_continue: bool = False
+        self.loop_depth = 0
+
+    # If an `except` is bare, catches `BaseException`, or `trio.Cancelled`
+    # set self.unraised, and if it's still set after visiting child nodes
+    # then there might be a code path that doesn't re-raise.
+    def visit_ExceptHandler(self, node: ast.ExceptHandler):
+
+        outer = self.get_state()
+        marker = critical_except(node)
+
+        # we need to *not* unset self.unraised if this is non-critical, to still
+        # warn about `return`s
+
+        if marker is not None:
+            # save name from `as <except_name>`
+            self.except_name = node.name
+
+            self.loop_depth = 0
+            self.unraised = True
+
+        # visit child nodes. Will unset self.unraised if all code paths `raise`
+        self.generic_visit(node)
+
+        if self.unraised and marker is not None:
+            self.error("TRIO103", marker, marker.name)
+
+        self.set_state(outer)
+
+    def visit_Raise(self, node: ast.Raise):
+        # if there's an unraised critical exception, the raise isn't bare,
+        # and the name doesn't match, signal a problem.
+        if (
+            self.unraised
+            and node.exc is not None
+            and not (isinstance(node.exc, ast.Name) and node.exc.id == self.except_name)
+        ):
+            self.error("TRIO104", node)
+
+        # treat it as safe regardless, to avoid unnecessary error messages.
+        self.unraised = False
+
+        self.generic_visit(node)
+
+    def visit_Return(self, node: Union[ast.Return, ast.Yield]):
+        if self.unraised:
+            # Error: must re-raise
+            self.error("TRIO104", node)
+        self.generic_visit(node)
+
+    visit_Yield = visit_Return
+
+    # Treat Try's as fully covering only if `finally` always raises.
+    def visit_Try(self, node: ast.Try):
+        if not self.unraised:
+            self.generic_visit(node)
+            return
+
+        # in theory it's okay if the try and all excepts re-raise,
+        # and there is a bare except
+        # but is a pain to parse and would require a special case for bare raises in
+        # nested excepts.
+        for n in (*node.body, *node.handlers, *node.orelse):
+            self.visit(n)
+            # re-set unraised to warn about returns in each block
+            self.unraised = True
+
+        # but it's fine if we raise in finally
+        self.visit_nodes(node.finalbody)
+
+    # Treat if's as fully covering if both `if` and `else` raise.
+    # `elif` is parsed by the ast as a new if statement inside the else.
+    def visit_If(self, node: ast.If):
+        if not self.unraised:
+            self.generic_visit(node)
+            return
+
+        body_raised = False
+        self.visit_nodes(node.body)
+
+        # does body always raise correctly
+        body_raised = not self.unraised
+
+        self.unraised = True
+        self.visit_nodes(node.orelse)
+
+        # if body didn't raise, or it's unraised after else, set unraise
+        self.unraised = not body_raised or self.unraised
+
+    # A loop is guaranteed to raise if:
+    # condition always raises, or
+    #   else always raises, and
+    #   always raise before break
+    # or body always raises (before break) and is guaranteed to run at least once
+    def visit_For(self, node: Union[ast.For, ast.While]):
+        if not self.unraised:
+            self.generic_visit(node)
+            return
+
+        infinite_loop = False
+        if isinstance(node, ast.While):
+            try:
+                infinite_loop = body_guaranteed_once = bool(ast.literal_eval(node.test))
+            except Exception:
+                body_guaranteed_once = False
+            self.visit_nodes(node.test)
+        else:
+            body_guaranteed_once = iter_guaranteed_once(node.iter)
+            self.visit_nodes(node.target)
+            self.visit_nodes(node.iter)
+
+        prebody = self.get_state("unraised_break", "unraised_continue")
+        self.unraised_break = False
+        self.unraised_continue = False
+
+        self.loop_depth += 1
+        self.visit_nodes(node.body)
+        self.loop_depth -= 1
+
+        # if body is not guaranteed to run, or can continue at unraised, reset
+        if not (infinite_loop or (body_guaranteed_once and not self.unraised_continue)):
+            self.unraised = True
+        self.visit_nodes(node.orelse)
+
+        # if we might break at an unraised point, set unraised
+        self.unraised |= self.unraised_break
+        self.set_state(prebody)
+
+    visit_While = visit_For
+
+    def visit_Break(self, node: ast.Break):
+        if self.unraised and self.loop_depth == 0:
+            self.error("TRIO104", node)
+        self.unraised_break |= self.unraised
+        self.generic_visit(node)
+
+    def visit_Continue(self, node: ast.Continue):
+        if self.unraised and self.loop_depth == 0:
+            self.error("TRIO104", node)
+        self.unraised_continue |= self.unraised
+        self.generic_visit(node)
+
+
+def iter_guaranteed_once(iterable: ast.expr) -> bool:
+    # static container with an "elts" attribute
+    if hasattr(iterable, "elts"):
+        elts: Iterable[ast.expr] = iterable.elts  # type: ignore
+        for elt in elts:
+            assert isinstance(elt, ast.expr)
+            # recurse starred expression
+            if isinstance(elt, ast.Starred):
+                if iter_guaranteed_once(elt.value):
+                    return True
+            else:
+                return True
+        return False
+    if isinstance(iterable, ast.Constant):
+        try:
+            return len(iterable.value) > 0
+        except Exception:
+            return False
+    if isinstance(iterable, ast.Dict):
+        for key, val in zip(iterable.keys, iterable.values):
+            # {**{...}, **{<...>}} is parsed as {None: {...}, None: {<...>}}
+            if key is None and isinstance(val, ast.Dict):
+                if iter_guaranteed_once(val):
+                    return True
+            else:
+                return True
+    # check for range() with literal parameters
+    if (
+        isinstance(iterable, ast.Call)
+        and isinstance(iterable.func, ast.Name)
+        and iterable.func.id == "range"
+    ):
+        try:
+            return len(range(*[ast.literal_eval(a) for a in iterable.args])) > 0
+        except Exception:
+            return False
+    return False
+
+
+trio_async_funcs = (
+    "aclose_forcefully",
+    "open_file",
+    "open_ssl_over_tcp_listeners",
+    "open_ssl_over_tcp_stream",
+    "open_tcp_listeners",
+    "open_tcp_stream",
+    "open_unix_socket",
+    "run_process",
+    "serve_listeners",
+    "serve_ssl_over_tcp",
+    "serve_tcp",
+    "sleep",
+    "sleep_forever",
+    "sleep_until",
+)
+
+
+def is_nursery_call(node: ast.AST, name: str) -> bool:  # pragma: no cover
+    assert name in ("start", "start_soon")
+    if isinstance(node, ast.Attribute):
+        if isinstance(node.value, ast.Name):
+            return node.attr == name and node.value.id.endswith("nursery")
+        if isinstance(node.value, ast.Attribute):
+            return node.attr == name and node.value.attr.endswith("nursery")
+    return False
+
+
+class Visitor105(Flake8TrioVisitor):
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        # keep a node stack so we can check whether calls are awaited
+        self.node_stack: List[ast.AST] = []
+
+    def visit(self, node: ast.AST):
+        self.node_stack.append(node)
+        super().visit(node)
+        self.node_stack.pop()
+
+    def visit_Call(self, node: ast.Call):
+        if (
+            isinstance(node.func, ast.Attribute)
+            and isinstance(node.func.value, ast.Name)
+            and (
+                (node.func.value.id == "trio" and node.func.attr in trio_async_funcs)
+                or is_nursery_call(node.func, "start")
+            )
+            and (
+                len(self.node_stack) < 2
+                or not isinstance(self.node_stack[-2], ast.Await)
+            )
+        ):
+            assert isinstance(node.func, ast.Attribute)
+            self.error("TRIO105", node, node.func.attr)
+        self.generic_visit(node)
+
+
+def empty_body(body: List[ast.stmt]) -> bool:
+    # Does the function body consist solely of `pass`, `...`, and (doc)string literals?
+    return all(
+        isinstance(stmt, ast.Pass)
+        or (
+            isinstance(stmt, ast.Expr)
+            and isinstance(stmt.value, ast.Constant)
+            and (stmt.value.value is Ellipsis or isinstance(stmt.value.value, str))
+        )
+        for stmt in body
+    )
+
+
+class Visitor107_108(Flake8TrioVisitor):
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        self.has_yield = False
+        self.safe_decorator = False
+        self.async_function = False
+
+        self.uncheckpointed_statements: Set[Statement] = set()
+        self.uncheckpointed_before_continue: Set[Statement] = set()
+        self.uncheckpointed_before_break: Set[Statement] = set()
+
+        self.default = self.get_state()
+
+    def visit(self, node: ast.AST):
+        if not self.async_function and not isinstance(node, ast.AsyncFunctionDef):
+            self.generic_visit(node)
+        else:
+            super().visit(node)
+
+    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
+        # don't lint functions whose bodies solely consist of pass or ellipsis
+        if has_decorator(node.decorator_list, "overload") or empty_body(node.body):
+            # no reason to generic_visit an empty body
+            return
+
+        outer = self.get_state()
+        self.set_state(self.default, copy=True)
+
+        # disable checks in asynccontextmanagers by saying the function isn't async
+        self.async_function = not fnmatch_qualified_name(
+            node.decorator_list, *self.options.no_checkpoint_warning_decorators
+        )
+
+        if self.async_function:
+            self.uncheckpointed_statements = {
+                Statement("function definition", node.lineno, node.col_offset)
+            }
+
+        self.generic_visit(node)
+
+        if self.async_function:
+            self.check_function_exit(node)
+
+        self.set_state(outer)
+
+    # error if function exits or returns with uncheckpointed statements
+    def check_function_exit(self, node: Union[ast.Return, ast.AsyncFunctionDef]):
+        for statement in self.uncheckpointed_statements:
+            self.error(
+                "TRIO108" if self.has_yield else "TRIO107",
+                node,
+                "return" if isinstance(node, ast.Return) else "exit",
+                statement,
+            )
+
+    def visit_Return(self, node: ast.Return):
+        self.generic_visit(node)
+        self.check_function_exit(node)
+
+        # avoid duplicate error messages
+        self.uncheckpointed_statements = set()
+
+    # disregard checkpoints in nested function definitions
+    def visit_FunctionDef(self, node: ast.FunctionDef):
+        outer = self.get_state()
+        self.set_state(self.default, copy=True)
+        self.generic_visit(node)
+        self.set_state(outer)
+
+    # checkpoint functions
+    def visit_Await(self, node: Union[ast.Await, ast.Raise]):
+        # the expression being awaited is not checkpointed
+        # so only set checkpoint after the await node
+        self.generic_visit(node)
+
+        # all nodes are now checkpointed
+        self.uncheckpointed_statements = set()
+
+    # raising exception means we don't need to checkpoint so we can treat it as one
+    visit_Raise = visit_Await
+
+    # Async context managers can reasonably checkpoint on either or both of entry and
+    # exit.  Given that we can't tell which, we assume "both" to avoid raising a
+    # missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
+    def visit_AsyncWith(self, node: ast.AsyncWith):
+        self.visit_nodes(node.items)
+
+        self.uncheckpointed_statements = set()
+        self.visit_nodes(node.body)
+
+        self.uncheckpointed_statements = set()
+
+    # error if no checkpoint since earlier yield or function entry
+    def visit_Yield(self, node: ast.Yield):
+        self.has_yield = True
+        self.generic_visit(node)
+        for statement in self.uncheckpointed_statements:
+            self.error("TRIO108", node, "yield", statement)
+
+        # mark as requiring checkpoint after
+        self.uncheckpointed_statements = {
+            Statement("yield", node.lineno, node.col_offset)
+        }
+
+    # valid checkpoint if there's valid checkpoints (or raise) in:
+    # (try or else) and all excepts, or in finally
+    #
+    # try can jump into any except or into the finally* at any point during it's
+    # execution so we need to make sure except & finally can handle worst-case
+    # * unless there's a bare except / except BaseException - not implemented.
+    def visit_Try(self, node: ast.Try):
+        # except & finally guaranteed to enter with checkpoint if checkpointed
+        # before try and no yield in try body.
+        body_uncheckpointed_statements = self.uncheckpointed_statements.copy()
+        for inner_node in self.walk(*node.body):
+            if isinstance(inner_node, ast.Yield):
+                body_uncheckpointed_statements.add(
+                    Statement("yield", inner_node.lineno, inner_node.col_offset)
+                )
+
+        # check try body
+        self.visit_nodes(node.body)
+
+        # save state at end of try for entering else
+        try_checkpoint = self.uncheckpointed_statements
+
+        # check that all except handlers checkpoint (await or most likely raise)
+        except_uncheckpointed_statements: Set[Statement] = set()
+
+        for handler in node.handlers:
+            # enter with worst case of try
+            self.uncheckpointed_statements = body_uncheckpointed_statements.copy()
+
+            self.visit_nodes(handler)
+
+            except_uncheckpointed_statements.update(self.uncheckpointed_statements)
+
+        # check else
+        # if else runs it's after all of try, so restore state to back then
+        self.uncheckpointed_statements = try_checkpoint
+        self.visit_nodes(node.orelse)
+
+        # checkpoint if else checkpoints, and all excepts checkpoint
+        self.uncheckpointed_statements.update(except_uncheckpointed_statements)
+
+        if node.finalbody:
+            added = body_uncheckpointed_statements.difference(
+                self.uncheckpointed_statements
+            )
+            # if there's no bare except or except BaseException, we can jump into
+            # finally from any point in try. But the exception will be reraised after
+            # finally, so track what we add so it can be removed later.
+            # (This is for catching return or yield in the finally, which is usually
+            # very bad)
+            if not any(
+                h.type is None
+                or (isinstance(h.type, ast.Name) and h.type.id == "BaseException")
+                for h in node.handlers
+            ):
+                self.uncheckpointed_statements.update(added)
+
+            self.visit_nodes(node.finalbody)
+            self.uncheckpointed_statements.difference_update(added)
+
+    # valid checkpoint if both body and orelse checkpoint
+    def visit_If(self, node: Union[ast.If, ast.IfExp]):
+        # visit condition
+        self.visit_nodes(node.test)
+        outer = self.uncheckpointed_statements.copy()
+
+        # visit body
+        self.visit_nodes(node.body)
+        body_outer = self.uncheckpointed_statements
+
+        # reset to after condition and visit orelse
+        self.uncheckpointed_statements = outer
+        self.visit_nodes(node.orelse)
+
+        # union of both branches is the new set of unhandled entries
+        self.uncheckpointed_statements.update(body_outer)
+
+    # inline if
+    visit_IfExp = visit_If
+
+    # Check for yields w/o checkpoint inbetween due to entering loop body the first time,
+    # after completing all of loop body, and after any continues.
+    # yield in else have same requirement
+    # state after the loop same as above, and in addition the state at any break
+    def visit_loop(self, node: Union[ast.While, ast.For, ast.AsyncFor]):
+        # visit condition
+        infinite_loop = False
+        if isinstance(node, ast.While):
+            try:
+                infinite_loop = body_guaranteed_once = bool(ast.literal_eval(node.test))
+            except Exception:
+                body_guaranteed_once = False
+            self.visit_nodes(node.test)
+        else:
+            self.visit_nodes(node.target)
+            self.visit_nodes(node.iter)
+            body_guaranteed_once = iter_guaranteed_once(node.iter)
+
+        # save state in case of nested loops
+        outer = self.get_state(
+            "uncheckpointed_before_continue",
+            "uncheckpointed_before_break",
+            "suppress_errors",
+        )
+
+        self.uncheckpointed_before_continue = set()
+
+        # AsyncFor guaranteed checkpoint at every iteration
+        if isinstance(node, ast.AsyncFor):
+            self.uncheckpointed_statements = set()
+
+        pre_body_uncheckpointed_statements = self.uncheckpointed_statements
+
+        # check for all possible uncheckpointed statements before entering start of loop
+        # due to `continue` or multiple iterations
+        if not isinstance(node, ast.AsyncFor):
+            # reset uncheckpointed_statements to not clear it if awaited
+            self.uncheckpointed_statements = set()
+
+            # avoid duplicate errors
+            self.suppress_errors = True
+
+            # set self.uncheckpointed_before_continue and uncheckpointed at end of loop
+            self.visit_nodes(node.body)
+
+            self.suppress_errors = outer["suppress_errors"]
+
+            self.uncheckpointed_statements.update(self.uncheckpointed_before_continue)
+
+        # add uncheckpointed on first iter
+        self.uncheckpointed_statements.update(pre_body_uncheckpointed_statements)
+
+        # visit body
+        self.uncheckpointed_before_break = set()
+        self.visit_nodes(node.body)
+
+        # AsyncFor guarantees checkpoint on running out of iterable
+        # so reset checkpoint state at end of loop. (but not state at break)
+        if isinstance(node, ast.AsyncFor):
+            self.uncheckpointed_statements = set()
+        else:
+            # enter orelse with worst case:
+            # loop body might execute fully before entering orelse
+            # (current state of self.uncheckpointed_statements)
+            # or not at all
+            if not body_guaranteed_once:
+                self.uncheckpointed_statements.update(
+                    pre_body_uncheckpointed_statements
+                )
+            # or at a continue, unless it's an infinite loop
+            if not infinite_loop:
+                self.uncheckpointed_statements.update(
+                    self.uncheckpointed_before_continue
+                )
+
+        # visit orelse
+        self.visit_nodes(node.orelse)
+
+        # if this is an infinite loop, with no break in it, don't raise
+        # alarms about the state after it.
+        if infinite_loop and not any(
+            isinstance(n, ast.Break) for n in self.walk(*node.body)
+        ):
+            self.uncheckpointed_statements = set()
+        else:
+            # We may exit from:
+            # orelse (covering: no body, body until continue, and all body)
+            # break
+            self.uncheckpointed_statements.update(self.uncheckpointed_before_break)
+
+        # reset break & continue in case of nested loops
+        self.set_state(outer)
+
+    visit_While = visit_loop
+    visit_For = visit_loop
+    visit_AsyncFor = visit_loop
+
+    # save state in case of continue/break at a point not guaranteed to checkpoint
+    def visit_Continue(self, node: ast.Continue):
+        self.uncheckpointed_before_continue.update(self.uncheckpointed_statements)
+
+    def visit_Break(self, node: ast.Break):
+        self.uncheckpointed_before_break.update(self.uncheckpointed_statements)
+
+    # first node in a condition is always evaluated, but may shortcut at any point
+    # after that so we track worst-case checkpoint (i.e. after yield)
+    def visit_BoolOp(self, node: ast.BoolOp):
+        self.visit(node.op)
+
+        # first value always evaluated
+        self.visit(node.values[0])
+
+        worst_case_shortcut = self.uncheckpointed_statements.copy()
+
+        for value in node.values[1:]:
+            self.visit(value)
+            worst_case_shortcut.update(self.uncheckpointed_statements)
+
+        self.uncheckpointed_statements = worst_case_shortcut
+
+
+def _get_identifier(node: ast.expr) -> str:
+    if isinstance(node, ast.Name):
+        return node.id
+    if isinstance(node, ast.Attribute):
+        return node.attr
+    return ""  # pragma: no cover
+
+
+STARTABLE_CALLS = (
+    "trio.run_process",
+    "trio.serve_ssl_over_tcp",
+    "trio.serve_tcp",
+    "trio.serve_listeners",
+)
+
+
+class Visitor113(Flake8TrioVisitor):
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        self.async_function = False
+        self.asynccontextmanager = False
+        self.aenter = False
+
+    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
+        outer = self.aenter
+
+        self.aenter = node.name == "__aenter__" or any(
+            _get_identifier(d) == "asynccontextmanager" for d in node.decorator_list
+        )
+
+        self.generic_visit(node)
+        self.aenter = outer
+
+    def visit_Yield(self, node: ast.Yield):
+        self.aenter = False
+
+    def visit_Call(self, node: ast.Call) -> None:
+        if (
+            self.aenter
+            and is_nursery_call(node.func, "start_soon")
+            and len(node.args) > 0
+            and fnmatch_qualified_name(
+                node.args[:1],
+                *STARTABLE_CALLS,
+                *self.options.startable_in_context_manager,
+            )
+        ):
+            self.error("TRIO113", node)
+
+
+class Plugin:
+    name = __name__
+    version = __version__
+    options: Namespace = Namespace()
+
+    def __init__(self, tree: ast.AST):
+        self._tree = tree
+
+    @classmethod
+    def from_filename(cls, filename: str) -> "Plugin":
+        with tokenize.open(filename) as f:
+            source = f.read()
+        return cls(ast.parse(source))
+
+    def run(self) -> Iterable[Error]:
+        for v in Flake8TrioVisitor.__subclasses__():
+            yield from v.run(self._tree, self.options)
+
+    @staticmethod
+    def add_options(option_manager: OptionManager):
+        option_manager.add_option(
+            "--no-checkpoint-warning-decorators",
+            default="asynccontextmanager",
+            parse_from_config=True,
+            required=False,
+            comma_separated_list=True,
+            help=(
+                "Comma-separated list of decorators to disable TRIO107 & TRIO108"
+                " checkpoint warnings for."
+                " Decorators can be dotted or not, as well as support * as a wildcard."
+                " For example, ``--no-checkpoint-warning-decorators=app.route,"
+                "mydecorator,mypackage.mydecorators.*``"
+            ),
+        )
+        option_manager.add_option(
+            "--startable-in-context-manager",
+            default="",
+            parse_from_config=True,
+            required=False,
+            comma_separated_list=True,
+            help=(
+                "Comma-separated list of method calls to enable TRIO113"
+                " warnings for."
+                " Use if you want to override the default methods the check is enabled for."
+                " Methods can be dotted or not, as well as support * as a wildcard."
+                " For example, ``--startable-in-context-manager=worker_serve,"
+                "mypackage.sub.*``"
+            ),
+        )
+
+    @staticmethod
+    def parse_options(options: Namespace):
+        Plugin.options = options
diff --git a/pyproject.toml b/pyproject.toml
index 0543492..48199ce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,2 +1,2 @@
-[tool.pyright]
-strict = ["*.py", "tests/test_flake8_trio.py", "tests/conftest.py"]
+[tool.pyright]
+strict = ["*.py", "tests/test_flake8_trio.py", "tests/conftest.py"]
diff --git a/setup.py b/setup.py
index f2d49d5..b479774 100644
--- a/setup.py
+++ b/setup.py
@@ -1,53 +1,53 @@
-#!/usr/bin/env python3
-
-from pathlib import Path
-
-import setuptools
-
-
-def local_file(name: str) -> Path:
-    return Path(__file__).parent / name
-
-
-with local_file("flake8_trio.py").open("r") as o:
-    for line in o:
-        if line.startswith("__version__"):
-            _, __version__, _ = line.split('"')
-            break
-    else:
-        raise AssertionError("__version__ must be set")
-
-
-setuptools.setup(
-    name="flake8-trio",
-    version=__version__,
-    author="Zac Hatfield-Dodds and Contributors",
-    author_email="me@cooperlees.com",
-    py_modules=["flake8_trio"],
-    url="https://github.com/Zac-HD/flake8-trio",
-    license="MIT",
-    description="A highly opinionated flake8 plugin for Trio-related problems.",
-    zip_safe=False,
-    install_requires=["flake8"],
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 3 - Alpha",
-        "Framework :: Flake8",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-    ],
-    long_description=(
-        local_file("README.md").open().read()
-        + "\n\n"
-        + local_file("CHANGELOG.md").open().read()
-    ),
-    long_description_content_type="text/markdown",
-    entry_points={
-        "flake8.extension": ["TRI = flake8_trio:Plugin"],
-    },
-)
+#!/usr/bin/env python3
+
+from pathlib import Path
+
+import setuptools
+
+
+def local_file(name: str) -> Path:
+    return Path(__file__).parent / name
+
+
+with local_file("flake8_trio.py").open("r") as o:
+    for line in o:
+        if line.startswith("__version__"):
+            _, __version__, _ = line.split('"')
+            break
+    else:
+        raise AssertionError("__version__ must be set")
+
+
+setuptools.setup(
+    name="flake8-trio",
+    version=__version__,
+    author="Zac Hatfield-Dodds and Contributors",
+    author_email="me@cooperlees.com",
+    py_modules=["flake8_trio"],
+    url="https://github.com/Zac-HD/flake8-trio",
+    license="MIT",
+    description="A highly opinionated flake8 plugin for Trio-related problems.",
+    zip_safe=False,
+    install_requires=["flake8"],
+    python_requires=">=3.8",
+    classifiers=[
+        "Development Status :: 3 - Alpha",
+        "Framework :: Flake8",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+    ],
+    long_description=(
+        local_file("README.md").open().read()
+        + "\n\n"
+        + local_file("CHANGELOG.md").open().read()
+    ),
+    long_description_content_type="text/markdown",
+    entry_points={
+        "flake8.extension": ["TRI = flake8_trio:Plugin"],
+    },
+)
diff --git a/tox.ini b/tox.ini
index 3436369..d1b39de 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,61 +1,61 @@
-# The test environment and commands
-[tox]
-# default environments to run without `-e`
-# `tox -e ALL` runs all environments
-envlist = check, test
-
-[testenv:check]
-description = Format code and run linters (quick)
-deps =
-    shed
-    flake8
-    flake8-builtins
-    flake8-bugbear
-    flake8-comprehensions
-    pyright
-    # Required for pyright strict mode
-    hypothesis
-    hypothesmith
-    pytest
-    trio
-setenv =
-    # Make sure pyright is always up to date
-    PYRIGHT_PYTHON_FORCE_VERSION = latest
-skip_install =
-    # don't install the plugin, which would register it with flake8
-    # and potentially stop the linter from functioning.
-    true
-ignore_errors =
-    # true means "run all, fail if any failed"
-    true
-commands =
-    shed
-    flake8 --exclude .*,tests/trio*.py
-    pyright --pythonversion 3.10 --warnings
-
-# generate py38-test py39-test and test
-[testenv:{py38-, py39-,}test]
-description = Runs pytest, optionally with posargs
-deps =
-    pytest
-    pytest-cov
-    #pytest-xdist
-    hypothesis
-    hypothesmith
-    trio
-commands =
-    pytest {posargs} #{posargs:-n auto}
-
-
-# Settings for other tools
-[pytest]
-addopts =
-    -Werror
-    --tb=native
-    --cov=flake8_trio
-    --cov-branch
-    --cov-report=term-missing:skip-covered
-    --cov-fail-under=100
-
-[flake8]
-ignore = E501,W503
+# The test environment and commands
+[tox]
+# default environments to run without `-e`
+# `tox -e ALL` runs all environments
+envlist = check, test
+
+[testenv:check]
+description = Format code and run linters (quick)
+deps =
+    shed
+    flake8
+    flake8-builtins
+    flake8-bugbear
+    flake8-comprehensions
+    pyright
+    # Required for pyright strict mode
+    hypothesis
+    hypothesmith
+    pytest
+    trio
+setenv =
+    # Make sure pyright is always up to date
+    PYRIGHT_PYTHON_FORCE_VERSION = latest
+skip_install =
+    # don't install the plugin, which would register it with flake8
+    # and potentially stop the linter from functioning.
+    true
+ignore_errors =
+    # true means "run all, fail if any failed"
+    true
+commands =
+    shed
+    flake8 --exclude .*,tests/trio*.py
+    pyright --pythonversion 3.10 --warnings
+
+# generate py38-test py39-test and test
+[testenv:{py38-, py39-,}test]
+description = Runs pytest, optionally with posargs
+deps =
+    pytest
+    pytest-cov
+    #pytest-xdist
+    hypothesis
+    hypothesmith
+    trio
+commands =
+    pytest {posargs} #{posargs:-n auto}
+
+
+# Settings for other tools
+[pytest]
+addopts =
+    -Werror
+    --tb=native
+    --cov=flake8_trio
+    --cov-branch
+    --cov-report=term-missing:skip-covered
+    --cov-fail-under=100
+
+[flake8]
+ignore = E501,W503
diff --git a/typings/flake8/options/manager.pyi b/typings/flake8/options/manager.pyi
index 12b8897..a3ab30b 100644
--- a/typings/flake8/options/manager.pyi
+++ b/typings/flake8/options/manager.pyi
@@ -1,202 +1,202 @@
-"""
-This type stub file was generated by pyright.
-"""
-
-import argparse
-from typing import (
-    Any,
-    Callable,
-    Dict,
-    List,
-    Mapping,
-    Optional,
-    Sequence,
-    Tuple,
-    Type,
-    Union,
-)
-from flake8.plugins.finder import Plugins
-
-"""Option handling and Option management logic."""
-LOG = ...
-_ARG = ...
-_optparse_callable_map: Dict[str, Union[Type[Any], _ARG]] = ...
-
-class _CallbackAction(argparse.Action):
-    """Shim for optparse-style callback actions."""
-
-    def __init__(
-        self,
-        *args: Any,
-        callback: Callable[..., Any],
-        callback_args: Sequence[Any] = ...,
-        callback_kwargs: Optional[Dict[str, Any]] = ...,
-        **kwargs: Any
-    ) -> None: ...
-    def __call__(
-        self,
-        parser: argparse.ArgumentParser,
-        namespace: argparse.Namespace,
-        values: Optional[Union[Sequence[str], str]],
-        option_string: Optional[str] = ...,
-    ) -> None: ...
-
-class Option:
-    """Our wrapper around an argparse argument parsers to add features."""
-
-    def __init__(
-        self,
-        short_option_name: Union[str, _ARG] = ...,
-        long_option_name: Union[str, _ARG] = ...,
-        action: Union[str, Type[argparse.Action], _ARG] = ...,
-        default: Union[Any, _ARG] = ...,
-        type: Union[str, Callable[..., Any], _ARG] = ...,
-        dest: Union[str, _ARG] = ...,
-        nargs: Union[int, str, _ARG] = ...,
-        const: Union[Any, _ARG] = ...,
-        choices: Union[Sequence[Any], _ARG] = ...,
-        help: Union[str, _ARG] = ...,
-        metavar: Union[str, _ARG] = ...,
-        callback: Union[Callable[..., Any], _ARG] = ...,
-        callback_args: Union[Sequence[Any], _ARG] = ...,
-        callback_kwargs: Union[Mapping[str, Any], _ARG] = ...,
-        required: Union[bool, _ARG] = ...,
-        parse_from_config: bool = ...,
-        comma_separated_list: bool = ...,
-        normalize_paths: bool = ...,
-    ) -> None:
-        """Initialize an Option instance.
-
-        The following are all passed directly through to argparse.
-
-        :param short_option_name:
-            The short name of the option (e.g., ``-x``). This will be the
-            first argument passed to ``ArgumentParser.add_argument``
-        :param long_option_name:
-            The long name of the option (e.g., ``--xtra-long-option``). This
-            will be the second argument passed to
-            ``ArgumentParser.add_argument``
-        :param default:
-            Default value of the option.
-        :param dest:
-            Attribute name to store parsed option value as.
-        :param nargs:
-            Number of arguments to parse for this option.
-        :param const:
-            Constant value to store on a common destination. Usually used in
-            conjunction with ``action="store_const"``.
-        :param choices:
-            Possible values for the option.
-        :param help:
-            Help text displayed in the usage information.
-        :param metavar:
-            Name to use instead of the long option name for help text.
-        :param required:
-            Whether this option is required or not.
-
-        The following options may be passed directly through to :mod:`argparse`
-        but may need some massaging.
-
-        :param type:
-            A callable to normalize the type (as is the case in
-            :mod:`argparse`).  Deprecated: you can also pass through type
-            strings such as ``'int'`` which are handled by :mod:`optparse`.
-        :param action:
-            Any action allowed by :mod:`argparse`.  Deprecated: this also
-            understands the ``action='callback'`` action from :mod:`optparse`.
-        :param callback:
-            Callback used if the action is ``"callback"``.  Deprecated: please
-            use ``action=`` instead.
-        :param callback_args:
-            Additional positional arguments to the callback callable.
-            Deprecated: please use ``action=`` instead (probably with
-            ``functools.partial``).
-        :param callback_kwargs:
-            Keyword arguments to the callback callable. Deprecated: please
-            use ``action=`` instead (probably with ``functools.partial``).
-
-        The following parameters are for Flake8's option handling alone.
-
-        :param parse_from_config:
-            Whether or not this option should be parsed out of config files.
-        :param comma_separated_list:
-            Whether the option is a comma separated list when parsing from a
-            config file.
-        :param normalize_paths:
-            Whether the option is expecting a path or list of paths and should
-            attempt to normalize the paths to absolute paths.
-        """
-        ...
-    @property
-    def filtered_option_kwargs(self) -> Dict[str, Any]:
-        """Return any actually-specified arguments."""
-        ...
-    def __repr__(self) -> str: ...
-    def normalize(self, value: Any, *normalize_args: str) -> Any:
-        """Normalize the value based on the option configuration."""
-        ...
-    def to_argparse(self) -> Tuple[List[str], Dict[str, Any]]:
-        """Convert a Flake8 Option to argparse ``add_argument`` arguments."""
-        ...
-
-class OptionManager:
-    """Manage Options and OptionParser while adding post-processing."""
-
-    def __init__(
-        self,
-        *,
-        version: str,
-        plugin_versions: str,
-        parents: List[argparse.ArgumentParser]
-    ) -> None:
-        """Initialize an instance of an OptionManager.
-
-        :param prog:
-            Name of the actual program (e.g., flake8).
-        :param version:
-            Version string for the program.
-        :param usage:
-            Basic usage string used by the OptionParser.
-        :param parents:
-            A list of ArgumentParser objects whose arguments should also be
-            included.
-        """
-        ...
-    def register_plugins(self, plugins: Plugins) -> None:
-        """Register the plugin options (if needed)."""
-        ...
-    def add_option(self, *args: Any, **kwargs: Any) -> None:
-        """Create and register a new option.
-
-        See parameters for :class:`~flake8.options.manager.Option` for
-        acceptable arguments to this method.
-
-        .. note::
-
-            ``short_option_name`` and ``long_option_name`` may be specified
-            positionally as they are with argparse normally.
-        """
-        ...
-    def extend_default_ignore(self, error_codes: Sequence[str]) -> None:
-        """Extend the default ignore list with the error codes provided.
-
-        :param error_codes:
-            List of strings that are the error/warning codes with which to
-            extend the default ignore list.
-        """
-        ...
-    def extend_default_select(self, error_codes: Sequence[str]) -> None:
-        """Extend the default select list with the error codes provided.
-
-        :param error_codes:
-            List of strings that are the error/warning codes with which
-            to extend the default select list.
-        """
-        ...
-    def parse_args(
-        self,
-        args: Optional[Sequence[str]] = ...,
-        values: Optional[argparse.Namespace] = ...,
-    ) -> argparse.Namespace:
-        """Proxy to calling the OptionParser's parse_args method."""
-        ...
+"""
+This type stub file was generated by pyright.
+"""
+
+import argparse
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Sequence,
+    Tuple,
+    Type,
+    Union,
+)
+from flake8.plugins.finder import Plugins
+
+"""Option handling and Option management logic."""
+LOG = ...
+_ARG = ...
+_optparse_callable_map: Dict[str, Union[Type[Any], _ARG]] = ...
+
+class _CallbackAction(argparse.Action):
+    """Shim for optparse-style callback actions."""
+
+    def __init__(
+        self,
+        *args: Any,
+        callback: Callable[..., Any],
+        callback_args: Sequence[Any] = ...,
+        callback_kwargs: Optional[Dict[str, Any]] = ...,
+        **kwargs: Any
+    ) -> None: ...
+    def __call__(
+        self,
+        parser: argparse.ArgumentParser,
+        namespace: argparse.Namespace,
+        values: Optional[Union[Sequence[str], str]],
+        option_string: Optional[str] = ...,
+    ) -> None: ...
+
+class Option:
+    """Our wrapper around an argparse argument parsers to add features."""
+
+    def __init__(
+        self,
+        short_option_name: Union[str, _ARG] = ...,
+        long_option_name: Union[str, _ARG] = ...,
+        action: Union[str, Type[argparse.Action], _ARG] = ...,
+        default: Union[Any, _ARG] = ...,
+        type: Union[str, Callable[..., Any], _ARG] = ...,
+        dest: Union[str, _ARG] = ...,
+        nargs: Union[int, str, _ARG] = ...,
+        const: Union[Any, _ARG] = ...,
+        choices: Union[Sequence[Any], _ARG] = ...,
+        help: Union[str, _ARG] = ...,
+        metavar: Union[str, _ARG] = ...,
+        callback: Union[Callable[..., Any], _ARG] = ...,
+        callback_args: Union[Sequence[Any], _ARG] = ...,
+        callback_kwargs: Union[Mapping[str, Any], _ARG] = ...,
+        required: Union[bool, _ARG] = ...,
+        parse_from_config: bool = ...,
+        comma_separated_list: bool = ...,
+        normalize_paths: bool = ...,
+    ) -> None:
+        """Initialize an Option instance.
+
+        The following are all passed directly through to argparse.
+
+        :param short_option_name:
+            The short name of the option (e.g., ``-x``). This will be the
+            first argument passed to ``ArgumentParser.add_argument``
+        :param long_option_name:
+            The long name of the option (e.g., ``--xtra-long-option``). This
+            will be the second argument passed to
+            ``ArgumentParser.add_argument``
+        :param default:
+            Default value of the option.
+        :param dest:
+            Attribute name to store parsed option value as.
+        :param nargs:
+            Number of arguments to parse for this option.
+        :param const:
+            Constant value to store on a common destination. Usually used in
+            conjunction with ``action="store_const"``.
+        :param choices:
+            Possible values for the option.
+        :param help:
+            Help text displayed in the usage information.
+        :param metavar:
+            Name to use instead of the long option name for help text.
+        :param required:
+            Whether this option is required or not.
+
+        The following options may be passed directly through to :mod:`argparse`
+        but may need some massaging.
+
+        :param type:
+            A callable to normalize the type (as is the case in
+            :mod:`argparse`).  Deprecated: you can also pass through type
+            strings such as ``'int'`` which are handled by :mod:`optparse`.
+        :param action:
+            Any action allowed by :mod:`argparse`.  Deprecated: this also
+            understands the ``action='callback'`` action from :mod:`optparse`.
+        :param callback:
+            Callback used if the action is ``"callback"``.  Deprecated: please
+            use ``action=`` instead.
+        :param callback_args:
+            Additional positional arguments to the callback callable.
+            Deprecated: please use ``action=`` instead (probably with
+            ``functools.partial``).
+        :param callback_kwargs:
+            Keyword arguments to the callback callable. Deprecated: please
+            use ``action=`` instead (probably with ``functools.partial``).
+
+        The following parameters are for Flake8's option handling alone.
+
+        :param parse_from_config:
+            Whether or not this option should be parsed out of config files.
+        :param comma_separated_list:
+            Whether the option is a comma separated list when parsing from a
+            config file.
+        :param normalize_paths:
+            Whether the option is expecting a path or list of paths and should
+            attempt to normalize the paths to absolute paths.
+        """
+        ...
+    @property
+    def filtered_option_kwargs(self) -> Dict[str, Any]:
+        """Return any actually-specified arguments."""
+        ...
+    def __repr__(self) -> str: ...
+    def normalize(self, value: Any, *normalize_args: str) -> Any:
+        """Normalize the value based on the option configuration."""
+        ...
+    def to_argparse(self) -> Tuple[List[str], Dict[str, Any]]:
+        """Convert a Flake8 Option to argparse ``add_argument`` arguments."""
+        ...
+
+class OptionManager:
+    """Manage Options and OptionParser while adding post-processing."""
+
+    def __init__(
+        self,
+        *,
+        version: str,
+        plugin_versions: str,
+        parents: List[argparse.ArgumentParser]
+    ) -> None:
+        """Initialize an instance of an OptionManager.
+
+        :param prog:
+            Name of the actual program (e.g., flake8).
+        :param version:
+            Version string for the program.
+        :param usage:
+            Basic usage string used by the OptionParser.
+        :param parents:
+            A list of ArgumentParser objects whose arguments should also be
+            included.
+        """
+        ...
+    def register_plugins(self, plugins: Plugins) -> None:
+        """Register the plugin options (if needed)."""
+        ...
+    def add_option(self, *args: Any, **kwargs: Any) -> None:
+        """Create and register a new option.
+
+        See parameters for :class:`~flake8.options.manager.Option` for
+        acceptable arguments to this method.
+
+        .. note::
+
+            ``short_option_name`` and ``long_option_name`` may be specified
+            positionally as they are with argparse normally.
+        """
+        ...
+    def extend_default_ignore(self, error_codes: Sequence[str]) -> None:
+        """Extend the default ignore list with the error codes provided.
+
+        :param error_codes:
+            List of strings that are the error/warning codes with which to
+            extend the default ignore list.
+        """
+        ...
+    def extend_default_select(self, error_codes: Sequence[str]) -> None:
+        """Extend the default select list with the error codes provided.
+
+        :param error_codes:
+            List of strings that are the error/warning codes with which
+            to extend the default select list.
+        """
+        ...
+    def parse_args(
+        self,
+        args: Optional[Sequence[str]] = ...,
+        values: Optional[argparse.Namespace] = ...,
+    ) -> argparse.Namespace:
+        """Proxy to calling the OptionParser's parse_args method."""
+        ...


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 0d6bbbb..af3a52d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,25 +1,25 @@
-from typing import List
-
-import pytest
-
-
-def pytest_addoption(parser: pytest.Parser):
-    parser.addoption(
-        "--runfuzz", action="store_true", default=False, help="run fuzz tests"
-    )
-
-
-def pytest_configure(config: pytest.Config):
-    config.addinivalue_line(
-        "markers", "fuzz: mark test as a slow fuzzer to not run by default"
-    )
-
-
-def pytest_collection_modifyitems(config: pytest.Config, items: List[pytest.Item]):
-    if config.getoption("--runfuzz"):
-        # --runfuzz given in cli: do not skip fuzz tests
-        return
-    skip_fuzz = pytest.mark.skip(reason="need --runfuzz option to run")
-    for item in items:
-        if "fuzz" in item.keywords:
-            item.add_marker(skip_fuzz)
+from typing import List
+
+import pytest
+
+
+def pytest_addoption(parser: pytest.Parser):
+    parser.addoption(
+        "--runfuzz", action="store_true", default=False, help="run fuzz tests"
+    )
+
+
+def pytest_configure(config: pytest.Config):
+    config.addinivalue_line(
+        "markers", "fuzz: mark test as a slow fuzzer to not run by default"
+    )
+
+
+def pytest_collection_modifyitems(config: pytest.Config, items: List[pytest.Item]):
+    if config.getoption("--runfuzz"):
+        # --runfuzz given in cli: do not skip fuzz tests
+        return
+    skip_fuzz = pytest.mark.skip(reason="need --runfuzz option to run")
+    for item in items:
+        if "fuzz" in item.keywords:
+            item.add_marker(skip_fuzz)
diff --git a/tests/test_changelog_and_version.py b/tests/test_changelog_and_version.py
index a185ccc..e925a28 100644
--- a/tests/test_changelog_and_version.py
+++ b/tests/test_changelog_and_version.py
@@ -1,86 +1,86 @@
-"""Tests for flake8-trio package metadata."""
-import os
-import re
-import unittest
-from pathlib import Path
-from typing import Dict, Iterable, NamedTuple, Set
-
-from test_flake8_trio import trio_test_files_regex
-
-import flake8_trio
-
-
-class Version(NamedTuple):
-    year: int
-    month: int
-    patch: int
-
-    @classmethod
-    def from_string(cls, string):
-        return cls(*map(int, string.split(".")))
-
-
-def get_releases() -> Iterable[Version]:
-    valid_pattern = re.compile(r"^## (\d\d\.\d?\d\.\d?\d)$")
-    with open(Path(__file__).parent.parent / "CHANGELOG.md", encoding="utf-8") as f:
-        lines = f.readlines()
-    for aline in lines:
-        version_match = valid_pattern.match(aline)
-        if version_match:
-            yield Version.from_string(version_match.group(1))
-
-
-def test_last_release_against_changelog():
-    """Ensure we have the latest version covered in CHANGELOG.md"""
-    latest_release, *_ = get_releases()
-    assert latest_release == Version.from_string(flake8_trio.__version__)
-
-
-def test_version_increments_are_correct():
-    versions = list(get_releases())
-    for prev, current in zip(versions[1:], versions):
-        assert prev < current  # remember that `versions` is newest-first
-        msg = f"{current=} does not follow {prev=}"
-        # for CalVer, we either increment the patch version by one, or
-        # increment the time-based parts and set the patch version to one.
-        if current.patch == 1:
-            assert prev[:2] < current[:2], msg
-        else:
-            assert current == prev._replace(patch=prev.patch + 1), msg
-
-
-class test_messages_documented(unittest.TestCase):
-    def runTest(self):
-        documented_errors: Dict[str, Set[str]] = {}
-        for filename in (
-            "CHANGELOG.md",
-            "README.md",
-        ):
-            with open(Path(__file__).parent.parent / filename, encoding="utf-8") as f:
-                lines = f.readlines()
-            documented_errors[filename] = set()
-            for line in lines:
-                for error_msg in re.findall(r"TRIO\d\d\d", line):
-                    documented_errors[filename].add(error_msg)
-
-        documented_errors["flake8_trio.py"] = set(flake8_trio.Error_codes.keys())
-
-        documented_errors["tests/trio*.py"] = {
-            os.path.splitext(f)[0].upper().split("_")[0]
-            for f in os.listdir("tests")
-            if re.match(trio_test_files_regex, f)
-        }
-
-        unique_errors: Dict[str, Set[str]] = {}
-        missing_errors: Dict[str, Set[str]] = {}
-        for key, codes in documented_errors.items():
-            unique_errors[key] = codes.copy()
-            missing_errors[key] = set()
-
-            for other_key, other_codes in documented_errors.items():
-                if key == other_key:
-                    continue
-                unique_errors[key].difference_update(other_codes)
-                missing_errors[key].update(other_codes - codes)
-
-        assert unique_errors == missing_errors
+"""Tests for flake8-trio package metadata."""
+import os
+import re
+import unittest
+from pathlib import Path
+from typing import Dict, Iterable, NamedTuple, Set
+
+from test_flake8_trio import trio_test_files_regex
+
+import flake8_trio
+
+
+class Version(NamedTuple):
+    year: int
+    month: int
+    patch: int
+
+    @classmethod
+    def from_string(cls, string):
+        return cls(*map(int, string.split(".")))
+
+
+def get_releases() -> Iterable[Version]:
+    valid_pattern = re.compile(r"^## (\d\d\.\d?\d\.\d?\d)$")
+    with open(Path(__file__).parent.parent / "CHANGELOG.md", encoding="utf-8") as f:
+        lines = f.readlines()
+    for aline in lines:
+        version_match = valid_pattern.match(aline)
+        if version_match:
+            yield Version.from_string(version_match.group(1))
+
+
+def test_last_release_against_changelog():
+    """Ensure we have the latest version covered in CHANGELOG.md"""
+    latest_release, *_ = get_releases()
+    assert latest_release == Version.from_string(flake8_trio.__version__)
+
+
+def test_version_increments_are_correct():
+    versions = list(get_releases())
+    for prev, current in zip(versions[1:], versions):
+        assert prev < current  # remember that `versions` is newest-first
+        msg = f"{current=} does not follow {prev=}"
+        # for CalVer, we either increment the patch version by one, or
+        # increment the time-based parts and set the patch version to one.
+        if current.patch == 1:
+            assert prev[:2] < current[:2], msg
+        else:
+            assert current == prev._replace(patch=prev.patch + 1), msg
+
+
+class test_messages_documented(unittest.TestCase):
+    def runTest(self):
+        documented_errors: Dict[str, Set[str]] = {}
+        for filename in (
+            "CHANGELOG.md",
+            "README.md",
+        ):
+            with open(Path(__file__).parent.parent / filename, encoding="utf-8") as f:
+                lines = f.readlines()
+            documented_errors[filename] = set()
+            for line in lines:
+                for error_msg in re.findall(r"TRIO\d\d\d", line):
+                    documented_errors[filename].add(error_msg)
+
+        documented_errors["flake8_trio.py"] = set(flake8_trio.Error_codes.keys())
+
+        documented_errors["tests/trio*.py"] = {
+            os.path.splitext(f)[0].upper().split("_")[0]
+            for f in os.listdir("tests")
+            if re.match(trio_test_files_regex, f)
+        }
+
+        unique_errors: Dict[str, Set[str]] = {}
+        missing_errors: Dict[str, Set[str]] = {}
+        for key, codes in documented_errors.items():
+            unique_errors[key] = codes.copy()
+            missing_errors[key] = set()
+
+            for other_key, other_codes in documented_errors.items():
+                if key == other_key:
+                    continue
+                unique_errors[key].difference_update(other_codes)
+                missing_errors[key].update(other_codes - codes)
+
+        assert unique_errors == missing_errors
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index c3808d9..90937b8 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -1,122 +1,122 @@
-import ast
-import sys
-from argparse import Namespace
-from typing import Tuple
-
-import pytest
-from flake8.main.application import Application
-
-from flake8_trio import Error_codes, Plugin, Statement, fnmatch_qualified_name
-
-
-def dec_list(*decorators: str) -> ast.Module:
-    source = ""
-    for dec in decorators:
-        source += f"@{dec}\n"
-    source += "async def f():\n  bar()"
-    tree = ast.parse(source)
-    return tree
-
-
-def wrap(decorators: Tuple[str, ...], decs2: str) -> bool:
-    tree = dec_list(*decorators)
-    assert isinstance(tree.body[0], ast.AsyncFunctionDef)
-    return fnmatch_qualified_name(tree.body[0].decorator_list, decs2)
-
-
-def test_basic():
-    assert wrap(("foo",), "foo")
-    assert wrap(("foo", "bar"), "foo")
-    assert wrap(("bar", "foo"), "foo")
-
-    assert not wrap(("foo",), "foob")
-
-
-def test_dotted():
-    assert wrap(("foo.bar",), "foo.bar")
-
-    assert not wrap(("foo.bar",), "foo")
-    assert not wrap(("foo.bar",), "bar")
-
-    assert not wrap(("foo",), "foo.bar")
-    assert not wrap(("bar",), "foo.bar")
-
-    assert not wrap(("foo.bar.jane",), "foo.bar")
-    assert not wrap(("foo.bar",), "foo.bar.jane")
-    assert not wrap(("jane.foo.bar",), "foo.bar")
-    assert not wrap(("foo.bar",), "jane.foo.bar")
-
-
-def test_multidotted():
-    assert wrap(("foo.bar.jane",), "foo.bar.jane")
-    assert not wrap(("foo.bar",), "foo.bar.jane")
-    assert not wrap(("foo.bar.jane",), "foo.bar")
-
-
-def test_wildcard():
-    assert wrap(("foo",), "*")
-    assert wrap(("foo.bar",), "*")
-    assert not wrap(("foo",), "foo.*")
-    assert wrap(("foo.bar",), "foo.*")
-    assert not wrap(("bar.foo",), "foo.*")
-
-    assert wrap(("foo",), "foo*")
-    assert wrap(("foobar",), "foo*")
-    assert wrap(("foobar.bar",), "foo*")
-    assert wrap(("foo.bar",), "*.bar")
-
-
-def test_at():
-    assert wrap(("foo",), "@foo")
-    assert wrap(("foo.bar",), "@foo.bar")
-
-
-def test_calls():
-    assert wrap(("foo()",), "@foo")
-    assert wrap(("foo(1, 2, *x, **y)",), "@foo")
-    assert wrap(("foo.bar()",), "@foo.bar")
-    assert wrap(("foo.bar(1, 2, *x, **y)",), "@foo.bar")
-
-
-@pytest.mark.skipif(sys.version_info[:2] < (3, 9), reason="not yet supported")
-def test_pep614():
-    # Just don't crash and we'll be good.
-    assert not wrap(("(any, expression, we, like)",), "no match here")
-
-
-def test_plugin():
-    tree = dec_list("app.route")
-    plugin = Plugin(tree)
-    plugin.options = Namespace(no_checkpoint_warning_decorators=[])
-    assert tuple(plugin.run())
-
-    plugin.options = Namespace(no_checkpoint_warning_decorators=["app.route"])
-    assert not tuple(plugin.run())
-
-
-common_flags = ["--select=TRIO", "tests/trio_options.py"]
-
-
-def test_command_line_1(capfd):
-    Application().run(common_flags + ["--no-checkpoint-warning-decorators=app.route"])
-    out, err = capfd.readouterr()
-    assert not out and not err
-
-
-expected_out = (
-    "tests/trio_options.py:2:1: TRIO107: "
-    + Error_codes["TRIO107"].format("exit", Statement("function definition", 2))
-    + "\n"
-)
-
-
-def test_command_line_2(capfd):
-    Application().run(common_flags + ["--no-checkpoint-warning-decorators=app"])
-    out, err = capfd.readouterr()
-    assert out == expected_out and not err
-
-
-def test_command_line_3(capfd):
-    Application().run(common_flags)
-    out, err = capfd.readouterr()
-    assert out == expected_out and not err
+import ast
+import sys
+from argparse import Namespace
+from typing import Tuple
+
+import pytest
+from flake8.main.application import Application
+
+from flake8_trio import Error_codes, Plugin, Statement, fnmatch_qualified_name
+
+
+def dec_list(*decorators: str) -> ast.Module:
+    source = ""
+    for dec in decorators:
+        source += f"@{dec}\n"
+    source += "async def f():\n  bar()"
+    tree = ast.parse(source)
+    return tree
+
+
+def wrap(decorators: Tuple[str, ...], decs2: str) -> bool:
+    tree = dec_list(*decorators)
+    assert isinstance(tree.body[0], ast.AsyncFunctionDef)
+    return fnmatch_qualified_name(tree.body[0].decorator_list, decs2)
+
+
+def test_basic():
+    assert wrap(("foo",), "foo")
+    assert wrap(("foo", "bar"), "foo")
+    assert wrap(("bar", "foo"), "foo")
+
+    assert not wrap(("foo",), "foob")
+
+
+def test_dotted():
+    assert wrap(("foo.bar",), "foo.bar")
+
+    assert not wrap(("foo.bar",), "foo")
+    assert not wrap(("foo.bar",), "bar")
+
+    assert not wrap(("foo",), "foo.bar")
+    assert not wrap(("bar",), "foo.bar")
+
+    assert not wrap(("foo.bar.jane",), "foo.bar")
+    assert not wrap(("foo.bar",), "foo.bar.jane")
+    assert not wrap(("jane.foo.bar",), "foo.bar")
+    assert not wrap(("foo.bar",), "jane.foo.bar")
+
+
+def test_multidotted():
+    assert wrap(("foo.bar.jane",), "foo.bar.jane")
+    assert not wrap(("foo.bar",), "foo.bar.jane")
+    assert not wrap(("foo.bar.jane",), "foo.bar")
+
+
+def test_wildcard():
+    assert wrap(("foo",), "*")
+    assert wrap(("foo.bar",), "*")
+    assert not wrap(("foo",), "foo.*")
+    assert wrap(("foo.bar",), "foo.*")
+    assert not wrap(("bar.foo",), "foo.*")
+
+    assert wrap(("foo",), "foo*")
+    assert wrap(("foobar",), "foo*")
+    assert wrap(("foobar.bar",), "foo*")
+    assert wrap(("foo.bar",), "*.bar")
+
+
+def test_at():
+    assert wrap(("foo",), "@foo")
+    assert wrap(("foo.bar",), "@foo.bar")
+
+
+def test_calls():
+    assert wrap(("foo()",), "@foo")
+    assert wrap(("foo(1, 2, *x, **y)",), "@foo")
+    assert wrap(("foo.bar()",), "@foo.bar")
+    assert wrap(("foo.bar(1, 2, *x, **y)",), "@foo.bar")
+
+
+@pytest.mark.skipif(sys.version_info[:2] < (3, 9), reason="not yet supported")
+def test_pep614():
+    # Just don't crash and we'll be good.
+    assert not wrap(("(any, expression, we, like)",), "no match here")
+
+
+def test_plugin():
+    tree = dec_list("app.route")
+    plugin = Plugin(tree)
+    plugin.options = Namespace(no_checkpoint_warning_decorators=[])
+    assert tuple(plugin.run())
+
+    plugin.options = Namespace(no_checkpoint_warning_decorators=["app.route"])
+    assert not tuple(plugin.run())
+
+
+common_flags = ["--select=TRIO", "tests/trio_options.py"]
+
+
+def test_command_line_1(capfd):
+    Application().run(common_flags + ["--no-checkpoint-warning-decorators=app.route"])
+    out, err = capfd.readouterr()
+    assert not out and not err
+
+
+expected_out = (
+    "tests/trio_options.py:2:1: TRIO107: "
+    + Error_codes["TRIO107"].format("exit", Statement("function definition", 2))
+    + "\n"
+)
+
+
+def test_command_line_2(capfd):
+    Application().run(common_flags + ["--no-checkpoint-warning-decorators=app"])
+    out, err = capfd.readouterr()
+    assert out == expected_out and not err
+
+
+def test_command_line_3(capfd):
+    Application().run(common_flags)
+    out, err = capfd.readouterr()
+    assert out == expected_out and not err
diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py
index b32e5ca..f5e0410 100644
--- a/tests/test_flake8_trio.py
+++ b/tests/test_flake8_trio.py
@@ -1,412 +1,412 @@
-import ast
-import copy
-import itertools
-import os
-import re
-import site
-import sys
-import tokenize
-import unittest
-from pathlib import Path
-from typing import DefaultDict, Iterable, List, Sequence, Tuple, Type
-
-import pytest
-from flake8.options.manager import OptionManager
-
-# import trio  # type: ignore
-from hypothesis import HealthCheck, given, settings
-from hypothesmith import from_grammar, from_node
-
-from flake8_trio import Error, Error_codes, Plugin, Statement
-
-trio_test_files_regex = re.compile(r"trio\d\d\d(_py.*)?.py")
-
-test_files: List[Tuple[str, str]] = sorted(
-    (os.path.splitext(f)[0].upper(), f)
-    for f in os.listdir("tests")
-    if re.match(trio_test_files_regex, f)
-)
-
-
-class ParseError(Exception):
-    ...
-
-
-# check for presence of _pyXX, skip if version is later, and prune parameter
-def check_version(test: str) -> str:
-    python_version = re.search(r"(?<=_PY)\d*", test)
-    if python_version:
-        version_str = python_version.group()
-        major, minor = version_str[0], version_str[1:]
-        v_i = sys.version_info
-        if (v_i.major, v_i.minor) < (int(major), int(minor)):
-            raise unittest.SkipTest("v_i, major, minor")
-        return test.split("_")[0]
-    return test
-
-
-@pytest.mark.parametrize("test, path", test_files)
-def test_eval(test: str, path: str):
-    # version check
-    test = check_version(test)
-
-    assert test in Error_codes.keys(), "error code not defined in flake8_trio.py"
-
-    include = [test]
-    expected: List[Error] = []
-    with open(os.path.join("tests", path), encoding="utf-8") as file:
-        lines = file.readlines()
-
-    for lineno, line in enumerate(lines, start=1):
-        line = line.strip()
-
-        if reg_match := re.search(r"(?<=INCLUDE).*", line):
-            for other_code in reg_match.group().split(" "):
-                if other_code.strip():
-                    include.append(other_code.strip())
-
-        # skip commented out lines
-        if not line or line[0] == "#":
-            continue
-
-        # get text between `error:` and (end of line or another comment)
-        k = re.findall(r"(error|TRIO...):([^#]*)(?=#|$)", line)
-
-        for err_code, err_args in k:
-            try:
-                # Append a bunch of empty strings so string formatting gives garbage
-                # instead of throwing an exception
-                try:
-                    args = eval(
-                        f"[{err_args}]",
-                        {
-                            "lineno": lineno,
-                            "line": lineno,
-                            "Statement": Statement,
-                            "Stmt": Statement,
-                        },
-                    )
-                except NameError:
-                    print(f"failed to eval on line {lineno}", file=sys.stderr)
-                    raise
-
-            except Exception as e:
-                print(f"lineno: {lineno}, line: {line}", file=sys.stderr)
-                raise e
-            if args:
-                col, *args = args
-            else:
-                col = 0
-            assert isinstance(
-                col, int
-            ), f'invalid column "{col}" @L{lineno}, in "{line}"'
-
-            # assert col.isdigit(), f'invalid column "{col}" @L{lineno}, in "{line}"'
-            try:
-                if err_code == "error":
-                    err_code = test
-                expected.append(Error(err_code, lineno, int(col), *args))
-            except AttributeError as e:
-                msg = f'Line {lineno}: Failed to format\n "{Error_codes[test]}"\nwith\n{args}'
-                raise ParseError(msg) from e
-
-    assert expected, f"failed to parse any errors in file {path}"
-
-    plugin = read_file(path)
-    assert_expected_errors(plugin, include, *expected)
-
-
-# codes that should never error when run on sync code
-sync_errors = ["TRIO102", "TRIO107", "TRIO108", "TRIO109", "TRIO110"]
-
-
-class SyncTransformer(ast.NodeTransformer):
-    def visit_Await(self, node: ast.Await):
-        newnode = self.generic_visit(node.value)
-        return newnode
-
-    def replace_async(self, node: ast.AST, target: Type[ast.AST]) -> ast.AST:
-        node = self.generic_visit(node)
-        newnode = target()
-        newnode.__dict__ = node.__dict__
-        return newnode
-
-    def visit_AsyncFunctionDef(self, node: ast.AST):
-        return self.replace_async(node, ast.FunctionDef)
-
-    def visit_AsyncWith(self, node: ast.AST):
-        return self.replace_async(node, ast.With)
-
-    def visit_AsyncFor(self, node: ast.AST):
-        return self.replace_async(node, ast.For)
-
-
-@pytest.mark.parametrize("test, path", test_files)
-def test_noerror_on_sync_code(test: str, path: str):
-    if all(e not in test for e in sync_errors):
-        return
-    with tokenize.open(f"tests/{path}") as f:
-        source = f.read()
-    tree = SyncTransformer().visit(ast.parse(source))
-
-    assert_expected_errors(Plugin(tree), sync_errors)
-
-
-def read_file(test_file: str):
-    filename = Path(__file__).absolute().parent / test_file
-    return Plugin.from_filename(str(filename))
-
-
-def assert_expected_errors(plugin: Plugin, include: Iterable[str], *expected: Error):
-    # initialize default option values
-    om = OptionManager(
-        version="",
-        plugin_versions="",
-        parents=[],
-        formatter_names=["default"],  # type: ignore
-    )
-    plugin.add_options(om)
-    plugin.parse_options(om.parse_args(args=[""]))
-
-    errors = sorted(e for e in plugin.run() if e.code in include)
-    expected_ = sorted(expected)
-
-    print_first_diff(errors, expected_)
-    assert_correct_lines_and_codes(errors, expected_)
-    assert_correct_columns(errors, expected_)
-    assert_correct_args(errors, expected_)
-
-    # full check
-    unittest.TestCase().assertEqual(errors, expected_)
-
-    # test tuple conversion and iter types
-    assert_tuple_and_types(errors, expected_)
-
-
-def print_first_diff(errors: Sequence[Error], expected: Sequence[Error]):
-    first_error_line: List[Error] = []
-    first_expected_line: List[Error] = []
-    for err, exp in zip(errors, expected):
-        if err == exp:
-            continue
-        if not first_error_line or err.line == first_error_line[0]:
-            first_error_line.append(err)
-        if not first_expected_line or exp.line == first_expected_line[0]:
-            first_expected_line.append(exp)
-
-    if first_expected_line != first_error_line:
-        print(
-            "First lines with different errors",
-            f"  actual: {[e.cmp() for e in first_error_line]}",
-            f"expected: {[e.cmp() for e in first_expected_line]}",
-            "",
-            sep="\n",
-            file=sys.stderr,
-        )
-
-
-def assert_correct_lines_and_codes(errors: Iterable[Error], expected: Iterable[Error]):
-    MyDict = DefaultDict[int, DefaultDict[str, int]]
-    # Check that errors are on correct lines
-    all_lines = sorted({e.line for e in (*errors, *expected)})
-    error_dict: MyDict = DefaultDict(lambda: DefaultDict(int))
-    expected_dict = copy.deepcopy(error_dict)
-
-    for e in errors:
-        error_dict[e.line][e.code] += 1
-    for e in expected:
-        expected_dict[e.line][e.code] += 1
-
-    any_error = False
-    for line in all_lines:
-        if error_dict[line] == expected_dict[line]:
-            continue
-        for code in sorted({*error_dict[line], *expected_dict[line]}):
-            if not any_error:
-                print(
-                    "Lines with different # of errors:",
-                    "-" * 38,
-                    f"| line | {'code':7} | actual | expected |",
-                    sep="\n",
-                    file=sys.stderr,
-                )
-                any_error = True
-
-            print(
-                f"| {line:4}",
-                f"{code}",
-                f"{error_dict[line][code]:6}",
-                f"{expected_dict[line][code]:8} |",
-                sep=" | ",
-                file=sys.stderr,
-            )
-    assert not any_error
-
-
-def assert_correct_columns(errors: Iterable[Error], expected: Iterable[Error]):
-    # check errors have correct columns
-    col_error = False
-    for err, exp in zip(errors, expected):
-        assert err.line == exp.line
-        if err.col != exp.col:
-            if not col_error:
-                print("Errors with same line but different columns:", file=sys.stderr)
-                print("| line | actual | expected |", file=sys.stderr)
-                col_error = True
-            print(
-                f"| {err.line:4} | {err.col:6} | {exp.col:8} |",
-                file=sys.stderr,
-            )
-    assert not col_error
-
-
-def assert_correct_args(errors: Iterable[Error], expected: Iterable[Error]):
-    # check errors have correct messages
-    args_error = False
-    for err, exp in zip(errors, expected):
-        assert (err.line, err.col, err.code) == (exp.line, exp.col, exp.code)
-        if err.args != exp.args:
-            if not args_error:
-                print(
-                    "Errors with different args:",
-                    "-" * 20,
-                    sep="\n",
-                    file=sys.stderr,
-                )
-                args_error = True
-            print(
-                f"*    line: {err.line:3} differs\n",
-                f"  actual: {err.args}\n",
-                f"expected: {exp.args}\n",
-                "-" * 20,
-                file=sys.stderr,
-            )
-    assert not args_error
-
-
-def assert_tuple_and_types(errors: Iterable[Error], expected: Iterable[Error]):
-    def info_tuple(error: Error):
-        try:
-            return tuple(error)
-        except IndexError:
-            print(
-                "Failed to format error message",
-                f"line: {error.line}",
-                f"col: {error.col}",
-                f"code: {error.code}",
-                f"args: {error.args}",
-                f'format string: "{Error_codes[error.code]}"',
-                sep="\n    ",
-                file=sys.stderr,
-            )
-            raise
-
-    for err, exp in zip(errors, expected):
-        err_msg = info_tuple(err)
-        for err, type_ in zip(err_msg, (int, int, str, type)):
-            assert isinstance(err, type_)
-        assert err_msg == info_tuple(exp)
-
-
-def test_107_permutations():
-    # since each test is so fast, and there's so many permutations, manually doing
-    # the permutations in a single test is much faster than the permutations from using
-    # pytest parametrization - and does not clutter up the output massively.
-    check = "await foo()"
-    for try_, exc1, exc2, bare_exc, else_, finally_ in itertools.product(
-        (check, "..."),
-        (check, "...", "raise", "return", None),
-        (check, "...", "raise", "return", None),
-        (check, "...", "raise", "return", None),
-        (check, "...", "return", None),
-        (check, "...", "return", None),
-    ):
-        if exc1 is None and exc2 is not None:
-            continue
-
-        function_str = f"async def foo():\n  try:\n    {try_}\n"
-
-        for arg, val in {
-            "except ValueError": exc1,
-            "except SyntaxError": exc2,
-            "except": bare_exc,
-            "else": else_,
-            "finally": finally_,
-        }.items():
-            if val is not None:
-                function_str += f"  {arg}:\n    {val}\n"
-
-        try:
-            tree = ast.parse(function_str)
-        except Exception:
-            assert exc1 is exc2 is bare_exc is None and (
-                finally_ is None or else_ is not None
-            )
-            return
-
-        errors = [e for e in Plugin(tree).run() if e.code == "TRIO107"]
-
-        if (
-            # return in exception
-            "return" in (exc1, exc2, bare_exc)
-            # exception and finally doesn't checkpoint, checkpoint in try might not run
-            or ("..." in (exc1, exc2, bare_exc) and finally_ != check)
-            # no checkpoints in normal control flow
-            or check not in (try_, finally_, else_)
-            # return in else|finally w/o checkpoint before
-            or ("return" in (else_, finally_) and check not in (else_, try_))
-            # return in finally with no bare exception, checkpoint in try might not run
-            or (finally_ == "return" and bare_exc is None)
-        ):
-            assert errors, "# missing alarm:\n" + function_str
-        else:
-            assert not errors, "# false alarm:\n" + function_str
-
-
-def test_113_options():
-    # check that no errors are given by default
-    plugin = read_file("trio113.py")
-    om = OptionManager(
-        version="",
-        plugin_versions="",
-        parents=[],
-        formatter_names=["default"],  # type: ignore
-    )
-    plugin.add_options(om)
-    plugin.parse_options(om.parse_args(args=["--startable-in-context-manager=''"]))
-    default = {repr(e) for e in plugin.run() if e.code == "TRIO113"}
-
-    # and that the expected errors are given if we empty it and then extend it
-    arg = "--startable-in-context-manager=custom_startable_function"
-    plugin.parse_options(om.parse_args(args=[arg]))
-    errors = {repr(e) for e in plugin.run() if e.code == "TRIO113"} - default
-    assert errors == {repr(Error("TRIO113", 58, 8))}
-
-
-@pytest.mark.fuzz
-class TestFuzz(unittest.TestCase):
-    @settings(max_examples=1_000, suppress_health_check=[HealthCheck.too_slow])
-    @given((from_grammar() | from_node()).map(ast.parse))
-    def test_does_not_crash_on_any_valid_code(self, syntax_tree: ast.AST):
-        # Given any syntatically-valid source code, the checker should
-        # not crash.  This tests doesn't check that we do the *right* thing,
-        # just that we don't crash on valid-if-poorly-styled code!
-        Plugin(syntax_tree).run()
-
-    @staticmethod
-    def _iter_python_files():
-        # Because the generator isn't perfect, we'll also test on all the code
-        # we can easily find in our current Python environment - this includes
-        # the standard library, and all installed packages.
-        for base in sorted(set(site.PREFIXES)):
-            for dirname, _, files in os.walk(base):
-                for f in files:
-                    if f.endswith(".py"):
-                        yield Path(dirname) / f
-
-    def test_does_not_crash_on_site_code(self):
-        for path in self._iter_python_files():
-            try:
-                Plugin.from_filename(str(path)).run()
-            except Exception as err:
-                raise AssertionError(f"Failed on {path}") from err
+import ast
+import copy
+import itertools
+import os
+import re
+import site
+import sys
+import tokenize
+import unittest
+from pathlib import Path
+from typing import DefaultDict, Iterable, List, Sequence, Tuple, Type
+
+import pytest
+from flake8.options.manager import OptionManager
+
+# import trio  # type: ignore
+from hypothesis import HealthCheck, given, settings
+from hypothesmith import from_grammar, from_node
+
+from flake8_trio import Error, Error_codes, Plugin, Statement
+
+trio_test_files_regex = re.compile(r"trio\d\d\d(_py.*)?.py")
+
+test_files: List[Tuple[str, str]] = sorted(
+    (os.path.splitext(f)[0].upper(), f)
+    for f in os.listdir("tests")
+    if re.match(trio_test_files_regex, f)
+)
+
+
+class ParseError(Exception):
+    ...
+
+
+# check for presence of _pyXX, skip if version is later, and prune parameter
+def check_version(test: str) -> str:
+    python_version = re.search(r"(?<=_PY)\d*", test)
+    if python_version:
+        version_str = python_version.group()
+        major, minor = version_str[0], version_str[1:]
+        v_i = sys.version_info
+        if (v_i.major, v_i.minor) < (int(major), int(minor)):
+            raise unittest.SkipTest("v_i, major, minor")
+        return test.split("_")[0]
+    return test
+
+
+@pytest.mark.parametrize("test, path", test_files)
+def test_eval(test: str, path: str):
+    # version check
+    test = check_version(test)
+
+    assert test in Error_codes.keys(), "error code not defined in flake8_trio.py"
+
+    include = [test]
+    expected: List[Error] = []
+    with open(os.path.join("tests", path), encoding="utf-8") as file:
+        lines = file.readlines()
+
+    for lineno, line in enumerate(lines, start=1):
+        line = line.strip()
+
+        if reg_match := re.search(r"(?<=INCLUDE).*", line):
+            for other_code in reg_match.group().split(" "):
+                if other_code.strip():
+                    include.append(other_code.strip())
+
+        # skip commented out lines
+        if not line or line[0] == "#":
+            continue
+
+        # get text between `error:` and (end of line or another comment)
+        k = re.findall(r"(error|TRIO...):([^#]*)(?=#|$)", line)
+
+        for err_code, err_args in k:
+            try:
+                # Append a bunch of empty strings so string formatting gives garbage
+                # instead of throwing an exception
+                try:
+                    args = eval(
+                        f"[{err_args}]",
+                        {
+                            "lineno": lineno,
+                            "line": lineno,
+                            "Statement": Statement,
+                            "Stmt": Statement,
+                        },
+                    )
+                except NameError:
+                    print(f"failed to eval on line {lineno}", file=sys.stderr)
+                    raise
+
+            except Exception as e:
+                print(f"lineno: {lineno}, line: {line}", file=sys.stderr)
+                raise e
+            if args:
+                col, *args = args
+            else:
+                col = 0
+            assert isinstance(
+                col, int
+            ), f'invalid column "{col}" @L{lineno}, in "{line}"'
+
+            # assert col.isdigit(), f'invalid column "{col}" @L{lineno}, in "{line}"'
+            try:
+                if err_code == "error":
+                    err_code = test
+                expected.append(Error(err_code, lineno, int(col), *args))
+            except AttributeError as e:
+                msg = f'Line {lineno}: Failed to format\n "{Error_codes[test]}"\nwith\n{args}'
+                raise ParseError(msg) from e
+
+    assert expected, f"failed to parse any errors in file {path}"
+
+    plugin = read_file(path)
+    assert_expected_errors(plugin, include, *expected)
+
+
+# codes that should never error when run on sync code
+sync_errors = ["TRIO102", "TRIO107", "TRIO108", "TRIO109", "TRIO110"]
+
+
+class SyncTransformer(ast.NodeTransformer):
+    def visit_Await(self, node: ast.Await):
+        newnode = self.generic_visit(node.value)
+        return newnode
+
+    def replace_async(self, node: ast.AST, target: Type[ast.AST]) -> ast.AST:
+        node = self.generic_visit(node)
+        newnode = target()
+        newnode.__dict__ = node.__dict__
+        return newnode
+
+    def visit_AsyncFunctionDef(self, node: ast.AST):
+        return self.replace_async(node, ast.FunctionDef)
+
+    def visit_AsyncWith(self, node: ast.AST):
+        return self.replace_async(node, ast.With)
+
+    def visit_AsyncFor(self, node: ast.AST):
+        return self.replace_async(node, ast.For)
+
+
+@pytest.mark.parametrize("test, path", test_files)
+def test_noerror_on_sync_code(test: str, path: str):
+    if all(e not in test for e in sync_errors):
+        return
+    with tokenize.open(f"tests/{path}") as f:
+        source = f.read()
+    tree = SyncTransformer().visit(ast.parse(source))
+
+    assert_expected_errors(Plugin(tree), sync_errors)
+
+
+def read_file(test_file: str):
+    filename = Path(__file__).absolute().parent / test_file
+    return Plugin.from_filename(str(filename))
+
+
+def assert_expected_errors(plugin: Plugin, include: Iterable[str], *expected: Error):
+    # initialize default option values
+    om = OptionManager(
+        version="",
+        plugin_versions="",
+        parents=[],
+        formatter_names=["default"],  # type: ignore
+    )
+    plugin.add_options(om)
+    plugin.parse_options(om.parse_args(args=[""]))
+
+    errors = sorted(e for e in plugin.run() if e.code in include)
+    expected_ = sorted(expected)
+
+    print_first_diff(errors, expected_)
+    assert_correct_lines_and_codes(errors, expected_)
+    assert_correct_columns(errors, expected_)
+    assert_correct_args(errors, expected_)
+
+    # full check
+    unittest.TestCase().assertEqual(errors, expected_)
+
+    # test tuple conversion and iter types
+    assert_tuple_and_types(errors, expected_)
+
+
+def print_first_diff(errors: Sequence[Error], expected: Sequence[Error]):
+    first_error_line: List[Error] = []
+    first_expected_line: List[Error] = []
+    for err, exp in zip(errors, expected):
+        if err == exp:
+            continue
+        if not first_error_line or err.line == first_error_line[0]:
+            first_error_line.append(err)
+        if not first_expected_line or exp.line == first_expected_line[0]:
+            first_expected_line.append(exp)
+
+    if first_expected_line != first_error_line:
+        print(
+            "First lines with different errors",
+            f"  actual: {[e.cmp() for e in first_error_line]}",
+            f"expected: {[e.cmp() for e in first_expected_line]}",
+            "",
+            sep="\n",
+            file=sys.stderr,
+        )
+
+
+def assert_correct_lines_and_codes(errors: Iterable[Error], expected: Iterable[Error]):
+    MyDict = DefaultDict[int, DefaultDict[str, int]]
+    # Check that errors are on correct lines
+    all_lines = sorted({e.line for e in (*errors, *expected)})
+    error_dict: MyDict = DefaultDict(lambda: DefaultDict(int))
+    expected_dict = copy.deepcopy(error_dict)
+
+    for e in errors:
+        error_dict[e.line][e.code] += 1
+    for e in expected:
+        expected_dict[e.line][e.code] += 1
+
+    any_error = False
+    for line in all_lines:
+        if error_dict[line] == expected_dict[line]:
+            continue
+        for code in sorted({*error_dict[line], *expected_dict[line]}):
+            if not any_error:
+                print(
+                    "Lines with different # of errors:",
+                    "-" * 38,
+                    f"| line | {'code':7} | actual | expected |",
+                    sep="\n",
+                    file=sys.stderr,
+                )
+                any_error = True
+
+            print(
+                f"| {line:4}",
+                f"{code}",
+                f"{error_dict[line][code]:6}",
+                f"{expected_dict[line][code]:8} |",
+                sep=" | ",
+                file=sys.stderr,
+            )
+    assert not any_error
+
+
+def assert_correct_columns(errors: Iterable[Error], expected: Iterable[Error]):
+    # check errors have correct columns
+    col_error = False
+    for err, exp in zip(errors, expected):
+        assert err.line == exp.line
+        if err.col != exp.col:
+            if not col_error:
+                print("Errors with same line but different columns:", file=sys.stderr)
+                print("| line | actual | expected |", file=sys.stderr)
+                col_error = True
+            print(
+                f"| {err.line:4} | {err.col:6} | {exp.col:8} |",
+                file=sys.stderr,
+            )
+    assert not col_error
+
+
+def assert_correct_args(errors: Iterable[Error], expected: Iterable[Error]):
+    # check errors have correct messages
+    args_error = False
+    for err, exp in zip(errors, expected):
+        assert (err.line, err.col, err.code) == (exp.line, exp.col, exp.code)
+        if err.args != exp.args:
+            if not args_error:
+                print(
+                    "Errors with different args:",
+                    "-" * 20,
+                    sep="\n",
+                    file=sys.stderr,
+                )
+                args_error = True
+            print(
+                f"*    line: {err.line:3} differs\n",
+                f"  actual: {err.args}\n",
+                f"expected: {exp.args}\n",
+                "-" * 20,
+                file=sys.stderr,
+            )
+    assert not args_error
+
+
+def assert_tuple_and_types(errors: Iterable[Error], expected: Iterable[Error]):
+    def info_tuple(error: Error):
+        try:
+            return tuple(error)
+        except IndexError:
+            print(
+                "Failed to format error message",
+                f"line: {error.line}",
+                f"col: {error.col}",
+                f"code: {error.code}",
+                f"args: {error.args}",
+                f'format string: "{Error_codes[error.code]}"',
+                sep="\n    ",
+                file=sys.stderr,
+            )
+            raise
+
+    for err, exp in zip(errors, expected):
+        err_msg = info_tuple(err)
+        for err, type_ in zip(err_msg, (int, int, str, type)):
+            assert isinstance(err, type_)
+        assert err_msg == info_tuple(exp)
+
+
+def test_107_permutations():
+    # since each test is so fast, and there's so many permutations, manually doing
+    # the permutations in a single test is much faster than the permutations from using
+    # pytest parametrization - and does not clutter up the output massively.
+    check = "await foo()"
+    for try_, exc1, exc2, bare_exc, else_, finally_ in itertools.product(
+        (check, "..."),
+        (check, "...", "raise", "return", None),
+        (check, "...", "raise", "return", None),
+        (check, "...", "raise", "return", None),
+        (check, "...", "return", None),
+        (check, "...", "return", None),
+    ):
+        if exc1 is None and exc2 is not None:
+            continue
+
+        function_str = f"async def foo():\n  try:\n    {try_}\n"
+
+        for arg, val in {
+            "except ValueError": exc1,
+            "except SyntaxError": exc2,
+            "except": bare_exc,
+            "else": else_,
+            "finally": finally_,
+        }.items():
+            if val is not None:
+                function_str += f"  {arg}:\n    {val}\n"
+
+        try:
+            tree = ast.parse(function_str)
+        except Exception:
+            assert exc1 is exc2 is bare_exc is None and (
+                finally_ is None or else_ is not None
+            )
+            return
+
+        errors = [e for e in Plugin(tree).run() if e.code == "TRIO107"]
+
+        if (
+            # return in exception
+            "return" in (exc1, exc2, bare_exc)
+            # exception and finally doesn't checkpoint, checkpoint in try might not run
+            or ("..." in (exc1, exc2, bare_exc) and finally_ != check)
+            # no checkpoints in normal control flow
+            or check not in (try_, finally_, else_)
+            # return in else|finally w/o checkpoint before
+            or ("return" in (else_, finally_) and check not in (else_, try_))
+            # return in finally with no bare exception, checkpoint in try might not run
+            or (finally_ == "return" and bare_exc is None)
+        ):
+            assert errors, "# missing alarm:\n" + function_str
+        else:
+            assert not errors, "# false alarm:\n" + function_str
+
+
+def test_113_options():
+    # check that no errors are given by default
+    plugin = read_file("trio113.py")
+    om = OptionManager(
+        version="",
+        plugin_versions="",
+        parents=[],
+        formatter_names=["default"],  # type: ignore
+    )
+    plugin.add_options(om)
+    plugin.parse_options(om.parse_args(args=["--startable-in-context-manager=''"]))
+    default = {repr(e) for e in plugin.run() if e.code == "TRIO113"}
+
+    # and that the expected errors are given if we empty it and then extend it
+    arg = "--startable-in-context-manager=custom_startable_function"
+    plugin.parse_options(om.parse_args(args=[arg]))
+    errors = {repr(e) for e in plugin.run() if e.code == "TRIO113"} - default
+    assert errors == {repr(Error("TRIO113", 58, 8))}
+
+
+@pytest.mark.fuzz
+class TestFuzz(unittest.TestCase):
+    @settings(max_examples=1_000, suppress_health_check=[HealthCheck.too_slow])
+    @given((from_grammar() | from_node()).map(ast.parse))
+    def test_does_not_crash_on_any_valid_code(self, syntax_tree: ast.AST):
+        # Given any syntatically-valid source code, the checker should
+        # not crash.  This tests doesn't check that we do the *right* thing,
+        # just that we don't crash on valid-if-poorly-styled code!
+        Plugin(syntax_tree).run()
+
+    @staticmethod
+    def _iter_python_files():
+        # Because the generator isn't perfect, we'll also test on all the code
+        # we can easily find in our current Python environment - this includes
+        # the standard library, and all installed packages.
+        for base in sorted(set(site.PREFIXES)):
+            for dirname, _, files in os.walk(base):
+                for f in files:
+                    if f.endswith(".py"):
+                        yield Path(dirname) / f
+
+    def test_does_not_crash_on_site_code(self):
+        for path in self._iter_python_files():
+            try:
+                Plugin.from_filename(str(path)).run()
+            except Exception as err:
+                raise AssertionError(f"Failed on {path}") from err
diff --git a/tests/trio100.py b/tests/trio100.py
index 9ea6820..44687ae 100644
--- a/tests/trio100.py
+++ b/tests/trio100.py
@@ -1,35 +1,35 @@
-import trio
-
-with trio.move_on_after(10):  # error: 5,"trio.move_on_after"
-    pass
-
-
-async def function_name():
-    async with trio.fail_after(10):  # error: 15,"trio.fail_after"
-        pass
-
-    with trio.move_on_after(10):
-        await trio.sleep(1)
-
-    with trio.move_on_after(10):
-        await trio.sleep(1)
-        print("hello")
-
-    with trio.move_on_after(10):
-        while True:
-            await trio.sleep(1)
-        print("hello")
-
-    with open("filename") as _:
-        pass
-
-    with trio.fail_after(10):  # error: 9,"trio.fail_after"
-        pass
-
-    send_channel, receive_channel = trio.open_memory_channel(0)
-    async with trio.fail_after(10):
-        async with send_channel:
-            pass
-    async with trio.fail_after(10):
-        async for _ in receive_channel:
-            pass
+import trio
+
+with trio.move_on_after(10):  # error: 5,"trio.move_on_after"
+    pass
+
+
+async def function_name():
+    async with trio.fail_after(10):  # error: 15,"trio.fail_after"
+        pass
+
+    with trio.move_on_after(10):
+        await trio.sleep(1)
+
+    with trio.move_on_after(10):
+        await trio.sleep(1)
+        print("hello")
+
+    with trio.move_on_after(10):
+        while True:
+            await trio.sleep(1)
+        print("hello")
+
+    with open("filename") as _:
+        pass
+
+    with trio.fail_after(10):  # error: 9,"trio.fail_after"
+        pass
+
+    send_channel, receive_channel = trio.open_memory_channel(0)
+    async with trio.fail_after(10):
+        async with send_channel:
+            pass
+    async with trio.fail_after(10):
+        async for _ in receive_channel:
+            pass
diff --git a/tests/trio100_py39.py b/tests/trio100_py39.py
index d2ee276..8f555c2 100644
--- a/tests/trio100_py39.py
+++ b/tests/trio100_py39.py
@@ -1,16 +1,16 @@
-import trio
-
-
-async def function_name():
-    with (
-        open("") as _,
-        trio.fail_after(10),  # error: 8, "trio.fail_after"
-    ):
-        pass
-
-    with (
-        trio.fail_after(5),  # error: 8, "trio.fail_after"
-        open("") as _,
-        trio.move_on_after(5),  # error: 8, "trio.move_on_after"
-    ):
-        pass
+import trio
+
+
+async def function_name():
+    with (
+        open("") as _,
+        trio.fail_after(10),  # error: 8, "trio.fail_after"
+    ):
+        pass
+
+    with (
+        trio.fail_after(5),  # error: 8, "trio.fail_after"
+        open("") as _,
+        trio.move_on_after(5),  # error: 8, "trio.move_on_after"
+    ):
+        pass
diff --git a/tests/trio101.py b/tests/trio101.py
index d8fecc0..0815d93 100644
--- a/tests/trio101.py
+++ b/tests/trio101.py
@@ -1,58 +1,58 @@
-import contextlib
-import contextlib as bla
-from contextlib import asynccontextmanager, contextmanager, contextmanager as blahabla
-
-import trio
-
-
-def foo0():
-    with trio.open_nursery() as _:
-        yield 1  # error: 8
-
-
-async def foo1():
-    async with trio.open_nursery() as _:
-        yield 1  # error: 8
-
-
-@contextmanager
-def foo2():
-    with trio.open_nursery() as _:
-        yield 1  # safe
-
-
-async def foo3():
-    async with trio.CancelScope() as _:
-        yield 1  # error: 8
-
-
-@asynccontextmanager
-async def foo4():
-    async with trio.open_nursery() as _:
-        yield 1  # safe
-
-
-async def foo5():
-    async with trio.open_nursery():
-        yield 1  # error: 8
-
-        def foo6():
-            yield 1  # safe
-
-
-@contextlib.asynccontextmanager
-async def foo7():
-    async with trio.open_nursery() as _:
-        yield 1  # safe
-
-
-@bla.contextmanager
-def foo8():
-    with trio.open_nursery() as _:
-        yield 1  # safe
-
-
-@blahabla
-def foo9():
-    with trio.open_nursery() as _:
-        yield 1  # error: 8
+import contextlib
+import contextlib as bla
+from contextlib import asynccontextmanager, contextmanager, contextmanager as blahabla
+
+import trio
+
+
+def foo0():
+    with trio.open_nursery() as _:
+        yield 1  # error: 8
+
+
+async def foo1():
+    async with trio.open_nursery() as _:
+        yield 1  # error: 8
+
+
+@contextmanager
+def foo2():
+    with trio.open_nursery() as _:
+        yield 1  # safe
+
+
+async def foo3():
+    async with trio.CancelScope() as _:
+        yield 1  # error: 8
+
+
+@asynccontextmanager
+async def foo4():
+    async with trio.open_nursery() as _:
+        yield 1  # safe
+
+
+async def foo5():
+    async with trio.open_nursery():
+        yield 1  # error: 8
+
+        def foo6():
+            yield 1  # safe
+
+
+@contextlib.asynccontextmanager
+async def foo7():
+    async with trio.open_nursery() as _:
+        yield 1  # safe
+
+
+@bla.contextmanager
+def foo8():
+    with trio.open_nursery() as _:
+        yield 1  # safe
+
+
+@blahabla
+def foo9():
+    with trio.open_nursery() as _:
+        yield 1  # error: 8
diff --git a/tests/trio102.py b/tests/trio102.py
index f68b508..143d8df 100644
--- a/tests/trio102.py
+++ b/tests/trio102.py
@@ -1,198 +1,198 @@
-from contextlib import asynccontextmanager
-
-import trio
-
-
-async def foo():
-    try:
-        ...
-    finally:
-        with trio.move_on_after(deadline=30) as s:
-            s.shield = True
-            await foo()
-
-    try:
-        pass
-    finally:
-        with trio.move_on_after(30) as s:
-            s.shield = True
-            await foo()
-
-    try:
-        pass
-    finally:
-        await foo()  # error: 8, Statement("try/finally", lineno-3)
-
-    try:
-        pass
-    finally:
-        with trio.move_on_after(30) as s:
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-
-    try:
-        pass
-    finally:
-        with trio.move_on_after(30):
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-
-    bar = 10
-
-    try:
-        pass
-    finally:
-        with trio.move_on_after(bar) as s:
-            s.shield = True
-            await foo()
-
-    try:
-        pass
-    finally:
-        with trio.move_on_after(bar) as s:
-            s.shield = False
-            s.shield = True
-            await foo()
-
-    try:
-        pass
-    finally:
-        with trio.move_on_after(bar) as s:
-            s.shield = True
-            await foo()
-            s.shield = False
-            await foo()  # error: 12, Statement("try/finally", lineno-7)
-            s.shield = True
-            await foo()
-
-    try:
-        pass
-    finally:
-        with open("bar"):
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-    try:
-        pass
-    finally:
-        with open("bar"):
-            pass
-    try:
-        pass
-    finally:
-        with trio.move_on_after():
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-    try:
-        pass
-    finally:
-        with trio.move_on_after(foo=bar):
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-    try:
-        pass
-    finally:
-        with trio.CancelScope(deadline=30, shield=True):
-            await foo()  # safe
-    try:
-        pass
-    finally:
-        with trio.CancelScope(shield=True):
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-    try:
-        pass
-    finally:
-        with trio.CancelScope(deadline=30):
-            await foo()  # error: 12, Statement("try/finally", lineno-4)
-    try:
-        pass
-    finally:
-        with trio.CancelScope(deadline=30, shield=(1 == 1)):
-            await foo()  # safe in theory, error: 12, Statement("try/finally", lineno-4)
-    try:
-        pass
-    finally:
-        myvar = True
-        with trio.open_nursery(10) as s:
-            s.shield = myvar
-            await foo()  # safe in theory, error: 12, Statement("try/finally", lineno-6)
-    try:
-        pass
-    finally:
-        with trio.CancelScope(deadline=30, shield=True):
-            with trio.move_on_after(30):
-                await foo()  # safe
-    try:
-        pass
-    finally:
-        async for i in trio.bypasslinters:  # error: 8, Statement("try/finally", lineno-3)
-            pass
-    try:
-        pass
-    finally:
-        async with trio.CancelScope(  # error: 8, Statement("try/finally", lineno-3)
-            deadline=30, shield=True
-        ):
-            await foo()  # safe
-
-    with trio.CancelScope(deadline=30, shield=True):
-        try:
-            pass
-        finally:
-            await foo()  # error: 12, Statement("try/finally", lineno-3)
-
-
-# change of functionality, no longer treated as safe
-# https://github.com/Zac-HD/flake8-trio/issues/54
-@asynccontextmanager
-async def foo2():
-    try:
-        yield 1
-    finally:
-        await foo()  # error: 8, Statement("try/finally", lineno-3)
-
-
-async def foo3():
-    try:
-        ...
-    finally:
-        with trio.move_on_after(30) as s, trio.fail_after(5):
-            s.shield = True
-            await foo()  # safe
-        with open(""), trio.CancelScope(deadline=30, shield=True):
-            await foo()  # safe
-        with trio.fail_after(5), trio.move_on_after(30) as s:
-            s.shield = True
-            await foo()  # safe in theory, error: 12, Statement("try/finally", lineno-10)
-
-
-# New: except cancelled/baseexception are also critical
-async def foo4():
-    try:
-        ...
-    except ValueError:
-        await foo()  # safe
-    except trio.Cancelled:
-        await foo()  # error: 8, Statement("trio.Cancelled", lineno-1)
-    except BaseException:
-        await foo()  # error: 8, Statement("BaseException", lineno-1)
-    except:
-        await foo()  # error: 8, Statement("bare except", lineno-1)
-
-
-async def foo5():
-    try:
-        ...
-    except trio.Cancelled:
-        with trio.CancelScope(deadline=30, shield=True):
-            await foo()  # safe
-    except BaseException:
-        with trio.CancelScope(deadline=30, shield=True):
-            await foo()  # safe
-    except:
-        with trio.CancelScope(deadline=30, shield=True):
-            await foo()  # safe
-
-
-# multiple errors on same line
-# fmt: off
-async def foo6():
-    try:
-        ...
-    except trio.Cancelled:
-        _ = await foo(), await foo()  # error: 12, Statement("trio.Cancelled", lineno-1) # error: 25, Statement("trio.Cancelled", lineno-1)
-# fmt: on
+from contextlib import asynccontextmanager
+
+import trio
+
+
+async def foo():
+    try:
+        ...
+    finally:
+        with trio.move_on_after(deadline=30) as s:
+            s.shield = True
+            await foo()
+
+    try:
+        pass
+    finally:
+        with trio.move_on_after(30) as s:
+            s.shield = True
+            await foo()
+
+    try:
+        pass
+    finally:
+        await foo()  # error: 8, Statement("try/finally", lineno-3)
+
+    try:
+        pass
+    finally:
+        with trio.move_on_after(30) as s:
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+
+    try:
+        pass
+    finally:
+        with trio.move_on_after(30):
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+
+    bar = 10
+
+    try:
+        pass
+    finally:
+        with trio.move_on_after(bar) as s:
+            s.shield = True
+            await foo()
+
+    try:
+        pass
+    finally:
+        with trio.move_on_after(bar) as s:
+            s.shield = False
+            s.shield = True
+            await foo()
+
+    try:
+        pass
+    finally:
+        with trio.move_on_after(bar) as s:
+            s.shield = True
+            await foo()
+            s.shield = False
+            await foo()  # error: 12, Statement("try/finally", lineno-7)
+            s.shield = True
+            await foo()
+
+    try:
+        pass
+    finally:
+        with open("bar"):
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+    try:
+        pass
+    finally:
+        with open("bar"):
+            pass
+    try:
+        pass
+    finally:
+        with trio.move_on_after():
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+    try:
+        pass
+    finally:
+        with trio.move_on_after(foo=bar):
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+    try:
+        pass
+    finally:
+        with trio.CancelScope(deadline=30, shield=True):
+            await foo()  # safe
+    try:
+        pass
+    finally:
+        with trio.CancelScope(shield=True):
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+    try:
+        pass
+    finally:
+        with trio.CancelScope(deadline=30):
+            await foo()  # error: 12, Statement("try/finally", lineno-4)
+    try:
+        pass
+    finally:
+        with trio.CancelScope(deadline=30, shield=(1 == 1)):
+            await foo()  # safe in theory, error: 12, Statement("try/finally", lineno-4)
+    try:
+        pass
+    finally:
+        myvar = True
+        with trio.open_nursery(10) as s:
+            s.shield = myvar
+            await foo()  # safe in theory, error: 12, Statement("try/finally", lineno-6)
+    try:
+        pass
+    finally:
+        with trio.CancelScope(deadline=30, shield=True):
+            with trio.move_on_after(30):
+                await foo()  # safe
+    try:
+        pass
+    finally:
+        async for i in trio.bypasslinters:  # error: 8, Statement("try/finally", lineno-3)
+            pass
+    try:
+        pass
+    finally:
+        async with trio.CancelScope(  # error: 8, Statement("try/finally", lineno-3)
+            deadline=30, shield=True
+        ):
+            await foo()  # safe
+
+    with trio.CancelScope(deadline=30, shield=True):
+        try:
+            pass
+        finally:
+            await foo()  # error: 12, Statement("try/finally", lineno-3)
+
+
+# change of functionality, no longer treated as safe
+# https://github.com/Zac-HD/flake8-trio/issues/54
+@asynccontextmanager
+async def foo2():
+    try:
+        yield 1
+    finally:
+        await foo()  # error: 8, Statement("try/finally", lineno-3)
+
+
+async def foo3():
+    try:
+        ...
+    finally:
+        with trio.move_on_after(30) as s, trio.fail_after(5):
+            s.shield = True
+            await foo()  # safe
+        with open(""), trio.CancelScope(deadline=30, shield=True):
+            await foo()  # safe
+        with trio.fail_after(5), trio.move_on_after(30) as s:
+            s.shield = True
+            await foo()  # safe in theory, error: 12, Statement("try/finally", lineno-10)
+
+
+# New: except cancelled/baseexception are also critical
+async def foo4():
+    try:
+        ...
+    except ValueError:
+        await foo()  # safe
+    except trio.Cancelled:
+        await foo()  # error: 8, Statement("trio.Cancelled", lineno-1)
+    except BaseException:
+        await foo()  # error: 8, Statement("BaseException", lineno-1)
+    except:
+        await foo()  # error: 8, Statement("bare except", lineno-1)
+
+
+async def foo5():
+    try:
+        ...
+    except trio.Cancelled:
+        with trio.CancelScope(deadline=30, shield=True):
+            await foo()  # safe
+    except BaseException:
+        with trio.CancelScope(deadline=30, shield=True):
+            await foo()  # safe
+    except:
+        with trio.CancelScope(deadline=30, shield=True):
+            await foo()  # safe
+
+
+# multiple errors on same line
+# fmt: off
+async def foo6():
+    try:
+        ...
+    except trio.Cancelled:
+        _ = await foo(), await foo()  # error: 12, Statement("trio.Cancelled", lineno-1) # error: 25, Statement("trio.Cancelled", lineno-1)
+# fmt: on
diff --git a/tests/trio103.py b/tests/trio103.py
index 539c639..a1ad29f 100644
--- a/tests/trio103.py
+++ b/tests/trio103.py
@@ -1,168 +1,168 @@
-from typing import Any
-
-import trio
-
-
-def foo() -> Any:
-    ...
-
-
-try:
-    pass
-except (SyntaxError, ValueError, BaseException):
-    raise
-except (SyntaxError, ValueError, trio.Cancelled) as p:  # error: 33, "trio.Cancelled"
-    pass
-except (SyntaxError, ValueError):
-    raise
-except trio.Cancelled as e:
-    raise e
-except trio.Cancelled as e:
-    raise  # acceptable - see https://peps.python.org/pep-0678/#example-usage
-except trio.Cancelled:  # error: 7, "trio.Cancelled"
-    pass
-
-# if
-except BaseException as e:  # error: 7, "BaseException"
-    if True:
-        raise e
-    elif True:
-        pass
-    else:
-        raise e
-except BaseException:  # error: 7, "BaseException"
-    if True:
-        raise
-except BaseException:  # safe
-    if True:
-        raise
-    elif True:
-        raise
-    else:
-        raise
-
-# loops
-# raises inside the body are never guaranteed to run and are ignored
-except trio.Cancelled:  # error: 7, "trio.Cancelled"
-    while foo():
-        raise
-
-# raise inside else are guaranteed to run, unless there's a break
-except trio.Cancelled:
-    while ...:
-        ...
-    else:
-        raise
-except trio.Cancelled:
-    for _ in "":
-        ...
-    else:
-        raise
-except BaseException:  # error: 7, "BaseException"
-    while ...:
-        if ...:
-            break
-        raise
-    else:
-        raise
-except BaseException:  # error: 7, "BaseException"
-    for _ in "":
-        if ...:
-            break
-        raise
-    else:
-        raise
-# ensure we don't ignore previous guaranteed raise (although that's unreachable code)
-except BaseException:
-    raise
-    for _ in "":
-        if ...:
-            break
-        raise
-    else:
-        raise
-
-# nested try
-# in theory safe if the try, and all excepts raises - and there's a bare except.
-# But is a very weird pattern that we don't handle.
-except BaseException as e:  # error: 7, "BaseException"
-    try:
-        raise e
-    except ValueError:
-        raise e
-    except:
-        raise e  # though sometimes okay, TRIO104
-except BaseException:  # safe
-    try:
-        pass
-    finally:
-        raise
-# check that nested non-critical exceptions are ignored
-except BaseException:
-    try:
-        pass
-    except ValueError:
-        pass  # safe
-    raise
-# check that name isn't lost
-except trio.Cancelled as e:
-    try:
-        pass
-    except BaseException as f:
-        raise f
-    raise e
-# don't bypass raise by raising from nested except
-except trio.Cancelled as e:
-    try:
-        pass
-    except ValueError as g:
-        raise g  # TRIO104
-    except BaseException as h:
-        raise h  # error? currently treated as safe
-    raise e
-# bare except, equivalent to `except baseException`
-except:  # error: 0, "bare except"
-    pass
-try:
-    pass
-except:
-    raise
-
-# point to correct exception in multi-line handlers
-my_super_mega_long_exception_so_it_gets_split = SyntaxError
-try:
-    pass
-except (
-    my_super_mega_long_exception_so_it_gets_split,
-    SyntaxError,
-    BaseException,  # error: 4, "BaseException"
-    ValueError,
-    trio.Cancelled,  # no complaint on this line
-):
-    pass
-
-# loop over non-empty static collection
-except BaseException as e:
-    for i in [1, 2, 3]:
-        raise
-except BaseException:  # error: 7, "BaseException"
-    for i in [1, 2, 3]:
-        ...
-except BaseException:  # error: 7, "BaseException"
-    for i in [1, 2, 3]:
-        if ...:
-            continue
-        raise
-except BaseException:
-    while True:
-        raise
-except BaseException:  # error: 7, "BaseException"
-    while True:
-        if ...:
-            break
-        raise
-except BaseException:
-    while True:
-        if ...:
-            continue
-        raise
+from typing import Any
+
+import trio
+
+
+def foo() -> Any:
+    ...
+
+
+try:
+    pass
+except (SyntaxError, ValueError, BaseException):
+    raise
+except (SyntaxError, ValueError, trio.Cancelled) as p:  # error: 33, "trio.Cancelled"
+    pass
+except (SyntaxError, ValueError):
+    raise
+except trio.Cancelled as e:
+    raise e
+except trio.Cancelled as e:
+    raise  # acceptable - see https://peps.python.org/pep-0678/#example-usage
+except trio.Cancelled:  # error: 7, "trio.Cancelled"
+    pass
+
+# if
+except BaseException as e:  # error: 7, "BaseException"
+    if True:
+        raise e
+    elif True:
+        pass
+    else:
+        raise e
+except BaseException:  # error: 7, "BaseException"
+    if True:
+        raise
+except BaseException:  # safe
+    if True:
+        raise
+    elif True:
+        raise
+    else:
+        raise
+
+# loops
+# raises inside the body are never guaranteed to run and are ignored
+except trio.Cancelled:  # error: 7, "trio.Cancelled"
+    while foo():
+        raise
+
+# raise inside else are guaranteed to run, unless there's a break
+except trio.Cancelled:
+    while ...:
+        ...
+    else:
+        raise
+except trio.Cancelled:
+    for _ in "":
+        ...
+    else:
+        raise
+except BaseException:  # error: 7, "BaseException"
+    while ...:
+        if ...:
+            break
+        raise
+    else:
+        raise
+except BaseException:  # error: 7, "BaseException"
+    for _ in "":
+        if ...:
+            break
+        raise
+    else:
+        raise
+# ensure we don't ignore previous guaranteed raise (although that's unreachable code)
+except BaseException:
+    raise
+    for _ in "":
+        if ...:
+            break
+        raise
+    else:
+        raise
+
+# nested try
+# in theory safe if the try, and all excepts raises - and there's a bare except.
+# But is a very weird pattern that we don't handle.
+except BaseException as e:  # error: 7, "BaseException"
+    try:
+        raise e
+    except ValueError:
+        raise e
+    except:
+        raise e  # though sometimes okay, TRIO104
+except BaseException:  # safe
+    try:
+        pass
+    finally:
+        raise
+# check that nested non-critical exceptions are ignored
+except BaseException:
+    try:
+        pass
+    except ValueError:
+        pass  # safe
+    raise
+# check that name isn't lost
+except trio.Cancelled as e:
+    try:
+        pass
+    except BaseException as f:
+        raise f
+    raise e
+# don't bypass raise by raising from nested except
+except trio.Cancelled as e:
+    try:
+        pass
+    except ValueError as g:
+        raise g  # TRIO104
+    except BaseException as h:
+        raise h  # error? currently treated as safe
+    raise e
+# bare except, equivalent to `except baseException`
+except:  # error: 0, "bare except"
+    pass
+try:
+    pass
+except:
+    raise
+
+# point to correct exception in multi-line handlers
+my_super_mega_long_exception_so_it_gets_split = SyntaxError
+try:
+    pass
+except (
+    my_super_mega_long_exception_so_it_gets_split,
+    SyntaxError,
+    BaseException,  # error: 4, "BaseException"
+    ValueError,
+    trio.Cancelled,  # no complaint on this line
+):
+    pass
+
+# loop over non-empty static collection
+except BaseException as e:
+    for i in [1, 2, 3]:
+        raise
+except BaseException:  # error: 7, "BaseException"
+    for i in [1, 2, 3]:
+        ...
+except BaseException:  # error: 7, "BaseException"
+    for i in [1, 2, 3]:
+        if ...:
+            continue
+        raise
+except BaseException:
+    while True:
+        raise
+except BaseException:  # error: 7, "BaseException"
+    while True:
+        if ...:
+            break
+        raise
+except BaseException:
+    while True:
+        if ...:
+            continue
+        raise
diff --git a/tests/trio104.py b/tests/trio104.py
index f054b8b..2512f79 100644
--- a/tests/trio104.py
+++ b/tests/trio104.py
@@ -1,134 +1,134 @@
-import trio
-
-try:
-    ...
-# raise different exception
-except BaseException:
-    raise ValueError()  # error: 4
-except trio.Cancelled as e:
-    raise ValueError() from e  # error: 4
-except trio.Cancelled as e:
-    # see https://github.com/Zac-HD/flake8-trio/pull/8#discussion_r932737341
-    raise BaseException() from e  # error: 4
-
-
-# nested try
-# in theory safe if the try, and all excepts raises - and there's a bare except.
-# But is a very weird pattern that we don't handle.
-except BaseException as e:  # TRIO103
-    try:
-        raise e
-    except ValueError:
-        raise e
-    except:
-        raise e  # though sometimes okay, error: 8
-except BaseException:  # safe
-    try:
-        pass
-    finally:
-        raise
-# check that nested non-critical exceptions are ignored
-except BaseException:
-    try:
-        pass
-    except ValueError:
-        pass  # safe
-    raise
-# check that name isn't lost
-except trio.Cancelled as e:
-    try:
-        pass
-    except BaseException as f:
-        raise f
-    raise e
-# don't bypass raise by raising from nested except
-except trio.Cancelled as e:
-    try:
-        pass
-    except ValueError as g:
-        raise g  # error: 8
-    except BaseException as h:
-        raise h  # error? currently treated as safe
-    raise e
-
-# avoid re-raise by raising parent exception
-try:
-    pass
-except ValueError as e:
-    try:
-        pass
-    except BaseException:
-        raise e  # error: 8
-
-# check for avoiding re-raise by returning from function
-def foo():
-    if True:  # for code coverage
-        return
-
-    try:
-        pass
-    except BaseException:  # TRIO103
-        return  # error: 8
-
-    # check that we properly iterate over all nodes in try
-    except BaseException:  # TRIO103
-        try:
-            return  # error: 12
-        except ValueError:
-            return  # error: 12
-        else:
-            return  # error: 12
-        finally:
-            return  # error: 12
-
-
-# don't avoid re-raise with continue/break
-while True:
-    try:
-        pass
-    except BaseException:
-        if True:
-            continue  # error: 12
-        raise
-
-while True:
-    try:
-        pass
-    except BaseException:
-        if True:
-            break  # error: 12
-        raise
-
-try:
-    pass
-except BaseException:  # safe
-    while True:
-        break
-    raise
-except BaseException:  # safe
-    while True:
-        continue
-    raise
-
-# check for avoiding re-raise by yielding from function
-def foo_yield():
-    if True:  # for code coverage
-        yield 1
-
-    try:
-        pass
-    except BaseException:
-        yield 1  # error: 8
-        raise
-
-    # check that we properly iterate over all nodes in try
-    except BaseException:
-        try:
-            yield 1  # error: 12
-        except ValueError:
-            yield 1  # error: 12
-        else:
-            yield 1  # error: 12
-        finally:
-            yield 1  # error: 12
-        raise
+import trio
+
+try:
+    ...
+# raise different exception
+except BaseException:
+    raise ValueError()  # error: 4
+except trio.Cancelled as e:
+    raise ValueError() from e  # error: 4
+except trio.Cancelled as e:
+    # see https://github.com/Zac-HD/flake8-trio/pull/8#discussion_r932737341
+    raise BaseException() from e  # error: 4
+
+
+# nested try
+# in theory safe if the try, and all excepts raises - and there's a bare except.
+# But is a very weird pattern that we don't handle.
+except BaseException as e:  # TRIO103
+    try:
+        raise e
+    except ValueError:
+        raise e
+    except:
+        raise e  # though sometimes okay, error: 8
+except BaseException:  # safe
+    try:
+        pass
+    finally:
+        raise
+# check that nested non-critical exceptions are ignored
+except BaseException:
+    try:
+        pass
+    except ValueError:
+        pass  # safe
+    raise
+# check that name isn't lost
+except trio.Cancelled as e:
+    try:
+        pass
+    except BaseException as f:
+        raise f
+    raise e
+# don't bypass raise by raising from nested except
+except trio.Cancelled as e:
+    try:
+        pass
+    except ValueError as g:
+        raise g  # error: 8
+    except BaseException as h:
+        raise h  # error? currently treated as safe
+    raise e
+
+# avoid re-raise by raising parent exception
+try:
+    pass
+except ValueError as e:
+    try:
+        pass
+    except BaseException:
+        raise e  # error: 8
+
+# check for avoiding re-raise by returning from function
+def foo():
+    if True:  # for code coverage
+        return
+
+    try:
+        pass
+    except BaseException:  # TRIO103
+        return  # error: 8
+
+    # check that we properly iterate over all nodes in try
+    except BaseException:  # TRIO103
+        try:
+            return  # error: 12
+        except ValueError:
+            return  # error: 12
+        else:
+            return  # error: 12
+        finally:
+            return  # error: 12
+
+
+# don't avoid re-raise with continue/break
+while True:
+    try:
+        pass
+    except BaseException:
+        if True:
+            continue  # error: 12
+        raise
+
+while True:
+    try:
+        pass
+    except BaseException:
+        if True:
+            break  # error: 12
+        raise
+
+try:
+    pass
+except BaseException:  # safe
+    while True:
+        break
+    raise
+except BaseException:  # safe
+    while True:
+        continue
+    raise
+
+# check for avoiding re-raise by yielding from function
+def foo_yield():
+    if True:  # for code coverage
+        yield 1
+
+    try:
+        pass
+    except BaseException:
+        yield 1  # error: 8
+        raise
+
+    # check that we properly iterate over all nodes in try
+    except BaseException:
+        try:
+            yield 1  # error: 12
+        except ValueError:
+            yield 1  # error: 12
+        else:
+            yield 1  # error: 12
+        finally:
+            yield 1  # error: 12
+        raise
diff --git a/tests/trio105.py b/tests/trio105.py
index 3c13534..5feb03e 100644
--- a/tests/trio105.py
+++ b/tests/trio105.py
@@ -1,59 +1,59 @@
-import trio
-
-
-async def foo():
-    # not async
-    trio.run()
-
-    # safely awaited
-    await trio.aclose_forcefully()
-    await trio.open_file()
-    await trio.open_ssl_over_tcp_listeners()
-    await trio.open_ssl_over_tcp_stream()
-    await trio.open_tcp_listeners()
-    await trio.open_tcp_stream()
-    await trio.open_unix_socket()
-    await trio.run_process()
-    await trio.serve_listeners()
-    await trio.serve_ssl_over_tcp()
-    await trio.serve_tcp()
-    await trio.sleep()
-    await trio.sleep_forever()
-    await trio.sleep_until()
-
-    # all async functions
-    trio.aclose_forcefully()  # error: 4, "aclose_forcefully"
-    trio.open_file()  # error: 4, "open_file"
-    trio.open_ssl_over_tcp_listeners()  # error: 4, "open_ssl_over_tcp_listeners"
-    trio.open_ssl_over_tcp_stream()  # error: 4, "open_ssl_over_tcp_stream"
-    trio.open_tcp_listeners()  # error: 4, "open_tcp_listeners"
-    trio.open_tcp_stream()  # error: 4, "open_tcp_stream"
-    trio.open_unix_socket()  # error: 4, "open_unix_socket"
-    trio.run_process()  # error: 4, "run_process"
-    trio.serve_listeners()  # error: 4, "serve_listeners"
-    trio.serve_ssl_over_tcp()  # error: 4, "serve_ssl_over_tcp"
-    trio.serve_tcp()  # error: 4, "serve_tcp"
-    trio.sleep()  # error: 4, "sleep"
-    trio.sleep_forever()  # error: 4, "sleep_forever"
-    trio.sleep_until()  # error: 4, "sleep_until"
-
-    # safe
-    async with await trio.open_file() as f:
-        pass
-
-    async with trio.open_file() as f:  # error: 15, "open_file"
-        pass
-
-    # safe in theory, but deemed sufficiently poor style that parsing
-    # it isn't supported
-    k = trio.open_file()  # error: 8, "open_file"
-    await k
-
-    # issue #56
-    nursery = trio.open_nursery()
-    await nursery.start()
-    await nursery.start_foo()
-
-    nursery.start()  # error: 4, "start"
-    nursery.start_soon()
-    nursery.start_foo()
+import trio
+
+
+async def foo():
+    # not async
+    trio.run()
+
+    # safely awaited
+    await trio.aclose_forcefully()
+    await trio.open_file()
+    await trio.open_ssl_over_tcp_listeners()
+    await trio.open_ssl_over_tcp_stream()
+    await trio.open_tcp_listeners()
+    await trio.open_tcp_stream()
+    await trio.open_unix_socket()
+    await trio.run_process()
+    await trio.serve_listeners()
+    await trio.serve_ssl_over_tcp()
+    await trio.serve_tcp()
+    await trio.sleep()
+    await trio.sleep_forever()
+    await trio.sleep_until()
+
+    # all async functions
+    trio.aclose_forcefully()  # error: 4, "aclose_forcefully"
+    trio.open_file()  # error: 4, "open_file"
+    trio.open_ssl_over_tcp_listeners()  # error: 4, "open_ssl_over_tcp_listeners"
+    trio.open_ssl_over_tcp_stream()  # error: 4, "open_ssl_over_tcp_stream"
+    trio.open_tcp_listeners()  # error: 4, "open_tcp_listeners"
+    trio.open_tcp_stream()  # error: 4, "open_tcp_stream"
+    trio.open_unix_socket()  # error: 4, "open_unix_socket"
+    trio.run_process()  # error: 4, "run_process"
+    trio.serve_listeners()  # error: 4, "serve_listeners"
+    trio.serve_ssl_over_tcp()  # error: 4, "serve_ssl_over_tcp"
+    trio.serve_tcp()  # error: 4, "serve_tcp"
+    trio.sleep()  # error: 4, "sleep"
+    trio.sleep_forever()  # error: 4, "sleep_forever"
+    trio.sleep_until()  # error: 4, "sleep_until"
+
+    # safe
+    async with await trio.open_file() as f:
+        pass
+
+    async with trio.open_file() as f:  # error: 15, "open_file"
+        pass
+
+    # safe in theory, but deemed sufficiently poor style that parsing
+    # it isn't supported
+    k = trio.open_file()  # error: 8, "open_file"
+    await k
+
+    # issue #56
+    nursery = trio.open_nursery()
+    await nursery.start()
+    await nursery.start_foo()
+
+    nursery.start()  # error: 4, "start"
+    nursery.start_soon()
+    nursery.start_foo()
diff --git a/tests/trio106.py b/tests/trio106.py
index 33aa2a1..4e1cf06 100644
--- a/tests/trio106.py
+++ b/tests/trio106.py
@@ -1,15 +1,15 @@
-import importlib
-
-import trio
-import trio as foo  # error: 0
-from trio import *  # type: ignore # noqa # error: 0
-from trio import blah, open_file as foo  # noqa # error: 0
-
-# Note that our tests exercise the Visitor classes, without going through the noqa filter later in flake8 - so these suppressions are picked up by our project-wide linter stack but not the tests.
-
-# ways of sidestepping the check, that no sane person would do
-k = importlib.import_module("trio")
-
-import trio
-
-l = trio
+import importlib
+
+import trio
+import trio as foo  # error: 0
+from trio import *  # type: ignore # noqa # error: 0
+from trio import blah, open_file as foo  # noqa # error: 0
+
+# Note that our tests exercise the Visitor classes, without going through the noqa filter later in flake8 - so these suppressions are picked up by our project-wide linter stack but not the tests.
+
+# ways of sidestepping the check, that no sane person would do
+k = importlib.import_module("trio")
+
+import trio
+
+l = trio
diff --git a/tests/trio107.py b/tests/trio107.py
index 568225d..ddd20d4 100644
--- a/tests/trio107.py
+++ b/tests/trio107.py
@@ -1,478 +1,478 @@
-import typing
-from typing import Any, Union, overload
-
-import trio
-
-_ = ""
-
-
-def __() -> Any:
-    ...
-
-
-# INCLUDE TRIO108
-
-# function whose body solely consists of pass, ellipsis, or string constants is safe
-async def foo_empty_1():
-    ...
-
-
-async def foo_empty_2():
-    pass
-
-
-async def foo_empty_3():
-    """comment"""
-
-
-async def foo_empty_4():
-    ...
-    """comment"""
-    ...
-    """comment2"""
-
-
-async def foo() -> Any:
-    await foo()
-
-
-async def foo1():  # error: 0, "exit", Statement("function definition", lineno)
-    __()
-
-
-# If
-async def foo_if_1():  # error: 0, "exit", Statement("function definition", lineno)
-    if _:
-        await foo()
-
-
-async def foo_if_2():
-    if _:
-        await foo()
-    else:
-        await foo()
-
-
-async def foo_if_3():
-    await foo()
-    if _:
-        ...
-
-
-async def foo_if_4():  # safe
-    await foo()
-    if ...:
-        ...
-    else:
-        ...
-
-
-# IfExp
-async def foo_ifexp_1():  # safe
-    print(await foo() if _ else await foo())
-
-
-async def foo_ifexp_2():  # error: 0, "exit", Statement("function definition", lineno)
-    print(_ if False and await foo() else await foo())
-
-
-# nested function definition
-async def foo_func_1():
-    await foo()
-
-    async def foo_func_2():  # error: 4, "exit", Statement("function definition", lineno)
-        __()
-
-
-async def foo_func_3():  # error: 0, "exit", Statement("function definition", lineno)
-    async def foo_func_4():
-        await foo()
-
-
-async def foo_func_5():  # error: 0, "exit", Statement("function definition", lineno)
-    def foo_func_6():  # safe
-        async def foo_func_7():  # error: 8, "exit", Statement("function definition", lineno)
-            __()
-
-
-async def foo_func_8():  # error: 0, "exit", Statement("function definition", lineno)
-    def foo_func_9():
-        raise
-
-
-# normal function
-def foo_normal_func_1():
-    return
-
-
-def foo_normal_func_2():
-    ...
-
-
-# overload decorator
-@overload
-async def foo_overload_1(_: bytes):
-    ...
-
-
-@typing.overload
-async def foo_overload_1(_: str):
-    ...
-
-
-async def foo_overload_1(_: Union[bytes, str]):
-    await foo()
-
-
-# conditions
-async def foo_condition_1():  # safe
-    if await foo():
-        ...
-
-
-async def foo_condition_2():  # error: 0, "exit", Statement("function definition", lineno)
-    if False and await foo():
-        ...
-
-
-async def foo_condition_3():  # error: 0, "exit", Statement("function definition", lineno)
-    if ... and await foo():
-        ...
-
-
-async def foo_condition_4():  # safe
-    while await foo():
-        ...
-
-
-async def foo_condition_5():  # safe
-    for i in await foo():
-        ...
-
-
-async def foo_condition_6():  # in theory error, but not worth parsing
-    for i in (None, await foo()):
-        break
-
-
-# loops
-async def foo_while_1():  # error: 0, "exit", Statement("function definition", lineno)
-    while _:
-        await foo()
-
-
-async def foo_while_2():  # now safe
-    while _:
-        await foo()
-    else:
-        await foo()
-
-
-async def foo_while_3():  # safe
-    await foo()
-    while _:
-        ...
-
-
-# for
-async def foo_for_1():  # error: 0, "exit", Statement("function definition", lineno)
-    for _ in "":
-        await foo()
-
-
-async def foo_for_2():  # now safe
-    for _ in "":
-        await foo()
-    else:
-        await foo()
-
-
-async def foo_while_break_1():  # safe
-    while foo():
-        await foo()
-        break
-    else:
-        await foo()
-
-
-async def foo_while_break_2():  # error: 0, "exit", Statement("function definition", lineno)
-    while foo():
-        break
-    else:
-        await foo()
-
-
-async def foo_while_break_3():  # error: 0, "exit", Statement("function definition", lineno)
-    while foo():
-        await foo()
-        break
-    else:
-        ...
-
-
-async def foo_while_break_4():  # error: 0, "exit", Statement("function definition", lineno)
-    while foo():
-        break
-    else:
-        ...
-
-
-async def foo_while_continue_1():  # safe
-    while foo():
-        await foo()
-        continue
-    else:
-        await foo()
-
-
-async def foo_while_continue_2():  # safe
-    while foo():
-        continue
-    else:
-        await foo()
-
-
-async def foo_while_continue_3():  # error: 0, "exit", Statement("function definition", lineno)
-    while foo():
-        await foo()
-        continue
-    else:
-        ...
-
-
-async def foo_while_continue_4():  # error: 0, "exit", Statement("function definition", lineno)
-    while foo():
-        continue
-    else:
-        ...
-
-
-async def foo_async_for_1():
-    async for _ in trio.trick_pyright:
-        ...
-
-
-# async with
-# async with guarantees checkpoint on at least one of entry or exit
-async def foo_async_with():
-    async with trio.trick_pyright:
-        ...
-
-
-# raise
-async def foo_raise_1():  # safe
-    raise ValueError()
-
-
-async def foo_raise_2():  # safe
-    if _:
-        await foo()
-    else:
-        raise ValueError()
-
-
-# try
-# safe only if (try or else) and all except bodies either await or raise
-# if foo() raises a ValueError it's not checkpointed
-async def foo_try_1():  # error: 0, "exit", Statement("function definition", lineno)
-    try:
-        await foo()
-    except ValueError:
-        ...
-    except:
-        raise
-    else:
-        await foo()
-
-
-async def foo_try_2():  # safe
-    try:
-        ...
-    except ValueError:
-        ...
-    except:
-        raise
-    finally:
-        await foo()
-
-
-async def foo_try_3():  # safe
-    try:
-        await foo()
-    except ValueError:
-        await foo()
-    except:
-        raise
-
-
-async def foo_try_4():  # safe
-    try:
-        ...
-    except ValueError:
-        raise
-    except:
-        raise
-    else:
-        await foo()
-
-
-async def foo_try_5():  # safe
-    await foo()
-    try:
-        pass
-    except:
-        pass
-    else:
-        pass
-
-
-async def foo_try_6():  # error: 0, "exit", Statement("function definition", lineno)
-    try:
-        pass
-    except:
-        pass
-    else:
-        pass
-
-
-async def foo_try_7():  # safe
-    try:
-        await foo()
-    except:
-        await foo()
-    else:
-        pass
-
-
-# https://github.com/Zac-HD/flake8-trio/issues/45
-async def to_queue(iter_func, queue):
-    async with iter_func() as it:
-        async for x in it:
-            queue.put(x)
-
-
-async def to_queue2(iter_func, queue):
-    try:
-        async with iter_func() as it:
-            async for x in it:
-                queue.put(x)
-    finally:
-        queue.put(None)
-
-
-# safe, exception is propagated out
-async def try_checkpoint_empty_finally():
-    try:
-        await trio.sleep(0)
-    finally:
-        ...
-
-
-# unsafe, exception is suppressed
-async def try_exception_suppressed():  # error: 0, 'exit', Statement('function definition', lineno)
-    try:
-        await trio.sleep(0)
-    except:
-        ...
-
-
-# safe
-async def try_bare_except_reraises():
-    try:
-        await trio.sleep(0)
-    except:
-        raise
-    finally:
-        ...
-
-
-async def return_in_finally_bare_except_checkpoint():
-    try:
-        await trio.sleep(0)
-    except:
-        await trio.sleep(0)
-    finally:
-        return
-
-
-async def return_in_finally_bare_except_empty():
-    try:
-        await trio.sleep(0)
-    except:
-        ...
-    finally:
-        return  # error: 8, 'return', Statement('function definition', lineno-6)
-
-
-# early return
-async def foo_return_1():
-    return  # error: 4, "return", Statement("function definition", lineno-1)
-
-
-async def foo_return_2():  # safe
-    if _:
-        return  # error: 8, "return", Statement("function definition", lineno-2)
-    await foo()
-
-
-async def foo_return_3():  # error: 0, "exit", Statement("function definition", lineno)
-    if _:
-        await foo()
-        return  # safe
-
-
-# loop over non-empty static collection
-async def foo_loop_static():
-    for i in [1, 2, 3]:
-        await foo()
-
-
-# also handle range with constants
-async def foo_range_1():
-    for i in range(5):
-        await foo()
-
-
-async def foo_range_2():
-    for i in range(5, 10):
-        await foo()
-
-
-async def foo_range_3():
-    for i in range(10, 5, -1):
-        await foo()
-
-
-async def foo_range_4():  # error: 0, "exit", Statement("function definition", lineno)
-    for i in range(10, 5):
-        await foo()
-
-
-# error on complex parameters
-async def foo_range_5():  # error: 0, "exit", Statement("function definition", lineno)
-    for i in range(2 - 2):
-        await foo()
-
-
-# https://github.com/Zac-HD/flake8-trio/issues/47
-async def f():
-    while True:
-        if ...:
-            await trio.sleep(0)
-            return
-        # If you delete this loop, no warning.
-        while foo():
-            await __()
-
-
-async def f1():
-    while True:
-        if ...:
-            await trio.sleep(0)
-            return
-
-
-async def f2():
-    while True:
-        await trio.sleep(0)
-        return
+import typing
+from typing import Any, Union, overload
+
+import trio
+
+_ = ""
+
+
+def __() -> Any:
+    ...
+
+
+# INCLUDE TRIO108
+
+# function whose body solely consists of pass, ellipsis, or string constants is safe
+async def foo_empty_1():
+    ...
+
+
+async def foo_empty_2():
+    pass
+
+
+async def foo_empty_3():
+    """comment"""
+
+
+async def foo_empty_4():
+    ...
+    """comment"""
+    ...
+    """comment2"""
+
+
+async def foo() -> Any:
+    await foo()
+
+
+async def foo1():  # error: 0, "exit", Statement("function definition", lineno)
+    __()
+
+
+# If
+async def foo_if_1():  # error: 0, "exit", Statement("function definition", lineno)
+    if _:
+        await foo()
+
+
+async def foo_if_2():
+    if _:
+        await foo()
+    else:
+        await foo()
+
+
+async def foo_if_3():
+    await foo()
+    if _:
+        ...
+
+
+async def foo_if_4():  # safe
+    await foo()
+    if ...:
+        ...
+    else:
+        ...
+
+
+# IfExp
+async def foo_ifexp_1():  # safe
+    print(await foo() if _ else await foo())
+
+
+async def foo_ifexp_2():  # error: 0, "exit", Statement("function definition", lineno)
+    print(_ if False and await foo() else await foo())
+
+
+# nested function definition
+async def foo_func_1():
+    await foo()
+
+    async def foo_func_2():  # error: 4, "exit", Statement("function definition", lineno)
+        __()
+
+
+async def foo_func_3():  # error: 0, "exit", Statement("function definition", lineno)
+    async def foo_func_4():
+        await foo()
+
+
+async def foo_func_5():  # error: 0, "exit", Statement("function definition", lineno)
+    def foo_func_6():  # safe
+        async def foo_func_7():  # error: 8, "exit", Statement("function definition", lineno)
+            __()
+
+
+async def foo_func_8():  # error: 0, "exit", Statement("function definition", lineno)
+    def foo_func_9():
+        raise
+
+
+# normal function
+def foo_normal_func_1():
+    return
+
+
+def foo_normal_func_2():
+    ...
+
+
+# overload decorator
+@overload
+async def foo_overload_1(_: bytes):
+    ...
+
+
+@typing.overload
+async def foo_overload_1(_: str):
+    ...
+
+
+async def foo_overload_1(_: Union[bytes, str]):
+    await foo()
+
+
+# conditions
+async def foo_condition_1():  # safe
+    if await foo():
+        ...
+
+
+async def foo_condition_2():  # error: 0, "exit", Statement("function definition", lineno)
+    if False and await foo():
+        ...
+
+
+async def foo_condition_3():  # error: 0, "exit", Statement("function definition", lineno)
+    if ... and await foo():
+        ...
+
+
+async def foo_condition_4():  # safe
+    while await foo():
+        ...
+
+
+async def foo_condition_5():  # safe
+    for i in await foo():
+        ...
+
+
+async def foo_condition_6():  # in theory error, but not worth parsing
+    for i in (None, await foo()):
+        break
+
+
+# loops
+async def foo_while_1():  # error: 0, "exit", Statement("function definition", lineno)
+    while _:
+        await foo()
+
+
+async def foo_while_2():  # now safe
+    while _:
+        await foo()
+    else:
+        await foo()
+
+
+async def foo_while_3():  # safe
+    await foo()
+    while _:
+        ...
+
+
+# for
+async def foo_for_1():  # error: 0, "exit", Statement("function definition", lineno)
+    for _ in "":
+        await foo()
+
+
+async def foo_for_2():  # now safe
+    for _ in "":
+        await foo()
+    else:
+        await foo()
+
+
+async def foo_while_break_1():  # safe
+    while foo():
+        await foo()
+        break
+    else:
+        await foo()
+
+
+async def foo_while_break_2():  # error: 0, "exit", Statement("function definition", lineno)
+    while foo():
+        break
+    else:
+        await foo()
+
+
+async def foo_while_break_3():  # error: 0, "exit", Statement("function definition", lineno)
+    while foo():
+        await foo()
+        break
+    else:
+        ...
+
+
+async def foo_while_break_4():  # error: 0, "exit", Statement("function definition", lineno)
+    while foo():
+        break
+    else:
+        ...
+
+
+async def foo_while_continue_1():  # safe
+    while foo():
+        await foo()
+        continue
+    else:
+        await foo()
+
+
+async def foo_while_continue_2():  # safe
+    while foo():
+        continue
+    else:
+        await foo()
+
+
+async def foo_while_continue_3():  # error: 0, "exit", Statement("function definition", lineno)
+    while foo():
+        await foo()
+        continue
+    else:
+        ...
+
+
+async def foo_while_continue_4():  # error: 0, "exit", Statement("function definition", lineno)
+    while foo():
+        continue
+    else:
+        ...
+
+
+async def foo_async_for_1():
+    async for _ in trio.trick_pyright:
+        ...
+
+
+# async with
+# async with guarantees checkpoint on at least one of entry or exit
+async def foo_async_with():
+    async with trio.trick_pyright:
+        ...
+
+
+# raise
+async def foo_raise_1():  # safe
+    raise ValueError()
+
+
+async def foo_raise_2():  # safe
+    if _:
+        await foo()
+    else:
+        raise ValueError()
+
+
+# try
+# safe only if (try or else) and all except bodies either await or raise
+# if foo() raises a ValueError it's not checkpointed
+async def foo_try_1():  # error: 0, "exit", Statement("function definition", lineno)
+    try:
+        await foo()
+    except ValueError:
+        ...
+    except:
+        raise
+    else:
+        await foo()
+
+
+async def foo_try_2():  # safe
+    try:
+        ...
+    except ValueError:
+        ...
+    except:
+        raise
+    finally:
+        await foo()
+
+
+async def foo_try_3():  # safe
+    try:
+        await foo()
+    except ValueError:
+        await foo()
+    except:
+        raise
+
+
+async def foo_try_4():  # safe
+    try:
+        ...
+    except ValueError:
+        raise
+    except:
+        raise
+    else:
+        await foo()
+
+
+async def foo_try_5():  # safe
+    await foo()
+    try:
+        pass
+    except:
+        pass
+    else:
+        pass
+
+
+async def foo_try_6():  # error: 0, "exit", Statement("function definition", lineno)
+    try:
+        pass
+    except:
+        pass
+    else:
+        pass
+
+
+async def foo_try_7():  # safe
+    try:
+        await foo()
+    except:
+        await foo()
+    else:
+        pass
+
+
+# https://github.com/Zac-HD/flake8-trio/issues/45
+async def to_queue(iter_func, queue):
+    async with iter_func() as it:
+        async for x in it:
+            queue.put(x)
+
+
+async def to_queue2(iter_func, queue):
+    try:
+        async with iter_func() as it:
+            async for x in it:
+                queue.put(x)
+    finally:
+        queue.put(None)
+
+
+# safe, exception is propagated out
+async def try_checkpoint_empty_finally():
+    try:
+        await trio.sleep(0)
+    finally:
+        ...
+
+
+# unsafe, exception is suppressed
+async def try_exception_suppressed():  # error: 0, 'exit', Statement('function definition', lineno)
+    try:
+        await trio.sleep(0)
+    except:
+        ...
+
+
+# safe
+async def try_bare_except_reraises():
+    try:
+        await trio.sleep(0)
+    except:
+        raise
+    finally:
+        ...
+
+
+async def return_in_finally_bare_except_checkpoint():
+    try:
+        await trio.sleep(0)
+    except:
+        await trio.sleep(0)
+    finally:
+        return
+
+
+async def return_in_finally_bare_except_empty():
+    try:
+        await trio.sleep(0)
+    except:
+        ...
+    finally:
+        return  # error: 8, 'return', Statement('function definition', lineno-6)
+
+
+# early return
+async def foo_return_1():
+    return  # error: 4, "return", Statement("function definition", lineno-1)
+
+
+async def foo_return_2():  # safe
+    if _:
+        return  # error: 8, "return", Statement("function definition", lineno-2)
+    await foo()
+
+
+async def foo_return_3():  # error: 0, "exit", Statement("function definition", lineno)
+    if _:
+        await foo()
+        return  # safe
+
+
+# loop over non-empty static collection
+async def foo_loop_static():
+    for i in [1, 2, 3]:
+        await foo()
+
+
+# also handle range with constants
+async def foo_range_1():
+    for i in range(5):
+        await foo()
+
+
+async def foo_range_2():
+    for i in range(5, 10):
+        await foo()
+
+
+async def foo_range_3():
+    for i in range(10, 5, -1):
+        await foo()
+
+
+async def foo_range_4():  # error: 0, "exit", Statement("function definition", lineno)
+    for i in range(10, 5):
+        await foo()
+
+
+# error on complex parameters
+async def foo_range_5():  # error: 0, "exit", Statement("function definition", lineno)
+    for i in range(2 - 2):
+        await foo()
+
+
+# https://github.com/Zac-HD/flake8-trio/issues/47
+async def f():
+    while True:
+        if ...:
+            await trio.sleep(0)
+            return
+        # If you delete this loop, no warning.
+        while foo():
+            await __()
+
+
+async def f1():
+    while True:
+        if ...:
+            await trio.sleep(0)
+            return
+
+
+async def f2():
+    while True:
+        await trio.sleep(0)
+        return
diff --git a/tests/trio108.py b/tests/trio108.py
index d859390..8d978d4 100644
--- a/tests/trio108.py
+++ b/tests/trio108.py
@@ -1,790 +1,790 @@
-from typing import Any
-
-import trio
-
-_: Any = ""
-
-# INCLUDE TRIO107
-
-
-async def foo() -> Any:
-    await foo()
-
-
-async def foo_yield_1():
-    await foo()
-    yield 5
-    await foo()
-
-
-async def foo_yield_2():
-    yield  # error: 4, "yield", Statement("function definition", lineno-1)
-    yield  # error: 4, "yield", Statement("yield", lineno-1)
-    await foo()
-
-
-async def foo_yield_3():  # error: 0, "exit", Statement("yield", lineno+2)
-    await foo()
-    yield
-
-
-async def foo_yield_4():  # error: 0, "exit", Statement("yield", lineno+3)
-    yield  # error: 4, "yield", Statement("function definition", lineno-1)
-    await (yield)  # error: 11, "yield", Statement("yield", lineno-1)
-    yield  # safe
-
-
-async def foo_yield_return_1():
-    yield  # error: 4, "yield", Statement("function definition", lineno-1)
-    return  # error: 4, "return", Statement("yield", lineno-1)
-
-
-async def foo_yield_return_2():
-    await foo()
-    yield
-    return  # error: 4, "return", Statement("yield", lineno-1)
-
-
-async def foo_yield_return_3():
-    await foo()
-    yield
-    await foo()
-    return
-
-
-# async with
-# checkpoint on both entry and exit
-async def foo_async_with():
-    async with trio.fail_after(5):
-        yield
-
-
-# fmt: off
-async def foo_async_with_2():
-    # with'd expression evaluated before checkpoint
-    async with (yield):  # error: 16, "yield", Statement("function definition", lineno-2)
-        yield
-# fmt: on
-
-
-async def foo_async_with_3():
-    async with trio.fail_after(5):
-        yield
-        yield  # error: 8, "yield", Statement("yield", lineno-1)
-
-
-# async for
-async def foo_async_for():  # error: 0, "exit", Statement("yield", lineno+6)
-    async for i in (
-        yield  # error: 8, "yield", Statement("function definition", lineno-2)
-    ):
-        yield  # safe
-    else:
-        yield  # safe
-
-
-# await anext(iter) is not called on break
-async def foo_async_for_2():  # error: 0, "exit", Statement("yield", lineno+2)
-    async for i in trio.trick_pyright:
-        yield
-        if ...:
-            break
-
-
-async def foo_async_for_3():  # safe
-    async for i in trio.trick_pyright:
-        yield
-
-
-async def foo_async_for_4():  # safe
-    async for i in trio.trick_pyright:
-        yield
-        continue
-
-
-# for
-async def foo_for():  # error: 0, "exit", Statement("yield", lineno+3)
-    await foo()
-    for i in "":
-        yield  # error: 8, "yield", Statement("yield", lineno)
-
-
-async def foo_for_1():  # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3)
-    for _ in "":
-        await foo()
-        yield
-
-
-# while
-
-# safe if checkpoint in else
-async def foo_while_1():  # error: 0, "exit", Statement("yield", lineno+5)
-    while foo():
-        ...
-    else:
-        await foo()  # will always run
-    yield  # safe
-
-
-# simple yield-in-loop case
-async def foo_while_2():  # error: 0, "exit", Statement("yield", lineno+3)
-    await foo()
-    while foo():
-        yield  # error: 8, "yield", Statement("yield", lineno)
-
-
-# no checkpoint after yield if else is entered
-async def foo_while_3():  # error: 0, "exit", Statement("yield", lineno+5)
-    while foo():
-        await foo()
-        yield
-    else:
-        yield  # error: 8, "yield", Statement("yield", lineno-2) # error: 8, "yield", Statement("function definition", lineno-5)
-
-
-# check that errors are suppressed in visit_While
-async def foo_while_4():  # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7)
-    await foo()
-    while foo():
-        yield  # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4)
-        while foo():
-            yield  # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2)
-            while foo():
-                yield  # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno)
-
-
-# check error suppression is reset
-async def foo_while_5():
-    await foo()
-    while foo():
-        yield  # error: 8, "yield", Statement("yield", lineno)
-
-        async def foo_nested_error():  # error: 8, "exit", Statement("yield", lineno+1)# error: 8, "exit", Statement("yield", lineno+1)
-            yield  # error: 12, "yield", Statement("function definition", lineno-1)# error: 12, "yield", Statement("function definition", lineno-1)
-
-    await foo()
-
-
-# --- while + continue ---
-# no checkpoint on continue
-async def foo_while_continue_1():  # error: 0, "exit", Statement("yield", lineno+3)
-    await foo()
-    while foo():
-        yield  # error: 8, "yield", Statement("yield", lineno)
-        if ...:
-            continue
-        await foo()
-
-
-# multiple continues
-async def foo_while_continue_2():  # error: 0, "exit", Statement("yield", lineno+3)
-    await foo()
-    while foo():
-        yield  # error: 8, "yield", Statement("yield", lineno)
-        if foo():
-            continue
-        await foo()
-        if ...:
-            continue
-        while foo():
-            yield  # safe
-            await foo()
-
-
-# --- while + break ---
-# else might not run
-async def foo_while_break_1():  # error: 0, "exit", Statement("yield", lineno+6)
-    while foo():
-        if ...:
-            break
-    else:
-        await foo()
-    yield  # error: 4, "yield", Statement("function definition", lineno-6)
-
-
-# no checkpoint on break
-async def foo_while_break_2():  # error: 0, "exit", Statement("yield", lineno+3)
-    await foo()
-    while foo():
-        yield  # safe
-        if ...:
-            break
-        await foo()
-
-
-# guaranteed if else and break
-async def foo_while_break_3():  # error: 0, "exit", Statement("yield", lineno+7)
-    while foo():
-        await foo()
-        if ...:
-            break  # if it breaks, have checkpointed
-    else:
-        await foo()  # runs if 0-iter
-    yield  # safe
-
-
-# break at non-guaranteed checkpoint
-async def foo_while_break_4():  # error: 0, "exit", Statement("yield", lineno+7)
-    while foo():
-        if ...:
-            break
-        await foo()  # might not run
-    else:
-        await foo()  # might not run
-    yield  # error: 4, "yield", Statement("function definition", lineno-7)
-
-
-# check break is reset on nested
-async def foo_while_break_5():  # error: 0, "exit", Statement("yield", lineno+12)
-    await foo()
-    while foo():
-        yield
-        if ...:
-            break
-        await foo()
-        while foo():
-            yield  # safe
-            await foo()
-        yield  # safe
-        await foo()
-    yield  # error: 4, "yield", Statement("yield", lineno-9)
-
-
-# check multiple breaks
-async def foo_while_break_6():  # error: 0, "exit", Statement("yield", lineno+11)
-    await foo()
-    while foo():
-        yield
-        if ...:
-            break
-        await foo()
-        yield
-        await foo()
-        if ...:
-            break
-    yield  # error: 4, "yield", Statement("yield", lineno-8)
-
-
-async def foo_while_break_7():  # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+5)
-    while foo():
-        await foo()
-        if ...:
-            break
-        yield
-        break
-
-
-async def foo_while_endless_1():
-    while True:
-        await foo()
-        yield
-
-
-async def foo_while_endless_2():  # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+3)
-    while foo():
-        await foo()
-        yield
-
-
-# try
-async def foo_try_1():  # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2)
-    try:
-        yield  # error: 8, "yield", Statement("function definition", lineno-2)
-    except:
-        pass
-
-
-# no checkpoint after yield in ValueError
-async def foo_try_2():  # error: 0, "exit", Statement("yield", lineno+5)
-    try:
-        await foo()
-    except ValueError:
-        # try might not have checkpointed
-        yield  # error: 8, "yield", Statement("function definition", lineno-5)
-    except:
-        await foo()
-    else:
-        pass
-
-
-async def foo_try_3():  # error: 0, "exit", Statement("yield", lineno+6)
-    try:
-        ...
-    except:
-        await foo()
-    else:
-        yield  # error: 8, "yield", Statement("function definition", lineno-6)
-
-
-async def foo_try_4():  # safe
-    try:
-        ...
-    except:
-        yield  # error: 8, "yield", Statement("function definition", lineno-4)
-    finally:
-        await foo()
-
-
-async def foo_try_5():
-    try:
-        await foo()
-    finally:
-        # try might crash before checkpoint
-        yield  # error: 8, "yield", Statement("function definition", lineno-5)
-        await foo()
-
-
-async def foo_try_6():  # error: 0, "exit", Statement("yield", lineno+5)
-    try:
-        await foo()
-    except ValueError:
-        pass
-    yield  # error: 4, "yield", Statement("function definition", lineno-5)
-
-
-async def foo_try_7():  # error: 0, "exit", Statement("yield", lineno+17)
-    await foo()
-    try:
-        yield
-        await foo()
-    except ValueError:
-        await foo()
-        yield
-        await foo()
-    except SyntaxError:
-        yield  # error: 8, "yield", Statement("yield", lineno-7)
-        await foo()
-    finally:
-        pass
-    # If the try raises an exception without checkpointing, and it's not caught
-    # by any of the excepts, jumping straight to the finally.
-    # Then the error will be propagated upwards
-    yield  # safe
-
-
-## safe only if (try or else) and all except bodies either await or raise
-## if foo() raises a ValueError it's not checkpointed, and may or may not yield
-async def foo_try_8():  # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3)
-    try:
-        await foo()
-        yield
-        await foo()
-    except ValueError:
-        ...
-    except:
-        raise
-    else:
-        await foo()
-
-
-# no checkpoint after yield in else
-async def foo_try_9():  # error: 0, "exit", Statement("yield", lineno+6)
-    try:
-        await foo()
-    except:
-        await foo()
-    else:
-        yield
-
-
-# bare except means we'll jump to finally after full execution of either try or the except
-async def foo_try_10():
-    try:
-        await foo()
-    except:
-        await foo()
-    finally:
-        yield
-        await foo()
-
-
-async def foo_try_10_BaseException():
-    try:
-        await foo()
-    except BaseException:
-        await foo()
-    finally:
-        yield
-        await foo()
-
-
-# not fully covering excepts
-async def foo_try_10_exception():
-    try:
-        await foo()
-    except ValueError:
-        await foo()
-    finally:
-        yield  # error: 8, "yield", Statement("function definition", lineno-6)
-        await foo()
-
-
-async def foo_try_10_no_except():
-    try:
-        await foo()
-    finally:
-        # try might crash before checkpoint
-        yield  # error: 8, "yield", Statement("function definition", lineno-5)
-        await foo()
-
-
-# if
-async def foo_if_1():
-    if ...:
-        yield  # error: 8, "yield", Statement("function definition", lineno-2)
-        await foo()
-    else:
-        yield  # error: 8, "yield", Statement("function definition", lineno-5)
-        await foo()
-
-
-async def foo_if_2():  # error: 0, "exit", Statement("yield", lineno+6)
-    await foo()
-    if ...:
-        ...
-    else:
-        yield
-    yield  # error: 4, "yield", Statement("yield", lineno-1)
-
-
-async def foo_if_3():  # error: 0, "exit", Statement("yield", lineno+6)
-    await foo()
-    if ...:
-        yield
-    else:
-        ...
-    yield  # error: 4, "yield", Statement("yield", lineno-3)
-
-
-async def foo_if_4():  # error: 0, "exit", Statement("yield", lineno+7)
-    await foo()
-    yield
-    if ...:
-        await foo()
-    else:
-        ...
-    yield  # error: 4, "yield", Statement("yield", lineno-5)
-
-
-async def foo_if_5():  # error: 0, "exit", Statement("yield", lineno+8)
-    await foo()
-    if ...:
-        yield
-        await foo()
-    else:
-        yield
-        ...
-    yield  # error: 4, "yield", Statement("yield", lineno-2)
-
-
-async def foo_if_6():  # error: 0, "exit", Statement("yield", lineno+8)
-    await foo()
-    if ...:
-        yield
-    else:
-        yield
-        await foo()
-        ...
-    yield  # error: 4, "yield", Statement("yield", lineno-5)
-
-
-async def foo_if_7():  # error: 0, "exit", Statement("function definition", lineno)
-    if ...:
-        await foo()
-        yield
-        await foo()
-
-
-async def foo_if_8():  # error: 0, "exit", Statement("function definition", lineno)
-    if ...:
-        ...
-    else:
-        await foo()
-        yield
-        await foo()
-
-
-# IfExp
-async def foo_ifexp_1():  # error: 0, "exit", Statement("yield", lineno+1) # error: 0, "exit", Statement("yield", lineno+1)
-    print((yield) if await foo() else (yield))
-
-
-# Will either enter else, and it's a guaranteed checkpoint - or enter if, in which
-# case the problem is the yield.
-async def foo_ifexp_2():  # error: 0, "exit", Statement("yield", lineno+2)
-    print(
-        (yield)  # error: 9, "yield", Statement("function definition", lineno-2)
-        if ... and await foo()
-        else await foo()
-    )
-
-
-# normal function
-def foo_sync_1():
-    return
-
-
-def foo_sync_2():
-    ...
-
-
-def foo_sync_3():
-    yield
-
-
-def foo_sync_4():
-    if ...:
-        return
-    yield
-
-
-def foo_sync_5():
-    if ...:
-        return
-    yield
-
-
-def foo_sync_6():
-    while foo():
-        yield
-
-
-def foo_sync_7():
-    while foo():
-        if ...:
-            return
-        yield
-
-
-# nested function definition
-async def foo_func_1():
-    await foo()
-
-    async def foo_func_2():  # error: 4, "exit", Statement("yield", lineno+1)
-        yield  # error: 8, "yield", Statement("function definition", lineno-1)
-
-
-async def foo_func_3():  # error: 0, "exit", Statement("yield", lineno+2)
-    await foo()
-    yield
-
-    async def foo_func_4():
-        await foo()
-
-
-async def foo_func_5():  # error: 0, "exit", Statement("yield", lineno+2)
-    await foo()
-    yield
-
-    def foo_func_6():  # safe
-        yield
-
-        async def foo_func_7():
-            await foo()
-            ...
-
-
-# No error from function definition, but may shortcut after yield
-async def foo_boolops_1():  # error: 0, "exit", Stmt("yield", line+1)
-    _ = await foo() and (yield) and await foo()
-
-
-# may shortcut after any of the yields
-async def foo_boolops_2():  # error: 0, "exit", Stmt("yield", line+1) # error: 0, "exit", Stmt("yield", line+1)
-    _ = await foo() and (yield) and await foo() and (yield)
-
-
-# fmt: off
-async def foo_boolops_3():  # error: 0, "exit", Stmt("yield", line+1) # error: 0, "exit", Stmt("yield", line+4) # error: 0, "exit", Stmt("yield", line+5)
-    _ = (await foo() or (yield) or await foo()) or (
-        ...
-        or (
-            (yield)  # error: 13, "yield", Stmt("yield", line-3)
-            and (yield))  # error: 17, "yield", Stmt("yield", line-1)
-    )
-# fmt: on
-
-# loop over non-empty static collection
-async def foo_loop_static():
-    # break/else behaviour on guaranteed body execution
-    for _ in [1, 2, 3]:
-        await foo()
-    else:
-        yield
-    await foo()
-    yield
-
-    for _ in [1, 2, 3]:
-        await foo()
-        if ...:
-            break
-    else:
-        yield
-        await foo()
-    yield
-
-    # continue
-    for _ in [1, 2, 3]:
-        if ...:
-            continue
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-7)
-
-    # continue/else
-    for _ in [1, 2, 3]:
-        if ...:
-            continue
-        await foo()
-    else:
-        yield  # error: 8, "yield", Stmt("yield", line-8)
-    await foo()
-    yield
-
-    for _ in [1, 2, 3]:
-        await foo()
-        if ...:
-            break
-    else:
-        yield
-        await foo()
-    yield
-
-    # test different containers
-    for _ in (1, 2, 3):
-        await foo()
-    yield
-
-    for _ in (foo(), foo()):
-        await foo()
-    yield
-
-    for _ in ((),):
-        await foo()
-    yield
-
-    for _ in "hello":
-        await foo()
-    yield
-
-    for _ in b"hello":
-        await foo()
-    yield
-
-    for _ in r"hello":
-        await foo()
-    yield
-
-    for _ in {1, 2, 3}:
-        await foo()
-    yield
-
-    for _ in ():
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in {1: 2, 3: 4}:
-        await foo()
-    yield
-
-    for _ in "   ".strip():
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in range(0):
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in (*range(0),):
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in (*(1, 2),):
-        await foo()
-    yield
-
-    for _ in {**{}}:
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in {**{}, **{}}:
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in {**{1: 2}}:
-        await foo()
-    yield
-
-    for _ in (*range(0), *[1, 2, 3]):
-        await foo()
-    yield
-
-    for _ in {**{}, **{1: 2}}:
-        await foo()
-    yield
-
-    x: Any = ...
-    for _ in (*x, *[1, 2, 3]):
-        await foo()
-    yield
-
-    for _ in {**x, **{1: 2}}:
-        await foo()
-    yield
-
-    for _ in {}:
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in "":
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in [[], []][0]:
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    for _ in [[], []].__getitem__(0):
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    # not handled
-    for _ in list((1, 2)):
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-5)
-
-    for _ in list():
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-4)
-
-    # while
-    while True:
-        await foo()
-        if ...:
-            break
-    yield
-
-    while True:
-        if ...:
-            break
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-6)
-
-    while True:
-        if ...:
-            continue
-        await foo()
-        if ...:
-            break
-    yield
-
-    # will get caught by any number of linters, but trio108 will also complain
-    for _ in 5:  # type: ignore
-        await foo()
-    yield  # error: 4, "yield", Stmt("yield", line-5)
-
-    # range with constant arguments also handled, see more extensive tests in 107
-    for i in range(5):
-        await foo()
-    yield
-
-    await foo()
+from typing import Any
+
+import trio
+
+_: Any = ""
+
+# INCLUDE TRIO107
+
+
+async def foo() -> Any:
+    await foo()
+
+
+async def foo_yield_1():
+    await foo()
+    yield 5
+    await foo()
+
+
+async def foo_yield_2():
+    yield  # error: 4, "yield", Statement("function definition", lineno-1)
+    yield  # error: 4, "yield", Statement("yield", lineno-1)
+    await foo()
+
+
+async def foo_yield_3():  # error: 0, "exit", Statement("yield", lineno+2)
+    await foo()
+    yield
+
+
+async def foo_yield_4():  # error: 0, "exit", Statement("yield", lineno+3)
+    yield  # error: 4, "yield", Statement("function definition", lineno-1)
+    await (yield)  # error: 11, "yield", Statement("yield", lineno-1)
+    yield  # safe
+
+
+async def foo_yield_return_1():
+    yield  # error: 4, "yield", Statement("function definition", lineno-1)
+    return  # error: 4, "return", Statement("yield", lineno-1)
+
+
+async def foo_yield_return_2():
+    await foo()
+    yield
+    return  # error: 4, "return", Statement("yield", lineno-1)
+
+
+async def foo_yield_return_3():
+    await foo()
+    yield
+    await foo()
+    return
+
+
+# async with
+# checkpoint on both entry and exit
+async def foo_async_with():
+    async with trio.fail_after(5):
+        yield
+
+
+# fmt: off
+async def foo_async_with_2():
+    # with'd expression evaluated before checkpoint
+    async with (yield):  # error: 16, "yield", Statement("function definition", lineno-2)
+        yield
+# fmt: on
+
+
+async def foo_async_with_3():
+    async with trio.fail_after(5):
+        yield
+        yield  # error: 8, "yield", Statement("yield", lineno-1)
+
+
+# async for
+async def foo_async_for():  # error: 0, "exit", Statement("yield", lineno+6)
+    async for i in (
+        yield  # error: 8, "yield", Statement("function definition", lineno-2)
+    ):
+        yield  # safe
+    else:
+        yield  # safe
+
+
+# await anext(iter) is not called on break
+async def foo_async_for_2():  # error: 0, "exit", Statement("yield", lineno+2)
+    async for i in trio.trick_pyright:
+        yield
+        if ...:
+            break
+
+
+async def foo_async_for_3():  # safe
+    async for i in trio.trick_pyright:
+        yield
+
+
+async def foo_async_for_4():  # safe
+    async for i in trio.trick_pyright:
+        yield
+        continue
+
+
+# for
+async def foo_for():  # error: 0, "exit", Statement("yield", lineno+3)
+    await foo()
+    for i in "":
+        yield  # error: 8, "yield", Statement("yield", lineno)
+
+
+async def foo_for_1():  # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3)
+    for _ in "":
+        await foo()
+        yield
+
+
+# while
+
+# safe if checkpoint in else
+async def foo_while_1():  # error: 0, "exit", Statement("yield", lineno+5)
+    while foo():
+        ...
+    else:
+        await foo()  # will always run
+    yield  # safe
+
+
+# simple yield-in-loop case
+async def foo_while_2():  # error: 0, "exit", Statement("yield", lineno+3)
+    await foo()
+    while foo():
+        yield  # error: 8, "yield", Statement("yield", lineno)
+
+
+# no checkpoint after yield if else is entered
+async def foo_while_3():  # error: 0, "exit", Statement("yield", lineno+5)
+    while foo():
+        await foo()
+        yield
+    else:
+        yield  # error: 8, "yield", Statement("yield", lineno-2) # error: 8, "yield", Statement("function definition", lineno-5)
+
+
+# check that errors are suppressed in visit_While
+async def foo_while_4():  # error: 0, "exit", Statement("yield", lineno+3) # error: 0, "exit", Statement("yield", lineno+5) # error: 0, "exit", Statement("yield", lineno+7)
+    await foo()
+    while foo():
+        yield  # error: 8, "yield", Statement("yield", lineno) # error: 8, "yield", Statement("yield", lineno+2) # error: 8, "yield", Statement("yield", lineno+4)
+        while foo():
+            yield  # error: 12, "yield", Statement("yield", lineno)# error: 12, "yield", Statement("yield", lineno-2)# error: 12, "yield", Statement("yield", lineno+2)
+            while foo():
+                yield  # error: 16, "yield", Statement("yield", lineno-2)# error: 16, "yield", Statement("yield", lineno)
+
+
+# check error suppression is reset
+async def foo_while_5():
+    await foo()
+    while foo():
+        yield  # error: 8, "yield", Statement("yield", lineno)
+
+        async def foo_nested_error():  # error: 8, "exit", Statement("yield", lineno+1)# error: 8, "exit", Statement("yield", lineno+1)
+            yield  # error: 12, "yield", Statement("function definition", lineno-1)# error: 12, "yield", Statement("function definition", lineno-1)
+
+    await foo()
+
+
+# --- while + continue ---
+# no checkpoint on continue
+async def foo_while_continue_1():  # error: 0, "exit", Statement("yield", lineno+3)
+    await foo()
+    while foo():
+        yield  # error: 8, "yield", Statement("yield", lineno)
+        if ...:
+            continue
+        await foo()
+
+
+# multiple continues
+async def foo_while_continue_2():  # error: 0, "exit", Statement("yield", lineno+3)
+    await foo()
+    while foo():
+        yield  # error: 8, "yield", Statement("yield", lineno)
+        if foo():
+            continue
+        await foo()
+        if ...:
+            continue
+        while foo():
+            yield  # safe
+            await foo()
+
+
+# --- while + break ---
+# else might not run
+async def foo_while_break_1():  # error: 0, "exit", Statement("yield", lineno+6)
+    while foo():
+        if ...:
+            break
+    else:
+        await foo()
+    yield  # error: 4, "yield", Statement("function definition", lineno-6)
+
+
+# no checkpoint on break
+async def foo_while_break_2():  # error: 0, "exit", Statement("yield", lineno+3)
+    await foo()
+    while foo():
+        yield  # safe
+        if ...:
+            break
+        await foo()
+
+
+# guaranteed if else and break
+async def foo_while_break_3():  # error: 0, "exit", Statement("yield", lineno+7)
+    while foo():
+        await foo()
+        if ...:
+            break  # if it breaks, have checkpointed
+    else:
+        await foo()  # runs if 0-iter
+    yield  # safe
+
+
+# break at non-guaranteed checkpoint
+async def foo_while_break_4():  # error: 0, "exit", Statement("yield", lineno+7)
+    while foo():
+        if ...:
+            break
+        await foo()  # might not run
+    else:
+        await foo()  # might not run
+    yield  # error: 4, "yield", Statement("function definition", lineno-7)
+
+
+# check break is reset on nested
+async def foo_while_break_5():  # error: 0, "exit", Statement("yield", lineno+12)
+    await foo()
+    while foo():
+        yield
+        if ...:
+            break
+        await foo()
+        while foo():
+            yield  # safe
+            await foo()
+        yield  # safe
+        await foo()
+    yield  # error: 4, "yield", Statement("yield", lineno-9)
+
+
+# check multiple breaks
+async def foo_while_break_6():  # error: 0, "exit", Statement("yield", lineno+11)
+    await foo()
+    while foo():
+        yield
+        if ...:
+            break
+        await foo()
+        yield
+        await foo()
+        if ...:
+            break
+    yield  # error: 4, "yield", Statement("yield", lineno-8)
+
+
+async def foo_while_break_7():  # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+5)
+    while foo():
+        await foo()
+        if ...:
+            break
+        yield
+        break
+
+
+async def foo_while_endless_1():
+    while True:
+        await foo()
+        yield
+
+
+async def foo_while_endless_2():  # error: 0, "exit", Statement("function definition", lineno)# error: 0, "exit", Statement("yield", lineno+3)
+    while foo():
+        await foo()
+        yield
+
+
+# try
+async def foo_try_1():  # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2)
+    try:
+        yield  # error: 8, "yield", Statement("function definition", lineno-2)
+    except:
+        pass
+
+
+# no checkpoint after yield in ValueError
+async def foo_try_2():  # error: 0, "exit", Statement("yield", lineno+5)
+    try:
+        await foo()
+    except ValueError:
+        # try might not have checkpointed
+        yield  # error: 8, "yield", Statement("function definition", lineno-5)
+    except:
+        await foo()
+    else:
+        pass
+
+
+async def foo_try_3():  # error: 0, "exit", Statement("yield", lineno+6)
+    try:
+        ...
+    except:
+        await foo()
+    else:
+        yield  # error: 8, "yield", Statement("function definition", lineno-6)
+
+
+async def foo_try_4():  # safe
+    try:
+        ...
+    except:
+        yield  # error: 8, "yield", Statement("function definition", lineno-4)
+    finally:
+        await foo()
+
+
+async def foo_try_5():
+    try:
+        await foo()
+    finally:
+        # try might crash before checkpoint
+        yield  # error: 8, "yield", Statement("function definition", lineno-5)
+        await foo()
+
+
+async def foo_try_6():  # error: 0, "exit", Statement("yield", lineno+5)
+    try:
+        await foo()
+    except ValueError:
+        pass
+    yield  # error: 4, "yield", Statement("function definition", lineno-5)
+
+
+async def foo_try_7():  # error: 0, "exit", Statement("yield", lineno+17)
+    await foo()
+    try:
+        yield
+        await foo()
+    except ValueError:
+        await foo()
+        yield
+        await foo()
+    except SyntaxError:
+        yield  # error: 8, "yield", Statement("yield", lineno-7)
+        await foo()
+    finally:
+        pass
+    # If the try raises an exception without checkpointing, and it's not caught
+    # by any of the excepts, jumping straight to the finally.
+    # Then the error will be propagated upwards
+    yield  # safe
+
+
+## safe only if (try or else) and all except bodies either await or raise
+## if foo() raises a ValueError it's not checkpointed, and may or may not yield
+async def foo_try_8():  # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+3)
+    try:
+        await foo()
+        yield
+        await foo()
+    except ValueError:
+        ...
+    except:
+        raise
+    else:
+        await foo()
+
+
+# no checkpoint after yield in else
+async def foo_try_9():  # error: 0, "exit", Statement("yield", lineno+6)
+    try:
+        await foo()
+    except:
+        await foo()
+    else:
+        yield
+
+
+# bare except means we'll jump to finally after full execution of either try or the except
+async def foo_try_10():
+    try:
+        await foo()
+    except:
+        await foo()
+    finally:
+        yield
+        await foo()
+
+
+async def foo_try_10_BaseException():
+    try:
+        await foo()
+    except BaseException:
+        await foo()
+    finally:
+        yield
+        await foo()
+
+
+# not fully covering excepts
+async def foo_try_10_exception():
+    try:
+        await foo()
+    except ValueError:
+        await foo()
+    finally:
+        yield  # error: 8, "yield", Statement("function definition", lineno-6)
+        await foo()
+
+
+async def foo_try_10_no_except():
+    try:
+        await foo()
+    finally:
+        # try might crash before checkpoint
+        yield  # error: 8, "yield", Statement("function definition", lineno-5)
+        await foo()
+
+
+# if
+async def foo_if_1():
+    if ...:
+        yield  # error: 8, "yield", Statement("function definition", lineno-2)
+        await foo()
+    else:
+        yield  # error: 8, "yield", Statement("function definition", lineno-5)
+        await foo()
+
+
+async def foo_if_2():  # error: 0, "exit", Statement("yield", lineno+6)
+    await foo()
+    if ...:
+        ...
+    else:
+        yield
+    yield  # error: 4, "yield", Statement("yield", lineno-1)
+
+
+async def foo_if_3():  # error: 0, "exit", Statement("yield", lineno+6)
+    await foo()
+    if ...:
+        yield
+    else:
+        ...
+    yield  # error: 4, "yield", Statement("yield", lineno-3)
+
+
+async def foo_if_4():  # error: 0, "exit", Statement("yield", lineno+7)
+    await foo()
+    yield
+    if ...:
+        await foo()
+    else:
+        ...
+    yield  # error: 4, "yield", Statement("yield", lineno-5)
+
+
+async def foo_if_5():  # error: 0, "exit", Statement("yield", lineno+8)
+    await foo()
+    if ...:
+        yield
+        await foo()
+    else:
+        yield
+        ...
+    yield  # error: 4, "yield", Statement("yield", lineno-2)
+
+
+async def foo_if_6():  # error: 0, "exit", Statement("yield", lineno+8)
+    await foo()
+    if ...:
+        yield
+    else:
+        yield
+        await foo()
+        ...
+    yield  # error: 4, "yield", Statement("yield", lineno-5)
+
+
+async def foo_if_7():  # error: 0, "exit", Statement("function definition", lineno)
+    if ...:
+        await foo()
+        yield
+        await foo()
+
+
+async def foo_if_8():  # error: 0, "exit", Statement("function definition", lineno)
+    if ...:
+        ...
+    else:
+        await foo()
+        yield
+        await foo()
+
+
+# IfExp
+async def foo_ifexp_1():  # error: 0, "exit", Statement("yield", lineno+1) # error: 0, "exit", Statement("yield", lineno+1)
+    print((yield) if await foo() else (yield))
+
+
+# Will either enter else, and it's a guaranteed checkpoint - or enter if, in which
+# case the problem is the yield.
+async def foo_ifexp_2():  # error: 0, "exit", Statement("yield", lineno+2)
+    print(
+        (yield)  # error: 9, "yield", Statement("function definition", lineno-2)
+        if ... and await foo()
+        else await foo()
+    )
+
+
+# normal function
+def foo_sync_1():
+    return
+
+
+def foo_sync_2():
+    ...
+
+
+def foo_sync_3():
+    yield
+
+
+def foo_sync_4():
+    if ...:
+        return
+    yield
+
+
+def foo_sync_5():
+    if ...:
+        return
+    yield
+
+
+def foo_sync_6():
+    while foo():
+        yield
+
+
+def foo_sync_7():
+    while foo():
+        if ...:
+            return
+        yield
+
+
+# nested function definition
+async def foo_func_1():
+    await foo()
+
+    async def foo_func_2():  # error: 4, "exit", Statement("yield", lineno+1)
+        yield  # error: 8, "yield", Statement("function definition", lineno-1)
+
+
+async def foo_func_3():  # error: 0, "exit", Statement("yield", lineno+2)
+    await foo()
+    yield
+
+    async def foo_func_4():
+        await foo()
+
+
+async def foo_func_5():  # error: 0, "exit", Statement("yield", lineno+2)
+    await foo()
+    yield
+
+    def foo_func_6():  # safe
+        yield
+
+        async def foo_func_7():
+            await foo()
+            ...
+
+
+# No error from function definition, but may shortcut after yield
+async def foo_boolops_1():  # error: 0, "exit", Stmt("yield", line+1)
+    _ = await foo() and (yield) and await foo()
+
+
+# may shortcut after any of the yields
+async def foo_boolops_2():  # error: 0, "exit", Stmt("yield", line+1) # error: 0, "exit", Stmt("yield", line+1)
+    _ = await foo() and (yield) and await foo() and (yield)
+
+
+# fmt: off
+async def foo_boolops_3():  # error: 0, "exit", Stmt("yield", line+1) # error: 0, "exit", Stmt("yield", line+4) # error: 0, "exit", Stmt("yield", line+5)
+    _ = (await foo() or (yield) or await foo()) or (
+        ...
+        or (
+            (yield)  # error: 13, "yield", Stmt("yield", line-3)
+            and (yield))  # error: 17, "yield", Stmt("yield", line-1)
+    )
+# fmt: on
+
+# loop over non-empty static collection
+async def foo_loop_static():
+    # break/else behaviour on guaranteed body execution
+    for _ in [1, 2, 3]:
+        await foo()
+    else:
+        yield
+    await foo()
+    yield
+
+    for _ in [1, 2, 3]:
+        await foo()
+        if ...:
+            break
+    else:
+        yield
+        await foo()
+    yield
+
+    # continue
+    for _ in [1, 2, 3]:
+        if ...:
+            continue
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-7)
+
+    # continue/else
+    for _ in [1, 2, 3]:
+        if ...:
+            continue
+        await foo()
+    else:
+        yield  # error: 8, "yield", Stmt("yield", line-8)
+    await foo()
+    yield
+
+    for _ in [1, 2, 3]:
+        await foo()
+        if ...:
+            break
+    else:
+        yield
+        await foo()
+    yield
+
+    # test different containers
+    for _ in (1, 2, 3):
+        await foo()
+    yield
+
+    for _ in (foo(), foo()):
+        await foo()
+    yield
+
+    for _ in ((),):
+        await foo()
+    yield
+
+    for _ in "hello":
+        await foo()
+    yield
+
+    for _ in b"hello":
+        await foo()
+    yield
+
+    for _ in r"hello":
+        await foo()
+    yield
+
+    for _ in {1, 2, 3}:
+        await foo()
+    yield
+
+    for _ in ():
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in {1: 2, 3: 4}:
+        await foo()
+    yield
+
+    for _ in "   ".strip():
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in range(0):
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in (*range(0),):
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in (*(1, 2),):
+        await foo()
+    yield
+
+    for _ in {**{}}:
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in {**{}, **{}}:
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in {**{1: 2}}:
+        await foo()
+    yield
+
+    for _ in (*range(0), *[1, 2, 3]):
+        await foo()
+    yield
+
+    for _ in {**{}, **{1: 2}}:
+        await foo()
+    yield
+
+    x: Any = ...
+    for _ in (*x, *[1, 2, 3]):
+        await foo()
+    yield
+
+    for _ in {**x, **{1: 2}}:
+        await foo()
+    yield
+
+    for _ in {}:
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in "":
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in [[], []][0]:
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    for _ in [[], []].__getitem__(0):
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    # not handled
+    for _ in list((1, 2)):
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-5)
+
+    for _ in list():
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-4)
+
+    # while
+    while True:
+        await foo()
+        if ...:
+            break
+    yield
+
+    while True:
+        if ...:
+            break
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-6)
+
+    while True:
+        if ...:
+            continue
+        await foo()
+        if ...:
+            break
+    yield
+
+    # will get caught by any number of linters, but trio108 will also complain
+    for _ in 5:  # type: ignore
+        await foo()
+    yield  # error: 4, "yield", Stmt("yield", line-5)
+
+    # range with constant arguments also handled, see more extensive tests in 107
+    for i in range(5):
+        await foo()
+    yield
+
+    await foo()
diff --git a/tests/trio109.py b/tests/trio109.py
index f4ea8ad..9ae09c9 100644
--- a/tests/trio109.py
+++ b/tests/trio109.py
@@ -1,96 +1,96 @@
-import trio as anything
-
-timeout = 10
-
-
-async def foo():
-    ...
-
-
-# args
-async def foo_1(timeout):  # error: 16
-    ...
-
-
-# arg in args wih default & annotation
-async def foo_2(timeout: int = 3):  # error: 16
-    ...
-
-
-# vararg
-async def foo_3(*timeout):  # ignored
-    ...
-
-
-# kwarg
-async def foo_4(**timeout):  # ignored
-    ...
-
-
-# correct line/col
-async def foo_5(
-    bar,
-    timeouts,
-    my_timeout,
-    timeout_,
-    timeout,  # error: 4
-):
-    ...
-
-
-# posonlyargs
-async def foo_6(
-    timeout,  # error: 4
-    /,
-    bar,
-):
-    ...
-
-
-# kwonlyargs
-async def foo_7(
-    *,
-    timeout,  # error: 4
-):
-    ...
-
-
-# kwonlyargs (and kw_defaults)
-async def foo_8(
-    *,
-    timeout=5,  # error: 4
-):
-    ...
-
-
-async def foo_9(k=timeout):
-    ...
-
-
-# normal functions are not checked
-def foo_10(timeout):
-    ...
-
-
-def foo_11(timeout, /):
-    ...
-
-
-def foo_12(*, timeout):
-    ...
-
-
-# ignore all functions with a decorator
-@anything.anything
-async def foo_decorator_1(timeout):
-    ...
-
-
-@anything.anything
-async def foo_decorator_2(*, timeout):
-    ...
-
-
-@anything
-async def foo_decorator_3(timeout, /):
-    ...
+import trio as anything
+
+timeout = 10
+
+
+async def foo():
+    ...
+
+
+# args
+async def foo_1(timeout):  # error: 16
+    ...
+
+
+# arg in args wih default & annotation
+async def foo_2(timeout: int = 3):  # error: 16
+    ...
+
+
+# vararg
+async def foo_3(*timeout):  # ignored
+    ...
+
+
+# kwarg
+async def foo_4(**timeout):  # ignored
+    ...
+
+
+# correct line/col
+async def foo_5(
+    bar,
+    timeouts,
+    my_timeout,
+    timeout_,
+    timeout,  # error: 4
+):
+    ...
+
+
+# posonlyargs
+async def foo_6(
+    timeout,  # error: 4
+    /,
+    bar,
+):
+    ...
+
+
+# kwonlyargs
+async def foo_7(
+    *,
+    timeout,  # error: 4
+):
+    ...
+
+
+# kwonlyargs (and kw_defaults)
+async def foo_8(
+    *,
+    timeout=5,  # error: 4
+):
+    ...
+
+
+async def foo_9(k=timeout):
+    ...
+
+
+# normal functions are not checked
+def foo_10(timeout):
+    ...
+
+
+def foo_11(timeout, /):
+    ...
+
+
+def foo_12(*, timeout):
+    ...
+
+
+# ignore all functions with a decorator
+@anything.anything
+async def foo_decorator_1(timeout):
+    ...
+
+
+@anything.anything
+async def foo_decorator_2(*, timeout):
+    ...
+
+
+@anything
+async def foo_decorator_3(timeout, /):
+    ...
diff --git a/tests/trio110.py b/tests/trio110.py
index 9b1a874..84e19c4 100644
--- a/tests/trio110.py
+++ b/tests/trio110.py
@@ -1,70 +1,70 @@
-import trio
-import trio as noerror
-
-
-async def foo():
-    # only trigger on while loop with body being exactly one sleep[_until] statement
-    while ...:  # error: 4
-        await trio.sleep()
-
-    while ...:  # error: 4
-        await trio.sleep_until()
-
-    # nested
-
-    while ...:  # safe
-        while ...:  # error: 8
-            await trio.sleep()
-        await trio.sleep()
-
-    while ...:  # safe
-        while ...:  # error: 8
-            await trio.sleep()
-
-    ### the rest are all safe
-
-    # don't trigger on bodies with more than one statement
-    while ...:
-        await trio.sleep()
-        await trio.sleep()
-
-    while ...:  # safe
-        ...
-        await trio.sleep()
-
-    while ...:
-        await trio.sleep()
-        await trio.sleep_until()
-
-    # check library name
-    while ...:
-        await noerror.sleep()
-
-    async def sleep():
-        ...
-
-    while ...:
-        await sleep()
-
-    # check function name
-    while ...:
-        await trio.sleepies()
-
-    # don't trigger on [async] for
-    for _ in "":
-        await trio.sleep()
-
-    async for _ in trio.blah:
-        await trio.sleep()
-
-    while ...:
-
-        async def blah():
-            await trio.sleep()
-
-    while ...:
-        if ...:
-            await trio.sleep()
-
-    while await trio.sleep():
-        ...
+import trio
+import trio as noerror
+
+
+async def foo():
+    # only trigger on while loop with body being exactly one sleep[_until] statement
+    while ...:  # error: 4
+        await trio.sleep()
+
+    while ...:  # error: 4
+        await trio.sleep_until()
+
+    # nested
+
+    while ...:  # safe
+        while ...:  # error: 8
+            await trio.sleep()
+        await trio.sleep()
+
+    while ...:  # safe
+        while ...:  # error: 8
+            await trio.sleep()
+
+    ### the rest are all safe
+
+    # don't trigger on bodies with more than one statement
+    while ...:
+        await trio.sleep()
+        await trio.sleep()
+
+    while ...:  # safe
+        ...
+        await trio.sleep()
+
+    while ...:
+        await trio.sleep()
+        await trio.sleep_until()
+
+    # check library name
+    while ...:
+        await noerror.sleep()
+
+    async def sleep():
+        ...
+
+    while ...:
+        await sleep()
+
+    # check function name
+    while ...:
+        await trio.sleepies()
+
+    # don't trigger on [async] for
+    for _ in "":
+        await trio.sleep()
+
+    async for _ in trio.blah:
+        await trio.sleep()
+
+    while ...:
+
+        async def blah():
+            await trio.sleep()
+
+    while ...:
+        if ...:
+            await trio.sleep()
+
+    while await trio.sleep():
+        ...
diff --git a/tests/trio111.py b/tests/trio111.py
index e90bde2..2352f19 100644
--- a/tests/trio111.py
+++ b/tests/trio111.py
@@ -1,240 +1,240 @@
-from typing import Any
-
-import trio
-import trio as noterror
-
-
-# shed/black breaks up a *ton* of lines since adding more detailed error messages, so
-# disable formatting to avoid having to adjust a ton of line references
-# fmt: off
-async def foo():
-    async with trio.open_nursery():
-        ...
-
-    # async nursery
-    async with trio.open_nursery() as nursery:
-        # async context manager
-        async with trio.open_process() as bar:
-            nursery.start(bar)  # error: 26, line-1, line-3, "bar", "start"
-            nursery.start(foo=bar)  # error: 30, line-2, line-4, "bar", "start"
-            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-5, "bar", "start"
-
-            nursery.start_soon(bar)  # error: 31, line-5, line-7, "bar", "start_soon"
-
-        # sync context manager
-        with open("") as bar:
-            nursery.start(bar)  # error: 26, line-1, line-11, "bar", "start"
-            nursery.start(foo=bar)  # error: 30, line-2, line-12, "bar", "start"
-            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-13, "bar", "start"
-
-            nursery.start_soon(bar)  # error: 31, line-5, line-15, "bar", "start_soon"
-
-    # sync nursery
-    with trio.open_nursery() as nursery:
-        # async context manager
-        async with trio.open_process() as bar:
-            nursery.start(bar)  # error: 26, line-1, line-3, "bar", "start"
-            nursery.start(foo=bar)  # error: 30, line-2, line-4, "bar", "start"
-            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-5, "bar", "start"
-
-            nursery.start_soon(bar)  # error: 31, line-5, line-7, "bar", "start_soon"
-
-        # sync context manager
-        with open("") as bar:
-            nursery.start(bar)  # error: 26, line-1, line-11, "bar", "start"
-            nursery.start(foo=bar)  # error: 30, line-2, line-12, "bar", "start"
-            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-13, "bar", "start"
-
-            nursery.start_soon(bar)  # error: 31, line-5, line-15, "bar", "start_soon"
-
-    # check all safe async/sync permutations
-    async with trio.open_process() as bar:
-        async with trio.open_nursery() as nursery:
-            nursery.start(bar)  # safe
-    async with trio.open_process() as bar:
-        with trio.open_nursery() as nursery:
-            nursery.start(bar)  # safe
-    with trio.open_process() as bar:
-        async with trio.open_nursery() as nursery:
-            nursery.start(bar)  # safe
-    with trio.open_process() as bar:
-        with trio.open_nursery() as nursery:
-            nursery.start(bar)  # safe
-
-# reset variables on nested function
-with trio.open_nursery() as nursery:
-    def foo_1():
-        nursery = noterror.something
-        with trio.open_process() as bar_2:
-            nursery.start(bar_2)  # safe
-
-    async def foo_2():
-        nursery = noterror.something
-        async with trio.open_process() as bar_2:
-            nursery.start(bar_2)  # safe
-
-# specifically check for *trio*.open_nursery
-with noterror.open_nursery() as nursery:
-    with trio.open("") as bar:
-        nursery.start(bar)
-
-# specifically check for trio.*open_nursery*
-with trio.open_nurse() as nursery:
-    with trio.open("") as bar:
-        nursery.start(bar)
-
-
-bar_1: Any = ""
-bar_2: Any = ""
-nursery_2: Any = ""
-with trio.open_nursery() as nursery_1:
-    nursery_1.start(bar_1)
-    nursery_1.start(bar_2)
-    nursery_2.start(bar_1)
-    nursery_2.start(bar_2)
-    with trio.open_nursery() as nursery_2:
-        nursery_1.start(bar_1)
-        nursery_1.start(bar_2)
-        nursery_2.start(bar_1)
-        nursery_2.start(bar_2)
-        with open("") as bar_1:
-            nursery_1.start(bar_1)  # error: 28, line-1, line-11, "bar_1", "start"
-            nursery_1.start(bar_2)
-            nursery_2.start(bar_1)  # error: 28, line-3, line-8, "bar_1", "start"
-            nursery_2.start(bar_2)
-            with trio.open("") as bar_2:
-                nursery_1.start(bar_1)  # error: 32, line-6, line-16, "bar_1", "start"
-                nursery_1.start(bar_2)  # error: 32, line-2, line-17, "bar_2", "start"
-                nursery_2.start(bar_1)  # error: 32, line-8, line-13, "bar_1", "start"
-                nursery_2.start(bar_2)  # error: 32, line-4, line-14, "bar_2", "start"
-            nursery_1.start(bar_1)  # error: 28, line-10, line-20, "bar_1", "start"
-            nursery_1.start(bar_2)
-            nursery_2.start(bar_1)  # error: 28, line-12, line-17, "bar_1", "start"
-            nursery_2.start(bar_2)
-        nursery_1.start(bar_1)
-        nursery_1.start(bar_2)
-        nursery_2.start(bar_1)
-        nursery_2.start(bar_2)
-    nursery_1.start(bar_1)
-    nursery_1.start(bar_2)
-    nursery_2.start(bar_1)
-    nursery_2.start(bar_2)
-
-# same as above, except second and third scope swapped
-with trio.open_nursery() as nursery_1:
-    nursery_1.start(bar_1)
-    nursery_1.start(bar_2)
-    nursery_2.start(bar_1)
-    nursery_2.start(bar_2)
-    with open("") as bar_1:
-        nursery_1.start(bar_1)  # error: 24, line-1, line-6, "bar_1", "start"
-        nursery_1.start(bar_2)
-        nursery_2.start(bar_1)
-        nursery_2.start(bar_2)
-        with trio.open_nursery() as nursery_2:
-            nursery_1.start(bar_1)  # error: 28, line-6, line-11, "bar_1", "start"
-            nursery_1.start(bar_2)
-            nursery_2.start(bar_1)
-            nursery_2.start(bar_2)
-            with trio.open("") as bar_2:
-                nursery_1.start(bar_1)  # error: 32, line-11, line-16, "bar_1", "start"
-                nursery_1.start(bar_2)  # error: 32, line-2, line-17, "bar_2", "start"
-                nursery_2.start(bar_1)
-                nursery_2.start(bar_2)  # error: 32, line-4, line-9, "bar_2", "start"
-            nursery_1.start(bar_1)  # error: 28, line-15, line-20, "bar_1", "start"
-            nursery_1.start(bar_2)
-            nursery_2.start(bar_1)
-            nursery_2.start(bar_2)
-        nursery_1.start(bar_1)  # error: 24, line-19, line-24, "bar_1", "start"
-        nursery_1.start(bar_2)
-        nursery_2.start(bar_1)
-        nursery_2.start(bar_2)
-    nursery_1.start(bar_1)
-    nursery_1.start(bar_2)
-    nursery_2.start(bar_1)
-    nursery_2.start(bar_2)
-
-# multiple withitems
-with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2:
-    nursery_1.start(bar_1)  # error: 20, line-1, line-1, "bar_1", "start"
-    nursery_1.start(bar_2)  # error: 20, line-2, line-2, "bar_2", "start"
-    nursery_2.start(bar_1)
-    nursery_2.start(bar_2)  # error: 20, line-4, line-4, "bar_2", "start"
-
-# attribute/name parameter modifications
-with trio.open_nursery() as nursery:
-    with trio.anything() as bar:
-        nursery.start(noterror.bar)  # safe
-        nursery.start(bar.anything)  # error: 22, line-2, line-3, "bar", "start"
-        nursery.start(bar.anything.anything)  # error: 22, line-3, line-4, "bar", "start"
-
-# nursery passed as parameter
-with trio.open_nursery() as nursery:
-    with trio.open_nursery() as nursery_2:
-        nursery.start(nursery_2)  # error: 22, line-1, line-2, "nursery_2", "start"
-        nursery_2.start(nursery)
-
-# in theory safe-ish, but treated as error and likely an error with typechecking
-with trio.open_nursery() as nursery:
-    nursery = noterror.anything
-    with trio.anything() as bar:
-        nursery.start_soon(bar)  # error: 27, line-1, line-3, "bar", "start_soon"
-
-# context manager with same variable name overrides nursery
-with trio.open_nursery() as nursery:
-    with trio.anything() as nursery:
-        with trio.anything() as bar:
-            nursery.start_soon(bar)
-
-# list unpack
-with trio.open_nursery() as nursery:
-    with trio.open_process() as bar:
-        nursery.start(*bar)  # error: 23, line-1, line-2, "bar", "start"
-        nursery.start(foo=[*bar])  # error: 28, line-2, line-3, "bar", "start"
-        nursery.start(..., ..., *bar, ...)  # error: 33, line-3, line-4, "bar", "start"
-        nursery.start_soon(*bar)  # error: 28, line-4, line-5, "bar", "start_soon"
-
-# dict unpack
-with trio.open_nursery() as nursery:
-    with trio.open_process() as bar:
-        nursery.start(**bar)  # error: 24, line-1, line-2, "bar", "start"
-        nursery.start(foo={**bar})  # error: 29, line-2, line-3, "bar", "start"
-        nursery.start(..., ..., **bar, foo=...)  # error: 34, line-3, line-4, "bar", "start"
-        nursery.start_soon(**bar)  # error: 29, line-4, line-5, "bar", "start_soon"
-
-# multi-line call with multiple errors
-with trio.open_nursery() as nursery:
-    with trio.open_process() as bar:
-        nursery.start(
-            ...,
-            bar,  # error: 12, line-3, line-4, "bar", "start"
-            ...,
-            *bar,  # error: 13, line-5, line-6, "bar", "start"
-            ...,
-            **bar,  # error: 14, line-7, line-8, "bar", "start"
-        )
-
-# variable nested deep inside parameter list
-with trio.open_nursery() as nursery:
-    with trio.open_process() as bar:
-        nursery.start(list((tuple([0]), (bar))))  # error: 41, line-1, line-2, "bar", "start"
-        from functools import partial
-        nursery.start(partial(noterror.bar, foo=bar))  # error: 48, line-3, line-4, "bar", "start"
-
-# tricky cases
-with trio.open_nursery() as nursery:
-    with trio.open_process() as bar:
-        nursery.start("bar")
-        nursery.start(lambda bar: bar+1)
-
-        def myfun(nursery, bar):
-            nursery.start(bar)
-
-# nursery overriden by non-expression context manager
-b = trio.open()
-with trio.open_nursery() as nursery:
-    with b as nursery:
-        with open("") as f:
-            nursery.start(f)
-
-# fmt: on
+from typing import Any
+
+import trio
+import trio as noterror
+
+
+# shed/black breaks up a *ton* of lines since adding more detailed error messages, so
+# disable formatting to avoid having to adjust a ton of line references
+# fmt: off
+async def foo():
+    async with trio.open_nursery():
+        ...
+
+    # async nursery
+    async with trio.open_nursery() as nursery:
+        # async context manager
+        async with trio.open_process() as bar:
+            nursery.start(bar)  # error: 26, line-1, line-3, "bar", "start"
+            nursery.start(foo=bar)  # error: 30, line-2, line-4, "bar", "start"
+            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-5, "bar", "start"
+
+            nursery.start_soon(bar)  # error: 31, line-5, line-7, "bar", "start_soon"
+
+        # sync context manager
+        with open("") as bar:
+            nursery.start(bar)  # error: 26, line-1, line-11, "bar", "start"
+            nursery.start(foo=bar)  # error: 30, line-2, line-12, "bar", "start"
+            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-13, "bar", "start"
+
+            nursery.start_soon(bar)  # error: 31, line-5, line-15, "bar", "start_soon"
+
+    # sync nursery
+    with trio.open_nursery() as nursery:
+        # async context manager
+        async with trio.open_process() as bar:
+            nursery.start(bar)  # error: 26, line-1, line-3, "bar", "start"
+            nursery.start(foo=bar)  # error: 30, line-2, line-4, "bar", "start"
+            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-5, "bar", "start"
+
+            nursery.start_soon(bar)  # error: 31, line-5, line-7, "bar", "start_soon"
+
+        # sync context manager
+        with open("") as bar:
+            nursery.start(bar)  # error: 26, line-1, line-11, "bar", "start"
+            nursery.start(foo=bar)  # error: 30, line-2, line-12, "bar", "start"
+            nursery.start(..., ..., bar, ...)  # error: 36, line-3, line-13, "bar", "start"
+
+            nursery.start_soon(bar)  # error: 31, line-5, line-15, "bar", "start_soon"
+
+    # check all safe async/sync permutations
+    async with trio.open_process() as bar:
+        async with trio.open_nursery() as nursery:
+            nursery.start(bar)  # safe
+    async with trio.open_process() as bar:
+        with trio.open_nursery() as nursery:
+            nursery.start(bar)  # safe
+    with trio.open_process() as bar:
+        async with trio.open_nursery() as nursery:
+            nursery.start(bar)  # safe
+    with trio.open_process() as bar:
+        with trio.open_nursery() as nursery:
+            nursery.start(bar)  # safe
+
+# reset variables on nested function
+with trio.open_nursery() as nursery:
+    def foo_1():
+        nursery = noterror.something
+        with trio.open_process() as bar_2:
+            nursery.start(bar_2)  # safe
+
+    async def foo_2():
+        nursery = noterror.something
+        async with trio.open_process() as bar_2:
+            nursery.start(bar_2)  # safe
+
+# specifically check for *trio*.open_nursery
+with noterror.open_nursery() as nursery:
+    with trio.open("") as bar:
+        nursery.start(bar)
+
+# specifically check for trio.*open_nursery*
+with trio.open_nurse() as nursery:
+    with trio.open("") as bar:
+        nursery.start(bar)
+
+
+bar_1: Any = ""
+bar_2: Any = ""
+nursery_2: Any = ""
+with trio.open_nursery() as nursery_1:
+    nursery_1.start(bar_1)
+    nursery_1.start(bar_2)
+    nursery_2.start(bar_1)
+    nursery_2.start(bar_2)
+    with trio.open_nursery() as nursery_2:
+        nursery_1.start(bar_1)
+        nursery_1.start(bar_2)
+        nursery_2.start(bar_1)
+        nursery_2.start(bar_2)
+        with open("") as bar_1:
+            nursery_1.start(bar_1)  # error: 28, line-1, line-11, "bar_1", "start"
+            nursery_1.start(bar_2)
+            nursery_2.start(bar_1)  # error: 28, line-3, line-8, "bar_1", "start"
+            nursery_2.start(bar_2)
+            with trio.open("") as bar_2:
+                nursery_1.start(bar_1)  # error: 32, line-6, line-16, "bar_1", "start"
+                nursery_1.start(bar_2)  # error: 32, line-2, line-17, "bar_2", "start"
+                nursery_2.start(bar_1)  # error: 32, line-8, line-13, "bar_1", "start"
+                nursery_2.start(bar_2)  # error: 32, line-4, line-14, "bar_2", "start"
+            nursery_1.start(bar_1)  # error: 28, line-10, line-20, "bar_1", "start"
+            nursery_1.start(bar_2)
+            nursery_2.start(bar_1)  # error: 28, line-12, line-17, "bar_1", "start"
+            nursery_2.start(bar_2)
+        nursery_1.start(bar_1)
+        nursery_1.start(bar_2)
+        nursery_2.start(bar_1)
+        nursery_2.start(bar_2)
+    nursery_1.start(bar_1)
+    nursery_1.start(bar_2)
+    nursery_2.start(bar_1)
+    nursery_2.start(bar_2)
+
+# same as above, except second and third scope swapped
+with trio.open_nursery() as nursery_1:
+    nursery_1.start(bar_1)
+    nursery_1.start(bar_2)
+    nursery_2.start(bar_1)
+    nursery_2.start(bar_2)
+    with open("") as bar_1:
+        nursery_1.start(bar_1)  # error: 24, line-1, line-6, "bar_1", "start"
+        nursery_1.start(bar_2)
+        nursery_2.start(bar_1)
+        nursery_2.start(bar_2)
+        with trio.open_nursery() as nursery_2:
+            nursery_1.start(bar_1)  # error: 28, line-6, line-11, "bar_1", "start"
+            nursery_1.start(bar_2)
+            nursery_2.start(bar_1)
+            nursery_2.start(bar_2)
+            with trio.open("") as bar_2:
+                nursery_1.start(bar_1)  # error: 32, line-11, line-16, "bar_1", "start"
+                nursery_1.start(bar_2)  # error: 32, line-2, line-17, "bar_2", "start"
+                nursery_2.start(bar_1)
+                nursery_2.start(bar_2)  # error: 32, line-4, line-9, "bar_2", "start"
+            nursery_1.start(bar_1)  # error: 28, line-15, line-20, "bar_1", "start"
+            nursery_1.start(bar_2)
+            nursery_2.start(bar_1)
+            nursery_2.start(bar_2)
+        nursery_1.start(bar_1)  # error: 24, line-19, line-24, "bar_1", "start"
+        nursery_1.start(bar_2)
+        nursery_2.start(bar_1)
+        nursery_2.start(bar_2)
+    nursery_1.start(bar_1)
+    nursery_1.start(bar_2)
+    nursery_2.start(bar_1)
+    nursery_2.start(bar_2)
+
+# multiple withitems
+with trio.open_nursery() as nursery_1, trio.anything() as bar_1, trio.open_nursery() as nursery_2, trio.anything() as bar_2:
+    nursery_1.start(bar_1)  # error: 20, line-1, line-1, "bar_1", "start"
+    nursery_1.start(bar_2)  # error: 20, line-2, line-2, "bar_2", "start"
+    nursery_2.start(bar_1)
+    nursery_2.start(bar_2)  # error: 20, line-4, line-4, "bar_2", "start"
+
+# attribute/name parameter modifications
+with trio.open_nursery() as nursery:
+    with trio.anything() as bar:
+        nursery.start(noterror.bar)  # safe
+        nursery.start(bar.anything)  # error: 22, line-2, line-3, "bar", "start"
+        nursery.start(bar.anything.anything)  # error: 22, line-3, line-4, "bar", "start"
+
+# nursery passed as parameter
+with trio.open_nursery() as nursery:
+    with trio.open_nursery() as nursery_2:
+        nursery.start(nursery_2)  # error: 22, line-1, line-2, "nursery_2", "start"
+        nursery_2.start(nursery)
+
+# in theory safe-ish, but treated as error and likely an error with typechecking
+with trio.open_nursery() as nursery:
+    nursery = noterror.anything
+    with trio.anything() as bar:
+        nursery.start_soon(bar)  # error: 27, line-1, line-3, "bar", "start_soon"
+
+# context manager with same variable name overrides nursery
+with trio.open_nursery() as nursery:
+    with trio.anything() as nursery:
+        with trio.anything() as bar:
+            nursery.start_soon(bar)
+
+# list unpack
+with trio.open_nursery() as nursery:
+    with trio.open_process() as bar:
+        nursery.start(*bar)  # error: 23, line-1, line-2, "bar", "start"
+        nursery.start(foo=[*bar])  # error: 28, line-2, line-3, "bar", "start"
+        nursery.start(..., ..., *bar, ...)  # error: 33, line-3, line-4, "bar", "start"
+        nursery.start_soon(*bar)  # error: 28, line-4, line-5, "bar", "start_soon"
+
+# dict unpack
+with trio.open_nursery() as nursery:
+    with trio.open_process() as bar:
+        nursery.start(**bar)  # error: 24, line-1, line-2, "bar", "start"
+        nursery.start(foo={**bar})  # error: 29, line-2, line-3, "bar", "start"
+        nursery.start(..., ..., **bar, foo=...)  # error: 34, line-3, line-4, "bar", "start"
+        nursery.start_soon(**bar)  # error: 29, line-4, line-5, "bar", "start_soon"
+
+# multi-line call with multiple errors
+with trio.open_nursery() as nursery:
+    with trio.open_process() as bar:
+        nursery.start(
+            ...,
+            bar,  # error: 12, line-3, line-4, "bar", "start"
+            ...,
+            *bar,  # error: 13, line-5, line-6, "bar", "start"
+            ...,
+            **bar,  # error: 14, line-7, line-8, "bar", "start"
+        )
+
+# variable nested deep inside parameter list
+with trio.open_nursery() as nursery:
+    with trio.open_process() as bar:
+        nursery.start(list((tuple([0]), (bar))))  # error: 41, line-1, line-2, "bar", "start"
+        from functools import partial
+        nursery.start(partial(noterror.bar, foo=bar))  # error: 48, line-3, line-4, "bar", "start"
+
+# tricky cases
+with trio.open_nursery() as nursery:
+    with trio.open_process() as bar:
+        nursery.start("bar")
+        nursery.start(lambda bar: bar+1)
+
+        def myfun(nursery, bar):
+            nursery.start(bar)
+
+# nursery overriden by non-expression context manager
+b = trio.open()
+with trio.open_nursery() as nursery:
+    with b as nursery:
+        with open("") as f:
+            nursery.start(f)
+
+# fmt: on
diff --git a/tests/trio112.py b/tests/trio112.py
index dee28cb..41bf5d6 100644
--- a/tests/trio112.py
+++ b/tests/trio112.py
@@ -1,88 +1,88 @@
-import trio
-import trio as noterror
-
-# error
-with trio.open_nursery() as n:  # error: 5, "n"
-    n.start(...)
-
-with trio.open_nursery(...) as nurse:  # error: 5, "nurse"
-    nurse.start_soon(...)
-
-with trio.open_nursery() as n:  # error: 5, "n"
-    n.start_soon(n=7)
-
-
-async def foo():
-    async with trio.open_nursery() as n:  # error: 15, "n"
-        n.start(...)
-
-
-# weird ones with multiple `withitem`s
-# but if split among several `with` they'd all be treated as error (or TRIO111), so
-# treating as error for now.
-with trio.open_nursery() as n, trio.open("") as n:  # error: 5, "n"
-    n.start(...)
-
-with open("") as o, trio.open_nursery() as n:  # error: 20, "n"
-    n.start(o)
-
-with trio.open_nursery() as n, trio.open_nursery() as nurse:  # error: 31, "nurse"
-    nurse.start(n.start(...))
-
-with trio.open_nursery() as n, trio.open_nursery() as n:  # error: 5, "n" # error: 31, "n"
-    n.start(...)
-
-# safe if passing variable as parameter
-with trio.open_nursery() as n:
-    n.start(..., n, ...)
-
-with trio.open_nursery() as n:
-    n.start(..., foo=n + 7, bar=...)
-
-with trio.open_nursery() as n:
-    n.start(foo=tuple(tuple(tuple(tuple(n)))))
-
-# safe if multiple lines
-with trio.open_nursery() as n:
-    ...
-    n.start_soon(...)
-
-with trio.open_nursery() as n:
-    n.start_soon(...)
-    ...
-
-# fmt: off
-with trio.open_nursery() as n:
-    n.start_soon(...) ; ...
-# fmt: on
-
-# n as a parameter to lambda is in fact not using it, but we don't parse
-with trio.open_nursery() as n:
-    n.start_soon(lambda n: n + 1)
-
-# body isn't a call to n.start
-async def foo_1():
-    with trio.open_nursery(...) as n:
-        await n.start(...)
-
-
-# not *trio*.open_nursery
-with noterror.open_nursery(...) as n:
-    n.start(...)
-
-# not trio.*open_nursery*
-with trio.not_error(...) as n:
-    n.start(...)
-
-p = trio.not_error
-# not *n*.start[_soon]
-with trio.open_nursery() as n:
-    p.start(...)
-
-# not n.*start[_soon]*
-with trio.open_nursery() as n:
-    n.start_never(...)
-
-# redundant nursery, not handled
-with trio.open_nursery():
-    pass
+import trio
+import trio as noterror
+
+# error
+with trio.open_nursery() as n:  # error: 5, "n"
+    n.start(...)
+
+with trio.open_nursery(...) as nurse:  # error: 5, "nurse"
+    nurse.start_soon(...)
+
+with trio.open_nursery() as n:  # error: 5, "n"
+    n.start_soon(n=7)
+
+
+async def foo():
+    async with trio.open_nursery() as n:  # error: 15, "n"
+        n.start(...)
+
+
+# weird ones with multiple `withitem`s
+# but if split among several `with` they'd all be treated as error (or TRIO111), so
+# treating as error for now.
+with trio.open_nursery() as n, trio.open("") as n:  # error: 5, "n"
+    n.start(...)
+
+with open("") as o, trio.open_nursery() as n:  # error: 20, "n"
+    n.start(o)
+
+with trio.open_nursery() as n, trio.open_nursery() as nurse:  # error: 31, "nurse"
+    nurse.start(n.start(...))
+
+with trio.open_nursery() as n, trio.open_nursery() as n:  # error: 5, "n" # error: 31, "n"
+    n.start(...)
+
+# safe if passing variable as parameter
+with trio.open_nursery() as n:
+    n.start(..., n, ...)
+
+with trio.open_nursery() as n:
+    n.start(..., foo=n + 7, bar=...)
+
+with trio.open_nursery() as n:
+    n.start(foo=tuple(tuple(tuple(tuple(n)))))
+
+# safe if multiple lines
+with trio.open_nursery() as n:
+    ...
+    n.start_soon(...)
+
+with trio.open_nursery() as n:
+    n.start_soon(...)
+    ...
+
+# fmt: off
+with trio.open_nursery() as n:
+    n.start_soon(...) ; ...
+# fmt: on
+
+# n as a parameter to lambda is in fact not using it, but we don't parse
+with trio.open_nursery() as n:
+    n.start_soon(lambda n: n + 1)
+
+# body isn't a call to n.start
+async def foo_1():
+    with trio.open_nursery(...) as n:
+        await n.start(...)
+
+
+# not *trio*.open_nursery
+with noterror.open_nursery(...) as n:
+    n.start(...)
+
+# not trio.*open_nursery*
+with trio.not_error(...) as n:
+    n.start(...)
+
+p = trio.not_error
+# not *n*.start[_soon]
+with trio.open_nursery() as n:
+    p.start(...)
+
+# not n.*start[_soon]*
+with trio.open_nursery() as n:
+    n.start_never(...)
+
+# redundant nursery, not handled
+with trio.open_nursery():
+    pass
diff --git a/tests/trio_options.py b/tests/trio_options.py
index 5b8fcf6..f5a916a 100644
--- a/tests/trio_options.py
+++ b/tests/trio_options.py
@@ -1,3 +1,3 @@
-@app.route  # type: ignore
-async def f():
-    print("hello world")
+@app.route  # type: ignore
+async def f():
+    print("hello world")