Compare commits
195 Commits
cjm/infere
...
david/more
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d440cad340 | ||
|
|
9169e35175 | ||
|
|
79f809cd13 | ||
|
|
b219c6fc2c | ||
|
|
edfaf896e4 | ||
|
|
da842bbc38 | ||
|
|
b425e6a34a | ||
|
|
a95b4d57c1 | ||
|
|
e069381a23 | ||
|
|
749f176ae5 | ||
|
|
e241c42212 | ||
|
|
09eeee99d4 | ||
|
|
04b636cba2 | ||
|
|
6048f331d9 | ||
|
|
93097f1c53 | ||
|
|
9bb4722ebf | ||
|
|
5caabe54b6 | ||
|
|
814ab47582 | ||
|
|
c3a3622e30 | ||
|
|
4ef422d3b4 | ||
|
|
58bc981677 | ||
|
|
dd5018ac55 | ||
|
|
63df94b521 | ||
|
|
e4c0dd6f96 | ||
|
|
3111dce5b4 | ||
|
|
8445e4725c | ||
|
|
defdc4dd8e | ||
|
|
46bc69d1d4 | ||
|
|
3209953276 | ||
|
|
6ae833e0c7 | ||
|
|
a3dc5c0529 | ||
|
|
d6b24b690a | ||
|
|
5b4afd30ca | ||
|
|
b9827a4122 | ||
|
|
93eff7f174 | ||
|
|
fc661e193a | ||
|
|
42fcbef876 | ||
|
|
71b52b83e4 | ||
|
|
fb90f5a13d | ||
|
|
d7484e6942 | ||
|
|
14ee5dbfde | ||
|
|
27ac34d683 | ||
|
|
31ca1c3064 | ||
|
|
646e4136d7 | ||
|
|
58a11b33da | ||
|
|
7856e90a2c | ||
|
|
98878c9bf2 | ||
|
|
73aa6ea417 | ||
|
|
38d872ea4c | ||
|
|
824def2194 | ||
|
|
2ab78dd6a5 | ||
|
|
03fa7f64dd | ||
|
|
43330225be | ||
|
|
383d9d9f6e | ||
|
|
8108f83810 | ||
|
|
f1205177fd | ||
|
|
1c2cafc101 | ||
|
|
7c5a7d909c | ||
|
|
2a365bb278 | ||
|
|
020f4d4a54 | ||
|
|
888930b7d3 | ||
|
|
d726f09cf0 | ||
|
|
975be9c1c6 | ||
|
|
99e4566fce | ||
|
|
7ad07c2c5d | ||
|
|
4aefe52393 | ||
|
|
cc1f766622 | ||
|
|
fdd0a22c03 | ||
|
|
3728d5b3a2 | ||
|
|
7e3894f5b3 | ||
|
|
c3b40da0d2 | ||
|
|
ef45185dbc | ||
|
|
961fc98344 | ||
|
|
0a6dc8e1b8 | ||
|
|
8d54996ffb | ||
|
|
73e884b232 | ||
|
|
edba60106b | ||
|
|
043fba7a57 | ||
|
|
20d997784d | ||
|
|
82324678cf | ||
|
|
cfd5d63917 | ||
|
|
2a36b47f13 | ||
|
|
6322639aca | ||
|
|
360af1bc32 | ||
|
|
3af3f74c66 | ||
|
|
45f01e7872 | ||
|
|
6cdf996af6 | ||
|
|
9d8a4c0057 | ||
|
|
c9c748a79e | ||
|
|
32c746bd82 | ||
|
|
e76f77d711 | ||
|
|
d86b73eb3d | ||
|
|
5f4b282327 | ||
|
|
d9267132d6 | ||
|
|
5118166d21 | ||
|
|
6fb1d6037a | ||
|
|
9237813e0c | ||
|
|
3bebde3ccc | ||
|
|
6c5cbad533 | ||
|
|
7a2f8d4463 | ||
|
|
ad87ea948d | ||
|
|
acfc34d615 | ||
|
|
668730cc28 | ||
|
|
bee498d635 | ||
|
|
ec72e675d9 | ||
|
|
1639488082 | ||
|
|
f3e464ea4c | ||
|
|
253f5f269a | ||
|
|
c046101b79 | ||
|
|
7706f561a9 | ||
|
|
f5e3662446 | ||
|
|
a354d9ead6 | ||
|
|
58a8e9c511 | ||
|
|
e83388dcea | ||
|
|
ae39ce56c0 | ||
|
|
ff2d214e11 | ||
|
|
9442cd8fae | ||
|
|
8012707348 | ||
|
|
d7ffe46054 | ||
|
|
7c83af419c | ||
|
|
bbb044ebda | ||
|
|
481065238b | ||
|
|
11f06e0d55 | ||
|
|
f27a8b8c7a | ||
|
|
ca0ae0a484 | ||
|
|
be1d5e3368 | ||
|
|
03503f7f56 | ||
|
|
ff4b6d11fa | ||
|
|
96e7f3f96f | ||
|
|
90dc7438ee | ||
|
|
3e99ab141c | ||
|
|
115745a8ac | ||
|
|
8bb59d7216 | ||
|
|
47aac060de | ||
|
|
7c55330534 | ||
|
|
047d77c60b | ||
|
|
18fddd458a | ||
|
|
db76000521 | ||
|
|
a2ed1e1cd1 | ||
|
|
7457679582 | ||
|
|
1d352872ba | ||
|
|
c8b905bc96 | ||
|
|
5b593d0397 | ||
|
|
c5c5acda23 | ||
|
|
26747aae75 | ||
|
|
85b825a2a1 | ||
|
|
9e764ef6d0 | ||
|
|
0e325a53ef | ||
|
|
2a136cfb57 | ||
|
|
7749164d4a | ||
|
|
da50e14524 | ||
|
|
1886b731a5 | ||
|
|
364eddc95a | ||
|
|
48fb340e3b | ||
|
|
71bb4d3bdc | ||
|
|
5c20f570d0 | ||
|
|
7441da287f | ||
|
|
c2a5179d75 | ||
|
|
17c4690b5e | ||
|
|
f06d44e6e5 | ||
|
|
653c09001a | ||
|
|
8921fbb54c | ||
|
|
3018303c87 | ||
|
|
6c303b2445 | ||
|
|
7579a792c7 | ||
|
|
0bbc138037 | ||
|
|
ff11db61b4 | ||
|
|
2823487bf8 | ||
|
|
910fac781d | ||
|
|
149fb2090e | ||
|
|
40c65dcfa7 | ||
|
|
03f3a4e855 | ||
|
|
531ebf6dff | ||
|
|
7c2011599f | ||
|
|
17e90823da | ||
|
|
d01cbf7f8f | ||
|
|
770b276c21 | ||
|
|
4e935f7d7d | ||
|
|
260c2ecd15 | ||
|
|
f110d80279 | ||
|
|
a6d3d2fccd | ||
|
|
afdb659111 | ||
|
|
a8d9104fa3 | ||
|
|
d3530ab997 | ||
|
|
cf1e91bb59 | ||
|
|
125eaafae0 | ||
|
|
7aae80903c | ||
|
|
4aca9b91ba | ||
|
|
8b3da1867e | ||
|
|
c173ec5bc7 | ||
|
|
44d916fb4e | ||
|
|
4eb849aed3 | ||
|
|
6ac61d7b89 | ||
|
|
c7b2e336f0 | ||
|
|
70748950ae |
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -148,7 +148,7 @@ jobs:
|
||||
# sync, not just public items. Eventually we should do this for all
|
||||
# crates; for now add crates here as they are warning-clean to prevent
|
||||
# regression.
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p ruff_db --document-private-items
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
@@ -518,6 +518,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
@@ -525,13 +527,15 @@ jobs:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: pip install -r docs/requirements-insiders.txt
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
- name: "Install dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
|
||||
run: pip install -r docs/requirements.txt
|
||||
run: uv pip install -r docs/requirements.txt --system
|
||||
- name: "Update README File"
|
||||
run: python scripts/transform_readme.py --target mkdocs
|
||||
- name: "Generate docs"
|
||||
@@ -608,7 +612,7 @@ jobs:
|
||||
just test
|
||||
|
||||
benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
@@ -64,7 +66,7 @@ jobs:
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.18.0/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
|
||||
- name: Cache cargo-dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
14
.github/workflows/sync_typeshed.yaml
vendored
14
.github/workflows/sync_typeshed.yaml
vendored
@@ -37,13 +37,13 @@ jobs:
|
||||
- name: Sync typeshed
|
||||
id: sync
|
||||
run: |
|
||||
rm -rf ruff/crates/red_knot_python_semantic/vendor/typeshed
|
||||
mkdir ruff/crates/red_knot_python_semantic/vendor/typeshed
|
||||
cp typeshed/README.md ruff/crates/red_knot_python_semantic/vendor/typeshed
|
||||
cp typeshed/LICENSE ruff/crates/red_knot_python_semantic/vendor/typeshed
|
||||
cp -r typeshed/stdlib ruff/crates/red_knot_python_semantic/vendor/typeshed/stdlib
|
||||
rm -rf ruff/crates/red_knot_python_semantic/vendor/typeshed/stdlib/@tests
|
||||
git -C typeshed rev-parse HEAD > ruff/crates/red_knot_python_semantic/vendor/typeshed/source_commit.txt
|
||||
rm -rf ruff/crates/red_knot_vendored/vendor/typeshed
|
||||
mkdir ruff/crates/red_knot_vendored/vendor/typeshed
|
||||
cp typeshed/README.md ruff/crates/red_knot_vendored/vendor/typeshed
|
||||
cp typeshed/LICENSE ruff/crates/red_knot_vendored/vendor/typeshed
|
||||
cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib
|
||||
rm -rf ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/@tests
|
||||
git -C typeshed rev-parse HEAD > ruff/crates/red_knot_vendored/vendor/typeshed/source_commit.txt
|
||||
- name: Commit the changes
|
||||
id: commit
|
||||
if: ${{ steps.sync.outcome == 'success' }}
|
||||
|
||||
@@ -2,7 +2,7 @@ fail_fast: true
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
crates/red_knot_python_semantic/vendor/.*|
|
||||
crates/red_knot_vendored/vendor/.*|
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
@@ -17,7 +17,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.19
|
||||
rev: v0.20.2
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -28,6 +28,7 @@ repos:
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
- mdformat-footnote
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/formatter/black\.md
|
||||
@@ -35,7 +36,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.41.0
|
||||
rev: v0.42.0
|
||||
hooks:
|
||||
- id: markdownlint-fix
|
||||
exclude: |
|
||||
@@ -45,7 +46,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.24.5
|
||||
rev: v1.25.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -59,7 +60,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.5
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -68,8 +69,8 @@ repos:
|
||||
require_serial: true
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.1.0
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.3.3
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
100
CHANGELOG.md
100
CHANGELOG.md
@@ -1,5 +1,105 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.9
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix codeblock dynamic line length calculation for indented docstring examples ([#13523](https://github.com/astral-sh/ruff/pull/13523))
|
||||
- \[`refurb`\] Mark `FURB118` fix as unsafe ([#13613](https://github.com/astral-sh/ruff/pull/13613))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pydocstyle`\] Don't raise `D208` when last line is non-empty ([#13372](https://github.com/astral-sh/ruff/pull/13372))
|
||||
- \[`pylint`\] Preserve trivia (i.e. comments) in `PLR5501` autofix ([#13573](https://github.com/astral-sh/ruff/pull/13573))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`pyflakes`\] Add `allow-unused-imports` setting for `unused-import` rule (`F401`) ([#13601](https://github.com/astral-sh/ruff/pull/13601))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Support ruff discovery in pip build environments ([#13591](https://github.com/astral-sh/ruff/pull/13591))
|
||||
- \[`flake8-bugbear`\] Avoid short circuiting `B017` for multiple context managers ([#13609](https://github.com/astral-sh/ruff/pull/13609))
|
||||
- \[`pylint`\] Do not offer an invalid fix for `PLR1716` when the comparisons contain parenthesis ([#13527](https://github.com/astral-sh/ruff/pull/13527))
|
||||
- \[`pyupgrade`\] Fix `UP043` to apply to `collections.abc.Generator` and `collections.abc.AsyncGenerator` ([#13611](https://github.com/astral-sh/ruff/pull/13611))
|
||||
- \[`refurb`\] Fix handling of slices in tuples for `FURB118`, e.g., `x[:, 1]` ([#13518](https://github.com/astral-sh/ruff/pull/13518))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update GitHub Action link to `astral-sh/ruff-action` ([#13551](https://github.com/astral-sh/ruff/pull/13551))
|
||||
|
||||
## 0.6.8
|
||||
|
||||
### Preview features
|
||||
|
||||
- Remove unnecessary parentheses around `match case` clauses ([#13510](https://github.com/astral-sh/ruff/pull/13510))
|
||||
- Parenthesize overlong `if` guards in `match..case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513))
|
||||
- Detect basic wildcard imports in `ruff analyze graph` ([#13486](https://github.com/astral-sh/ruff/pull/13486))
|
||||
- \[`pylint`\] Implement `boolean-chained-comparison` (`R1716`) ([#13435](https://github.com/astral-sh/ruff/pull/13435))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`lake8-simplify`\] Detect `SIM910` when using variadic keyword arguments, i.e., `**kwargs` ([#13503](https://github.com/astral-sh/ruff/pull/13503))
|
||||
- \[`pyupgrade`\] Avoid false negatives with non-reference shadowed bindings of loop variables (`UP028`) ([#13504](https://github.com/astral-sh/ruff/pull/13504))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Detect tuples bound to variadic positional arguments i.e. `*args` ([#13512](https://github.com/astral-sh/ruff/pull/13512))
|
||||
- Exit gracefully on broken pipe errors ([#13485](https://github.com/astral-sh/ruff/pull/13485))
|
||||
- Avoid panic when analyze graph hits broken pipe ([#13484](https://github.com/astral-sh/ruff/pull/13484))
|
||||
|
||||
### Performance
|
||||
|
||||
- Reuse `BTreeSets` in module resolver ([#13440](https://github.com/astral-sh/ruff/pull/13440))
|
||||
- Skip traversal for non-compound statements ([#13441](https://github.com/astral-sh/ruff/pull/13441))
|
||||
|
||||
## 0.6.7
|
||||
|
||||
### Preview features
|
||||
|
||||
- Add Python version support to ruff analyze CLI ([#13426](https://github.com/astral-sh/ruff/pull/13426))
|
||||
- Add `exclude` support to `ruff analyze` ([#13425](https://github.com/astral-sh/ruff/pull/13425))
|
||||
- Fix parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pycodestyle`\] Fix: Don't autofix if the first line ends in a question mark? (D400) ([#13399](https://github.com/astral-sh/ruff/pull/13399))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Respect `lint.exclude` in ruff check `--add-noqa` ([#13427](https://github.com/astral-sh/ruff/pull/13427))
|
||||
|
||||
### Performance
|
||||
|
||||
- Avoid tracking module resolver files in Salsa ([#13437](https://github.com/astral-sh/ruff/pull/13437))
|
||||
- Use `forget` for module resolver database ([#13438](https://github.com/astral-sh/ruff/pull/13438))
|
||||
|
||||
## 0.6.6
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`refurb`\] Skip `slice-to-remove-prefix-or-suffix` (`FURB188`) when non-trivial slice steps are present ([#13405](https://github.com/astral-sh/ruff/pull/13405))
|
||||
- Add a subcommand to generate dependency graphs ([#13402](https://github.com/astral-sh/ruff/pull/13402))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Fix placement of inline parameter comments ([#13379](https://github.com/astral-sh/ruff/pull/13379))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix off-by one error in the `LineIndex::offset` calculation ([#13407](https://github.com/astral-sh/ruff/pull/13407))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`fastapi`\] Respect FastAPI aliases in route definitions ([#13394](https://github.com/astral-sh/ruff/pull/13394))
|
||||
- \[`pydocstyle`\] Respect word boundaries when detecting function signature in docs ([#13388](https://github.com/astral-sh/ruff/pull/13388))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add backlinks to rule overview linter ([#13368](https://github.com/astral-sh/ruff/pull/13368))
|
||||
- Fix documentation for editor vim plugin ALE ([#13348](https://github.com/astral-sh/ruff/pull/13348))
|
||||
- Fix rendering of `FURB188` docs ([#13406](https://github.com/astral-sh/ruff/pull/13406))
|
||||
|
||||
## 0.6.5
|
||||
|
||||
### Preview features
|
||||
|
||||
@@ -29,16 +29,14 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
|
||||
cargo install cargo-insta
|
||||
```
|
||||
|
||||
And you'll need pre-commit to run some validation checks:
|
||||
|
||||
```shell
|
||||
pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
|
||||
```
|
||||
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
|
||||
run Python utility commands.
|
||||
|
||||
You can optionally install pre-commit hooks to automatically run the validation checks
|
||||
when making a commit:
|
||||
|
||||
```shell
|
||||
uv tool install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
@@ -66,7 +64,7 @@ and that it passes both the lint and test validation checks:
|
||||
```shell
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
|
||||
RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
|
||||
pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
|
||||
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
|
||||
```
|
||||
|
||||
These checks will run on GitHub Actions when you open your pull request, but running them locally
|
||||
@@ -267,26 +265,20 @@ To preview any changes to the documentation locally:
|
||||
|
||||
1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install).
|
||||
|
||||
1. Install MkDocs and Material for MkDocs with:
|
||||
|
||||
```shell
|
||||
pip install -r docs/requirements.txt
|
||||
```
|
||||
|
||||
1. Generate the MkDocs site with:
|
||||
|
||||
```shell
|
||||
python scripts/generate_mkdocs.py
|
||||
uv run --no-project --isolated --with-requirements docs/requirements.txt scripts/generate_mkdocs.py
|
||||
```
|
||||
|
||||
1. Run the development server with:
|
||||
|
||||
```shell
|
||||
# For contributors.
|
||||
mkdocs serve -f mkdocs.public.yml
|
||||
uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.public.yml
|
||||
|
||||
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
|
||||
mkdocs serve -f mkdocs.insiders.yml
|
||||
uvx --with-requirements docs/requirements-insiders.txt -- mkdocs serve -f mkdocs.insiders.yml
|
||||
```
|
||||
|
||||
The documentation should then be available locally at
|
||||
@@ -368,9 +360,8 @@ GitHub Actions will run your changes against a number of real-world projects fro
|
||||
report on any linter or formatter differences. You can also run those checks locally via:
|
||||
|
||||
```shell
|
||||
pip install -e ./python/ruff-ecosystem
|
||||
ruff-ecosystem check ruff "./target/debug/ruff"
|
||||
ruff-ecosystem format ruff "./target/debug/ruff"
|
||||
uvx --from ./python/ruff-ecosystem ruff-ecosystem check ruff "./target/debug/ruff"
|
||||
uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ruff"
|
||||
```
|
||||
|
||||
See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/python/ruff-ecosystem) for more details.
|
||||
|
||||
471
Cargo.lock
generated
471
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
91
Cargo.toml
91
Cargo.toml
@@ -14,9 +14,10 @@ license = "MIT"
|
||||
[workspace.dependencies]
|
||||
ruff = { path = "crates/ruff" }
|
||||
ruff_cache = { path = "crates/ruff_cache" }
|
||||
ruff_db = { path = "crates/ruff_db" }
|
||||
ruff_db = { path = "crates/ruff_db", default-features = false }
|
||||
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
|
||||
ruff_formatter = { path = "crates/ruff_formatter" }
|
||||
ruff_graph = { path = "crates/ruff_graph" }
|
||||
ruff_index = { path = "crates/ruff_index" }
|
||||
ruff_linter = { path = "crates/ruff_linter" }
|
||||
ruff_macros = { path = "crates/ruff_macros" }
|
||||
@@ -33,15 +34,18 @@ ruff_python_trivia = { path = "crates/ruff_python_trivia" }
|
||||
ruff_server = { path = "crates/ruff_server" }
|
||||
ruff_source_file = { path = "crates/ruff_source_file" }
|
||||
ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
red_knot_vendored = { path = "crates/red_knot_vendored" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_workspace = { path = "crates/red_knot_workspace" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_workspace = { path = "crates/red_knot_workspace", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
anyhow = { version = "1.0.80" }
|
||||
assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.5.0" }
|
||||
@@ -68,7 +72,11 @@ fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
hashbrown = "0.14.3"
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
"raw-entry",
|
||||
"inline-more",
|
||||
] }
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
@@ -86,7 +94,7 @@ libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
|
||||
"proposed",
|
||||
"proposed",
|
||||
] }
|
||||
matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
@@ -107,8 +115,9 @@ quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rstest = { version = "0.22.0", default-features = false }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "f608ff8b24f07706492027199f51132244034f29" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "4a7c955255e707e64e43f3ce5eabb771ae067768" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -116,7 +125,7 @@ serde-wasm-bindgen = { version = "0.6.4" }
|
||||
serde_json = { version = "1.0.113" }
|
||||
serde_test = { version = "1.0.152" }
|
||||
serde_with = { version = "3.6.0", default-features = false, features = [
|
||||
"macros",
|
||||
"macros",
|
||||
] }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
@@ -133,7 +142,10 @@ toml = { version = "0.8.11" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-flame = { version = "0.2.0" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["env-filter", "fmt"] }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
] }
|
||||
tracing-tree = { version = "0.4.0" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
@@ -144,10 +156,10 @@ unicode-normalization = { version = "0.1.23" }
|
||||
ureq = { version = "2.9.6" }
|
||||
url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"js",
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"js",
|
||||
] }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.92" }
|
||||
@@ -158,7 +170,10 @@ zip = { version = "0.6.6", default-features = false }
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(fuzzing)", "cfg(codspeed)"] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
"cfg(fuzzing)",
|
||||
"cfg(codspeed)",
|
||||
] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -2 }
|
||||
@@ -230,9 +245,9 @@ inherits = "release"
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.18.0"
|
||||
cargo-dist-version = "0.22.1"
|
||||
# CI backends to support
|
||||
ci = ["github"]
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell", "powershell"]
|
||||
# The archive format to use for windows builds (defaults .zip)
|
||||
@@ -241,33 +256,33 @@ windows-archive = ".zip"
|
||||
unix-archive = ".tar.gz"
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
]
|
||||
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
|
||||
auto-includes = false
|
||||
# Whether cargo-dist should create a GitHub Release or use an existing draft
|
||||
create-release = true
|
||||
# Publish jobs to run in CI
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "skip"
|
||||
# Whether CI should trigger releases with dispatches instead of tag pushes
|
||||
dispatch-releases = true
|
||||
# The stage during which the GitHub Release should be created
|
||||
# Which phase cargo-dist should use to create the GitHub release
|
||||
github-release = "announce"
|
||||
# Whether CI should include auto-generated code to build local artifacts
|
||||
build-local-artifacts = false
|
||||
@@ -275,9 +290,15 @@ build-local-artifacts = false
|
||||
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Announcement jobs to run in CI
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = [
|
||||
"./notify-dependents",
|
||||
"./publish-docs",
|
||||
"./publish-playground",
|
||||
]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
# Path that installers should place binaries in
|
||||
install-path = "CARGO_HOME"
|
||||
|
||||
10
README.md
10
README.md
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.6.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.6.9/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.9/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.5
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or with [various other editors](https://docs.astral.sh/ruff/editors/setup).
|
||||
|
||||
Ruff can also be used as a [GitHub Action](https://github.com/features/actions) via
|
||||
[`ruff-action`](https://github.com/chartboost/ruff-action):
|
||||
[`ruff-action`](https://github.com/astral-sh/ruff-action):
|
||||
|
||||
```yaml
|
||||
name: Ruff
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: chartboost/ruff-action@v1
|
||||
- uses: astral-sh/ruff-action@v1
|
||||
```
|
||||
|
||||
### Configuration<a id="configuration"></a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[files]
|
||||
# https://github.com/crate-ci/typos/issues/868
|
||||
extend-exclude = ["crates/red_knot_python_semantic/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
|
||||
extend-exclude = ["crates/red_knot_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
|
||||
|
||||
[default.extend-words]
|
||||
"arange" = "arange" # e.g. `numpy.arange`
|
||||
@@ -8,7 +8,7 @@ hel = "hel"
|
||||
whos = "whos"
|
||||
spawnve = "spawnve"
|
||||
ned = "ned"
|
||||
pn = "pn" # `import panel as pd` is a thing
|
||||
pn = "pn" # `import panel as pn` is a thing
|
||||
poit = "poit"
|
||||
BA = "BA" # acronym for "Bad Allowed", used in testing.
|
||||
jod = "jod" # e.g., `jod-thread`
|
||||
|
||||
@@ -13,9 +13,8 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
red_knot_workspace = { workspace = true, features = ["zstd"] }
|
||||
red_knot_server = { workspace = true }
|
||||
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -160,7 +160,7 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
SystemPathBuf::from_path_buf(cwd)
|
||||
.map_err(|path| {
|
||||
anyhow!(
|
||||
"The current working directory '{}' contains non-unicode characters. Red Knot only supports unicode paths.",
|
||||
"The current working directory `{}` contains non-Unicode characters. Red Knot only supports Unicode paths.",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
@@ -174,7 +174,7 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
Ok(SystemPath::absolute(cwd, &cli_base_path))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided current-directory path '{cwd}' is not a directory."
|
||||
"Provided current-directory path `{cwd}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,14 +42,14 @@ impl TestCase {
|
||||
|
||||
fn stop_watch(&mut self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_stop_watch(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none.")
|
||||
.expect("Expected watch changes but observed none")
|
||||
}
|
||||
|
||||
fn try_stop_watch(&mut self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
let watcher = self
|
||||
.watcher
|
||||
.take()
|
||||
.expect("Cannot call `stop_watch` more than once.");
|
||||
.expect("Cannot call `stop_watch` more than once");
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
@@ -72,7 +72,7 @@ impl TestCase {
|
||||
#[cfg(unix)]
|
||||
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_take_watch_changes(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none.")
|
||||
.expect("Expected watch changes but observed none")
|
||||
}
|
||||
|
||||
fn try_take_watch_changes(&self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
@@ -150,14 +150,14 @@ where
|
||||
let absolute_path = workspace_path.join(relative_path);
|
||||
if let Some(parent) = absolute_path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("Failed to create parent directory for file '{relative_path}'.",)
|
||||
format!("Failed to create parent directory for file `{relative_path}`")
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(absolute_path.as_std_path())
|
||||
.with_context(|| format!("Failed to open file '{relative_path}'"))?;
|
||||
.with_context(|| format!("Failed to open file `{relative_path}`"))?;
|
||||
file.write_all(content.as_bytes())
|
||||
.with_context(|| format!("Failed to write to file '{relative_path}'"))?;
|
||||
.with_context(|| format!("Failed to write to file `{relative_path}`"))?;
|
||||
file.sync_data()?;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ where
|
||||
|
||||
let root_path = SystemPath::from_std_path(temp_dir.path()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Temp directory '{}' is not a valid UTF-8 path.",
|
||||
"Temporary directory `{}` is not a valid UTF-8 path.",
|
||||
temp_dir.path().display()
|
||||
)
|
||||
})?;
|
||||
@@ -209,7 +209,7 @@ where
|
||||
let workspace_path = root_path.join("workspace");
|
||||
|
||||
std::fs::create_dir_all(workspace_path.as_std_path())
|
||||
.with_context(|| format!("Failed to create workspace directory '{workspace_path}'",))?;
|
||||
.with_context(|| format!("Failed to create workspace directory `{workspace_path}`"))?;
|
||||
|
||||
setup_files
|
||||
.setup(&root_path, &workspace_path)
|
||||
@@ -233,7 +233,7 @@ where
|
||||
}))
|
||||
{
|
||||
std::fs::create_dir_all(path.as_std_path())
|
||||
.with_context(|| format!("Failed to create search path '{path}'"))?;
|
||||
.with_context(|| format!("Failed to create search path `{path}`"))?;
|
||||
}
|
||||
|
||||
let configuration = Configuration {
|
||||
@@ -665,7 +665,7 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some(),);
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
|
||||
let sub_path = case.workspace_path("sub");
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
itertools = { workspace = true}
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -34,21 +34,18 @@ hashbrown = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
path-slash = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true, features = ["zstd", "deflate"] }
|
||||
memchr = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["os", "testing"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,87 +1,4 @@
|
||||
//! Build script to package our vendored typeshed files
|
||||
//! into a zip archive that can be included in the Ruff binary.
|
||||
//!
|
||||
//! This script should be automatically run at build time
|
||||
//! whenever the script itself changes, or whenever any files
|
||||
//! in `crates/red_knot_python_semantic/vendor/typeshed` change.
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
use path_slash::PathExt;
|
||||
use zip::result::ZipResult;
|
||||
use zip::write::{FileOptions, ZipWriter};
|
||||
use zip::CompressionMethod;
|
||||
|
||||
const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed";
|
||||
const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
|
||||
|
||||
/// Recursively zip the contents of an entire directory.
|
||||
///
|
||||
/// This routine is adapted from a recipe at
|
||||
/// <https://github.com/zip-rs/zip-old/blob/5d0f198124946b7be4e5969719a7f29f363118cd/examples/write_dir.rs>
|
||||
fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
let mut zip = ZipWriter::new(writer);
|
||||
|
||||
// Use deflated compression for WASM builds because compiling `zstd-sys` requires clang
|
||||
// [source](https://github.com/gyscos/zstd-rs/wiki/Compile-for-WASM) which complicates the build
|
||||
// by a lot. Deflated compression is slower but it shouldn't matter much for the WASM use case
|
||||
// (WASM itself is already slower than a native build for a specific platform).
|
||||
// We can't use `#[cfg(...)]` here because the target-arch in a build script is the
|
||||
// architecture of the system running the build script and not the architecture of the build-target.
|
||||
// That's why we use the `TARGET` environment variable here.
|
||||
let method = if std::env::var("TARGET").unwrap().contains("wasm32") {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Zstd
|
||||
};
|
||||
|
||||
let options = FileOptions::default()
|
||||
.compression_method(method)
|
||||
.unix_permissions(0o644);
|
||||
|
||||
for entry in walkdir::WalkDir::new(directory_path) {
|
||||
let dir_entry = entry.unwrap();
|
||||
let absolute_path = dir_entry.path();
|
||||
let normalized_relative_path = absolute_path
|
||||
.strip_prefix(Path::new(directory_path))
|
||||
.unwrap()
|
||||
.to_slash()
|
||||
.expect("Unexpected non-utf8 typeshed path!");
|
||||
|
||||
// Write file or directory explicitly
|
||||
// Some unzip tools unzip files with directory paths correctly, some do not!
|
||||
if absolute_path.is_file() {
|
||||
println!("adding file {absolute_path:?} as {normalized_relative_path:?} ...");
|
||||
zip.start_file(normalized_relative_path, options)?;
|
||||
let mut f = File::open(absolute_path)?;
|
||||
std::io::copy(&mut f, &mut zip).unwrap();
|
||||
} else if !normalized_relative_path.is_empty() {
|
||||
// Only if not root! Avoids path spec / warning
|
||||
// and mapname conversion failed error on unzip
|
||||
println!("adding dir {absolute_path:?} as {normalized_relative_path:?} ...");
|
||||
zip.add_directory(normalized_relative_path, options)?;
|
||||
}
|
||||
}
|
||||
zip.finish()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed={TYPESHED_SOURCE_DIR}");
|
||||
assert!(
|
||||
Path::new(TYPESHED_SOURCE_DIR).is_dir(),
|
||||
"Where is typeshed?"
|
||||
);
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
|
||||
// N.B. Deliberately using `format!()` instead of `Path::join()` here,
|
||||
// so that we use `/` as a path separator on all platforms.
|
||||
// That enables us to load the typeshed zip at compile time in `module.rs`
|
||||
// (otherwise we'd have to dynamically determine the exact path to the typeshed zip
|
||||
// based on the default path separator for the specific platform we're on,
|
||||
// which can't be done at compile time.)
|
||||
let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}");
|
||||
|
||||
let zipped_typeshed = File::create(zipped_typeshed_location).unwrap();
|
||||
zip_dir(TYPESHED_SOURCE_DIR, zipped_typeshed).unwrap();
|
||||
/// Rebuild the crate if a test file is added or removed from
|
||||
pub fn main() {
|
||||
println!("cargo:rerun-if-changed=resources/mdtest");
|
||||
}
|
||||
|
||||
4
crates/red_knot_python_semantic/resources/README.md
Normal file
4
crates/red_knot_python_semantic/resources/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking;
|
||||
executed by the `tests/mdtest.rs` integration test.
|
||||
|
||||
See `crates/red_knot_test/README.md` for documentation of this test format.
|
||||
@@ -0,0 +1,43 @@
|
||||
### Comparison: Byte literals
|
||||
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects
|
||||
inferred as having `Literal` bytes types:
|
||||
|
||||
```py
|
||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" != b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
|
||||
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" <= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" <= b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
|
||||
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" >= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" >= b"abd") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"" in b"") # revealed: Literal[True]
|
||||
reveal_type(b"" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"") # revealed: Literal[False]
|
||||
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"d" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"\x81\x82" in b"\x80\x81\x82") # revealed: Literal[True]
|
||||
reveal_type(b"\x82\x83" in b"\x80\x81\x82") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" is b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" is not b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Narrowing for `is` conditionals
|
||||
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
|
||||
if x is None:
|
||||
# TODO the following should be simplified to 'None'
|
||||
reveal_type(x) # revealed: None | Literal[1] & None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
class A:
|
||||
...
|
||||
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
if y is x:
|
||||
# TODO the following should be simplified to 'A'
|
||||
reveal_type(y) # revealed: A | None & A
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Narrowing for `is not` conditionals
|
||||
|
||||
## `is not None`
|
||||
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
if x is not False:
|
||||
# TODO the following should be `Literal[True]`
|
||||
reveal_type(x) # revealed: bool & ~Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for non-singleton types
|
||||
|
||||
Non-singleton types should *not* narrow the type: two instances of a
|
||||
non-singleton class may occupy different addresses in memory even if
|
||||
they compare equal.
|
||||
|
||||
```py
|
||||
x = [1]
|
||||
y = [1]
|
||||
|
||||
if x is not y:
|
||||
# TODO: should include type parameter: list[int]
|
||||
reveal_type(x) # revealed: list
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# Narrowing for `match` statements
|
||||
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
y = 0
|
||||
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
# TODO intersection simplification: should be just Literal[0] | None
|
||||
reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None
|
||||
```
|
||||
35
crates/red_knot_python_semantic/resources/mdtest/numbers.md
Normal file
35
crates/red_knot_python_semantic/resources/mdtest/numbers.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Numbers
|
||||
|
||||
## Integers
|
||||
|
||||
### Literals
|
||||
|
||||
We can infer an integer literal type:
|
||||
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Overflow
|
||||
|
||||
We only track integer literals within the range of an i64:
|
||||
|
||||
```py
|
||||
reveal_type(9223372036854775808) # revealed: int
|
||||
```
|
||||
|
||||
## Floats
|
||||
|
||||
There aren't literal float types, but we infer the general float type:
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
Same for complex:
|
||||
|
||||
```py
|
||||
reveal_type(2j) # revealed: complex
|
||||
```
|
||||
@@ -11,7 +11,6 @@ pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::module_resolver::vendored_typeshed_stubs;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
@@ -33,7 +32,7 @@ pub(crate) mod tests {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: vendored_typeshed_stubs().clone(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: std::sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use rustc_hash::FxHasher;
|
||||
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, vendored_typeshed_stubs};
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, Module};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
pub(crate) use module::Module;
|
||||
pub use module::Module;
|
||||
pub use resolver::resolve_module;
|
||||
pub(crate) use resolver::{file_to_module, SearchPaths};
|
||||
use ruff_db::system::SystemPath;
|
||||
pub use typeshed::vendored_typeshed_stubs;
|
||||
|
||||
use crate::module_resolver::resolver::search_paths;
|
||||
use crate::Db;
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
use std::ops::Deref;
|
||||
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::{VendoredFileSystem, VendoredPath};
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
|
||||
use crate::site_packages::VirtualEnvironment;
|
||||
use crate::{Program, PythonVersion, SearchPathSettings, SitePackages};
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
|
||||
let interned_name = ModuleNameIngredient::new(db, module_name);
|
||||
@@ -35,14 +36,14 @@ pub(crate) fn resolve_module_query<'db>(
|
||||
let _span = tracing::trace_span!("resolve_module", %name).entered();
|
||||
|
||||
let Some((search_path, module_file, kind)) = resolve_name(db, name) else {
|
||||
tracing::debug!("Module '{name}' not found in the search paths.");
|
||||
tracing::debug!("Module `{name}` not found in search paths");
|
||||
return None;
|
||||
};
|
||||
|
||||
let module = Module::new(name.clone(), kind, search_path, module_file);
|
||||
|
||||
tracing::trace!(
|
||||
"Resolved module '{name}' to '{path}'.",
|
||||
"Resolved module `{name}` to `{path}`",
|
||||
path = module_file.path(db)
|
||||
);
|
||||
|
||||
@@ -136,7 +137,7 @@ pub(crate) struct SearchPaths {
|
||||
/// for the first `site-packages` path
|
||||
site_packages: Vec<SearchPath>,
|
||||
|
||||
typeshed_versions: ResolvedTypeshedVersions,
|
||||
typeshed_versions: TypeshedVersions,
|
||||
}
|
||||
|
||||
impl SearchPaths {
|
||||
@@ -202,11 +203,11 @@ impl SearchPaths {
|
||||
|
||||
let search_path = SearchPath::custom_stdlib(db, &custom_typeshed)?;
|
||||
|
||||
(ResolvedTypeshedVersions::Custom(parsed), search_path)
|
||||
(parsed, search_path)
|
||||
} else {
|
||||
tracing::debug!("Using vendored stdlib");
|
||||
(
|
||||
ResolvedTypeshedVersions::Vendored(vendored_typeshed_versions()),
|
||||
vendored_typeshed_versions(db),
|
||||
SearchPath::vendored_stdlib(),
|
||||
)
|
||||
};
|
||||
@@ -279,23 +280,6 @@ impl SearchPaths {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum ResolvedTypeshedVersions {
|
||||
Vendored(&'static TypeshedVersions),
|
||||
Custom(TypeshedVersions),
|
||||
}
|
||||
|
||||
impl Deref for ResolvedTypeshedVersions {
|
||||
type Target = TypeshedVersions;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
ResolvedTypeshedVersions::Vendored(versions) => versions,
|
||||
ResolvedTypeshedVersions::Custom(versions) => versions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect all dynamic search paths. For each `site-packages` path:
|
||||
/// - Collect that `site-packages` path
|
||||
/// - Collect any search paths listed in `.pth` files in that `site-packages` directory
|
||||
@@ -340,7 +324,7 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
|
||||
let site_packages_root = files
|
||||
.root(db.upcast(), site_packages_dir)
|
||||
.expect("Site-package root to have been created.");
|
||||
.expect("Site-package root to have been created");
|
||||
|
||||
// This query needs to be re-executed each time a `.pth` file
|
||||
// is added, modified or removed from the `site-packages` directory.
|
||||
|
||||
@@ -4,25 +4,19 @@ use std::num::{NonZeroU16, NonZeroUsize};
|
||||
use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::{Program, PythonVersion};
|
||||
|
||||
static VENDORED_VERSIONS: Lazy<TypeshedVersions> = Lazy::new(|| {
|
||||
pub(in crate::module_resolver) fn vendored_typeshed_versions(db: &dyn Db) -> TypeshedVersions {
|
||||
TypeshedVersions::from_str(
|
||||
&vendored_typeshed_stubs()
|
||||
&db.vendored()
|
||||
.read_to_string("stdlib/VERSIONS")
|
||||
.unwrap(),
|
||||
.expect("The vendored typeshed stubs should contain a VERSIONS file"),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub(crate) fn vendored_typeshed_versions() -> &'static TypeshedVersions {
|
||||
&VENDORED_VERSIONS
|
||||
.expect("The VERSIONS file in the vendored typeshed stubs should be well-formed")
|
||||
}
|
||||
|
||||
pub(crate) fn typeshed_versions(db: &dyn Db) -> &TypeshedVersions {
|
||||
@@ -332,6 +326,8 @@ mod tests {
|
||||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use super::*;
|
||||
|
||||
const TYPESHED_STDLIB_DIR: &str = "stdlib";
|
||||
@@ -353,12 +349,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_parse_vendored_versions_file() {
|
||||
let versions_data = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/vendor/typeshed/stdlib/VERSIONS"
|
||||
));
|
||||
let db = TestDb::new();
|
||||
|
||||
let versions = TypeshedVersions::from_str(versions_data).unwrap();
|
||||
let versions = vendored_typeshed_versions(&db);
|
||||
assert!(versions.len() > 100);
|
||||
assert!(versions.len() < 1000);
|
||||
|
||||
@@ -395,9 +388,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn typeshed_versions_consistent_with_vendored_stubs() {
|
||||
const VERSIONS_DATA: &str = include_str!("../../../vendor/typeshed/stdlib/VERSIONS");
|
||||
let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap();
|
||||
let vendored_typeshed_versions = TypeshedVersions::from_str(VERSIONS_DATA).unwrap();
|
||||
let db = TestDb::new();
|
||||
let vendored_typeshed_versions = vendored_typeshed_versions(&db);
|
||||
let vendored_typeshed_dir =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../red_knot_vendored/vendor/typeshed");
|
||||
|
||||
let mut empty_iterator = true;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
pub use self::vendored::vendored_typeshed_stubs;
|
||||
pub(super) use self::versions::{
|
||||
typeshed_versions, vendored_typeshed_versions, TypeshedVersions, TypeshedVersionsParseError,
|
||||
TypeshedVersionsQueryResult,
|
||||
};
|
||||
|
||||
mod vendored;
|
||||
mod versions;
|
||||
@@ -54,6 +54,13 @@ impl TryFrom<(&str, &str)> for PythonVersion {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u8, u8)> for PythonVersion {
|
||||
fn from(value: (u8, u8)) -> Self {
|
||||
let (major, minor) = value;
|
||||
Self { major, minor }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PythonVersion { major, minor } = self;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
@@ -31,7 +31,7 @@ pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
|
||||
/// Returns the semantic index for `file`.
|
||||
///
|
||||
@@ -115,6 +115,9 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// Note: We should not depend on this map when analysing other files or
|
||||
/// changing a file invalidates all dependents.
|
||||
ast_ids: IndexVec<FileScopeId, AstIds>,
|
||||
|
||||
/// Flags about the global scope (code usage impacting inference)
|
||||
has_future_annotations: bool,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndex<'db> {
|
||||
@@ -215,6 +218,12 @@ impl<'db> SemanticIndex<'db> {
|
||||
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
||||
self.scopes_by_node[&node.node_key()]
|
||||
}
|
||||
|
||||
/// Checks if there is an import of `__future__.annotations` in the global scope, which affects
|
||||
/// the logic for type inference.
|
||||
pub(super) fn has_future_annotations(&self) -> bool {
|
||||
self.has_future_annotations
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AncestorsIter<'a> {
|
||||
@@ -1027,7 +1036,7 @@ class C[T]:
|
||||
}
|
||||
|
||||
let TestCase { db, file } = test_case(
|
||||
r#"
|
||||
r"
|
||||
class Test:
|
||||
def foo():
|
||||
def bar():
|
||||
@@ -1036,7 +1045,7 @@ class Test:
|
||||
pass
|
||||
|
||||
def x():
|
||||
pass"#,
|
||||
pass",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
|
||||
@@ -45,6 +45,9 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
/// Flow states at each `break` in the current loop.
|
||||
loop_break_states: Vec<FlowSnapshot>,
|
||||
|
||||
/// Flags about the file's global scope
|
||||
has_future_annotations: bool,
|
||||
|
||||
// Semantic Index fields
|
||||
scopes: IndexVec<FileScopeId, Scope>,
|
||||
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
||||
@@ -68,6 +71,8 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
current_match_case: None,
|
||||
loop_break_states: vec![],
|
||||
|
||||
has_future_annotations: false,
|
||||
|
||||
scopes: IndexVec::new(),
|
||||
symbol_tables: IndexVec::new(),
|
||||
ast_ids: IndexVec::new(),
|
||||
@@ -450,6 +455,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scopes_by_expression: self.scopes_by_expression,
|
||||
scopes_by_node: self.scopes_by_node,
|
||||
use_def_maps,
|
||||
has_future_annotations: self.has_future_annotations,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -543,7 +549,16 @@ where
|
||||
&alias.name.id
|
||||
};
|
||||
|
||||
// Look for imports `from __future__ import annotations`, ignore `as ...`
|
||||
// We intentionally don't enforce the rules about location of `__future__`
|
||||
// imports here, we assume the user's intent was to apply the `__future__`
|
||||
// import, so we still check using it (and will also emit a diagnostic about a
|
||||
// miss-placed `__future__` import.)
|
||||
self.has_future_annotations |= alias.name.id == "annotations"
|
||||
&& node.module.as_deref() == Some("__future__");
|
||||
|
||||
let symbol = self.add_symbol(symbol_name.clone());
|
||||
|
||||
self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_db::parsed::ParsedModule;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::Db;
|
||||
@@ -45,6 +46,14 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_binding()
|
||||
}
|
||||
|
||||
/// Return true if this is a symbol was defined in the `typing` or `typing_extensions` modules
|
||||
pub(crate) fn is_typing_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db)).is_some_and(|module| {
|
||||
module.search_path().is_standard_library()
|
||||
&& matches!(&**module.name(), "typing" | "typing_extensions")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -330,7 +339,7 @@ impl DefinitionCategory {
|
||||
/// If so, any assignments reached by this definition are in error if they assign a value of a
|
||||
/// type not assignable to the declared type.
|
||||
///
|
||||
/// Annotations establish a declared type. So do function and class definition.
|
||||
/// Annotations establish a declared type. So do function and class definitions, and imports.
|
||||
pub(crate) fn is_declaration(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
@@ -371,10 +380,11 @@ pub enum DefinitionKind {
|
||||
impl DefinitionKind {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions and classes always bind a value, and we always consider them declarations
|
||||
DefinitionKind::Function(_) | DefinitionKind::Class(_) => {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
}
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
DefinitionKind::Function(_)
|
||||
| DefinitionKind::Class(_)
|
||||
| DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_) => DefinitionCategory::DeclarationAndBinding,
|
||||
// a parameter always binds a value, but is only a declaration if annotated
|
||||
DefinitionKind::Parameter(parameter) => {
|
||||
if parameter.annotation.is_some() {
|
||||
@@ -400,9 +410,7 @@ impl DefinitionKind {
|
||||
}
|
||||
}
|
||||
// all of these bind values without declaring a type
|
||||
DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_)
|
||||
| DefinitionKind::NamedExpression(_)
|
||||
DefinitionKind::NamedExpression(_)
|
||||
| DefinitionKind::Assignment(_)
|
||||
| DefinitionKind::AugmentedAssignment(_)
|
||||
| DefinitionKind::For(_)
|
||||
|
||||
@@ -192,7 +192,7 @@ impl VirtualEnvironment {
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Failed to resolve `sys.prefix` of the system Python installation \
|
||||
from the `home` value in the `pyvenv.cfg` file at '{}'. \
|
||||
from the `home` value in the `pyvenv.cfg` file at `{}`. \
|
||||
System site-packages will not be used for module resolution.",
|
||||
venv_path.join("pyvenv.cfg")
|
||||
);
|
||||
@@ -426,7 +426,7 @@ impl Deref for SysPrefixPath {
|
||||
|
||||
impl fmt::Display for SysPrefixPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "`sys.prefix` path '{}'", self.0)
|
||||
write!(f, "`sys.prefix` path `{}`", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +483,7 @@ impl Deref for PythonHomePath {
|
||||
|
||||
impl fmt::Display for PythonHomePath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "`home` location '{}'", self.0)
|
||||
write!(f, "`home` location `{}`", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ enum CoreStdlibModule {
|
||||
Builtins,
|
||||
Types,
|
||||
Typeshed,
|
||||
TypingExtensions,
|
||||
}
|
||||
|
||||
impl CoreStdlibModule {
|
||||
@@ -19,6 +20,7 @@ impl CoreStdlibModule {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Types => "types",
|
||||
Self::Typeshed => "_typeshed",
|
||||
Self::TypingExtensions => "typing_extensions",
|
||||
};
|
||||
ModuleName::new_static(module_name)
|
||||
.unwrap_or_else(|| panic!("{module_name} should be a valid module name!"))
|
||||
@@ -62,6 +64,14 @@ pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,12 +25,14 @@
|
||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use ordermap::set::MutableValues;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::KnownClass;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: FxOrderSet<Type<'db>>,
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
@@ -38,7 +40,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
db,
|
||||
elements: FxOrderSet::default(),
|
||||
elements: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,47 +48,70 @@ impl<'db> UnionBuilder<'db> {
|
||||
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
|
||||
match ty {
|
||||
Type::Union(union) => {
|
||||
self.elements.extend(union.elements(self.db));
|
||||
let new_elements = union.elements(self.db);
|
||||
self.elements.reserve(new_elements.len());
|
||||
for element in new_elements {
|
||||
self = self.add(*element);
|
||||
}
|
||||
}
|
||||
Type::Never => {}
|
||||
_ => {
|
||||
self.elements.insert(ty);
|
||||
let bool_pair = if let Type::BooleanLiteral(b) = ty {
|
||||
Some(Type::BooleanLiteral(!b))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut to_add = ty;
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
for (index, element) in self.elements.iter().enumerate() {
|
||||
if Some(*element) == bool_pair {
|
||||
to_add = KnownClass::Bool.to_instance(self.db);
|
||||
to_remove.push(index);
|
||||
// The type we are adding is a BooleanLiteral, which doesn't have any
|
||||
// subtypes. And we just found that the union already contained our
|
||||
// mirror-image BooleanLiteral, so it can't also contain bool or any
|
||||
// supertype of bool. Therefore, we are done.
|
||||
break;
|
||||
}
|
||||
if ty.is_subtype_of(self.db, *element) {
|
||||
return self;
|
||||
} else if element.is_subtype_of(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
match to_remove[..] {
|
||||
[] => self.elements.push(to_add),
|
||||
[index] => self.elements[index] = to_add,
|
||||
_ => {
|
||||
let mut current_index = 0;
|
||||
let mut to_remove = to_remove.into_iter();
|
||||
let mut next_to_remove_index = to_remove.next();
|
||||
self.elements.retain(|_| {
|
||||
let retain = if Some(current_index) == next_to_remove_index {
|
||||
next_to_remove_index = to_remove.next();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
current_index += 1;
|
||||
retain
|
||||
});
|
||||
self.elements.push(to_add);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Performs the following normalizations:
|
||||
/// - Replaces `Literal[True,False]` with `bool`.
|
||||
/// - TODO For enums `E` with members `X1`,...,`Xn`, replaces
|
||||
/// `Literal[E.X1,...,E.Xn]` with `E`.
|
||||
fn simplify(&mut self) {
|
||||
if let Some(true_index) = self.elements.get_index_of(&Type::BooleanLiteral(true)) {
|
||||
if self.elements.contains(&Type::BooleanLiteral(false)) {
|
||||
*self.elements.get_index_mut2(true_index).unwrap() =
|
||||
builtins_symbol_ty(self.db, "bool");
|
||||
self.elements.remove(&Type::BooleanLiteral(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build(mut self) -> Type<'db> {
|
||||
pub(crate) fn build(self) -> Type<'db> {
|
||||
match self.elements.len() {
|
||||
0 => Type::Never,
|
||||
1 => self.elements[0],
|
||||
_ => {
|
||||
self.simplify();
|
||||
|
||||
match self.elements.len() {
|
||||
0 => Type::Never,
|
||||
1 => self.elements[0],
|
||||
_ => {
|
||||
self.elements.shrink_to_fit();
|
||||
Type::Union(UnionType::new(self.db, self.elements))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Type::Union(UnionType::new(self.db, self.elements.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,6 +217,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
/// Adds a positive type to this intersection.
|
||||
fn add_positive(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
// TODO `Any`/`Unknown`/`Todo` actually should not self-cancel
|
||||
match ty {
|
||||
Type::Intersection(inter) => {
|
||||
let pos = inter.positive(db);
|
||||
@@ -211,7 +237,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
/// Adds a negative type to this intersection.
|
||||
fn add_negative(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
// TODO Any/Unknown actually should not self-cancel
|
||||
// TODO `Any`/`Unknown`/`Todo` actually should not self-cancel
|
||||
match ty {
|
||||
Type::Intersection(intersection) => {
|
||||
let pos = intersection.negative(db);
|
||||
@@ -276,16 +302,10 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::{builtins_symbol_ty, UnionBuilder};
|
||||
use crate::types::{KnownClass, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
impl<'db> UnionType<'db> {
|
||||
fn elements_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.elements(db).into_iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
@@ -313,7 +333,7 @@ mod tests {
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
|
||||
assert_eq!(union.elements_vec(&db), &[t0, t1]);
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -342,7 +362,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_ty = builtins_symbol_ty(&db, "bool");
|
||||
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
@@ -350,10 +370,10 @@ mod tests {
|
||||
let t3 = Type::IntLiteral(17);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
|
||||
assert_eq!(union.elements_vec(&db), &[t0, t3]);
|
||||
assert_eq!(union.elements(&db), &[t0, t3]);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements_vec(&db), &[bool_ty, t3]);
|
||||
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -365,7 +385,44 @@ mod tests {
|
||||
let u1 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
|
||||
|
||||
assert_eq!(union.elements_vec(&db), &[t0, t1, t2]);
|
||||
assert_eq!(union.elements(&db), &[t0, t1, t2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_subtype() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::LiteralString;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0, t0);
|
||||
assert_eq!(u1, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_no_simplify_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
|
||||
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
let str_ty = KnownClass::Str.to_instance(&db);
|
||||
let int_ty = KnownClass::Int.to_instance(&db);
|
||||
let object_ty = KnownClass::Object.to_instance(&db);
|
||||
let unknown_ty = Type::Unknown;
|
||||
|
||||
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
@@ -446,7 +503,7 @@ mod tests {
|
||||
.add_positive(u0)
|
||||
.build()
|
||||
.expect_union();
|
||||
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements_vec(&db)[..] else {
|
||||
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
|
||||
panic!("expected a union of two intersections");
|
||||
};
|
||||
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Display for DisplayType<'_> {
|
||||
| Type::Class(_)
|
||||
| Type::Function(_)
|
||||
) {
|
||||
write!(f, "Literal[{representation}]",)
|
||||
write!(f, "Literal[{representation}]")
|
||||
} else {
|
||||
representation.fmt(f)
|
||||
}
|
||||
@@ -66,6 +66,9 @@ impl Display for DisplayRepresentation<'_> {
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Unbound => f.write_str("Unbound"),
|
||||
Type::None => f.write_str("None"),
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo => f.write_str("@Todo"),
|
||||
Type::Module(file) => {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
}
|
||||
@@ -332,7 +335,7 @@ mod tests {
|
||||
class B: ...
|
||||
",
|
||||
)?;
|
||||
let mod_file = system_path_to_file(&db, "src/main.py").expect("Expected file to exist.");
|
||||
let mod_file = system_path_to_file(&db, "src/main.py").expect("file to exist");
|
||||
|
||||
let union_elements = &[
|
||||
Type::Unknown,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -155,13 +155,24 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
|
||||
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
|
||||
if matches!(op, ast::CmpOp::IsNot) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
};
|
||||
// TODO other comparison types
|
||||
match op {
|
||||
ast::CmpOp::IsNot => {
|
||||
if comp_ty.is_singleton(self.db) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
} else {
|
||||
// Non-singletons cannot be safely narrowed using `is not`
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Is => {
|
||||
self.constraints.insert(symbol, comp_ty);
|
||||
}
|
||||
_ => {
|
||||
// TODO other comparison types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
crates/red_knot_python_semantic/tests/mdtest.rs
Normal file
14
crates/red_knot_python_semantic/tests/mdtest.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use red_knot_test::run;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// See `crates/red_knot_test/README.md` for documentation on these tests.
|
||||
#[rstest::rstest]
|
||||
fn mdtest(#[files("resources/mdtest/**/*.md")] path: PathBuf) {
|
||||
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("mdtest")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let title = path.strip_prefix(crate_dir).unwrap();
|
||||
run(&path, title.as_os_str().to_str().unwrap());
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
9e506eb5e8fc2823db8c60ad561b1145ff114947
|
||||
@@ -1,46 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from contextlib import AbstractContextManager
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any, BinaryIO, TextIO
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
from importlib.abc import Traversable
|
||||
|
||||
__all__ = ["Package", "Resource", "contents", "is_resource", "open_binary", "open_text", "path", "read_binary", "read_text"]
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
__all__ += ["as_file", "files"]
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
__all__ += ["ResourceReader"]
|
||||
|
||||
Package: TypeAlias = str | ModuleType
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
Resource: TypeAlias = str
|
||||
else:
|
||||
Resource: TypeAlias = str | os.PathLike[Any]
|
||||
|
||||
def open_binary(package: Package, resource: Resource) -> BinaryIO: ...
|
||||
def open_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> TextIO: ...
|
||||
def read_binary(package: Package, resource: Resource) -> bytes: ...
|
||||
def read_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> str: ...
|
||||
def path(package: Package, resource: Resource) -> AbstractContextManager[Path]: ...
|
||||
def is_resource(package: Package, name: str) -> bool: ...
|
||||
def contents(package: Package) -> Iterator[str]: ...
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
def as_file(path: Traversable) -> AbstractContextManager[Path]: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
def files(anchor: Package | None = ...) -> Traversable: ...
|
||||
|
||||
elif sys.version_info >= (3, 9):
|
||||
def files(package: Package) -> Traversable: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from importlib.abc import ResourceReader as ResourceReader
|
||||
@@ -47,7 +47,7 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &RootDatabase) -> Vec<Diagnostic> {
|
||||
let Some(file) = snapshot.file(db) else {
|
||||
tracing::info!(
|
||||
"No file found for snapshot for '{}'",
|
||||
"No file found for snapshot for `{}`",
|
||||
snapshot.query().file_url()
|
||||
);
|
||||
return vec![];
|
||||
|
||||
33
crates/red_knot_test/Cargo.toml
Normal file
33
crates/red_knot_test/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "red_knot_test"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
470
crates/red_knot_test/README.md
Normal file
470
crates/red_knot_test/README.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# Writing type-checking / type-inference tests
|
||||
|
||||
Any Markdown file can be a test suite.
|
||||
|
||||
In order for it to be run as one, `red_knot_test::run` must be called with its path; see
|
||||
`crates/red_knot_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files
|
||||
under a certain directory as test suites.
|
||||
|
||||
A Markdown test suite can contain any number of tests. A test consists of one or more embedded
|
||||
"files", each defined by a triple-backticks fenced code block. The code block must have a tag string
|
||||
specifying its language; currently only `py` (Python files) and `pyi` (type stub files) are
|
||||
supported.
|
||||
|
||||
The simplest possible test suite consists of just a single test, with a single embedded file:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
````
|
||||
|
||||
When running this test, the mdtest framework will write a file with these contents to the default
|
||||
file path (`/src/test.py`) in its in-memory file system, run a type check on that file, and then
|
||||
match the resulting diagnostics with the assertions in the test. Assertions are in the form of
|
||||
Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, it
|
||||
fails.
|
||||
|
||||
<!---
|
||||
(If you are reading this document in raw Markdown source rather than rendered Markdown, note that
|
||||
the quadruple-backtick-fenced "markdown" language code block above is NOT itself part of the mdtest
|
||||
syntax, it's just how this README embeds an example mdtest Markdown document.)
|
||||
--->
|
||||
|
||||
See actual example mdtest suites in
|
||||
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
|
||||
|
||||
> ℹ️ Note: If you use `rstest` to generate a separate test for all Markdown files in a certain directory,
|
||||
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
|
||||
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
|
||||
> Markdown file is added or removed from the directory. See
|
||||
> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs)
|
||||
> for an example of how to do this.
|
||||
>
|
||||
> This is because `rstest` generates its tests at build time rather than at runtime.
|
||||
> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed,
|
||||
> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild
|
||||
> following the addition of the new test file.
|
||||
|
||||
## Assertions
|
||||
|
||||
Two kinds of assertions are supported: `# revealed:` (shown above) and `# error:`.
|
||||
|
||||
### Assertion kinds
|
||||
|
||||
#### revealed
|
||||
|
||||
A `# revealed:` assertion should always be paired with a call to the `reveal_type` utility, which
|
||||
reveals (via a diagnostic) the inferred type of its argument (which can be any expression). The text
|
||||
after `# revealed:` must match exactly with the displayed form of the revealed type of that
|
||||
expression.
|
||||
|
||||
The `reveal_type` function can be imported from the `typing` standard library module (or, for older
|
||||
Python versions, from the `typing_extensions` pseudo-standard-library module[^extensions]):
|
||||
|
||||
```py
|
||||
from typing import reveal_type
|
||||
|
||||
reveal_type("foo") # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
For convenience, type checkers also pretend that `reveal_type` is a built-in, so that this import is
|
||||
not required. Using `reveal_type` without importing it issues a diagnostic warning that it was used
|
||||
without importing it, in addition to the diagnostic revealing the type of the expression.
|
||||
|
||||
The `# revealed:` assertion must always match a revealed-type diagnostic, and will also match the
|
||||
undefined-reveal diagnostic, if present, so it's safe to use `reveal_type` in tests either with or
|
||||
without importing it. (Style preference is to not import it in tests, unless specifically testing
|
||||
something about the behavior of importing it.)
|
||||
|
||||
#### error
|
||||
|
||||
A comment beginning with `# error:` is an assertion that a type checker diagnostic will be emitted,
|
||||
with text span starting on that line. The matching can be narrowed in three ways:
|
||||
|
||||
- `# error: [invalid-assignment]` requires that the matched diagnostic have the rule code
|
||||
`invalid-assignment`. (The square brackets are required.)
|
||||
- `# error: "Some text"` requires that the diagnostic's full message contain the text `Some text`.
|
||||
(The double quotes are required in the assertion comment; they are not part of the matched text.)
|
||||
- `# error: 8 [rule-code]` or `# error: 8 "Some text"` additionally requires that the matched
|
||||
diagnostic's text span begins on column 8 (one-indexed) of this line.
|
||||
|
||||
Assertions must contain either a rule code or a contains-text, or both, and may optionally also
|
||||
include a column number. They must come in order: first column, if present; then rule code, if
|
||||
present; then contains-text, if present. For example, an assertion using all three would look like
|
||||
`# error: 8 [invalid-assignment] "Some text"`.
|
||||
|
||||
Error assertions in tests intended to test type checker semantics should primarily use rule-code
|
||||
assertions, with occasional contains-text assertions where needed to disambiguate or validate some
|
||||
details of the diagnostic message.
|
||||
|
||||
### Assertion locations
|
||||
|
||||
An assertion comment may be a line-trailing comment, in which case it applies to the line it is on:
|
||||
|
||||
```py
|
||||
x: str = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
Or it may be a comment on its own line, in which case it applies to the next line that does not
|
||||
contain an assertion comment:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment]
|
||||
x: str = 1
|
||||
```
|
||||
|
||||
Multiple assertions applying to the same line may be stacked:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment]
|
||||
# revealed: Literal[1]
|
||||
x: str = reveal_type(1)
|
||||
```
|
||||
|
||||
Intervening empty lines or non-assertion comments are not allowed; an assertion stack must be one
|
||||
assertion per line, immediately following each other, with the line immediately following the last
|
||||
assertion as the line of source code on which the matched diagnostics are emitted.
|
||||
|
||||
## Multi-file tests
|
||||
|
||||
Some tests require multiple files, with imports from one file into another. Multiple fenced code
|
||||
blocks represent multiple embedded files. Since files must have unique names, at most one file can
|
||||
use the default name of `/src/test.py`. Other files must explicitly specify their file name:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
from b import C
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
```
|
||||
````
|
||||
|
||||
Relative file names are always relative to the "workspace root", which is also an import root (that
|
||||
is, the equivalent of a runtime entry on `sys.path`).
|
||||
|
||||
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
|
||||
this is a feature we will want to add in the future.
|
||||
|
||||
So the above test creates two files, `/src/test.py` and `/src/b.py`, and sets the workspace root to
|
||||
`/src/`, allowing `test.py` to import from `b.py` using the module name `b`.
|
||||
|
||||
## Multi-test suites
|
||||
|
||||
A single test suite (Markdown file) can contain multiple tests, by demarcating them using Markdown
|
||||
header lines:
|
||||
|
||||
````markdown
|
||||
# Same-file invalid assignment
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Cross-file invalid assignment
|
||||
|
||||
```py
|
||||
from b import y
|
||||
x: int = y # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
y = "foo"
|
||||
```
|
||||
````
|
||||
|
||||
This test suite contains two tests, one named "Same-file invalid assignment" and the other named
|
||||
"Cross-file invalid assignment". The first test involves only a single embedded file, and the second
|
||||
test involves two embedded files.
|
||||
|
||||
The tests are run independently, in independent in-memory file systems and with new red-knot
|
||||
[Salsa](https://github.com/salsa-rs/salsa) databases. This means that each is a from-scratch run of
|
||||
the type checker, with no data persisting from any previous test.
|
||||
|
||||
Due to `cargo test` limitations, an entire test suite (Markdown file) is run as a single Rust test,
|
||||
so it's not possible to select individual tests within it to run.
|
||||
|
||||
## Structured test suites
|
||||
|
||||
Markdown headers can also be used to group related tests within a suite:
|
||||
|
||||
````markdown
|
||||
# Literals
|
||||
|
||||
## Numbers
|
||||
|
||||
### Integer
|
||||
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Float
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
```py
|
||||
reveal_type("foo") # revealed: Literal["foo"]
|
||||
```
|
||||
````
|
||||
|
||||
This test suite contains three tests, named "Literals - Numbers - Integer", "Literals - Numbers -
|
||||
Float", and "Literals - Strings".
|
||||
|
||||
A header-demarcated section must either be a test or a grouping header; it cannot be both. That is,
|
||||
a header section can either contain embedded files (making it a test), or it can contain more
|
||||
deeply-nested headers (headers with more `#`), but it cannot contain both.
|
||||
|
||||
## Documentation of tests
|
||||
|
||||
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
|
||||
the test framework) between fenced code blocks. This permits natural documentation of
|
||||
why a test exists, and what it intends to assert:
|
||||
|
||||
````markdown
|
||||
Assigning a string to a variable annotated as `int` is not permitted:
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
````
|
||||
|
||||
## Planned features
|
||||
|
||||
There are some designed features that we intend for the test framework to have, but have not yet
|
||||
implemented:
|
||||
|
||||
### Multi-line diagnostic assertions
|
||||
|
||||
We may want to be able to assert that a diagnostic spans multiple lines, and to assert the columns it
|
||||
begins and/or ends on. The planned syntax for this will use `<<<` and `>>>` to mark the start and end lines for
|
||||
an assertion:
|
||||
|
||||
```py
|
||||
(3 # error: 2 [unsupported-operands] <<<
|
||||
+
|
||||
"foo") # error: 6 >>>
|
||||
```
|
||||
|
||||
The column assertion `6` on the ending line should be optional.
|
||||
|
||||
In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins
|
||||
an assertion ended by `>>>>`, etc.
|
||||
|
||||
### Non-Python files
|
||||
|
||||
Some tests may need to specify non-Python embedded files: typeshed `stdlib/VERSIONS`, `pth` files,
|
||||
`py.typed` files, `pyvenv.cfg` files...
|
||||
|
||||
We will allow specifying any of these using the `text` language in the code block tag string:
|
||||
|
||||
````markdown
|
||||
```text path=/third-party/foo/py.typed
|
||||
partial
|
||||
```
|
||||
````
|
||||
|
||||
We may want to also support testing Jupyter notebooks as embedded files; exact syntax for this is
|
||||
yet to be determined.
|
||||
|
||||
Of course, red-knot is only run directly on `py` and `pyi` files, and assertion comments are only
|
||||
possible in these files.
|
||||
|
||||
A fenced code block with no language will always be an error.
|
||||
|
||||
### Configuration
|
||||
|
||||
We will add the ability to specify non-default red-knot configurations to use in tests, by including
|
||||
a TOML code block:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot]
|
||||
warn-on-any = true
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any): # error: [use-of-any]
|
||||
pass
|
||||
```
|
||||
````
|
||||
|
||||
It should be possible to include a TOML code block in a single test (as shown), or in a grouping
|
||||
section, in which case it applies to all nested tests within that grouping section. Configurations
|
||||
at multiple level are allowed and merged, with the most-nested (closest to the test) taking
|
||||
precedence.
|
||||
|
||||
### Running just a single test from a suite
|
||||
|
||||
Having each test in a suite always run as a distinct Rust test would require writing our own test
|
||||
runner or code-generating tests in a build script; neither of these is planned.
|
||||
|
||||
We could still allow running just a single test from a suite, for debugging purposes, either via
|
||||
some "focus" syntax that could be easily temporarily added to a test, or via an environment
|
||||
variable.
|
||||
|
||||
### Configuring search paths and kinds
|
||||
|
||||
The red-knot TOML configuration format hasn't been designed yet, and we may want to implement
|
||||
support in the test framework for configuring search paths before it is designed. If so, we can
|
||||
define some configuration options for now under the `[tool.knot.tests]` namespace. In the future,
|
||||
perhaps some of these can be replaced by real red-knot configuration options; some or all may also
|
||||
be kept long-term as test-specific options.
|
||||
|
||||
Some configuration options we will want to provide:
|
||||
|
||||
- We should be able to configure the default workspace root to something other than `/src/` using a
|
||||
`workspace-root` configuration option.
|
||||
|
||||
- We should be able to add a third-party root using the `third-party-root` configuration option.
|
||||
|
||||
- We may want to add additional configuration options for setting additional search path kinds.
|
||||
|
||||
Paths for `workspace-root` and `third-party-root` must be absolute.
|
||||
|
||||
Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a
|
||||
non-default value using the `workspace-root` config.
|
||||
|
||||
### Specifying a custom typeshed
|
||||
|
||||
Some tests will need to override the default typeshed with custom files. The `[tool.knot.tests]`
|
||||
configuration option `typeshed-root` should be usable for this:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot.tests]
|
||||
typeshed-root = "/typeshed"
|
||||
```
|
||||
|
||||
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we
|
||||
configured above as our custom typeshed root:
|
||||
|
||||
```py path=/typeshed/stdlib/builtins.pyi
|
||||
I_AM_THE_ONLY_BUILTIN = 1
|
||||
```
|
||||
|
||||
This file is written to `/src/test.py`, because the default workspace root is `/src/ and the default
|
||||
file path is `test.py`:
|
||||
|
||||
```py
|
||||
reveal_type(I_AM_THE_ONLY_BUILTIN) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
|
||||
custom typeshed root. If no such file is created explicitly, one should be created implicitly
|
||||
including entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python
|
||||
versions.
|
||||
|
||||
### I/O errors
|
||||
|
||||
We could use an `error=` configuration option in the tag string to make an embedded file cause an
|
||||
I/O error on read.
|
||||
|
||||
### Asserting on full diagnostic output
|
||||
|
||||
The inline comment diagnostic assertions are useful for making quick, readable assertions about
|
||||
diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic
|
||||
output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will
|
||||
want to assert on diagnostics in a file, without impacting the contents of that file by changing a
|
||||
comment in it. In these cases, a Python code block in a test could be followed by a fenced code
|
||||
block with language `output`; this would contain the full diagnostic output for the preceding test
|
||||
file:
|
||||
|
||||
````markdown
|
||||
# full output
|
||||
|
||||
```py
|
||||
x = 1
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
This is just an example, not a proposal that red-knot would ever actually output diagnostics in
|
||||
precisely this format:
|
||||
|
||||
```output
|
||||
test.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
````
|
||||
|
||||
We will want to build tooling to automatically capture and update these “full diagnostic output”
|
||||
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
|
||||
|
||||
By default, an `output` block will specify diagnostic output for the file `<workspace-root>/test.py`.
|
||||
An `output` block can have a `path=` option, to explicitly specify the Python file for which it
|
||||
asserts diagnostic output, and a `stage=` option, to specify which stage of an incremental test it
|
||||
specifies diagnostic output at. (See “incremental tests” below.)
|
||||
|
||||
It is an error for an `output` block to exist, if there is no `py` or `python` block in the same
|
||||
test for the same file path.
|
||||
|
||||
### Incremental tests
|
||||
|
||||
Some tests should validate incremental checking, by initially creating some files, checking them,
|
||||
and then modifying/adding/deleting files and checking again.
|
||||
|
||||
We should add the capability to create an incremental test by using the `stage=` option on some
|
||||
fenced code blocks in the test:
|
||||
|
||||
````markdown
|
||||
# Incremental
|
||||
|
||||
## modify a file
|
||||
|
||||
Initial version of `test.py` and `b.py`:
|
||||
|
||||
```py
|
||||
from b import x
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
Initial expected output for `test.py`:
|
||||
|
||||
```output
|
||||
/src/test.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
|
||||
Now in our first incremental stage, modify the contents of `b.py`:
|
||||
|
||||
```py path=b.py stage=1
|
||||
# b.py
|
||||
x = 2
|
||||
```
|
||||
|
||||
And this is our updated expected output for `test.py` at stage 1:
|
||||
|
||||
```output stage=1
|
||||
/src/test.py, line 1, col 1: revealed type is 'Literal[2]'
|
||||
```
|
||||
|
||||
(One reason to use full-diagnostic-output blocks in this test is that updating
|
||||
inline-comment diagnostic assertions for `test.py` would require specifying new
|
||||
contents for `test.py` in stage 1, which we don't want to do in this test.)
|
||||
````
|
||||
|
||||
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies
|
||||
a filename that was specified in a previous stage (or the initial stage), that file is modified. A
|
||||
new filename appearing for the first time in a new stage will create a new file. To delete a
|
||||
previously created file, specify that file with the tag `delete` in its tag string (in this case, it
|
||||
is an error to provide non-empty contents). Any previously-created files that are not re-specified
|
||||
in a later stage continue to exist with their previously-specified contents, and are not "touched".
|
||||
|
||||
All stages should be run in order, incrementally, and then the final state should also be re-checked
|
||||
cold, to validate equivalence of cold and incremental check results.
|
||||
|
||||
[^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers
|
||||
also, treat it as part of the standard library.
|
||||
621
crates/red_knot_test/src/assertion.rs
Normal file
621
crates/red_knot_test/src/assertion.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
//! Parse type and type-error assertions in Python comment form.
|
||||
//!
|
||||
//! Parses comments of the form `# revealed: SomeType` and `# error: 8 [rule-code] "message text"`.
|
||||
//! In the latter case, the `8` is a column number, and `"message text"` asserts that the full
|
||||
//! diagnostic message contains the text `"message text"`; all three are optional (`# error:` will
|
||||
//! match any error.)
|
||||
//!
|
||||
//! Assertion comments may be placed at end-of-line:
|
||||
//!
|
||||
//! ```py
|
||||
//! x: int = "foo" # error: [invalid-assignment]
|
||||
//! ```
|
||||
//!
|
||||
//! Or as a full-line comment on the preceding line:
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [invalid-assignment]
|
||||
//! x: int = "foo"
|
||||
//! ```
|
||||
//!
|
||||
//! Multiple assertion comments may apply to the same line; in this case all (or all but the last)
|
||||
//! must be full-line comments:
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [unbound-name]
|
||||
//! reveal_type(x) # revealed: Unbound
|
||||
//! ```
|
||||
//!
|
||||
//! or
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [unbound-name]
|
||||
//! # revealed: Unbound
|
||||
//! reveal_type(x)
|
||||
//! ```
|
||||
|
||||
use crate::db::Db;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::{LineIndex, Locator, OneIndexed};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Diagnostic assertion comments in a single embedded file.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct InlineFileAssertions {
|
||||
comment_ranges: CommentRanges,
|
||||
source: SourceText,
|
||||
lines: LineIndex,
|
||||
}
|
||||
|
||||
impl InlineFileAssertions {
|
||||
pub(crate) fn from_file(db: &Db, file: File) -> Self {
|
||||
let source = source_text(db, file);
|
||||
let lines = line_index(db, file);
|
||||
let parsed = parsed_module(db, file);
|
||||
let comment_ranges = CommentRanges::from(parsed.tokens());
|
||||
Self {
|
||||
comment_ranges,
|
||||
source,
|
||||
lines,
|
||||
}
|
||||
}
|
||||
|
||||
fn locator(&self) -> Locator {
|
||||
Locator::with_index(&self.source, self.lines.clone())
|
||||
}
|
||||
|
||||
fn line_number(&self, range: &impl Ranged) -> OneIndexed {
|
||||
self.lines.line_index(range.start())
|
||||
}
|
||||
|
||||
fn is_own_line_comment(&self, ranged_assertion: &AssertionWithRange) -> bool {
|
||||
CommentRanges::is_own_line(ranged_assertion.start(), &self.locator())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a InlineFileAssertions {
|
||||
type Item = LineAssertions<'a>;
|
||||
type IntoIter = LineAssertionsIterator<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Self::IntoIter {
|
||||
file_assertions: self,
|
||||
inner: AssertionWithRangeIterator {
|
||||
file_assertions: self,
|
||||
inner: self.comment_ranges.into_iter(),
|
||||
}
|
||||
.peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Assertion`] with the [`TextRange`] of its original inline comment.
|
||||
#[derive(Debug)]
|
||||
struct AssertionWithRange<'a>(Assertion<'a>, TextRange);
|
||||
|
||||
impl<'a> Deref for AssertionWithRange<'a> {
|
||||
type Target = Assertion<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AssertionWithRange<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AssertionWithRange<'a>> for Assertion<'a> {
|
||||
fn from(value: AssertionWithRange<'a>) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that yields all assertions within a single embedded Python file.
|
||||
#[derive(Debug)]
|
||||
struct AssertionWithRangeIterator<'a> {
|
||||
file_assertions: &'a InlineFileAssertions,
|
||||
inner: std::iter::Copied<std::slice::Iter<'a, TextRange>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AssertionWithRangeIterator<'a> {
|
||||
type Item = AssertionWithRange<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let locator = self.file_assertions.locator();
|
||||
loop {
|
||||
let inner_next = self.inner.next()?;
|
||||
let comment = locator.slice(inner_next);
|
||||
if let Some(assertion) = Assertion::from_comment(comment) {
|
||||
return Some(AssertionWithRange(assertion, inner_next));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for AssertionWithRangeIterator<'_> {}
|
||||
|
||||
/// A vector of [`Assertion`]s belonging to a single line.
|
||||
///
|
||||
/// Most lines will have zero or one assertion, so we use a [`SmallVec`] optimized for a single
|
||||
/// element to avoid most heap vector allocations.
|
||||
type AssertionVec<'a> = SmallVec<[Assertion<'a>; 1]>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineAssertionsIterator<'a> {
|
||||
file_assertions: &'a InlineFileAssertions,
|
||||
inner: std::iter::Peekable<AssertionWithRangeIterator<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LineAssertionsIterator<'a> {
|
||||
type Item = LineAssertions<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let file = self.file_assertions;
|
||||
let ranged_assertion = self.inner.next()?;
|
||||
let mut collector = AssertionVec::new();
|
||||
let mut line_number = file.line_number(&ranged_assertion);
|
||||
// Collect all own-line comments on consecutive lines; these all apply to the same line of
|
||||
// code. For example:
|
||||
//
|
||||
// ```py
|
||||
// # error: [unbound-name]
|
||||
// # revealed: Unbound
|
||||
// reveal_type(x)
|
||||
// ```
|
||||
//
|
||||
if file.is_own_line_comment(&ranged_assertion) {
|
||||
collector.push(ranged_assertion.into());
|
||||
let mut only_own_line = true;
|
||||
while let Some(ranged_assertion) = self.inner.peek() {
|
||||
let next_line_number = line_number.saturating_add(1);
|
||||
if file.line_number(ranged_assertion) == next_line_number {
|
||||
if !file.is_own_line_comment(ranged_assertion) {
|
||||
only_own_line = false;
|
||||
}
|
||||
line_number = next_line_number;
|
||||
collector.push(self.inner.next().unwrap().into());
|
||||
// If we see an end-of-line comment, it has to be the end of the stack,
|
||||
// otherwise we'd botch this case, attributing all three errors to the `bar`
|
||||
// line:
|
||||
//
|
||||
// ```py
|
||||
// # error:
|
||||
// foo # error:
|
||||
// bar # error:
|
||||
// ```
|
||||
//
|
||||
if !only_own_line {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if only_own_line {
|
||||
// The collected comments apply to the _next_ line in the code.
|
||||
line_number = line_number.saturating_add(1);
|
||||
}
|
||||
} else {
|
||||
// We have a line-trailing comment; it applies to its own line, and is not grouped.
|
||||
collector.push(ranged_assertion.into());
|
||||
}
|
||||
Some(LineAssertions {
|
||||
line_number,
|
||||
assertions: collector,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for LineAssertionsIterator<'_> {}
|
||||
|
||||
/// One or more assertions referring to the same line of code.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineAssertions<'a> {
|
||||
/// The line these assertions refer to.
|
||||
///
|
||||
/// Not necessarily the same line the assertion comment is located on; for an own-line comment,
|
||||
/// it's the next non-assertion line.
|
||||
pub(crate) line_number: OneIndexed,
|
||||
|
||||
/// The assertions referring to this line.
|
||||
pub(crate) assertions: AssertionVec<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for LineAssertions<'a> {
|
||||
type Target = [Assertion<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.assertions
|
||||
}
|
||||
}
|
||||
|
||||
static TYPE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
|
||||
|
||||
static ERROR_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^#\s*error:(\s*(?<column>\d+))?(\s*\[(?<rule>.+?)\])?(\s*"(?<message>.+?)")?\s*$"#,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// A single diagnostic assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Assertion<'a> {
|
||||
/// A `revealed: ` assertion.
|
||||
Revealed(&'a str),
|
||||
|
||||
/// An `error: ` assertion.
|
||||
Error(ErrorAssertion<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Assertion<'a> {
|
||||
fn from_comment(comment: &'a str) -> Option<Self> {
|
||||
if let Some(caps) = TYPE_RE.captures(comment) {
|
||||
Some(Self::Revealed(caps.name("ty_display").unwrap().as_str()))
|
||||
} else {
|
||||
ERROR_RE.captures(comment).map(|caps| {
|
||||
Self::Error(ErrorAssertion {
|
||||
rule: caps.name("rule").map(|m| m.as_str()),
|
||||
column: caps.name("column").and_then(|m| m.as_str().parse().ok()),
|
||||
message_contains: caps.name("message").map(|m| m.as_str()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Assertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Revealed(expected_type) => write!(f, "revealed: {expected_type}"),
|
||||
Self::Error(assertion) => assertion.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An `error: ` assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ErrorAssertion<'a> {
|
||||
/// The diagnostic rule code we expect.
|
||||
pub(crate) rule: Option<&'a str>,
|
||||
|
||||
/// The column we expect the diagnostic range to start at.
|
||||
pub(crate) column: Option<OneIndexed>,
|
||||
|
||||
/// A string we expect to be contained in the diagnostic message.
|
||||
pub(crate) message_contains: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ErrorAssertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("error:")?;
|
||||
if let Some(column) = self.column {
|
||||
write!(f, " {column}")?;
|
||||
}
|
||||
if let Some(rule) = self.rule {
|
||||
write!(f, " [{rule}]")?;
|
||||
}
|
||||
if let Some(message) = self.message_contains {
|
||||
write!(f, r#" "{message}""#)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Assertion, InlineFileAssertions, LineAssertions};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
fn get_assertions(source: &str) -> InlineFileAssertions {
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
InlineFileAssertions::from_file(&db, file)
|
||||
}
|
||||
|
||||
fn as_vec(assertions: &InlineFileAssertions) -> Vec<LineAssertions> {
|
||||
assertions.into_iter().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ty_display() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "revealed: Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error:
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prior_line() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Literal[1]
|
||||
reveal_type(1)
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "revealed: Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_prior_line() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Unbound
|
||||
# error: [unbound-name]
|
||||
reveal_type(x)
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [assert1, assert2] = &line.assertions[..] else {
|
||||
panic!("expected two assertions");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert1}"), "revealed: Unbound");
|
||||
assert_eq!(format!("{assert2}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_mixed() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Unbound
|
||||
reveal_type(x) # error: [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert1, assert2] = &line.assertions[..] else {
|
||||
panic!("expected two assertions");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert1}"), "revealed: Unbound");
|
||||
assert_eq!(format!("{assert2}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lines() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [invalid-assignment]
|
||||
x: int = "foo"
|
||||
y # error: [unbound-name]
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line1, line2] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2));
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [Assertion::Error(error1)] = &line1.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error1.rule, Some("invalid-assignment"));
|
||||
|
||||
let [Assertion::Error(error2)] = &line2.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error2.rule, Some("unbound-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lines_mixed_stack() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [invalid-assignment]
|
||||
x: int = reveal_type("foo") # revealed: str
|
||||
y # error: [unbound-name]
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line1, line2] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2));
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [Assertion::Error(error1), Assertion::Revealed(expected_ty)] = &line1.assertions[..]
|
||||
else {
|
||||
panic!("expected one error assertion and one Revealed assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error1.rule, Some("invalid-assignment"));
|
||||
assert_eq!(*expected_ty, "str");
|
||||
|
||||
let [Assertion::Error(error2)] = &line2.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error2.rule, Some("unbound-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error: [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error: 1 [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error: 1 [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_message() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [unbound-name] "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format!("{assert}"),
|
||||
r#"error: [unbound-name] "`x` is unbound""#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_message_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: 1 "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), r#"error: 1 "`x` is unbound""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_message_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: 1 [unbound-name] "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format!("{assert}"),
|
||||
r#"error: 1 [unbound-name] "`x` is unbound""#
|
||||
);
|
||||
}
|
||||
}
|
||||
88
crates/red_knot_test/src/db.rs
Normal file
88
crates/red_knot_test/src/db.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use red_knot_python_semantic::{
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonVersion, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct Db {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let db = Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&workspace_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(workspace_root),
|
||||
},
|
||||
)
|
||||
.expect("Invalid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for Db {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for Db {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for Db {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for Db {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for Db {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
||||
173
crates/red_knot_test/src/diagnostic.rs
Normal file
173
crates/red_knot_test/src/diagnostic.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Sort and group diagnostics by line number, so they can be correlated with assertions.
|
||||
//!
|
||||
//! We don't assume that we will get the diagnostics in source order.
|
||||
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::ops::{Deref, Range};
|
||||
|
||||
/// All diagnostics for one embedded Python file, sorted and grouped by start line number.
|
||||
///
|
||||
/// The diagnostics are kept in a flat vector, sorted by line number. A separate vector of
|
||||
/// [`LineDiagnosticRange`] has one entry for each contiguous slice of the diagnostics vector
|
||||
/// containing diagnostics which all start on the same line.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SortedDiagnostics<T> {
|
||||
diagnostics: Vec<T>,
|
||||
line_ranges: Vec<LineDiagnosticRange>,
|
||||
}
|
||||
|
||||
impl<T> SortedDiagnostics<T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
{
|
||||
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
||||
let mut diagnostics: Vec<_> = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| DiagnosticWithLine {
|
||||
line_number: line_index.line_index(diagnostic.start()),
|
||||
diagnostic,
|
||||
})
|
||||
.collect();
|
||||
diagnostics.sort_unstable_by_key(|diagnostic_with_line| diagnostic_with_line.line_number);
|
||||
|
||||
let mut diags = Self {
|
||||
diagnostics: Vec::with_capacity(diagnostics.len()),
|
||||
line_ranges: vec![],
|
||||
};
|
||||
|
||||
let mut current_line_number = None;
|
||||
let mut start = 0;
|
||||
for DiagnosticWithLine {
|
||||
line_number,
|
||||
diagnostic,
|
||||
} in diagnostics
|
||||
{
|
||||
match current_line_number {
|
||||
None => {
|
||||
current_line_number = Some(line_number);
|
||||
}
|
||||
Some(current) => {
|
||||
if line_number != current {
|
||||
let end = diags.diagnostics.len();
|
||||
diags.line_ranges.push(LineDiagnosticRange {
|
||||
line_number: current,
|
||||
diagnostic_index_range: start..end,
|
||||
});
|
||||
start = end;
|
||||
current_line_number = Some(line_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
diags.diagnostics.push(diagnostic);
|
||||
}
|
||||
if let Some(line_number) = current_line_number {
|
||||
diags.line_ranges.push(LineDiagnosticRange {
|
||||
line_number,
|
||||
diagnostic_index_range: start..diags.diagnostics.len(),
|
||||
});
|
||||
}
|
||||
|
||||
diags
|
||||
}
|
||||
|
||||
pub(crate) fn iter_lines(&self) -> LineDiagnosticsIterator<T> {
|
||||
LineDiagnosticsIterator {
|
||||
diagnostics: self.diagnostics.as_slice(),
|
||||
inner: self.line_ranges.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Range delineating diagnostics in [`SortedDiagnostics`] that begin on a single line.
|
||||
#[derive(Debug)]
|
||||
struct LineDiagnosticRange {
|
||||
line_number: OneIndexed,
|
||||
diagnostic_index_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Iterator to group sorted diagnostics by line.
|
||||
pub(crate) struct LineDiagnosticsIterator<'a, T> {
|
||||
diagnostics: &'a [T],
|
||||
inner: std::slice::Iter<'a, LineDiagnosticRange>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
{
|
||||
type Item = LineDiagnostics<'a, T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let LineDiagnosticRange {
|
||||
line_number,
|
||||
diagnostic_index_range,
|
||||
} = self.inner.next()?;
|
||||
Some(LineDiagnostics {
|
||||
line_number: *line_number,
|
||||
diagnostics: &self.diagnostics[diagnostic_index_range.clone()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Clone + Ranged {}
|
||||
|
||||
/// All diagnostics that start on a single line of source code in one embedded Python file.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineDiagnostics<'a, T> {
|
||||
/// Line number on which these diagnostics start.
|
||||
pub(crate) line_number: OneIndexed,
|
||||
|
||||
/// Diagnostics starting on this line.
|
||||
pub(crate) diagnostics: &'a [T],
|
||||
}
|
||||
|
||||
impl<T> Deref for LineDiagnostics<'_, T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiagnosticWithLine<T> {
|
||||
line_number: OneIndexed,
|
||||
diagnostic: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
#[test]
|
||||
fn sort_and_group() {
|
||||
let mut db = Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", "one\ntwo\n").unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
let lines = line_index(&db, file);
|
||||
|
||||
let ranges = vec![
|
||||
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
||||
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
||||
TextRange::new(TextSize::new(1), TextSize::new(7)),
|
||||
];
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(&ranges, &lines);
|
||||
let grouped = sorted.iter_lines().collect::<Vec<_>>();
|
||||
|
||||
let [line1, line2] = &grouped[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(0));
|
||||
assert_eq!(line1.diagnostics.len(), 2);
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
||||
assert_eq!(line2.diagnostics.len(), 1);
|
||||
}
|
||||
}
|
||||
95
crates/red_knot_test/src/lib.rs
Normal file
95
crates/red_knot_test/src/lib.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
type Failures = BTreeMap<SystemPathBuf, matcher::FailuresByLine>;
|
||||
|
||||
mod assertion;
|
||||
mod db;
|
||||
mod diagnostic;
|
||||
mod matcher;
|
||||
mod parser;
|
||||
|
||||
/// Run `path` as a markdown test suite with given `title`.
|
||||
///
|
||||
/// Panic on test failure, and print failure details.
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub fn run(path: &PathBuf, title: &str) {
|
||||
let source = std::fs::read_to_string(path).unwrap();
|
||||
let suite = match test_parser::parse(title, &source) {
|
||||
Ok(suite) => suite,
|
||||
Err(err) => {
|
||||
panic!("Error parsing `{}`: {err}", path.to_str().unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
let mut any_failures = false;
|
||||
for test in suite.tests() {
|
||||
if let Err(failures) = run_test(&test) {
|
||||
any_failures = true;
|
||||
println!("\n{}\n", test.name().bold().underline());
|
||||
|
||||
for (path, by_line) in failures {
|
||||
println!("{}", path.as_str().bold());
|
||||
for (line_number, failures) in by_line.iter() {
|
||||
for failure in failures {
|
||||
let line_info = format!("line {line_number}:").cyan();
|
||||
println!(" {line_info} {failure}");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}\n", "-".repeat(50));
|
||||
|
||||
assert!(!any_failures, "Some tests failed.");
|
||||
}
|
||||
|
||||
fn run_test(test: &parser::MarkdownTest) -> Result<(), Failures> {
|
||||
let workspace_root = SystemPathBuf::from("/src");
|
||||
let mut db = db::Db::setup(workspace_root.clone());
|
||||
|
||||
let mut system_paths = vec![];
|
||||
|
||||
for file in test.files() {
|
||||
assert!(
|
||||
matches!(file.lang, "py" | "pyi"),
|
||||
"Non-Python files not supported yet."
|
||||
);
|
||||
let full_path = workspace_root.join(file.path);
|
||||
db.write_file(&full_path, file.code).unwrap();
|
||||
system_paths.push(full_path);
|
||||
}
|
||||
|
||||
let mut failures = BTreeMap::default();
|
||||
|
||||
for path in system_paths {
|
||||
let file = system_path_to_file(&db, path.clone()).unwrap();
|
||||
let parsed = parsed_module(&db, file);
|
||||
|
||||
// TODO allow testing against code with syntax errors
|
||||
assert!(
|
||||
parsed.errors().is_empty(),
|
||||
"Python syntax errors in {}, {:?}: {:?}",
|
||||
test.name(),
|
||||
path,
|
||||
parsed.errors()
|
||||
);
|
||||
|
||||
matcher::match_file(&db, file, check_types(&db, file)).unwrap_or_else(|line_failures| {
|
||||
failures.insert(path, line_failures);
|
||||
});
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
929
crates/red_knot_test/src/matcher.rs
Normal file
929
crates/red_knot_test/src/matcher.rs
Normal file
@@ -0,0 +1,929 @@
|
||||
//! Match [`TypeCheckDiagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! mismatches.
|
||||
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use red_knot_python_semantic::types::TypeCheckDiagnostic;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct FailuresByLine {
|
||||
failures: Vec<String>,
|
||||
lines: Vec<LineFailures>,
|
||||
}
|
||||
|
||||
impl FailuresByLine {
|
||||
pub(super) fn iter(&self) -> impl Iterator<Item = (OneIndexed, &[String])> {
|
||||
self.lines.iter().map(|line_failures| {
|
||||
(
|
||||
line_failures.line_number,
|
||||
&self.failures[line_failures.range.clone()],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
|
||||
let start = self.failures.len();
|
||||
self.failures.extend(messages);
|
||||
self.lines.push(LineFailures {
|
||||
line_number,
|
||||
range: start..self.failures.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.lines.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LineFailures {
|
||||
line_number: OneIndexed,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
pub(super) fn match_file<T>(
|
||||
db: &Db,
|
||||
file: File,
|
||||
diagnostics: impl IntoIterator<Item = T>,
|
||||
) -> Result<(), FailuresByLine>
|
||||
where
|
||||
T: Diagnostic + Clone,
|
||||
{
|
||||
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
||||
// ordered by line number.
|
||||
let assertions = InlineFileAssertions::from_file(db, file);
|
||||
let diagnostics = SortedDiagnostics::new(diagnostics, &line_index(db, file));
|
||||
|
||||
// Get iterators over assertions and diagnostics grouped by line, in ascending line order.
|
||||
let mut line_assertions = assertions.into_iter();
|
||||
let mut line_diagnostics = diagnostics.iter_lines();
|
||||
|
||||
let mut current_assertions = line_assertions.next();
|
||||
let mut current_diagnostics = line_diagnostics.next();
|
||||
|
||||
let matcher = Matcher::from_file(db, file);
|
||||
let mut failures = FailuresByLine::default();
|
||||
|
||||
loop {
|
||||
match (¤t_assertions, ¤t_diagnostics) {
|
||||
(Some(assertions), Some(diagnostics)) => {
|
||||
match assertions.line_number.cmp(&diagnostics.line_number) {
|
||||
Ordering::Equal => {
|
||||
// We have assertions and diagnostics on the same line; check for
|
||||
// matches and error on any that don't match, then advance both
|
||||
// iterators.
|
||||
matcher
|
||||
.match_line(diagnostics, assertions)
|
||||
.unwrap_or_else(|messages| {
|
||||
failures.push(assertions.line_number, messages);
|
||||
});
|
||||
current_assertions = line_assertions.next();
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
Ordering::Less => {
|
||||
// We have assertions on an earlier line than diagnostics; report these
|
||||
// assertions as all unmatched, and advance the assertions iterator.
|
||||
failures.push(assertions.line_number, unmatched(assertions));
|
||||
current_assertions = line_assertions.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
// We have diagnostics on an earlier line than assertions; report these
|
||||
// diagnostics as all unmatched, and advance the diagnostics iterator.
|
||||
failures.push(diagnostics.line_number, unmatched(diagnostics));
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(assertions), None) => {
|
||||
// We've exhausted diagnostics but still have assertions; report these assertions
|
||||
// as unmatched and advance the assertions iterator.
|
||||
failures.push(assertions.line_number, unmatched(assertions));
|
||||
current_assertions = line_assertions.next();
|
||||
}
|
||||
(None, Some(diagnostics)) => {
|
||||
// We've exhausted assertions but still have diagnostics; report these
|
||||
// diagnostics as unmatched and advance the diagnostics iterator.
|
||||
failures.push(diagnostics.line_number, unmatched(diagnostics));
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
// When we've exhausted both diagnostics and assertions, break.
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait Diagnostic: Ranged {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> &str;
|
||||
}
|
||||
|
||||
impl Diagnostic for Arc<TypeCheckDiagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
self.as_ref().rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.as_ref().message()
|
||||
}
|
||||
}
|
||||
|
||||
trait Unmatched {
|
||||
fn unmatched(&self) -> String;
|
||||
}
|
||||
|
||||
fn unmatched<'a, T: Unmatched + 'a>(unmatched: &'a [T]) -> Vec<String> {
|
||||
unmatched.iter().map(Unmatched::unmatched).collect()
|
||||
}
|
||||
|
||||
trait UnmatchedWithColumn {
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String;
|
||||
}
|
||||
|
||||
impl Unmatched for Assertion<'_> {
|
||||
fn unmatched(&self) -> String {
|
||||
format!("{} {self}", "unmatched assertion:".red())
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
|
||||
diagnostic: &T,
|
||||
original: std::fmt::Arguments,
|
||||
) -> String {
|
||||
if diagnostic.rule() == "undefined-reveal" {
|
||||
format!(
|
||||
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
|
||||
"used built-in `reveal_type`:".yellow()
|
||||
)
|
||||
} else {
|
||||
format!("{} {original}", "unexpected error:".red())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Unmatched for T
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn unmatched(&self) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"[{}] "{}""#, self.rule(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UnmatchedWithColumn for T
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"{column} [{}] "{}""#, self.rule(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Matcher {
|
||||
line_index: LineIndex,
|
||||
source: SourceText,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
fn from_file(db: &Db, file: File) -> Self {
|
||||
Self {
|
||||
line_index: line_index(db, file),
|
||||
source: source_text(db, file),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check a slice of [`Diagnostic`]s against a slice of [`Assertion`]s.
|
||||
///
|
||||
/// Return vector of [`Unmatched`] for any unmatched diagnostics or assertions.
|
||||
fn match_line<'a, 'b, T: Diagnostic + 'a>(
|
||||
&self,
|
||||
diagnostics: &'a [T],
|
||||
assertions: &'a [Assertion<'b>],
|
||||
) -> Result<(), Vec<String>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut failures = vec![];
|
||||
let mut unmatched: Vec<_> = diagnostics.iter().collect();
|
||||
for assertion in assertions {
|
||||
if matches!(
|
||||
assertion,
|
||||
Assertion::Error(ErrorAssertion {
|
||||
rule: None,
|
||||
message_contains: None,
|
||||
..
|
||||
})
|
||||
) {
|
||||
failures.push(format!(
|
||||
"{} no rule or message text",
|
||||
"invalid assertion:".red()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if !self.matches(assertion, &mut unmatched) {
|
||||
failures.push(assertion.unmatched());
|
||||
}
|
||||
}
|
||||
for diagnostic in unmatched {
|
||||
failures.push(diagnostic.unmatched_with_column(self.column(diagnostic)));
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
fn column<T: Ranged>(&self, ranged: &T) -> OneIndexed {
|
||||
self.line_index
|
||||
.source_location(ranged.start(), &self.source)
|
||||
.column
|
||||
}
|
||||
|
||||
/// Check if `assertion` matches any [`Diagnostic`]s in `unmatched`.
|
||||
///
|
||||
/// If so, return `true` and remove the matched diagnostics from `unmatched`. Otherwise, return
|
||||
/// `false`.
|
||||
///
|
||||
/// An `Error` assertion can only match one diagnostic; even if it could match more than one,
|
||||
/// we short-circuit after the first match.
|
||||
///
|
||||
/// A `Revealed` assertion must match a revealed-type diagnostic, and may also match an
|
||||
/// undefined-reveal diagnostic, if present.
|
||||
fn matches<T: Diagnostic>(&self, assertion: &Assertion, unmatched: &mut Vec<&T>) -> bool {
|
||||
match assertion {
|
||||
Assertion::Error(error) => {
|
||||
let position = unmatched.iter().position(|diagnostic| {
|
||||
!error.rule.is_some_and(|rule| rule != diagnostic.rule())
|
||||
&& !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
&& !error
|
||||
.message_contains
|
||||
.is_some_and(|needle| !diagnostic.message().contains(needle))
|
||||
});
|
||||
if let Some(position) = position {
|
||||
unmatched.swap_remove(position);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Assertion::Revealed(expected_type) => {
|
||||
let mut matched_revealed_type = None;
|
||||
let mut matched_undefined_reveal = None;
|
||||
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
|
||||
for (index, diagnostic) in unmatched.iter().enumerate() {
|
||||
if matched_revealed_type.is_none()
|
||||
&& diagnostic.rule() == "revealed-type"
|
||||
&& diagnostic.message() == expected_reveal_type_message
|
||||
{
|
||||
matched_revealed_type = Some(index);
|
||||
} else if matched_undefined_reveal.is_none()
|
||||
&& diagnostic.rule() == "undefined-reveal"
|
||||
{
|
||||
matched_undefined_reveal = Some(index);
|
||||
}
|
||||
if matched_revealed_type.is_some() && matched_undefined_reveal.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut idx = 0;
|
||||
unmatched.retain(|_| {
|
||||
let retain =
|
||||
Some(idx) != matched_revealed_type && Some(idx) != matched_undefined_reveal;
|
||||
idx += 1;
|
||||
retain
|
||||
});
|
||||
matched_revealed_type.is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl TestDiagnostic {
|
||||
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
|
||||
let offset: u32 = offset.try_into().unwrap();
|
||||
Self {
|
||||
rule,
|
||||
message,
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TestDiagnostic {
|
||||
fn range(&self) -> ruff_text_size::TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
fn get_result(source: &str, diagnostics: Vec<TestDiagnostic>) -> Result<(), FailuresByLine> {
|
||||
colored::control::set_override(false);
|
||||
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
|
||||
super::match_file(&db, file, diagnostics)
|
||||
}
|
||||
|
||||
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {
|
||||
let Err(failures) = result else {
|
||||
panic!("expected a failure");
|
||||
};
|
||||
|
||||
let expected: Vec<(OneIndexed, Vec<String>)> = messages
|
||||
.iter()
|
||||
.map(|(idx, msgs)| {
|
||||
(
|
||||
OneIndexed::from_zero_indexed(*idx),
|
||||
msgs.iter().map(ToString::to_string).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let failures: Vec<(OneIndexed, Vec<String>)> = failures
|
||||
.iter()
|
||||
.map(|(idx, msgs)| (idx, msgs.to_vec()))
|
||||
.collect();
|
||||
|
||||
assert_eq!(failures, expected);
|
||||
}
|
||||
|
||||
fn assert_ok(result: &Result<(), FailuresByLine>) {
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_wrong_message() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("revealed-type", "Something else", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [revealed-type] "Something else""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_unmatched() {
|
||||
let result = get_result("x # revealed: Foo", vec![]);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match_with_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match_with_only_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0)],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_mismatch_with_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [revealed-type] "Revealed type is `Bar`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type_unmatched() {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type_mismatched() {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [something-else]",
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_unmatched() {
|
||||
let result = get_result("x # error: [rule]", vec![]);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: error: [rule]"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [some-rule]",
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: "contains this""#,
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_column_and_rule() {
|
||||
let result = get_result(
|
||||
"x # error: 1 [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![TestDiagnostic::new("rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: 2 [rule]",
|
||||
r#"unexpected error: 1 [rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_column_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_rule_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_column() {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_rule() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new(
|
||||
"other-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [other-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interspersed_matches_and_mismatches() {
|
||||
let source = dedent(
|
||||
r#"
|
||||
1 # error: [line-one]
|
||||
2
|
||||
3 # error: [line-three]
|
||||
4 # error: [line-four]
|
||||
5
|
||||
6: # error: [line-six]
|
||||
"#,
|
||||
);
|
||||
let two = source.find('2').unwrap();
|
||||
let three = source.find('3').unwrap();
|
||||
let five = source.find('5').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
TestDiagnostic::new("line-three", "msg", three),
|
||||
TestDiagnostic::new("line-five", "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[
|
||||
(1, &["unmatched assertion: error: [line-one]"]),
|
||||
(2, &[r#"unexpected error: [line-two] "msg""#]),
|
||||
(4, &["unmatched assertion: error: [line-four]"]),
|
||||
(5, &[r#"unexpected error: [line-five] "msg""#]),
|
||||
(6, &["unmatched assertion: error: [line-six]"]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_diagnostics_than_assertions() {
|
||||
let source = dedent(
|
||||
r#"
|
||||
1 # error: [line-one]
|
||||
2
|
||||
"#,
|
||||
);
|
||||
let one = source.find('1').unwrap();
|
||||
let two = source.find('2').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-one", "msg", one),
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [other-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line_all_same() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [one-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line_mismatch() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [other-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
TestDiagnostic::new("third-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parenthesized_expression() {
|
||||
let source = dedent(
|
||||
"
|
||||
a = b + (
|
||||
error: [undefined-reveal]
|
||||
reveal_type(5) # revealed: Literal[5]
|
||||
)
|
||||
",
|
||||
);
|
||||
let reveal = source.find("reveal_type").unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_error_assertion_not_allowed() {
|
||||
let source = "x # error:";
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_only_error_assertion_not_allowed() {
|
||||
let source = "x # error: 1";
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
}
|
||||
576
crates/red_knot_test/src/parser.rs
Normal file
576
crates/red_knot_test/src/parser.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
/// Parse the Markdown `source` as a test suite with given `title`.
|
||||
pub(crate) fn parse<'s>(title: &'s str, source: &'s str) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
let parser = Parser::new(title, source);
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
/// A parsed markdown file containing tests.
|
||||
///
|
||||
/// Borrows from the source string and filepath it was created from.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTestSuite<'s> {
|
||||
/// Header sections.
|
||||
sections: IndexVec<SectionId, Section<'s>>,
|
||||
|
||||
/// Test files embedded within the Markdown file.
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
}
|
||||
|
||||
impl<'s> MarkdownTestSuite<'s> {
|
||||
pub(crate) fn tests(&self) -> MarkdownTestIterator<'_, 's> {
|
||||
MarkdownTestIterator {
|
||||
suite: self,
|
||||
current_file_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single test inside a [`MarkdownTestSuite`].
|
||||
///
|
||||
/// A test is a single header section (or the implicit root section, if there are no Markdown
|
||||
/// headers in the file), containing one or more embedded Python files as fenced code blocks, and
|
||||
/// containing no nested header subsections.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTest<'m, 's> {
|
||||
suite: &'m MarkdownTestSuite<'s>,
|
||||
section: &'m Section<'s>,
|
||||
files: &'m [EmbeddedFile<'s>],
|
||||
}
|
||||
|
||||
impl<'m, 's> MarkdownTest<'m, 's> {
|
||||
pub(crate) fn name(&self) -> String {
|
||||
let mut name = String::new();
|
||||
let mut parent_id = self.section.parent_id;
|
||||
while let Some(next_id) = parent_id {
|
||||
let parent = &self.suite.sections[next_id];
|
||||
parent_id = parent.parent_id;
|
||||
if !name.is_empty() {
|
||||
name.insert_str(0, " - ");
|
||||
}
|
||||
name.insert_str(0, parent.title);
|
||||
}
|
||||
if !name.is_empty() {
|
||||
name.push_str(" - ");
|
||||
}
|
||||
name.push_str(self.section.title);
|
||||
name
|
||||
}
|
||||
|
||||
pub(crate) fn files(&self) -> impl Iterator<Item = &'m EmbeddedFile<'s>> {
|
||||
self.files.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator yielding all [`MarkdownTest`]s in a [`MarkdownTestSuite`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTestIterator<'m, 's> {
|
||||
suite: &'m MarkdownTestSuite<'s>,
|
||||
current_file_index: usize,
|
||||
}
|
||||
|
||||
impl<'m, 's> Iterator for MarkdownTestIterator<'m, 's> {
|
||||
type Item = MarkdownTest<'m, 's>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut current_file_index = self.current_file_index;
|
||||
let mut file = self.suite.files.get(current_file_index.into());
|
||||
let section_id = file?.section;
|
||||
while file.is_some_and(|file| file.section == section_id) {
|
||||
current_file_index += 1;
|
||||
file = self.suite.files.get(current_file_index.into());
|
||||
}
|
||||
let files = &self.suite.files[EmbeddedFileId::from_usize(self.current_file_index)
|
||||
..EmbeddedFileId::from_usize(current_file_index)];
|
||||
self.current_file_index = current_file_index;
|
||||
Some(MarkdownTest {
|
||||
suite: self.suite,
|
||||
section: &self.suite.sections[section_id],
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct SectionId;
|
||||
|
||||
/// A single header section of a [`MarkdownTestSuite`], or the implicit root "section".
|
||||
///
|
||||
/// A header section is the part of a Markdown file beginning with a `#`-prefixed header line, and
|
||||
/// extending until the next header line at the same or higher outline level (that is, with the
|
||||
/// same number or fewer `#` characters).
|
||||
///
|
||||
/// A header section may either contain one or more embedded Python files (making it a
|
||||
/// [`MarkdownTest`]), or it may contain nested sections (headers with more `#` characters), but
|
||||
/// not both.
|
||||
#[derive(Debug)]
|
||||
struct Section<'s> {
|
||||
title: &'s str,
|
||||
level: u8,
|
||||
parent_id: Option<SectionId>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct EmbeddedFileId;
|
||||
|
||||
/// A single file embedded in a [`Section`] as a fenced code block.
|
||||
///
|
||||
/// Currently must be a Python file (`py` language) or type stub (`pyi`). In the future we plan
|
||||
/// support other kinds of files as well (TOML configuration, typeshed VERSIONS, `pth` files...).
|
||||
///
|
||||
/// A Python embedded file makes its containing [`Section`] into a [`MarkdownTest`], and will be
|
||||
/// type-checked and searched for inline-comment assertions to match against the diagnostics from
|
||||
/// type checking.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EmbeddedFile<'s> {
|
||||
section: SectionId,
|
||||
pub(crate) path: &'s str,
|
||||
pub(crate) lang: &'s str,
|
||||
pub(crate) code: &'s str,
|
||||
}
|
||||
|
||||
/// Matches an arbitrary amount of whitespace (including newlines), followed by a sequence of `#`
|
||||
/// characters, followed by a title heading, followed by a newline.
|
||||
static HEADER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(\s*\n)*(?<level>#+)\s+(?<title>.+)\s*\n").unwrap());
|
||||
|
||||
/// Matches a code block fenced by triple backticks, possibly with language and `key=val`
|
||||
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
||||
static CODE_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"^```(?<lang>\w+)(?<config>( +\S+)*)\s*\n(?<code>(.|\n)*?)\n```\s*\n").unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SectionStack(Vec<SectionId>);
|
||||
|
||||
impl SectionStack {
|
||||
fn new(root_section_id: SectionId) -> Self {
|
||||
Self(vec![root_section_id])
|
||||
}
|
||||
|
||||
fn push(&mut self, section_id: SectionId) {
|
||||
self.0.push(section_id);
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<SectionId> {
|
||||
let popped = self.0.pop();
|
||||
debug_assert_ne!(popped, None, "Should never pop the implicit root section");
|
||||
debug_assert!(
|
||||
!self.0.is_empty(),
|
||||
"Should never pop the implicit root section"
|
||||
);
|
||||
popped
|
||||
}
|
||||
|
||||
fn parent(&mut self) -> SectionId {
|
||||
*self
|
||||
.0
|
||||
.last()
|
||||
.expect("Should never pop the implicit root section")
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the source of a Markdown file into a [`MarkdownTestSuite`].
|
||||
#[derive(Debug)]
|
||||
struct Parser<'s> {
|
||||
/// [`Section`]s of the final [`MarkdownTestSuite`].
|
||||
sections: IndexVec<SectionId, Section<'s>>,
|
||||
|
||||
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
unparsed: &'s str,
|
||||
|
||||
/// Stack of ancestor sections.
|
||||
stack: SectionStack,
|
||||
|
||||
/// Names of embedded files in current active section.
|
||||
current_section_files: Option<FxHashSet<&'s str>>,
|
||||
}
|
||||
|
||||
impl<'s> Parser<'s> {
|
||||
fn new(title: &'s str, source: &'s str) -> Self {
|
||||
let mut sections = IndexVec::default();
|
||||
let root_section_id = sections.push(Section {
|
||||
title,
|
||||
level: 0,
|
||||
parent_id: None,
|
||||
});
|
||||
Self {
|
||||
sections,
|
||||
files: IndexVec::default(),
|
||||
unparsed: source,
|
||||
stack: SectionStack::new(root_section_id),
|
||||
current_section_files: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(mut self) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
self.parse_impl()?;
|
||||
Ok(self.finish())
|
||||
}
|
||||
|
||||
fn finish(mut self) -> MarkdownTestSuite<'s> {
|
||||
self.sections.shrink_to_fit();
|
||||
self.files.shrink_to_fit();
|
||||
|
||||
MarkdownTestSuite {
|
||||
sections: self.sections,
|
||||
files: self.files,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_impl(&mut self) -> anyhow::Result<()> {
|
||||
while !self.unparsed.is_empty() {
|
||||
if let Some(captures) = self.scan(&HEADER_RE) {
|
||||
self.parse_header(&captures)?;
|
||||
} else if let Some(captures) = self.scan(&CODE_RE) {
|
||||
self.parse_code_block(&captures)?;
|
||||
} else {
|
||||
// ignore other Markdown syntax (paragraphs, etc) used as comments in the test
|
||||
if let Some(next_newline) = self.unparsed.find('\n') {
|
||||
(_, self.unparsed) = self.unparsed.split_at(next_newline + 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_header(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
let header_level = captures["level"].len();
|
||||
self.pop_sections_to_level(header_level);
|
||||
|
||||
let parent = self.stack.parent();
|
||||
|
||||
let section = Section {
|
||||
// HEADER_RE can't match without a match for group 'title'.
|
||||
title: captures.name("title").unwrap().into(),
|
||||
level: header_level.try_into()?,
|
||||
parent_id: Some(parent),
|
||||
};
|
||||
|
||||
if self.current_section_files.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Header '{}' not valid inside a test case; parent '{}' has code files.",
|
||||
section.title,
|
||||
self.sections[parent].title,
|
||||
));
|
||||
}
|
||||
|
||||
let section_id = self.sections.push(section);
|
||||
self.stack.push(section_id);
|
||||
|
||||
self.current_section_files = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_code_block(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
// We never pop the implicit root section.
|
||||
let parent = self.stack.parent();
|
||||
|
||||
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
|
||||
|
||||
if let Some(config_match) = captures.name("config") {
|
||||
for item in config_match.as_str().split_whitespace() {
|
||||
let mut parts = item.split('=');
|
||||
let key = parts.next().unwrap();
|
||||
let Some(val) = parts.next() else {
|
||||
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
|
||||
};
|
||||
if parts.next().is_some() {
|
||||
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
|
||||
}
|
||||
if config.insert(key, val).is_some() {
|
||||
return Err(anyhow::anyhow!("Duplicate config item `{}`.", item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = config.get("path").copied().unwrap_or("test.py");
|
||||
|
||||
self.files.push(EmbeddedFile {
|
||||
path,
|
||||
section: parent,
|
||||
// CODE_RE can't match without matches for 'lang' and 'code'.
|
||||
lang: captures.name("lang").unwrap().into(),
|
||||
code: captures.name("code").unwrap().into(),
|
||||
});
|
||||
|
||||
if let Some(current_files) = &mut self.current_section_files {
|
||||
if !current_files.insert(path) {
|
||||
if path == "test.py" {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`. \
|
||||
(This is the default filename; \
|
||||
consider giving some files an explicit name with `path=...`.)",
|
||||
self.sections[parent].title
|
||||
));
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`.",
|
||||
self.sections[parent].title
|
||||
));
|
||||
};
|
||||
} else {
|
||||
self.current_section_files = Some(FxHashSet::from_iter([path]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pop_sections_to_level(&mut self, level: usize) {
|
||||
while level <= self.sections[self.stack.parent()].level.into() {
|
||||
self.stack.pop();
|
||||
// We would have errored before pushing a child section if there were files, so we know
|
||||
// no parent section can have files.
|
||||
self.current_section_files = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get capture groups and advance cursor past match if unparsed text matches `pattern`.
|
||||
fn scan(&mut self, pattern: &Regex) -> Option<Captures<'s>> {
|
||||
if let Some(captures) = pattern.captures(self.unparsed) {
|
||||
let (_, unparsed) = self.unparsed.split_at(captures.get(0).unwrap().end());
|
||||
self.unparsed = unparsed;
|
||||
Some(captures)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let mf = super::parse("file.md", "").unwrap();
|
||||
|
||||
assert!(mf.tests().next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_file_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
|
||||
assert_eq!(test.name(), "file.md");
|
||||
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_tests() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
# Two
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test1, test2] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected two tests");
|
||||
};
|
||||
|
||||
assert_eq!(test1.name(), "file.md - One");
|
||||
assert_eq!(test2.name(), "file.md - Two");
|
||||
|
||||
let [file] = test1.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
|
||||
let [file] = test2.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "y = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_file_path() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_line_file() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.code, "x = 1\ny = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_header_inside_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
## Two
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Header 'Two' not valid inside a test case; parent 'One' has code files."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_no_equals() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Invalid config item `foo`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_too_many_equals() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar=baz
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Invalid config item `foo=bar=baz`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_duplicate() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar foo=baz
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Duplicate config item `foo=baz`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_name_files_in_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `file.md` has duplicate files named `test.py`. \
|
||||
(This is the default filename; consider giving some files an explicit name \
|
||||
with `path=...`.)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_name_files_in_test_non_default() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py path=foo.py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `file.md` has duplicate files named `foo.py`."
|
||||
);
|
||||
}
|
||||
}
|
||||
32
crates/red_knot_vendored/Cargo.toml
Normal file
32
crates/red_knot_vendored/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "red_knot_vendored"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
path-slash = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true, features = ["zstd", "deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[features]
|
||||
zstd = ["zip/zstd"]
|
||||
deflate = ["zip/deflate"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Red Knot
|
||||
# Vendored types for the stdlib
|
||||
|
||||
Semantic analysis for the red-knot project.
|
||||
|
||||
## Vendored types for the stdlib
|
||||
|
||||
This crate vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/red_knot_python_semantic/vendor/typeshed`. The file `crates/red_knot_python_semantic/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to.
|
||||
This crate vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/red_knot_vendored/vendor/typeshed`. The file `crates/red_knot_vendored/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to.
|
||||
|
||||
The typeshed stubs are updated every two weeks via an automated PR using the `sync_typeshed.yaml` workflow in the `.github/workflows` directory. This workflow can also be triggered at any time via [workflow dispatch](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow#running-a-workflow).
|
||||
89
crates/red_knot_vendored/build.rs
Normal file
89
crates/red_knot_vendored/build.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! Build script to package our vendored typeshed files
|
||||
//! into a zip archive that can be included in the Ruff binary.
|
||||
//!
|
||||
//! This script should be automatically run at build time
|
||||
//! whenever the script itself changes, or whenever any files
|
||||
//! in `crates/red_knot_vendored/vendor/typeshed` change.
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
use path_slash::PathExt;
|
||||
use zip::result::ZipResult;
|
||||
use zip::write::{FileOptions, ZipWriter};
|
||||
use zip::CompressionMethod;
|
||||
|
||||
const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed";
|
||||
const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
|
||||
|
||||
/// Recursively zip the contents of an entire directory.
|
||||
///
|
||||
/// This routine is adapted from a recipe at
|
||||
/// <https://github.com/zip-rs/zip-old/blob/5d0f198124946b7be4e5969719a7f29f363118cd/examples/write_dir.rs>
|
||||
fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
let mut zip = ZipWriter::new(writer);
|
||||
|
||||
// Use deflated compression for WASM builds because compiling `zstd-sys` requires clang
|
||||
// [source](https://github.com/gyscos/zstd-rs/wiki/Compile-for-WASM) which complicates the build
|
||||
// by a lot. Deflated compression is slower but it shouldn't matter much for the WASM use case
|
||||
// (WASM itself is already slower than a native build for a specific platform).
|
||||
// We can't use `#[cfg(...)]` here because the target-arch in a build script is the
|
||||
// architecture of the system running the build script and not the architecture of the build-target.
|
||||
// That's why we use the `TARGET` environment variable here.
|
||||
let method = if cfg!(feature = "zstd") {
|
||||
CompressionMethod::Zstd
|
||||
} else if cfg!(feature = "deflate") {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Stored
|
||||
};
|
||||
|
||||
let options = FileOptions::default()
|
||||
.compression_method(method)
|
||||
.unix_permissions(0o644);
|
||||
|
||||
for entry in walkdir::WalkDir::new(directory_path) {
|
||||
let dir_entry = entry.unwrap();
|
||||
let absolute_path = dir_entry.path();
|
||||
let normalized_relative_path = absolute_path
|
||||
.strip_prefix(Path::new(directory_path))
|
||||
.unwrap()
|
||||
.to_slash()
|
||||
.expect("Unexpected non-utf8 typeshed path!");
|
||||
|
||||
// Write file or directory explicitly
|
||||
// Some unzip tools unzip files with directory paths correctly, some do not!
|
||||
if absolute_path.is_file() {
|
||||
println!("adding file {absolute_path:?} as {normalized_relative_path:?} ...");
|
||||
zip.start_file(normalized_relative_path, options)?;
|
||||
let mut f = File::open(absolute_path)?;
|
||||
std::io::copy(&mut f, &mut zip).unwrap();
|
||||
} else if !normalized_relative_path.is_empty() {
|
||||
// Only if not root! Avoids path spec / warning
|
||||
// and mapname conversion failed error on unzip
|
||||
println!("adding dir {absolute_path:?} as {normalized_relative_path:?} ...");
|
||||
zip.add_directory(normalized_relative_path, options)?;
|
||||
}
|
||||
}
|
||||
zip.finish()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed={TYPESHED_SOURCE_DIR}");
|
||||
assert!(
|
||||
Path::new(TYPESHED_SOURCE_DIR).is_dir(),
|
||||
"Where is typeshed?"
|
||||
);
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
|
||||
// N.B. Deliberately using `format!()` instead of `Path::join()` here,
|
||||
// so that we use `/` as a path separator on all platforms.
|
||||
// That enables us to load the typeshed zip at compile time in `module.rs`
|
||||
// (otherwise we'd have to dynamically determine the exact path to the typeshed zip
|
||||
// based on the default path separator for the specific platform we're on,
|
||||
// which can't be done at compile time.)
|
||||
let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}");
|
||||
|
||||
let zipped_typeshed = File::create(zipped_typeshed_location).unwrap();
|
||||
zip_dir(TYPESHED_SOURCE_DIR, zipped_typeshed).unwrap();
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use ruff_db::vendored::VendoredFileSystem;
|
||||
// Luckily this crate will fail to build if this file isn't available at build time.
|
||||
static TYPESHED_ZIP_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip"));
|
||||
|
||||
pub fn vendored_typeshed_stubs() -> &'static VendoredFileSystem {
|
||||
pub fn file_system() -> &'static VendoredFileSystem {
|
||||
static VENDORED_TYPESHED_STUBS: Lazy<VendoredFileSystem> =
|
||||
Lazy::new(|| VendoredFileSystem::new_static(TYPESHED_ZIP_BYTES).unwrap());
|
||||
&VENDORED_TYPESHED_STUBS
|
||||
@@ -42,7 +42,7 @@ mod tests {
|
||||
#[test]
|
||||
fn typeshed_vfs_consistent_with_vendored_stubs() {
|
||||
let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap();
|
||||
let vendored_typeshed_stubs = vendored_typeshed_stubs();
|
||||
let vendored_typeshed_stubs = file_system();
|
||||
|
||||
let mut empty_iterator = true;
|
||||
for entry in walkdir::WalkDir::new(&vendored_typeshed_dir).min_depth(1) {
|
||||
1
crates/red_knot_vendored/vendor/typeshed/source_commit.txt
vendored
Normal file
1
crates/red_knot_vendored/vendor/typeshed/source_commit.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
91a58b07cdd807b1d965e04ba85af2adab8bf924
|
||||
@@ -161,6 +161,8 @@ importlib.metadata._meta: 3.10-
|
||||
importlib.metadata.diagnose: 3.13-
|
||||
importlib.readers: 3.10-
|
||||
importlib.resources: 3.7-
|
||||
importlib.resources._common: 3.11-
|
||||
importlib.resources._functional: 3.13-
|
||||
importlib.resources.abc: 3.11-
|
||||
importlib.resources.readers: 3.11-
|
||||
importlib.resources.simple: 3.11-
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user