Compare commits
157 Commits
0.5.5
...
charlie/wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23bf395e2 | ||
|
|
383676e332 | ||
|
|
feba5031dc | ||
|
|
0c2b88f224 | ||
|
|
cf1a57df5a | ||
|
|
597c5f9124 | ||
|
|
69e1c567d4 | ||
|
|
37b9bac403 | ||
|
|
83db48d316 | ||
|
|
c4e651921b | ||
|
|
b595346213 | ||
|
|
253474b312 | ||
|
|
a176679b24 | ||
|
|
1f51048fa4 | ||
|
|
2abfab0f9b | ||
|
|
64f1f3468d | ||
|
|
ffaa35eafe | ||
|
|
c906b0183b | ||
|
|
bc5b9b81dd | ||
|
|
221ea662e0 | ||
|
|
d28c5afd14 | ||
|
|
f1de08c2a0 | ||
|
|
33e9a6a54e | ||
|
|
f577e03021 | ||
|
|
f53733525c | ||
|
|
2daa914334 | ||
|
|
6d9205e346 | ||
|
|
df7345e118 | ||
|
|
dc6aafecc2 | ||
|
|
5107a50ae7 | ||
|
|
a631d600ac | ||
|
|
f34b9a77f0 | ||
|
|
7997da47f5 | ||
|
|
d380b37a09 | ||
|
|
b14fee9320 | ||
|
|
037e817450 | ||
|
|
7fcfedd430 | ||
|
|
50ff5c7544 | ||
|
|
90e5bc2bd9 | ||
|
|
aae9619d3d | ||
|
|
7fa76a2b2b | ||
|
|
14dd6d980e | ||
|
|
846f57fd15 | ||
|
|
8e6aa78796 | ||
|
|
e91a0fe94a | ||
|
|
d2c627efb3 | ||
|
|
10e977d5f5 | ||
|
|
f0318ff889 | ||
|
|
5cc3fed9a8 | ||
|
|
39dd732e27 | ||
|
|
52630a1d55 | ||
|
|
7b5fd63ce8 | ||
|
|
5499821c67 | ||
|
|
7ee7c68f36 | ||
|
|
2393d19f91 | ||
|
|
a8e2ba508e | ||
|
|
0b4d3ce39b | ||
|
|
0a345dc627 | ||
|
|
ff2aa3ea00 | ||
|
|
0d3bad877d | ||
|
|
756060d676 | ||
|
|
b647f3fba8 | ||
|
|
82e69ebf23 | ||
|
|
2c79045342 | ||
|
|
3497f5257b | ||
|
|
25aabec814 | ||
|
|
0e71485ea9 | ||
|
|
43a9d282f7 | ||
|
|
6f357b8b45 | ||
|
|
73d9f11a9c | ||
|
|
d6c6db5a44 | ||
|
|
56d985a972 | ||
|
|
b3e0655cc9 | ||
|
|
06baffec9e | ||
|
|
67a2ae800a | ||
|
|
7a2c75e2fc | ||
|
|
9ee44637ca | ||
|
|
733341ab39 | ||
|
|
341a25eec1 | ||
|
|
38e178e914 | ||
|
|
daccb3f4f3 | ||
|
|
c858afe03a | ||
|
|
3c1c3199d0 | ||
|
|
fbfe2cb2f5 | ||
|
|
1c311e4fdb | ||
|
|
12177a42e3 | ||
|
|
dfb08856eb | ||
|
|
94d817e1a5 | ||
|
|
9296bd4e3f | ||
|
|
da824ba316 | ||
|
|
012198a1b0 | ||
|
|
fbab04fbe1 | ||
|
|
9aa43d5f91 | ||
|
|
966563c79b | ||
|
|
27edadec29 | ||
|
|
2e2b1b460f | ||
|
|
a3e67abf4c | ||
|
|
ee0518e8f7 | ||
|
|
d774a3bd48 | ||
|
|
7e6b19048e | ||
|
|
8e383b9587 | ||
|
|
3f49ab126f | ||
|
|
c1bc7f4dee | ||
|
|
a44d579f21 | ||
|
|
a3900d2b0b | ||
|
|
83b1c48a93 | ||
|
|
138e70bd5c | ||
|
|
ee103ffb25 | ||
|
|
18f87b9497 | ||
|
|
adc8d4e1e7 | ||
|
|
90db361199 | ||
|
|
4738135801 | ||
|
|
264cd750e9 | ||
|
|
7a4419a2a5 | ||
|
|
ac1666d6e2 | ||
|
|
459c85ba27 | ||
|
|
aaa56eb0bd | ||
|
|
f3c14a4276 | ||
|
|
3169d408fa | ||
|
|
a2286c8e47 | ||
|
|
fb9f566f56 | ||
|
|
381bd1ff4a | ||
|
|
2f54d05d97 | ||
|
|
e18b4e42d3 | ||
|
|
9495331a5f | ||
|
|
e1076db7d0 | ||
|
|
1986c9e8e2 | ||
|
|
d7e80dc955 | ||
|
|
87d09f77cd | ||
|
|
bd37ef13b8 | ||
|
|
ec23c974db | ||
|
|
122e5ab428 | ||
|
|
2f2149aca8 | ||
|
|
9d5c31e7da | ||
|
|
25f3ad6238 | ||
|
|
79926329a4 | ||
|
|
9cdc578dd9 | ||
|
|
665c75f7ab | ||
|
|
f37b39d6cc | ||
|
|
e18c45c310 | ||
|
|
d930052de8 | ||
|
|
7ad4df9e9f | ||
|
|
425761e960 | ||
|
|
4b69271809 | ||
|
|
bf23d38a21 | ||
|
|
49f51583fa | ||
|
|
1fe4a5faed | ||
|
|
998bfe0847 | ||
|
|
6f4db8675b | ||
|
|
71f7aa4971 | ||
|
|
9f72f474e6 | ||
|
|
10c993e21a | ||
|
|
2d3914296d | ||
|
|
7571da8778 | ||
|
|
2ceac5f868 | ||
|
|
5ce80827d2 | ||
|
|
e047b9685a |
13
.github/renovate.json5
vendored
13
.github/renovate.json5
vendored
@@ -8,7 +8,7 @@
|
||||
semanticCommits: "disabled",
|
||||
separateMajorMinor: false,
|
||||
prHourlyLimit: 10,
|
||||
enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "npm"],
|
||||
enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "pip_requirements", "npm"],
|
||||
cargo: {
|
||||
// See https://docs.renovatebot.com/configuration-options/#rangestrategy
|
||||
rangeStrategy: "update-lockfile",
|
||||
@@ -16,6 +16,9 @@
|
||||
pep621: {
|
||||
fileMatch: ["^(python|scripts)/.*pyproject\\.toml$"],
|
||||
},
|
||||
pip_requirements: {
|
||||
fileMatch: ["^docs/requirements.*\\.txt$"],
|
||||
},
|
||||
npm: {
|
||||
fileMatch: ["^playground/.*package\\.json$"],
|
||||
},
|
||||
@@ -48,6 +51,14 @@
|
||||
matchManagers: ["cargo"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// `mkdocs-material` requires a manual update to keep the version in sync
|
||||
// with `mkdocs-material-insider`.
|
||||
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
|
||||
matchManagers: ["pip_requirements"],
|
||||
matchPackagePatterns: ["mkdocs-material"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
groupName: "pre-commit dependencies",
|
||||
matchManagers: ["pre-commit"],
|
||||
|
||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
- name: "Clippy"
|
||||
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
- name: "Clippy (wasm)"
|
||||
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
|
||||
run: cargo clippy -p ruff_wasm -p red_knot_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
|
||||
|
||||
cargo-test-linux:
|
||||
name: "cargo test (linux)"
|
||||
@@ -191,10 +191,14 @@ jobs:
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run wasm-pack"
|
||||
- name: "Test ruff_wasm"
|
||||
run: |
|
||||
cd crates/ruff_wasm
|
||||
wasm-pack test --node
|
||||
- name: "Test red_knot_wasm"
|
||||
run: |
|
||||
cd crates/red_knot_wasm
|
||||
wasm-pack test --node
|
||||
|
||||
cargo-build-release:
|
||||
name: "cargo build (release)"
|
||||
@@ -619,7 +623,7 @@ jobs:
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@v2
|
||||
uses: CodSpeedHQ/action@v3
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
2
.github/workflows/pr-comment.yaml
vendored
2
.github/workflows/pr-comment.yaml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
name: pr-number
|
||||
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Parse pull request number
|
||||
id: pr-number
|
||||
@@ -43,6 +44,7 @@ jobs:
|
||||
path: pr/ecosystem
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -104,8 +104,8 @@ jobs:
|
||||
run: |
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
git config user.name "astral-docs-bot"
|
||||
git config user.email "176161322+astral-docs-bot@users.noreply.github.com"
|
||||
|
||||
git checkout -b $branch_name
|
||||
git add site/ruff
|
||||
|
||||
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_module_resolver/vendor/typeshed
|
||||
mkdir ruff/crates/red_knot_module_resolver/vendor/typeshed
|
||||
cp typeshed/README.md ruff/crates/red_knot_module_resolver/vendor/typeshed
|
||||
cp typeshed/LICENSE ruff/crates/red_knot_module_resolver/vendor/typeshed
|
||||
cp -r typeshed/stdlib ruff/crates/red_knot_module_resolver/vendor/typeshed/stdlib
|
||||
rm -rf ruff/crates/red_knot_module_resolver/vendor/typeshed/stdlib/@tests
|
||||
git -C typeshed rev-parse HEAD > ruff/crates/red_knot_module_resolver/vendor/typeshed/source_commit.txt
|
||||
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
|
||||
- name: Commit the changes
|
||||
id: commit
|
||||
if: ${{ steps.sync.outcome == 'success' }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -21,6 +21,14 @@ flamegraph.svg
|
||||
# `CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib`
|
||||
/target*
|
||||
|
||||
# samply profiles
|
||||
profile.json
|
||||
|
||||
# tracing-flame traces
|
||||
tracing.folded
|
||||
tracing-flamechart.svg
|
||||
tracing-flamegraph.svg
|
||||
|
||||
###
|
||||
# Rust.gitignore
|
||||
###
|
||||
|
||||
@@ -2,7 +2,8 @@ fail_fast: true
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
crates/red_knot_module_resolver/vendor/.*|
|
||||
crates/red_knot_python_semantic/vendor/.*|
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
crates/ruff/resources/.*|
|
||||
@@ -42,7 +43,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.23.2
|
||||
rev: v1.23.6
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -56,18 +57,13 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.4
|
||||
rev: v0.5.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
exclude: |
|
||||
(?x)^(
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_python_formatter/resources/.*
|
||||
)$
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
|
||||
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,5 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.7
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-comprehensions`\] Account for list and set comprehensions in `unnecessary-literal-within-tuple-call` (`C409`) ([#12657](https://github.com/astral-sh/ruff/pull/12657))
|
||||
- \[`flake8-pyi`\] Add autofix for `future-annotations-in-stub` (`PYI044`) ([#12676](https://github.com/astral-sh/ruff/pull/12676))
|
||||
- \[`flake8-return`\] Avoid syntax error when auto-fixing `RET505` with mixed indentation (space and tabs) ([#12740](https://github.com/astral-sh/ruff/pull/12740))
|
||||
- \[`pydoclint`\] Add `docstring-missing-yields` (`DOC402`) and `docstring-extraneous-yields` (`DOC403`) ([#12538](https://github.com/astral-sh/ruff/pull/12538))
|
||||
- \[`pydoclint`\] Avoid `DOC201` if docstring begins with "Return", "Returns", "Yield", or "Yields" ([#12675](https://github.com/astral-sh/ruff/pull/12675))
|
||||
- \[`pydoclint`\] Deduplicate collected exceptions after traversing function bodies (`DOC501`) ([#12642](https://github.com/astral-sh/ruff/pull/12642))
|
||||
- \[`pydoclint`\] Ignore `DOC` errors for stub functions ([#12651](https://github.com/astral-sh/ruff/pull/12651))
|
||||
- \[`pydoclint`\] Teach rules to understand reraised exceptions as being explicitly raised (`DOC501`, `DOC502`) ([#12639](https://github.com/astral-sh/ruff/pull/12639))
|
||||
- \[`ruff`\] Implement `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#12480](https://github.com/astral-sh/ruff/pull/12480))
|
||||
- \[`ruff`\] Mark `RUF023` fix as unsafe if `__slots__` is not a set and the binding is used elsewhere ([#12692](https://github.com/astral-sh/ruff/pull/12692))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`refurb`\] Add autofix for `implicit-cwd` (`FURB177`) ([#12708](https://github.com/astral-sh/ruff/pull/12708))
|
||||
- \[`ruff`\] Add autofix for `zip-instead-of-pairwise` (`RUF007`) ([#12663](https://github.com/astral-sh/ruff/pull/12663))
|
||||
- \[`tryceratops`\] Add `BaseException` to `raise-vanilla-class` rule (`TRY002`) ([#12620](https://github.com/astral-sh/ruff/pull/12620))
|
||||
|
||||
### Server
|
||||
|
||||
- Ignore non-file workspace URL; Ruff will display a warning notification in this case ([#12725](https://github.com/astral-sh/ruff/pull/12725))
|
||||
|
||||
### CLI
|
||||
|
||||
- Fix cache invalidation for nested `pyproject.toml` files ([#12727](https://github.com/astral-sh/ruff/pull/12727))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-async`\] Fix false positives with multiple `async with` items (`ASYNC100`) ([#12643](https://github.com/astral-sh/ruff/pull/12643))
|
||||
- \[`flake8-bandit`\] Avoid false-positives for list concatenations in SQL construction (`S608`) ([#12720](https://github.com/astral-sh/ruff/pull/12720))
|
||||
- \[`flake8-bugbear`\] Treat `return` as equivalent to `break` (`B909`) ([#12646](https://github.com/astral-sh/ruff/pull/12646))
|
||||
- \[`flake8-comprehensions`\] Set comprehensions not a violation for `sum` in `unnecessary-comprehension-in-call` (`C419`) ([#12691](https://github.com/astral-sh/ruff/pull/12691))
|
||||
- \[`flake8-simplify`\] Parenthesize conditions based on precedence when merging if arms (`SIM114`) ([#12737](https://github.com/astral-sh/ruff/pull/12737))
|
||||
- \[`pydoclint`\] Try both 'Raises' section styles when convention is unspecified (`DOC501`) ([#12649](https://github.com/astral-sh/ruff/pull/12649))
|
||||
|
||||
## 0.5.6
|
||||
|
||||
Ruff 0.5.6 automatically enables linting and formatting of notebooks in *preview mode*.
|
||||
You can opt-out of this behavior by adding `*.ipynb` to the `extend-exclude` setting.
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
### Preview features
|
||||
|
||||
- Enable notebooks by default in preview mode ([#12621](https://github.com/astral-sh/ruff/pull/12621))
|
||||
- \[`flake8-builtins`\] Implement import, lambda, and module shadowing ([#12546](https://github.com/astral-sh/ruff/pull/12546))
|
||||
- \[`pydoclint`\] Add `docstring-missing-returns` (`DOC201`) and `docstring-extraneous-returns` (`DOC202`) ([#12485](https://github.com/astral-sh/ruff/pull/12485))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-return`\] Exempt cached properties and other property-like decorators from explicit return rule (`RET501`) ([#12563](https://github.com/astral-sh/ruff/pull/12563))
|
||||
|
||||
### Server
|
||||
|
||||
- Make server panic hook more error resilient ([#12610](https://github.com/astral-sh/ruff/pull/12610))
|
||||
- Use `$/logTrace` for server trace logs in Zed and VS Code ([#12564](https://github.com/astral-sh/ruff/pull/12564))
|
||||
- Keep track of deleted cells for reorder change request ([#12575](https://github.com/astral-sh/ruff/pull/12575))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-implicit-str-concat`\] Always allow explicit multi-line concatenations when implicit concatenations are banned ([#12532](https://github.com/astral-sh/ruff/pull/12532))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-async`\] Avoid flagging `asyncio.timeout`s as unused when the context manager includes `asyncio.TaskGroup` ([#12605](https://github.com/astral-sh/ruff/pull/12605))
|
||||
- \[`flake8-slots`\] Avoid recommending `__slots__` for classes that inherit from more than `namedtuple` ([#12531](https://github.com/astral-sh/ruff/pull/12531))
|
||||
- \[`isort`\] Avoid marking required imports as unused ([#12537](https://github.com/astral-sh/ruff/pull/12537))
|
||||
- \[`isort`\] Preserve trailing inline comments on import-from statements ([#12498](https://github.com/astral-sh/ruff/pull/12498))
|
||||
- \[`pycodestyle`\] Add newlines before comments (`E305`) ([#12606](https://github.com/astral-sh/ruff/pull/12606))
|
||||
- \[`pycodestyle`\] Don't attach comments with mismatched indents ([#12604](https://github.com/astral-sh/ruff/pull/12604))
|
||||
- \[`pyflakes`\] Fix preview-mode bugs in `F401` when attempting to autofix unused first-party submodule imports in an `__init__.py` file ([#12569](https://github.com/astral-sh/ruff/pull/12569))
|
||||
- \[`pylint`\] Respect start index in `unnecessary-list-index-lookup` ([#12603](https://github.com/astral-sh/ruff/pull/12603))
|
||||
- \[`pyupgrade`\] Avoid recommending no-argument super in `slots=True` dataclasses ([#12530](https://github.com/astral-sh/ruff/pull/12530))
|
||||
- \[`pyupgrade`\] Use colon rather than dot formatting for integer-only types ([#12534](https://github.com/astral-sh/ruff/pull/12534))
|
||||
- Fix NFKC normalization bug when removing unused imports ([#12571](https://github.com/astral-sh/ruff/pull/12571))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Consider more stdlib decorators to be property-like ([#12583](https://github.com/astral-sh/ruff/pull/12583))
|
||||
- Improve handling of metaclasses in various linter rules ([#12579](https://github.com/astral-sh/ruff/pull/12579))
|
||||
- Improve consistency between linter rules in determining whether a function is property ([#12581](https://github.com/astral-sh/ruff/pull/12581))
|
||||
|
||||
## 0.5.5
|
||||
|
||||
### Preview features
|
||||
|
||||
244
Cargo.lock
generated
244
Cargo.lock
generated
@@ -133,6 +133,12 @@ version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d9f7083455f1a474276ccd32374958d2cb591024aac45101c7623b10271347"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
@@ -141,9 +147,9 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c5c8e418080ef8aa932039d12eda7b6f5043baf48f1523c166fbc32d004534"
|
||||
checksum = "0a1cc0ba69de57db40674c66f7cf2caee3981ddef084388482c95c0e2133e5e8"
|
||||
dependencies = [
|
||||
"fs-err",
|
||||
"os_str_bytes",
|
||||
@@ -190,9 +196,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.9.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
|
||||
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.6",
|
||||
@@ -314,9 +320,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.9"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
|
||||
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -324,9 +330,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.9"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
|
||||
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -367,9 +373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.8"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -759,9 +765,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.3"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
|
||||
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -930,9 +936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.8.4"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
@@ -1021,9 +1027,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imara-diff"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af13c8ceb376860ff0c6a66d83a8cdd4ecd9e464da24621bbffcd02b49619434"
|
||||
checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashbrown",
|
||||
@@ -1031,9 +1037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imperative"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b70798296d538cdaa6d652941fcc795963f8b9878b9e300c9fab7a522bd2fc0"
|
||||
checksum = "29a1f6526af721f9aec9ceed7ab8ebfca47f3399d08b80056c2acca3fcb694a9"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"rust-stemmers",
|
||||
@@ -1041,9 +1047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@@ -1194,9 +1200,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.31"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
|
||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -1476,11 +1482,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.0"
|
||||
version = "0.50.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14"
|
||||
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1518,18 +1524,18 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5a8e22be64dfa1123429350872e7be33594dbf5ae5212c90c5890e71966d1d"
|
||||
checksum = "8c81974681ab4f0cc9fe49cad56f821d1cc67a08cd2caa9b5d58b0adaa5dd36d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.6.1"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||
checksum = "7ac44c994af577c799b1b4bd80dc214701e349873ad894d6cdf96f4f7526e0b9"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -1640,9 +1646,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pep440_rs"
|
||||
version = "0.6.0"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca0a570e7ec9171250cac57614e901f62408094b54b3798bb920d3cf0d4a0e09"
|
||||
checksum = "466eada3179c2e069ca897b99006cbb33f816290eaeec62464eea907e22ae385"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -1854,36 +1860,46 @@ name = "red_knot"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"countme",
|
||||
"crossbeam",
|
||||
"ctrlc",
|
||||
"notify",
|
||||
"filetime",
|
||||
"rayon",
|
||||
"red_knot_module_resolver",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_server",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"tracing-flame",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_module_resolver"
|
||||
name = "red_knot_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.6.0",
|
||||
"camino",
|
||||
"compact_str",
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"insta",
|
||||
"once_cell",
|
||||
"ordermap",
|
||||
"path-slash",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
@@ -1893,22 +1909,61 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_python_semantic"
|
||||
name = "red_knot_server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.6.0",
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"ordermap",
|
||||
"red_knot_module_resolver",
|
||||
"crossbeam",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_linter",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_wasm"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"js-sys",
|
||||
"log",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_workspace"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
"notify",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -1934,9 +1989,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.5"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -1993,7 +2048,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.5"
|
||||
version = "0.5.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2051,7 +2106,8 @@ dependencies = [
|
||||
"criterion",
|
||||
"mimalloc",
|
||||
"once_cell",
|
||||
"red_knot",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_linter",
|
||||
"ruff_python_ast",
|
||||
@@ -2088,6 +2144,8 @@ dependencies = [
|
||||
"filetime",
|
||||
"ignore",
|
||||
"insta",
|
||||
"matchit",
|
||||
"path-slash",
|
||||
"ruff_cache",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -2099,6 +2157,9 @@ dependencies = [
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
"web-time",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -2178,7 +2239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.5.5"
|
||||
version = "0.5.7"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2202,7 +2263,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
"pep440_rs 0.6.0",
|
||||
"pep440_rs 0.6.6",
|
||||
"pyproject-toml",
|
||||
"quick-junit",
|
||||
"regex",
|
||||
@@ -2232,6 +2293,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
"unicode-width",
|
||||
"unicode_names2",
|
||||
"url",
|
||||
@@ -2401,13 +2463,17 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2493,7 +2559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.5.5"
|
||||
version = "0.5.7"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -2532,7 +2598,7 @@ dependencies = [
|
||||
"matchit",
|
||||
"path-absolutize",
|
||||
"path-slash",
|
||||
"pep440_rs 0.6.0",
|
||||
"pep440_rs 0.6.6",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_formatter",
|
||||
@@ -2540,6 +2606,7 @@ dependencies = [
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_semantic",
|
||||
"ruff_source_file",
|
||||
"rustc-hash 2.0.0",
|
||||
"schemars",
|
||||
@@ -2632,25 +2699,34 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a1bf3a613f451af7fc0a59411c56abc47fe8e8e1#a1bf3a613f451af7fc0a59411c56abc47fe8e8e1"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?tag=red-knot-0.0.1#ece083e15b79f155f9e4368ec1318cec9a08d88b"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
"crossbeam",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap 6.0.1",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"log",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?tag=red-knot-0.0.1#ece083e15b79f155f9e4368ec1318cec9a08d88b"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a1bf3a613f451af7fc0a59411c56abc47fe8e8e1#a1bf3a613f451af7fc0a59411c56abc47fe8e8e1"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?tag=red-knot-0.0.1#ece083e15b79f155f9e4368ec1318cec9a08d88b"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -2752,11 +2828,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.120"
|
||||
version = "1.0.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -2774,9 +2851,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -2934,12 +3011,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -3077,9 +3155,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.15"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -3089,18 +3167,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.16"
|
||||
version = "0.22.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -3142,6 +3220,17 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-flame"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bae117ee14789185e129aaee5d93750abe67fdc5a9a62650452bfe4e122a3a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.6"
|
||||
@@ -3189,7 +3278,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f459ca79f1b0d5f71c54ddfde6debfc59c8b6eeb46808ae492077f739dc7b49c"
|
||||
dependencies = [
|
||||
"nu-ansi-term 0.50.0",
|
||||
"nu-ansi-term 0.50.1",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
@@ -3523,6 +3612,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.1"
|
||||
@@ -3734,9 +3833,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.6"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -3797,6 +3896,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
rust-version = "1.76"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -35,9 +35,9 @@ ruff_source_file = { path = "crates/ruff_source_file" }
|
||||
ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot = { path = "crates/red_knot" }
|
||||
red_knot_module_resolver = { path = "crates/red_knot_module_resolver" }
|
||||
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" }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
@@ -108,7 +108,7 @@ rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a1bf3a613f451af7fc0a59411c56abc47fe8e8e1" }
|
||||
salsa = { git = "https://github.com/MichaReiser/salsa.git", tag = "red-knot-0.0.1" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -131,8 +131,9 @@ thiserror = { version = "1.0.58" }
|
||||
tikv-jemallocator = { version = "0.6.0" }
|
||||
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", features = ["env-filter"] }
|
||||
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" }
|
||||
@@ -152,11 +153,12 @@ walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.92" }
|
||||
wasm-bindgen-test = { version = "0.3.42" }
|
||||
wild = { version = "2" }
|
||||
zip = { version = "0.6.6", default-features = false, features = ["zstd"] }
|
||||
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)"] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -2 }
|
||||
|
||||
16
README.md
16
README.md
@@ -29,14 +29,14 @@ An extremely fast Python linter and code formatter, written in Rust.
|
||||
- 🐍 Installable via `pip`
|
||||
- 🛠️ `pyproject.toml` support
|
||||
- 🤝 Python 3.13 compatibility
|
||||
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8), isort, and Black
|
||||
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8), isort, and [Black](https://docs.astral.sh/ruff/faq/#how-does-ruffs-formatter-compare-to-black)
|
||||
- 📦 Built-in caching, to avoid re-analyzing unchanged files
|
||||
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
|
||||
- 📏 Over [800 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
|
||||
of popular Flake8 plugins, like flake8-bugbear
|
||||
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/integrations/) for
|
||||
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
|
||||
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#pyprojecttoml-discovery)
|
||||
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup)
|
||||
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#config-file-discovery)
|
||||
|
||||
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
|
||||
functionality behind a single, common interface.
|
||||
@@ -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.5.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.5.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.7/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.5.5
|
||||
rev: v0.5.7
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -179,8 +179,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
- id: ruff-format
|
||||
```
|
||||
|
||||
Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or
|
||||
alongside any other editor through the [Ruff LSP](https://github.com/astral-sh/ruff-lsp).
|
||||
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):
|
||||
@@ -424,6 +423,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [Dagger](https://github.com/dagger/dagger)
|
||||
- [Dagster](https://github.com/dagster-io/dagster)
|
||||
- Databricks ([MLflow](https://github.com/mlflow/mlflow))
|
||||
- [Dify](https://github.com/langgenius/dify)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Godot](https://github.com/godotengine/godot)
|
||||
- [Gradio](https://github.com/gradio-app/gradio)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[files]
|
||||
# https://github.com/crate-ci/typos/issues/868
|
||||
extend-exclude = ["crates/red_knot_module_resolver/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
|
||||
extend-exclude = ["crates/red_knot_python_semantic/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
|
||||
|
||||
[default.extend-words]
|
||||
"arange" = "arange" # e.g. `numpy.arange`
|
||||
|
||||
@@ -10,4 +10,12 @@ doc-valid-idents = [
|
||||
"SCREAMING_SNAKE_CASE",
|
||||
"SQLAlchemy",
|
||||
"StackOverflow",
|
||||
"PyCharm",
|
||||
]
|
||||
|
||||
ignore-interior-mutability = [
|
||||
# Interned is read-only. The wrapped `Rc` never gets updated.
|
||||
"ruff_formatter::format_element::Interned",
|
||||
# The expression is read-only.
|
||||
"ruff_python_ast::hashable::HashableExpr",
|
||||
]
|
||||
|
||||
@@ -12,28 +12,29 @@ license.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
red_knot_module_resolver = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
red_knot_server = { workspace = true }
|
||||
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
notify = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing = { workspace = true, features = ["release_max_level_debug"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||
tracing-flame = { workspace = true }
|
||||
tracing-tree = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
filetime = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
BIN
crates/red_knot/docs/tracing-flamegraph.png
Normal file
BIN
crates/red_knot/docs/tracing-flamegraph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
123
crates/red_knot/docs/tracing.md
Normal file
123
crates/red_knot/docs/tracing.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Tracing
|
||||
|
||||
Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a particular thing.
|
||||
Note, tracing messages with severity `debug` or greater are user-facing. They should be phrased accordingly.
|
||||
Tracing spans are only shown when using `-vvv`.
|
||||
|
||||
## Verbosity levels
|
||||
|
||||
The CLI supports different verbosity levels.
|
||||
|
||||
- default: Only show errors and warnings.
|
||||
- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you which packages are fresh.
|
||||
- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change.
|
||||
- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating.
|
||||
|
||||
## `RED_KNOT_LOG`
|
||||
|
||||
By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown.
|
||||
The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one
|
||||
or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives).
|
||||
|
||||
### Examples
|
||||
|
||||
#### Show all debug messages
|
||||
|
||||
Shows debug messages from all crates.
|
||||
|
||||
```bash
|
||||
RED_KNOT_LOG=debug
|
||||
```
|
||||
|
||||
#### Show salsa query execution messages
|
||||
|
||||
Show the salsa `execute: my_query` messages in addition to all red knot messages.
|
||||
|
||||
```bash
|
||||
RED_KNOT_LOG=ruff=trace,red_knot=trace,salsa=info
|
||||
```
|
||||
|
||||
#### Show typing traces
|
||||
|
||||
Only show traces for the `red_knot_python_semantic::types` module.
|
||||
|
||||
```bash
|
||||
RED_KNOT_LOG="red_knot_python_semantic::types"
|
||||
```
|
||||
|
||||
Note: Ensure that you use `-vvv` to see tracing spans.
|
||||
|
||||
#### Show messages for a single file
|
||||
|
||||
Shows all messages that are inside of a span for a specific file.
|
||||
|
||||
```bash
|
||||
RED_KNOT_LOG=red_knot[{file=/home/micha/astral/test/x.py}]=trace
|
||||
```
|
||||
|
||||
**Note**: Tracing still shows all spans because tracing can't know at the time of entering the span
|
||||
whether one if its children has the file `x.py`.
|
||||
|
||||
**Note**: Salsa currently logs the entire memoized values. In our case, the source text and parsed AST.
|
||||
This very quickly leads to extremely long outputs.
|
||||
|
||||
## Tracing and Salsa
|
||||
|
||||
Be mindful about using `tracing` in Salsa queries, especially when using `warn` or `error` because it isn't guaranteed
|
||||
that the query will execute after restoring from a persistent cache. In which case the user won't see the message.
|
||||
|
||||
For example, don't use `tracing` to show the user a message when generating a lint violation failed
|
||||
because the message would only be shown when linting the file the first time, but not on subsequent analysis
|
||||
runs or when restoring from a persistent cache. This can be confusing for users because they
|
||||
don't understand why a specific lint violation isn't raised. Instead, change your
|
||||
query to return the failure as part of the query's result or use a Salsa accumulator.
|
||||
|
||||
## Tracing in tests
|
||||
|
||||
You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in tests.
|
||||
|
||||
```rust
|
||||
use ruff_db::testing::setup_logging;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let _logging = setup_logging();
|
||||
|
||||
tracing::info!("This message will be printed to stderr");
|
||||
}
|
||||
```
|
||||
|
||||
Note: Most test runners capture stderr and only show its output when a test fails.
|
||||
|
||||
Note also that `setup_logging` only sets up logging for the current thread because [`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be
|
||||
called **once**.
|
||||
|
||||
## Release builds
|
||||
|
||||
`trace!` events are removed in release builds.
|
||||
|
||||
## Profiling
|
||||
|
||||
Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`.
|
||||
|
||||
```bash
|
||||
RED_KNOT_LOG_PROFILE=1 red_knot -- --current-directory=../test -vvv
|
||||
```
|
||||
|
||||
You can convert the textual representation into a visual one using `inferno`.
|
||||
|
||||
```shell
|
||||
cargo install inferno
|
||||
```
|
||||
|
||||
```shell
|
||||
# flamegraph
|
||||
cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg
|
||||
|
||||
# flamechart
|
||||
cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg
|
||||
```
|
||||
|
||||

|
||||
|
||||
See [`tracing-flame`](https://crates.io/crates/tracing-flame) for more details.
|
||||
@@ -1,2 +0,0 @@
|
||||
pub(crate) mod target_version;
|
||||
pub(crate) mod verbosity;
|
||||
@@ -1,34 +0,0 @@
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
#[default]
|
||||
Py38,
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
ruff_db::program::TargetVersion::from(*self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for ruff_db::program::TargetVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::Py37,
|
||||
TargetVersion::Py38 => Self::Py38,
|
||||
TargetVersion::Py39 => Self::Py39,
|
||||
TargetVersion::Py310 => Self::Py310,
|
||||
TargetVersion::Py311 => Self::Py311,
|
||||
TargetVersion::Py312 => Self::Py312,
|
||||
TargetVersion::Py313 => Self::Py313,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub(crate) enum VerbosityLevel {
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
/// Logging flags to `#[command(flatten)]` into your CLI
|
||||
#[derive(clap::Args, Debug, Clone, Default)]
|
||||
#[command(about = None, long_about = None)]
|
||||
pub(crate) struct Verbosity {
|
||||
#[arg(
|
||||
long,
|
||||
short = 'v',
|
||||
help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)",
|
||||
action = clap::ArgAction::Count,
|
||||
global = true,
|
||||
)]
|
||||
verbose: u8,
|
||||
}
|
||||
|
||||
impl Verbosity {
|
||||
/// Returns the verbosity level based on the number of `-v` flags.
|
||||
///
|
||||
/// Returns `None` if the user did not specify any verbosity flags.
|
||||
pub(crate) fn level(&self) -> Option<VerbosityLevel> {
|
||||
match self.verbose {
|
||||
0 => None,
|
||||
1 => Some(VerbosityLevel::Info),
|
||||
2 => Some(VerbosityLevel::Debug),
|
||||
_ => Some(VerbosityLevel::Trace),
|
||||
}
|
||||
}
|
||||
}
|
||||
254
crates/red_knot/src/logging.rs
Normal file
254
crates/red_knot/src/logging.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
//! Sets up logging for Red Knot
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use tracing::log::LevelFilter;
|
||||
use tracing::{Event, Subscriber};
|
||||
use tracing_subscriber::fmt::format::Writer;
|
||||
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// Logging flags to `#[command(flatten)]` into your CLI
|
||||
#[derive(clap::Args, Debug, Clone, Default)]
|
||||
#[command(about = None, long_about = None)]
|
||||
pub(crate) struct Verbosity {
|
||||
#[arg(
|
||||
long,
|
||||
short = 'v',
|
||||
help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)",
|
||||
action = clap::ArgAction::Count,
|
||||
global = true,
|
||||
)]
|
||||
verbose: u8,
|
||||
}
|
||||
|
||||
impl Verbosity {
|
||||
/// Returns the verbosity level based on the number of `-v` flags.
|
||||
///
|
||||
/// Returns `None` if the user did not specify any verbosity flags.
|
||||
pub(crate) fn level(&self) -> VerbosityLevel {
|
||||
match self.verbose {
|
||||
0 => VerbosityLevel::Default,
|
||||
1 => VerbosityLevel::Verbose,
|
||||
2 => VerbosityLevel::ExtraVerbose,
|
||||
_ => VerbosityLevel::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub(crate) enum VerbosityLevel {
|
||||
/// Default output level. Only shows Ruff and Red Knot events up to the [`WARN`](tracing::Level::WARN).
|
||||
Default,
|
||||
|
||||
/// Enables verbose output. Emits Ruff and Red Knot events up to the [`INFO`](tracing::Level::INFO).
|
||||
/// Corresponds to `-v`.
|
||||
Verbose,
|
||||
|
||||
/// Enables a more verbose tracing format and emits Ruff and Red Knot events up to [`DEBUG`](tracing::Level::DEBUG).
|
||||
/// Corresponds to `-vv`
|
||||
ExtraVerbose,
|
||||
|
||||
/// Enables all tracing events and uses a tree-like output format. Corresponds to `-vvv`.
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl VerbosityLevel {
|
||||
const fn level_filter(self) -> LevelFilter {
|
||||
match self {
|
||||
VerbosityLevel::Default => LevelFilter::Warn,
|
||||
VerbosityLevel::Verbose => LevelFilter::Info,
|
||||
VerbosityLevel::ExtraVerbose => LevelFilter::Debug,
|
||||
VerbosityLevel::Trace => LevelFilter::Trace,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_trace(self) -> bool {
|
||||
matches!(self, VerbosityLevel::Trace)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_extra_verbose(self) -> bool {
|
||||
matches!(self, VerbosityLevel::ExtraVerbose)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result<TracingGuard> {
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
// The `RED_KNOT_LOG` environment variable overrides the default log level.
|
||||
let filter = if let Ok(log_env_variable) = std::env::var("RED_KNOT_LOG") {
|
||||
EnvFilter::builder()
|
||||
.parse(log_env_variable)
|
||||
.context("Failed to parse directives specified in RED_KNOT_LOG environment variable.")?
|
||||
} else {
|
||||
match level {
|
||||
VerbosityLevel::Default => {
|
||||
// Show warning traces
|
||||
EnvFilter::default().add_directive(tracing::level_filters::LevelFilter::WARN.into())
|
||||
}
|
||||
level => {
|
||||
let level_filter = level.level_filter();
|
||||
|
||||
// Show info|debug|trace events, but allow `RED_KNOT_LOG` to override
|
||||
let filter = EnvFilter::default().add_directive(
|
||||
format!("red_knot={level_filter}")
|
||||
.parse()
|
||||
.expect("Hardcoded directive to be valid"),
|
||||
);
|
||||
|
||||
filter.add_directive(
|
||||
format!("ruff={level_filter}")
|
||||
.parse()
|
||||
.expect("Hardcoded directive to be valid"),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (profiling_layer, guard) = setup_profile();
|
||||
|
||||
let registry = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(profiling_layer);
|
||||
|
||||
if level.is_trace() {
|
||||
let subscriber = registry.with(
|
||||
tracing_tree::HierarchicalLayer::default()
|
||||
.with_indent_lines(true)
|
||||
.with_indent_amount(2)
|
||||
.with_bracketed_fields(true)
|
||||
.with_thread_ids(true)
|
||||
.with_targets(true)
|
||||
.with_writer(std::io::stderr)
|
||||
.with_timer(tracing_tree::time::Uptime::default()),
|
||||
);
|
||||
|
||||
subscriber.init();
|
||||
} else {
|
||||
let subscriber = registry.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.event_format(RedKnotFormat {
|
||||
display_level: true,
|
||||
display_timestamp: level.is_extra_verbose(),
|
||||
show_spans: false,
|
||||
})
|
||||
.with_writer(std::io::stderr),
|
||||
);
|
||||
|
||||
subscriber.init();
|
||||
}
|
||||
|
||||
Ok(TracingGuard {
|
||||
_flame_guard: guard,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn setup_profile<S>() -> (
|
||||
Option<tracing_flame::FlameLayer<S, BufWriter<File>>>,
|
||||
Option<tracing_flame::FlushGuard<BufWriter<File>>>,
|
||||
)
|
||||
where
|
||||
S: Subscriber + for<'span> LookupSpan<'span>,
|
||||
{
|
||||
if let Ok("1" | "true") = std::env::var("RED_KNOT_LOG_PROFILE").as_deref() {
|
||||
let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded")
|
||||
.expect("Flame layer to be created");
|
||||
(Some(layer), Some(guard))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TracingGuard {
|
||||
_flame_guard: Option<tracing_flame::FlushGuard<BufWriter<File>>>,
|
||||
}
|
||||
|
||||
struct RedKnotFormat {
|
||||
display_timestamp: bool,
|
||||
display_level: bool,
|
||||
show_spans: bool,
|
||||
}
|
||||
|
||||
/// See <https://docs.rs/tracing-subscriber/0.3.18/src/tracing_subscriber/fmt/format/mod.rs.html#1026-1156>
|
||||
impl<S, N> FormatEvent<S, N> for RedKnotFormat
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &FmtContext<'_, S, N>,
|
||||
mut writer: Writer<'_>,
|
||||
event: &Event<'_>,
|
||||
) -> fmt::Result {
|
||||
let meta = event.metadata();
|
||||
let ansi = writer.has_ansi_escapes();
|
||||
|
||||
if self.display_timestamp {
|
||||
let timestamp = chrono::Local::now()
|
||||
.format("%Y-%m-%d %H:%M:%S.%f")
|
||||
.to_string();
|
||||
if ansi {
|
||||
write!(writer, "{} ", timestamp.dimmed())?;
|
||||
} else {
|
||||
write!(
|
||||
writer,
|
||||
"{} ",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f")
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.display_level {
|
||||
let level = meta.level();
|
||||
// Same colors as tracing
|
||||
if ansi {
|
||||
let formatted_level = level.to_string();
|
||||
match *level {
|
||||
tracing::Level::TRACE => {
|
||||
write!(writer, "{} ", formatted_level.purple().bold())?;
|
||||
}
|
||||
tracing::Level::DEBUG => write!(writer, "{} ", formatted_level.blue().bold())?,
|
||||
tracing::Level::INFO => write!(writer, "{} ", formatted_level.green().bold())?,
|
||||
tracing::Level::WARN => write!(writer, "{} ", formatted_level.yellow().bold())?,
|
||||
tracing::Level::ERROR => write!(writer, "{} ", level.to_string().red().bold())?,
|
||||
}
|
||||
} else {
|
||||
write!(writer, "{level} ")?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_spans {
|
||||
let span = event.parent();
|
||||
let mut seen = false;
|
||||
|
||||
let span = span
|
||||
.and_then(|id| ctx.span(id))
|
||||
.or_else(|| ctx.lookup_current());
|
||||
|
||||
let scope = span.into_iter().flat_map(|span| span.scope().from_root());
|
||||
|
||||
for span in scope {
|
||||
seen = true;
|
||||
if ansi {
|
||||
write!(writer, "{}:", span.metadata().name().bold())?;
|
||||
} else {
|
||||
write!(writer, "{}:", span.metadata().name())?;
|
||||
}
|
||||
}
|
||||
|
||||
if seen {
|
||||
writer.write_char(' ')?;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.field_format().format_fields(writer.by_ref(), event)?;
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,39 @@
|
||||
use std::process::{ExitCode, Termination};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::ParallelDatabase;
|
||||
use tracing::subscriber::Interest;
|
||||
use tracing::{Level, Metadata};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::layer::{Context, Filter, SubscriberExt};
|
||||
use tracing_subscriber::{Layer, Registry};
|
||||
use tracing_tree::time::Uptime;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot::db::RootDatabase;
|
||||
use red_knot::watch;
|
||||
use red_knot::watch::WorkspaceWatcher;
|
||||
use red_knot::workspace::WorkspaceMetadata;
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings};
|
||||
use ruff_db::system::{OsSystem, System, SystemPathBuf};
|
||||
use red_knot_python_semantic::{ProgramSettings, SearchPathSettings};
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::site_packages::VirtualEnvironment;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use target_version::TargetVersion;
|
||||
|
||||
use cli::target_version::TargetVersion;
|
||||
use cli::verbosity::{Verbosity, VerbosityLevel};
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
|
||||
mod cli;
|
||||
mod logging;
|
||||
mod target_version;
|
||||
mod verbosity;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
name = "red-knot",
|
||||
about = "An experimental multifile analysis backend for Ruff"
|
||||
about = "An extremely fast Python type checker."
|
||||
)]
|
||||
#[command(version)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Option<Command>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Changes the current working directory.",
|
||||
@@ -38,6 +42,17 @@ struct Args {
|
||||
)]
|
||||
current_directory: Option<SystemPathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Path to the virtual environment the project uses",
|
||||
long_help = "\
|
||||
Path to the virtual environment the project uses. \
|
||||
If provided, red-knot will use the `site-packages` directory of this virtual environment \
|
||||
to resolve type information for the project's third-party dependencies.",
|
||||
value_name = "PATH"
|
||||
)]
|
||||
venv_path: Option<SystemPathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIRECTORY",
|
||||
@@ -52,7 +67,12 @@ struct Args {
|
||||
)]
|
||||
extra_search_path: Vec<SystemPathBuf>,
|
||||
|
||||
#[arg(long, help = "Python version to assume when resolving types", default_value_t = TargetVersion::default(), value_name="VERSION")]
|
||||
#[arg(
|
||||
long,
|
||||
help = "Python version to assume when resolving types",
|
||||
default_value_t = TargetVersion::default(),
|
||||
value_name="VERSION")
|
||||
]
|
||||
target_version: TargetVersion,
|
||||
|
||||
#[clap(flatten)]
|
||||
@@ -66,46 +86,99 @@ struct Args {
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::print_stdout,
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::print_stderr,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum Command {
|
||||
/// Start the language server
|
||||
Server,
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
|
||||
pub fn main() -> ExitStatus {
|
||||
run().unwrap_or_else(|error| {
|
||||
use std::io::Write;
|
||||
|
||||
// Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
|
||||
// This communicates that this isn't a linter error but Red Knot itself hard-errored for
|
||||
// some reason (e.g. failed to resolve the configuration)
|
||||
writeln!(stderr, "{}", "Red Knot failed".red().bold()).ok();
|
||||
// Currently we generally only see one error, but e.g. with io errors when resolving
|
||||
// the configuration it is help to chain errors ("resolving configuration failed" ->
|
||||
// "failed to read file: subdir/pyproject.toml")
|
||||
for cause in error.chain() {
|
||||
writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
|
||||
}
|
||||
|
||||
ExitStatus::Error
|
||||
})
|
||||
}
|
||||
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let Args {
|
||||
command,
|
||||
current_directory,
|
||||
custom_typeshed_dir,
|
||||
extra_search_path: extra_paths,
|
||||
venv_path,
|
||||
target_version,
|
||||
verbosity,
|
||||
watch,
|
||||
} = Args::parse_from(std::env::args().collect::<Vec<_>>());
|
||||
|
||||
let verbosity = verbosity.level();
|
||||
countme::enable(verbosity == Some(VerbosityLevel::Trace));
|
||||
setup_tracing(verbosity);
|
||||
if matches!(command, Some(Command::Server)) {
|
||||
return run_server().map(|()| ExitStatus::Success);
|
||||
}
|
||||
|
||||
let cwd = if let Some(cwd) = current_directory {
|
||||
let canonicalized = cwd.as_utf8_path().canonicalize_utf8().unwrap();
|
||||
SystemPathBuf::from_utf8_path_buf(canonicalized)
|
||||
} else {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
SystemPathBuf::from_path_buf(cwd).unwrap()
|
||||
let verbosity = verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cli_base_path = {
|
||||
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
|
||||
SystemPathBuf::from_path_buf(cwd)
|
||||
.map_err(|path| {
|
||||
anyhow!(
|
||||
"The current working directory '{}' contains non-unicode characters. Red Knot only supports unicode paths.",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let cwd = current_directory
|
||||
.map(|cwd| {
|
||||
if cwd.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(&cwd, &cli_base_path))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided current-directory path '{cwd}' is not a directory."
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cli_base_path.clone());
|
||||
|
||||
let system = OsSystem::new(cwd.clone());
|
||||
let workspace_metadata =
|
||||
WorkspaceMetadata::from_path(system.current_directory(), &system).unwrap();
|
||||
let workspace_metadata = WorkspaceMetadata::from_path(system.current_directory(), &system)?;
|
||||
|
||||
// TODO: Verify the remaining search path settings eagerly.
|
||||
let site_packages = venv_path
|
||||
.map(|path| {
|
||||
VirtualEnvironment::new(path, &OsSystem::new(cli_base_path))
|
||||
.and_then(|venv| venv.site_packages_directories(&system))
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
// TODO: Respect the settings from the workspace metadata. when resolving the program settings.
|
||||
let program_settings = ProgramSettings {
|
||||
target_version: target_version.into(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths,
|
||||
workspace_root: workspace_metadata.root().to_path_buf(),
|
||||
src_root: workspace_metadata.root().to_path_buf(),
|
||||
custom_typeshed: custom_typeshed_dir,
|
||||
site_packages: None,
|
||||
site_packages,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -113,7 +186,7 @@ pub fn main() -> anyhow::Result<()> {
|
||||
// cache and load the cache if it exists.
|
||||
let mut db = RootDatabase::new(workspace_metadata, program_settings, system);
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(verbosity);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new();
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -125,13 +198,35 @@ pub fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
})?;
|
||||
|
||||
if watch {
|
||||
main_loop.watch(&mut db)?;
|
||||
let exit_status = if watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db);
|
||||
}
|
||||
main_loop.run(&mut db)
|
||||
};
|
||||
|
||||
Ok(())
|
||||
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
|
||||
|
||||
std::mem::forget(db);
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ExitStatus {
|
||||
/// Checking was successful and there were no errors.
|
||||
Success = 0,
|
||||
|
||||
/// Checking was successful but there were errors.
|
||||
Failure = 1,
|
||||
|
||||
/// Checking failed.
|
||||
Error = 2,
|
||||
}
|
||||
|
||||
impl Termination for ExitStatus {
|
||||
fn report(self) -> ExitCode {
|
||||
ExitCode::from(self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
struct MainLoop {
|
||||
@@ -143,12 +238,10 @@ struct MainLoop {
|
||||
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<WorkspaceWatcher>,
|
||||
|
||||
verbosity: Option<VerbosityLevel>,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(verbosity: Option<VerbosityLevel>) -> (Self, MainLoopCancellationToken) {
|
||||
fn new() -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
@@ -156,33 +249,42 @@ impl MainLoop {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
verbosity,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
}
|
||||
|
||||
fn watch(mut self, db: &mut RootDatabase) -> anyhow::Result<()> {
|
||||
fn watch(mut self, db: &mut RootDatabase) -> anyhow::Result<ExitStatus> {
|
||||
tracing::debug!("Starting watch mode");
|
||||
let sender = self.sender.clone();
|
||||
let watcher = watch::directory_watcher(move |event| {
|
||||
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
|
||||
})?;
|
||||
|
||||
self.watcher = Some(WorkspaceWatcher::new(watcher, db));
|
||||
|
||||
self.run(db);
|
||||
|
||||
Ok(())
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
fn run(mut self, db: &mut RootDatabase) {
|
||||
// Schedule the first check.
|
||||
fn run(mut self, db: &mut RootDatabase) -> ExitStatus {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
let mut revision = 0usize;
|
||||
|
||||
let result = self.main_loop(db);
|
||||
|
||||
tracing::debug!("Exiting main loop");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn main_loop(&mut self, db: &mut RootDatabase) -> ExitStatus {
|
||||
// Schedule the first check.
|
||||
tracing::debug!("Starting main loop");
|
||||
|
||||
let mut revision = 0u64;
|
||||
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
tracing::trace!("Main Loop: Tick");
|
||||
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.snapshot();
|
||||
@@ -195,7 +297,7 @@ impl MainLoop {
|
||||
// Send the result back to the main loop for printing.
|
||||
sender
|
||||
.send(MainLoopMessage::CheckCompleted { result, revision })
|
||||
.ok();
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -204,17 +306,26 @@ impl MainLoop {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let has_diagnostics = !result.is_empty();
|
||||
if check_revision == revision {
|
||||
eprintln!("{}", result.join("\n"));
|
||||
|
||||
if self.verbosity == Some(VerbosityLevel::Trace) {
|
||||
eprintln!("{}", countme::get_all());
|
||||
for diagnostic in result {
|
||||
tracing::error!("{}", diagnostic);
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return self.exit();
|
||||
return if has_diagnostics {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
};
|
||||
}
|
||||
|
||||
tracing::trace!("Counts after last check:\n{}", countme::get_all());
|
||||
}
|
||||
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
@@ -227,18 +338,18 @@ impl MainLoop {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
}
|
||||
MainLoopMessage::Exit => {
|
||||
return self.exit();
|
||||
// Cancel any pending queries and wait for them to complete.
|
||||
// TODO: Don't use Salsa internal APIs
|
||||
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
|
||||
let _ = db.zalsa_mut();
|
||||
return ExitStatus::Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stderr, clippy::unused_self)]
|
||||
fn exit(self) {
|
||||
if self.verbosity == Some(VerbosityLevel::Trace) {
|
||||
eprintln!("Exit");
|
||||
eprintln!("{}", countme::get_all());
|
||||
tracing::debug!("Waiting for next main loop message.");
|
||||
}
|
||||
|
||||
ExitStatus::Success
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,67 +368,7 @@ impl MainLoopCancellationToken {
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
result: Vec<String>,
|
||||
revision: usize,
|
||||
},
|
||||
CheckCompleted { result: Vec<String>, revision: u64 },
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn setup_tracing(verbosity: Option<VerbosityLevel>) {
|
||||
let trace_level = match verbosity {
|
||||
None => Level::WARN,
|
||||
Some(VerbosityLevel::Info) => Level::INFO,
|
||||
Some(VerbosityLevel::Debug) => Level::DEBUG,
|
||||
Some(VerbosityLevel::Trace) => Level::TRACE,
|
||||
};
|
||||
|
||||
let subscriber = Registry::default().with(
|
||||
tracing_tree::HierarchicalLayer::default()
|
||||
.with_indent_lines(true)
|
||||
.with_indent_amount(2)
|
||||
.with_bracketed_fields(true)
|
||||
.with_thread_ids(true)
|
||||
.with_targets(true)
|
||||
.with_writer(|| Box::new(std::io::stderr()))
|
||||
.with_timer(Uptime::default())
|
||||
.with_filter(LoggingFilter { trace_level }),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
}
|
||||
|
||||
struct LoggingFilter {
|
||||
trace_level: Level,
|
||||
}
|
||||
|
||||
impl LoggingFilter {
|
||||
fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
|
||||
let filter = if meta.target().starts_with("red_knot") || meta.target().starts_with("ruff") {
|
||||
self.trace_level
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
|
||||
meta.level() <= &filter
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Filter<S> for LoggingFilter {
|
||||
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
|
||||
self.is_enabled(meta)
|
||||
}
|
||||
|
||||
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
|
||||
if self.is_enabled(meta) {
|
||||
Interest::always()
|
||||
} else {
|
||||
Interest::never()
|
||||
}
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<LevelFilter> {
|
||||
Some(LevelFilter::from_level(self.trace_level))
|
||||
}
|
||||
}
|
||||
|
||||
48
crates/red_knot/src/target_version.rs
Normal file
48
crates/red_knot/src/target_version.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
#[default]
|
||||
Py38,
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl TargetVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::PY37,
|
||||
TargetVersion::Py38 => Self::PY38,
|
||||
TargetVersion::Py39 => Self::PY39,
|
||||
TargetVersion::Py310 => Self::PY310,
|
||||
TargetVersion::Py311 => Self::PY311,
|
||||
TargetVersion::Py312 => Self::PY312,
|
||||
TargetVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
1
crates/red_knot/src/verbosity.rs
Normal file
1
crates/red_knot/src/verbosity.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
use crate::db::RootDatabase;
|
||||
use crate::watch::Watcher;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use tracing::info;
|
||||
|
||||
/// Wrapper around a [`Watcher`] that watches the relevant paths of a workspace.
|
||||
pub struct WorkspaceWatcher {
|
||||
watcher: Watcher,
|
||||
|
||||
/// The paths that need to be watched. This includes paths for which setting up file watching failed.
|
||||
watched_paths: FxHashSet<SystemPathBuf>,
|
||||
|
||||
/// Paths that should be watched but setting up the watcher failed for some reason.
|
||||
/// This should be rare.
|
||||
errored_paths: Vec<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl WorkspaceWatcher {
|
||||
/// Create a new workspace watcher.
|
||||
pub fn new(watcher: Watcher, db: &RootDatabase) -> Self {
|
||||
let mut watcher = Self {
|
||||
watcher,
|
||||
watched_paths: FxHashSet::default(),
|
||||
errored_paths: Vec::new(),
|
||||
};
|
||||
|
||||
watcher.update(db);
|
||||
|
||||
watcher
|
||||
}
|
||||
|
||||
pub fn update(&mut self, db: &RootDatabase) {
|
||||
let new_watch_paths = db.workspace().paths_to_watch(db);
|
||||
|
||||
let mut added_folders = new_watch_paths.difference(&self.watched_paths).peekable();
|
||||
let mut removed_folders = self.watched_paths.difference(&new_watch_paths).peekable();
|
||||
|
||||
if added_folders.peek().is_none() && removed_folders.peek().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for added_folder in added_folders {
|
||||
// Log a warning. It's not worth aborting if registering a single folder fails because
|
||||
// Ruff otherwise stills works as expected.
|
||||
if let Err(error) = self.watcher.watch(added_folder) {
|
||||
// TODO: Log a user-facing warning.
|
||||
tracing::warn!("Failed to setup watcher for path '{added_folder}': {error}. You have to restart Ruff after making changes to files under this path or you might see stale results.");
|
||||
self.errored_paths.push(added_folder.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for removed_path in removed_folders {
|
||||
if let Some(index) = self
|
||||
.errored_paths
|
||||
.iter()
|
||||
.position(|path| path == removed_path)
|
||||
{
|
||||
self.errored_paths.swap_remove(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(error) = self.watcher.unwatch(removed_path) {
|
||||
info!("Failed to remove the file watcher for the path '{removed_path}: {error}.");
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Set up file watchers for {}",
|
||||
DisplayWatchedPaths {
|
||||
paths: &new_watch_paths
|
||||
}
|
||||
);
|
||||
|
||||
self.watched_paths = new_watch_paths;
|
||||
}
|
||||
|
||||
/// Returns `true` if setting up watching for any path failed.
|
||||
pub fn has_errored_paths(&self) -> bool {
|
||||
!self.errored_paths.is_empty()
|
||||
}
|
||||
|
||||
pub fn flush(&self) {
|
||||
self.watcher.flush();
|
||||
}
|
||||
|
||||
pub fn stop(self) {
|
||||
self.watcher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayWatchedPaths<'a> {
|
||||
paths: &'a FxHashSet<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisplayWatchedPaths<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_char('[')?;
|
||||
|
||||
let mut iter = self.paths.iter();
|
||||
if let Some(first) = iter.next() {
|
||||
write!(f, "\"{first}\"")?;
|
||||
|
||||
for path in iter {
|
||||
write!(f, ", \"{path}\"")?;
|
||||
}
|
||||
}
|
||||
|
||||
f.write_char(']')
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use salsa::Setter;
|
||||
|
||||
use red_knot::db::RootDatabase;
|
||||
use red_knot::watch;
|
||||
use red_knot::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot::workspace::WorkspaceMetadata;
|
||||
use red_knot_module_resolver::{resolve_module, ModuleName};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::program::{Program, ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use red_knot_python_semantic::{
|
||||
resolve_module, ModuleName, Program, ProgramSettings, PythonVersion, SearchPathSettings,
|
||||
};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::Upcast;
|
||||
@@ -19,7 +22,10 @@ struct TestCase {
|
||||
db: RootDatabase,
|
||||
watcher: Option<WorkspaceWatcher>,
|
||||
changes_receiver: crossbeam::channel::Receiver<Vec<watch::ChangeEvent>>,
|
||||
temp_dir: tempfile::TempDir,
|
||||
/// The temporary directory that contains the test files.
|
||||
/// We need to hold on to it in the test case or the temp files get deleted.
|
||||
_temp_dir: tempfile::TempDir,
|
||||
root_dir: SystemPathBuf,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
@@ -28,7 +34,7 @@ impl TestCase {
|
||||
}
|
||||
|
||||
fn root_path(&self) -> &SystemPath {
|
||||
SystemPath::from_std_path(self.temp_dir.path()).unwrap()
|
||||
&self.root_dir
|
||||
}
|
||||
|
||||
fn db(&self) -> &RootDatabase {
|
||||
@@ -40,19 +46,63 @@ impl TestCase {
|
||||
}
|
||||
|
||||
fn stop_watch(&mut self) -> Vec<watch::ChangeEvent> {
|
||||
if let Some(watcher) = self.watcher.take() {
|
||||
// Give the watcher some time to catch up.
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
self.try_stop_watch(Duration::from_secs(10))
|
||||
.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.");
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
watcher.flush();
|
||||
watcher.stop();
|
||||
|
||||
for event in &self.changes_receiver {
|
||||
all_events.extend(event);
|
||||
}
|
||||
|
||||
if all_events.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
#[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.")
|
||||
}
|
||||
|
||||
fn try_take_watch_changes(&self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
let Some(watcher) = &self.watcher else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
watcher.flush();
|
||||
|
||||
while let Ok(event) = self
|
||||
.changes_receiver
|
||||
.recv_timeout(Duration::from_millis(10))
|
||||
{
|
||||
all_events.extend(event);
|
||||
watcher.flush();
|
||||
watcher.stop();
|
||||
}
|
||||
|
||||
let mut all_events = Vec::new();
|
||||
for events in &self.changes_receiver {
|
||||
all_events.extend(events);
|
||||
if all_events.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
all_events
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
fn update_search_path_settings(
|
||||
@@ -81,33 +131,67 @@ impl TestCase {
|
||||
collected
|
||||
}
|
||||
|
||||
fn system_file(&self, path: impl AsRef<SystemPath>) -> Option<File> {
|
||||
fn system_file(&self, path: impl AsRef<SystemPath>) -> Result<File, FileError> {
|
||||
system_path_to_file(self.db(), path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
fn setup<I, P>(workspace_files: I) -> anyhow::Result<TestCase>
|
||||
trait SetupFiles {
|
||||
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
|
||||
where
|
||||
I: IntoIterator<Item = (P, &'static str)>,
|
||||
P: AsRef<SystemPath>,
|
||||
{
|
||||
setup_with_search_paths(workspace_files, |_root, workspace_path| {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: None,
|
||||
fn setup(self, _root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()> {
|
||||
for (relative_path, content) in self {
|
||||
let relative_path = relative_path.as_ref();
|
||||
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}'.",)
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(absolute_path.as_std_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}'"))?;
|
||||
file.sync_data()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> SetupFiles for F
|
||||
where
|
||||
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
|
||||
{
|
||||
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()> {
|
||||
self(root_path, workspace_path)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
{
|
||||
setup_with_search_paths(setup_files, |_root, workspace_path| SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn setup_with_search_paths<I, P>(
|
||||
workspace_files: I,
|
||||
fn setup_with_search_paths<F>(
|
||||
setup_files: F,
|
||||
create_search_paths: impl FnOnce(&SystemPath, &SystemPath) -> SearchPathSettings,
|
||||
) -> anyhow::Result<TestCase>
|
||||
where
|
||||
I: IntoIterator<Item = (P, &'static str)>,
|
||||
P: AsRef<SystemPath>,
|
||||
F: SetupFiles,
|
||||
{
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
||||
@@ -130,18 +214,9 @@ where
|
||||
std::fs::create_dir_all(workspace_path.as_std_path())
|
||||
.with_context(|| format!("Failed to create workspace directory '{workspace_path}'",))?;
|
||||
|
||||
for (relative_path, content) in workspace_files {
|
||||
let relative_path = relative_path.as_ref();
|
||||
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}'.",)
|
||||
})?;
|
||||
}
|
||||
|
||||
std::fs::write(absolute_path.as_std_path(), content)
|
||||
.with_context(|| format!("Failed to write file '{relative_path}'"))?;
|
||||
}
|
||||
setup_files
|
||||
.setup(&root_path, &workspace_path)
|
||||
.context("Failed to setup test files")?;
|
||||
|
||||
let system = OsSystem::new(&workspace_path);
|
||||
|
||||
@@ -159,7 +234,7 @@ where
|
||||
}
|
||||
|
||||
let settings = ProgramSettings {
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths,
|
||||
};
|
||||
|
||||
@@ -176,12 +251,46 @@ where
|
||||
db,
|
||||
changes_receiver: receiver,
|
||||
watcher: Some(watcher),
|
||||
temp_dir,
|
||||
_temp_dir: temp_dir,
|
||||
root_dir: root_path,
|
||||
};
|
||||
|
||||
// Sometimes the file watcher reports changes for events that happened before the watcher was started.
|
||||
// Do a best effort at dropping these events.
|
||||
test_case.try_take_watch_changes(Duration::from_millis(100));
|
||||
|
||||
Ok(test_case)
|
||||
}
|
||||
|
||||
/// Updates the content of a file and ensures that the last modified file time is updated.
|
||||
fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref().as_std_path();
|
||||
|
||||
let metadata = path.metadata()?;
|
||||
let last_modified_time = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(false)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
loop {
|
||||
file.sync_all()?;
|
||||
|
||||
let modified_time = filetime::FileTime::from_last_modification_time(&path.metadata()?);
|
||||
|
||||
if modified_time != last_modified_time {
|
||||
break Ok(());
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_nanos(10));
|
||||
|
||||
filetime::set_file_handle_times(&file, None, Some(filetime::FileTime::now()))?;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_file() -> anyhow::Result<()> {
|
||||
let mut case = setup([("bar.py", "")])?;
|
||||
@@ -189,7 +298,7 @@ fn new_file() -> anyhow::Result<()> {
|
||||
let bar_file = case.system_file(&bar_path).unwrap();
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
assert_eq!(case.system_file(&foo_path), None);
|
||||
assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
|
||||
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
|
||||
@@ -212,7 +321,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
let bar_file = case.system_file(&bar_path).unwrap();
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
assert_eq!(case.system_file(&foo_path), None);
|
||||
assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
|
||||
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
|
||||
@@ -221,7 +330,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert!(case.system_file(&foo_path).is_some());
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
|
||||
|
||||
Ok(())
|
||||
@@ -233,16 +342,16 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
let mut case = setup([("foo.py", foo_source)])?;
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
let foo = case
|
||||
.system_file(&foo_path)
|
||||
.ok_or_else(|| anyhow!("Foo not found"))?;
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), foo_source);
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 2')")?;
|
||||
update_file(&foo_path, "print('Version 2')")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
assert!(!changes.is_empty());
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
@@ -251,59 +360,13 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn changed_metadata() -> anyhow::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut case = setup([("foo.py", "")])?;
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
let foo = case
|
||||
.system_file(&foo_path)
|
||||
.ok_or_else(|| anyhow!("Foo not found"))?;
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
std::fs::set_permissions(
|
||||
foo_path.as_std_path(),
|
||||
std::fs::Permissions::from_mode(0o777),
|
||||
)
|
||||
.with_context(|| "Failed to set file permissions.")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_file() -> anyhow::Result<()> {
|
||||
let foo_source = "print('Hello, world!')";
|
||||
let mut case = setup([("foo.py", foo_source)])?;
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
let foo = case
|
||||
.system_file(&foo_path)
|
||||
.ok_or_else(|| anyhow!("Foo not found"))?;
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
|
||||
assert!(foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
|
||||
@@ -332,9 +395,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
||||
let trash_path = case.root_path().join(".trash");
|
||||
std::fs::create_dir_all(trash_path.as_std_path())?;
|
||||
|
||||
let foo = case
|
||||
.system_file(&foo_path)
|
||||
.ok_or_else(|| anyhow!("Foo not found"))?;
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
|
||||
assert!(foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
|
||||
@@ -366,7 +427,7 @@ fn move_file_to_workspace() -> anyhow::Result<()> {
|
||||
|
||||
let foo_in_workspace_path = case.workspace_path("foo.py");
|
||||
|
||||
assert!(case.system_file(&foo_path).is_some());
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
assert_eq!(&case.collect_package_files(&bar_path), &[bar]);
|
||||
assert!(case
|
||||
.db()
|
||||
@@ -380,9 +441,7 @@ fn move_file_to_workspace() -> anyhow::Result<()> {
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
let foo_in_workspace = case
|
||||
.system_file(&foo_in_workspace_path)
|
||||
.ok_or_else(|| anyhow!("Foo not found"))?;
|
||||
let foo_in_workspace = case.system_file(&foo_in_workspace_path)?;
|
||||
|
||||
assert!(foo_in_workspace.exists(case.db()));
|
||||
assert_eq!(
|
||||
@@ -400,9 +459,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
let bar_path = case.workspace_path("bar.py");
|
||||
|
||||
let foo = case
|
||||
.system_file(&foo_path)
|
||||
.ok_or_else(|| anyhow!("Foo not found"))?;
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
|
||||
assert_eq!(case.collect_package_files(&foo_path), [foo]);
|
||||
|
||||
@@ -414,9 +471,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
|
||||
let bar = case
|
||||
.system_file(&bar_path)
|
||||
.ok_or_else(|| anyhow!("Bar not found"))?;
|
||||
let bar = case.system_file(&bar_path)?;
|
||||
|
||||
assert!(bar.exists(case.db()));
|
||||
assert_eq!(case.collect_package_files(&foo_path), [bar]);
|
||||
@@ -482,7 +537,7 @@ fn directory_moved_to_trash() -> 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");
|
||||
let init_file = case
|
||||
@@ -641,9 +696,9 @@ fn search_path() -> anyhow::Result<()> {
|
||||
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, workspace_path| {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
src_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(root_path.join("site_packages")),
|
||||
site_packages: vec![root_path.join("site_packages")],
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -655,7 +710,6 @@ fn search_path() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
std::fs::write(site_packages.join("__init__.py").as_std_path(), "")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
@@ -681,12 +735,11 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
// Register site-packages as a search path.
|
||||
case.update_search_path_settings(|settings| SearchPathSettings {
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
..settings.clone()
|
||||
});
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
std::fs::write(site_packages.join("__init__.py").as_std_path(), "")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
@@ -703,24 +756,480 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, workspace_path| {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
src_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(root_path.join("site_packages")),
|
||||
site_packages: vec![root_path.join("site_packages")],
|
||||
}
|
||||
})?;
|
||||
|
||||
// Remove site packages from the search path settings.
|
||||
let site_packages = case.root_path().join("site_packages");
|
||||
case.update_search_path_settings(|settings| SearchPathSettings {
|
||||
site_packages: None,
|
||||
site_packages: vec![],
|
||||
..settings.clone()
|
||||
});
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
let changes = case.try_stop_watch(Duration::from_millis(100));
|
||||
|
||||
assert_eq!(changes, &[]);
|
||||
assert_eq!(changes, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch a workspace that contains two files where one file is a hardlink to another.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - workspace
|
||||
/// |- foo.py
|
||||
/// |- bar.py (hard link to foo.py)
|
||||
/// ```
|
||||
///
|
||||
/// # Linux
|
||||
/// `inotify` only emits a single change event for the file that was changed.
|
||||
/// Other files that point to the same inode (hardlinks) won't get updated.
|
||||
///
|
||||
/// For reference: VS Code and PyCharm have the same behavior where the results for one of the
|
||||
/// files are stale.
|
||||
///
|
||||
/// # Windows
|
||||
/// I haven't found any documentation that states the notification behavior on Windows but what
|
||||
/// we're seeing is that Windows only emits a single event, similar to Linux.
|
||||
#[test]
|
||||
fn hard_links_in_workspace() -> anyhow::Result<()> {
|
||||
let mut case = setup(|_root: &SystemPath, workspace: &SystemPath| {
|
||||
let foo_path = workspace.join("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
// Create a hardlink to `foo`
|
||||
let bar_path = workspace.join("bar.py");
|
||||
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
|
||||
.context("Failed to create hard link from foo.py -> bar.py")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
let foo = case.system_file(&foo_path).unwrap();
|
||||
let bar_path = case.workspace_path("bar.py");
|
||||
let bar = case.system_file(&bar_path).unwrap();
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
|
||||
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
|
||||
// macOS is the only platform that emits events for every hardlink.
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch a workspace that contains one file that is a hardlink to a file outside the workspace.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - foo.py
|
||||
/// - workspace
|
||||
/// |- bar.py (hard link to /foo.py)
|
||||
/// ```
|
||||
///
|
||||
/// # Linux
|
||||
/// inotiyf doesn't support observing changes to hard linked files.
|
||||
///
|
||||
/// > Note: when monitoring a directory, events are not generated for
|
||||
/// > the files inside the directory when the events are performed via
|
||||
/// > a pathname (i.e., a link) that lies outside the monitored
|
||||
/// > directory. [source](https://man7.org/linux/man-pages/man7/inotify.7.html)
|
||||
///
|
||||
/// # Windows
|
||||
/// > Retrieves information that describes the changes within the specified directory.
|
||||
///
|
||||
/// [source](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw)
|
||||
///
|
||||
/// My interpretation of this is that Windows doesn't support observing changes made to
|
||||
/// hard linked files outside the workspace.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "linux",
|
||||
ignore = "inotify doesn't support observing changes to hard linked files."
|
||||
)]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "windows doesn't support observing changes to hard linked files."
|
||||
)]
|
||||
fn hard_links_to_target_outside_workspace() -> anyhow::Result<()> {
|
||||
let mut case = setup(|root: &SystemPath, workspace: &SystemPath| {
|
||||
let foo_path = root.join("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
// Create a hardlink to `foo`
|
||||
let bar_path = workspace.join("bar.py");
|
||||
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
|
||||
.context("Failed to create hard link from foo.py -> bar.py")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let foo_path = case.root_path().join("foo.py");
|
||||
let foo = case.system_file(&foo_path).unwrap();
|
||||
let bar_path = case.workspace_path("bar.py");
|
||||
let bar = case.system_file(&bar_path).unwrap();
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
|
||||
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix {
|
||||
//! Tests that make use of unix specific file-system features.
|
||||
use super::*;
|
||||
|
||||
/// Changes the metadata of the only file in the workspace.
|
||||
#[test]
|
||||
fn changed_metadata() -> anyhow::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut case = setup([("foo.py", "")])?;
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
std::fs::set_permissions(
|
||||
foo_path.as_std_path(),
|
||||
std::fs::Permissions::from_mode(0o777),
|
||||
)
|
||||
.with_context(|| "Failed to set file permissions.")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A workspace path is a symlink to a file outside the workspace.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - bar
|
||||
/// |- baz.py
|
||||
///
|
||||
/// - workspace
|
||||
/// |- bar -> /bar
|
||||
/// ```
|
||||
///
|
||||
/// # macOS
|
||||
/// This test case isn't supported on macOS.
|
||||
/// macOS uses `FSEvents` and `FSEvents` doesn't emit an event if a file in a symlinked directory is changed.
|
||||
///
|
||||
/// > Generally speaking, when working with file system event notifications, you will probably want to use lstat,
|
||||
/// > because changes to the underlying file will not result in a change notification for the directory containing
|
||||
/// > the symbolic link to that file. However, if you are working with a controlled file structure in
|
||||
/// > which symbolic links always point within your watched tree, you might have reason to use stat.
|
||||
///
|
||||
/// [source](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html#//apple_ref/doc/uid/TP40005289-CH4-SW4)
|
||||
///
|
||||
/// Pyright also does not support this case.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "macos",
|
||||
ignore = "FSEvents doesn't emit change events for symlinked directories outside of the watched paths."
|
||||
)]
|
||||
fn symlink_target_outside_watched_paths() -> anyhow::Result<()> {
|
||||
let mut case = setup(|root: &SystemPath, workspace: &SystemPath| {
|
||||
// Set up the symlink target.
|
||||
let link_target = root.join("bar");
|
||||
std::fs::create_dir_all(link_target.as_std_path())
|
||||
.context("Failed to create link target directory")?;
|
||||
let baz_original = link_target.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.context("Failed to write link target file")?;
|
||||
|
||||
// Create a symlink inside the workspace
|
||||
let bar = workspace.join("bar");
|
||||
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
|
||||
.context("Failed to create symlink to bar package")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_workspace = case.workspace_path("bar/baz.py");
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(
|
||||
baz.file().path(case.db()).as_system_path(),
|
||||
Some(&*baz_workspace)
|
||||
);
|
||||
|
||||
let baz_original = case.root_path().join("bar/baz.py");
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(baz_original, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.take_watch_changes();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
// Write to the symlink source.
|
||||
update_file(baz_workspace, "def baz(): print('Version 3')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): print('Version 3')"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Workspace contains a symlink to another directory inside the workspace.
|
||||
/// Changes to files in the symlinked directory should be reflected
|
||||
/// to all files.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - workspace
|
||||
/// | - bar -> /workspace/patched/bar
|
||||
/// |
|
||||
/// | - patched
|
||||
/// | |-- bar
|
||||
/// | | |- baz.py
|
||||
/// |
|
||||
/// |-- foo.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlink_inside_workspace() -> anyhow::Result<()> {
|
||||
let mut case = setup(|_root: &SystemPath, workspace: &SystemPath| {
|
||||
// Set up the symlink target.
|
||||
let link_target = workspace.join("patched/bar");
|
||||
std::fs::create_dir_all(link_target.as_std_path())
|
||||
.context("Failed to create link target directory")?;
|
||||
let baz_original = link_target.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.context("Failed to write link target file")?;
|
||||
|
||||
// Create a symlink inside site-packages
|
||||
let bar_in_workspace = workspace.join("bar");
|
||||
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_workspace.as_std_path())
|
||||
.context("Failed to create symlink to bar package")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let bar_baz = case.workspace_path("bar/baz.py");
|
||||
|
||||
let patched_bar_baz = case.workspace_path("patched/bar/baz.py");
|
||||
let patched_bar_baz_file = case.system_file(&patched_bar_baz).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), patched_bar_baz_file).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz));
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(&patched_bar_baz, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
// The file watcher is guaranteed to emit one event for the changed file, but it isn't specified
|
||||
// if the event is emitted for the "original" or linked path because both paths are watched.
|
||||
// The best we can assert here is that one of the files should have been updated.
|
||||
//
|
||||
// In a perfect world, the file watcher would emit two events, one for the original file and
|
||||
// one for the symlink. I tried parcel/watcher, node's `fs.watch` and `chokidar` and
|
||||
// only `chokidar seems to support it (used by Pyright).
|
||||
//
|
||||
// I further tested how good editor support is for symlinked files and it is not good ;)
|
||||
// * VS Code doesn't update the file content if a file gets changed through a symlink
|
||||
// * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot).
|
||||
//
|
||||
// That's why I think it's fine to not support this case for now.
|
||||
|
||||
let patched_baz_text = source_text(case.db(), patched_bar_baz_file);
|
||||
let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')";
|
||||
|
||||
let bar_baz_text = source_text(case.db(), baz.file());
|
||||
let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')";
|
||||
|
||||
assert!(
|
||||
did_update_patched_baz || did_update_bar_baz,
|
||||
"Expected one of the files to be updated but neither file was updated.\nOriginal: {patched_baz_text}\nSymlinked: {bar_baz_text}",
|
||||
patched_baz_text = patched_baz_text.as_str(),
|
||||
bar_baz_text = bar_baz_text.as_str()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A module search path is a symlink.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - site-packages
|
||||
/// | - bar/baz.py
|
||||
///
|
||||
/// - workspace
|
||||
/// |-- .venv/lib/python3.12/site-packages -> /site-packages
|
||||
/// |
|
||||
/// |-- foo.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlinked_module_search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_search_paths(
|
||||
|root: &SystemPath, workspace: &SystemPath| {
|
||||
// Set up the symlink target.
|
||||
let site_packages = root.join("site-packages");
|
||||
let bar = site_packages.join("bar");
|
||||
std::fs::create_dir_all(bar.as_std_path())
|
||||
.context("Failed to create bar directory")?;
|
||||
let baz_original = bar.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.context("Failed to write baz.py")?;
|
||||
|
||||
// Symlink the site packages in the venv to the global site packages
|
||||
let venv_site_packages = workspace.join(".venv/lib/python3.12/site-packages");
|
||||
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
|
||||
.context("Failed to create .venv directory")?;
|
||||
std::os::unix::fs::symlink(
|
||||
site_packages.as_std_path(),
|
||||
venv_site_packages.as_std_path(),
|
||||
)
|
||||
.context("Failed to create symlink to site-packages")?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|_root, workspace| SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: workspace.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![workspace.join(".venv/lib/python3.12/site-packages")],
|
||||
},
|
||||
)?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_site_packages_path =
|
||||
case.workspace_path(".venv/lib/python3.12/site-packages/bar/baz.py");
|
||||
let baz_site_packages = case.system_file(&baz_site_packages_path).unwrap();
|
||||
let baz_original = case.root_path().join("site-packages/bar/baz.py");
|
||||
let baz_original_file = case.system_file(&baz_original).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_site_packages).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(
|
||||
baz.file().path(case.db()).as_system_path(),
|
||||
Some(&*baz_original)
|
||||
);
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(&baz_original, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
// It would be nice if this is supported but the underlying file system watchers
|
||||
// only emit a single event. For reference
|
||||
// * VS Code doesn't update the file content if a file gets changed through a symlink
|
||||
// * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot).
|
||||
// We could add support for it by keeping a reverse map from `real_path` to symlinked path but
|
||||
// it doesn't seem worth doing considering that as prominent tools like PyCharm don't support it.
|
||||
// Pyright does support it, thanks to chokidar.
|
||||
assert_ne!(
|
||||
source_text(case.db(), baz_site_packages).as_str(),
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "red_knot_module_resolver"
|
||||
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 }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
|
||||
compact_str = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
path-slash = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["os"] }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,129 +0,0 @@
|
||||
use ruff_db::Upcast;
|
||||
|
||||
use crate::resolver::{
|
||||
editable_install_resolution_paths, file_to_module, internal::ModuleNameIngredient,
|
||||
module_resolution_settings, resolve_module_query,
|
||||
};
|
||||
use crate::typeshed::parse_typeshed_versions;
|
||||
|
||||
#[salsa::jar(db=Db)]
|
||||
pub struct Jar(
|
||||
ModuleNameIngredient<'_>,
|
||||
module_resolution_settings,
|
||||
editable_install_resolution_paths,
|
||||
resolve_module_query,
|
||||
file_to_module,
|
||||
parse_typeshed_versions,
|
||||
);
|
||||
|
||||
pub trait Db: salsa::DbWithJar<Jar> + ruff_db::Db + Upcast<dyn ruff_db::Db> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync;
|
||||
|
||||
use salsa::DebugWithDb;
|
||||
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
||||
use crate::vendored_typeshed_stubs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[salsa::db(Jar, ruff_db::Jar)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
files: Files,
|
||||
events: sync::Arc<sync::Mutex<Vec<salsa::Event>>>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: vendored_typeshed_stubs().snapshot(),
|
||||
events: sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = sync::Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn ruff_db::Db> for TestDb {
|
||||
fn upcast(&self) -> &(dyn ruff_db::Db + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn ruff_db::Db + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ruff_db::Db for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn ruff_db::system::System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for TestDb {}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: salsa::Event) {
|
||||
tracing::trace!("event: {:?}", event.debug(self));
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
impl salsa::ParallelDatabase for TestDb {
|
||||
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||
salsa::Snapshot::new(Self {
|
||||
storage: self.storage.snapshot(),
|
||||
system: self.system.snapshot(),
|
||||
vendored: self.vendored.snapshot(),
|
||||
files: self.files.snapshot(),
|
||||
events: self.events.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
f863db6bc5242348ceaa6a3bca4e59aa9e62faaa
|
||||
@@ -1,117 +0,0 @@
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
SF_APPEND: Literal[0x00040000]
|
||||
SF_ARCHIVED: Literal[0x00010000]
|
||||
SF_IMMUTABLE: Literal[0x00020000]
|
||||
SF_NOUNLINK: Literal[0x00100000]
|
||||
SF_SNAPSHOT: Literal[0x00200000]
|
||||
|
||||
ST_MODE: Literal[0]
|
||||
ST_INO: Literal[1]
|
||||
ST_DEV: Literal[2]
|
||||
ST_NLINK: Literal[3]
|
||||
ST_UID: Literal[4]
|
||||
ST_GID: Literal[5]
|
||||
ST_SIZE: Literal[6]
|
||||
ST_ATIME: Literal[7]
|
||||
ST_MTIME: Literal[8]
|
||||
ST_CTIME: Literal[9]
|
||||
|
||||
S_IFIFO: Literal[0o010000]
|
||||
S_IFLNK: Literal[0o120000]
|
||||
S_IFREG: Literal[0o100000]
|
||||
S_IFSOCK: Literal[0o140000]
|
||||
S_IFBLK: Literal[0o060000]
|
||||
S_IFCHR: Literal[0o020000]
|
||||
S_IFDIR: Literal[0o040000]
|
||||
|
||||
# These are 0 on systems that don't support the specific kind of file.
|
||||
# Example: Linux doesn't support door files, so S_IFDOOR is 0 on linux.
|
||||
S_IFDOOR: int
|
||||
S_IFPORT: int
|
||||
S_IFWHT: int
|
||||
|
||||
S_ISUID: Literal[0o4000]
|
||||
S_ISGID: Literal[0o2000]
|
||||
S_ISVTX: Literal[0o1000]
|
||||
|
||||
S_IRWXU: Literal[0o0700]
|
||||
S_IRUSR: Literal[0o0400]
|
||||
S_IWUSR: Literal[0o0200]
|
||||
S_IXUSR: Literal[0o0100]
|
||||
|
||||
S_IRWXG: Literal[0o0070]
|
||||
S_IRGRP: Literal[0o0040]
|
||||
S_IWGRP: Literal[0o0020]
|
||||
S_IXGRP: Literal[0o0010]
|
||||
|
||||
S_IRWXO: Literal[0o0007]
|
||||
S_IROTH: Literal[0o0004]
|
||||
S_IWOTH: Literal[0o0002]
|
||||
S_IXOTH: Literal[0o0001]
|
||||
|
||||
S_ENFMT: Literal[0o2000]
|
||||
S_IREAD: Literal[0o0400]
|
||||
S_IWRITE: Literal[0o0200]
|
||||
S_IEXEC: Literal[0o0100]
|
||||
|
||||
UF_APPEND: Literal[0x00000004]
|
||||
UF_COMPRESSED: Literal[0x00000020] # OS X 10.6+ only
|
||||
UF_HIDDEN: Literal[0x00008000] # OX X 10.5+ only
|
||||
UF_IMMUTABLE: Literal[0x00000002]
|
||||
UF_NODUMP: Literal[0x00000001]
|
||||
UF_NOUNLINK: Literal[0x00000010]
|
||||
UF_OPAQUE: Literal[0x00000008]
|
||||
|
||||
def S_IMODE(mode: int, /) -> int: ...
|
||||
def S_IFMT(mode: int, /) -> int: ...
|
||||
def S_ISBLK(mode: int, /) -> bool: ...
|
||||
def S_ISCHR(mode: int, /) -> bool: ...
|
||||
def S_ISDIR(mode: int, /) -> bool: ...
|
||||
def S_ISDOOR(mode: int, /) -> bool: ...
|
||||
def S_ISFIFO(mode: int, /) -> bool: ...
|
||||
def S_ISLNK(mode: int, /) -> bool: ...
|
||||
def S_ISPORT(mode: int, /) -> bool: ...
|
||||
def S_ISREG(mode: int, /) -> bool: ...
|
||||
def S_ISSOCK(mode: int, /) -> bool: ...
|
||||
def S_ISWHT(mode: int, /) -> bool: ...
|
||||
def filemode(mode: int, /) -> str: ...
|
||||
|
||||
if sys.platform == "win32":
|
||||
IO_REPARSE_TAG_SYMLINK: int
|
||||
IO_REPARSE_TAG_MOUNT_POINT: int
|
||||
IO_REPARSE_TAG_APPEXECLINK: int
|
||||
|
||||
if sys.platform == "win32":
|
||||
FILE_ATTRIBUTE_ARCHIVE: Literal[32]
|
||||
FILE_ATTRIBUTE_COMPRESSED: Literal[2048]
|
||||
FILE_ATTRIBUTE_DEVICE: Literal[64]
|
||||
FILE_ATTRIBUTE_DIRECTORY: Literal[16]
|
||||
FILE_ATTRIBUTE_ENCRYPTED: Literal[16384]
|
||||
FILE_ATTRIBUTE_HIDDEN: Literal[2]
|
||||
FILE_ATTRIBUTE_INTEGRITY_STREAM: Literal[32768]
|
||||
FILE_ATTRIBUTE_NORMAL: Literal[128]
|
||||
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: Literal[8192]
|
||||
FILE_ATTRIBUTE_NO_SCRUB_DATA: Literal[131072]
|
||||
FILE_ATTRIBUTE_OFFLINE: Literal[4096]
|
||||
FILE_ATTRIBUTE_READONLY: Literal[1]
|
||||
FILE_ATTRIBUTE_REPARSE_POINT: Literal[1024]
|
||||
FILE_ATTRIBUTE_SPARSE_FILE: Literal[512]
|
||||
FILE_ATTRIBUTE_SYSTEM: Literal[4]
|
||||
FILE_ATTRIBUTE_TEMPORARY: Literal[256]
|
||||
FILE_ATTRIBUTE_VIRTUAL: Literal[65536]
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
SF_SETTABLE: Literal[0x3FFF0000]
|
||||
# https://github.com/python/cpython/issues/114081#issuecomment-2119017790
|
||||
# SF_RESTRICTED: Literal[0x00080000]
|
||||
SF_FIRMLINK: Literal[0x00800000]
|
||||
SF_DATALESS: Literal[0x40000000]
|
||||
|
||||
SF_SUPPORTED: Literal[0x9F0000]
|
||||
SF_SYNTHETIC: Literal[0xC0000000]
|
||||
|
||||
UF_TRACKED: Literal[0x00000040]
|
||||
UF_DATAVAULT: Literal[0x00000080]
|
||||
UF_SETTABLE: Literal[0x0000FFFF]
|
||||
@@ -1,20 +0,0 @@
|
||||
import enum
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
LOG_THRESHOLD_FOR_CONNLOST_WRITES: Literal[5]
|
||||
ACCEPT_RETRY_DELAY: Literal[1]
|
||||
DEBUG_STACK_DEPTH: Literal[10]
|
||||
SSL_HANDSHAKE_TIMEOUT: float
|
||||
SENDFILE_FALLBACK_READBUFFER_SIZE: Literal[262144]
|
||||
if sys.version_info >= (3, 11):
|
||||
SSL_SHUTDOWN_TIMEOUT: float
|
||||
FLOW_CONTROL_HIGH_WATER_SSL_READ: Literal[256]
|
||||
FLOW_CONTROL_HIGH_WATER_SSL_WRITE: Literal[512]
|
||||
if sys.version_info >= (3, 12):
|
||||
THREAD_JOIN_TIMEOUT: Literal[300]
|
||||
|
||||
class _SendfileMode(enum.Enum):
|
||||
UNSUPPORTED = 1
|
||||
TRY_NATIVE = 2
|
||||
FALLBACK = 3
|
||||
@@ -1,16 +0,0 @@
|
||||
from typing import Any, TypeVar
|
||||
|
||||
__all__ = ["Error", "copy", "deepcopy"]
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
# None in CPython but non-None in Jython
|
||||
PyStringMap: Any
|
||||
|
||||
# Note: memo and _nil are internal kwargs.
|
||||
def deepcopy(x: _T, memo: dict[int, Any] | None = None, _nil: Any = []) -> _T: ...
|
||||
def copy(x: _T) -> _T: ...
|
||||
|
||||
class Error(Exception): ...
|
||||
|
||||
error = Error
|
||||
@@ -1,25 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from ..cmd import Command
|
||||
|
||||
def show_formats() -> None: ...
|
||||
|
||||
class bdist(Command):
|
||||
description: str
|
||||
user_options: Any
|
||||
boolean_options: Any
|
||||
help_options: Any
|
||||
no_format_option: Any
|
||||
default_format: Any
|
||||
format_commands: Any
|
||||
format_command: Any
|
||||
bdist_base: Any
|
||||
plat_name: Any
|
||||
formats: Any
|
||||
dist_dir: Any
|
||||
skip_build: int
|
||||
group: Any
|
||||
owner: Any
|
||||
def initialize_options(self) -> None: ...
|
||||
def finalize_options(self) -> None: ...
|
||||
def run(self) -> None: ...
|
||||
@@ -1,67 +0,0 @@
|
||||
ENDMARKER: int
|
||||
NAME: int
|
||||
NUMBER: int
|
||||
STRING: int
|
||||
NEWLINE: int
|
||||
INDENT: int
|
||||
DEDENT: int
|
||||
LPAR: int
|
||||
RPAR: int
|
||||
LSQB: int
|
||||
RSQB: int
|
||||
COLON: int
|
||||
COMMA: int
|
||||
SEMI: int
|
||||
PLUS: int
|
||||
MINUS: int
|
||||
STAR: int
|
||||
SLASH: int
|
||||
VBAR: int
|
||||
AMPER: int
|
||||
LESS: int
|
||||
GREATER: int
|
||||
EQUAL: int
|
||||
DOT: int
|
||||
PERCENT: int
|
||||
BACKQUOTE: int
|
||||
LBRACE: int
|
||||
RBRACE: int
|
||||
EQEQUAL: int
|
||||
NOTEQUAL: int
|
||||
LESSEQUAL: int
|
||||
GREATEREQUAL: int
|
||||
TILDE: int
|
||||
CIRCUMFLEX: int
|
||||
LEFTSHIFT: int
|
||||
RIGHTSHIFT: int
|
||||
DOUBLESTAR: int
|
||||
PLUSEQUAL: int
|
||||
MINEQUAL: int
|
||||
STAREQUAL: int
|
||||
SLASHEQUAL: int
|
||||
PERCENTEQUAL: int
|
||||
AMPEREQUAL: int
|
||||
VBAREQUAL: int
|
||||
CIRCUMFLEXEQUAL: int
|
||||
LEFTSHIFTEQUAL: int
|
||||
RIGHTSHIFTEQUAL: int
|
||||
DOUBLESTAREQUAL: int
|
||||
DOUBLESLASH: int
|
||||
DOUBLESLASHEQUAL: int
|
||||
OP: int
|
||||
COMMENT: int
|
||||
NL: int
|
||||
RARROW: int
|
||||
AT: int
|
||||
ATEQUAL: int
|
||||
AWAIT: int
|
||||
ASYNC: int
|
||||
ERRORTOKEN: int
|
||||
COLONEQUAL: int
|
||||
N_TOKENS: int
|
||||
NT_OFFSET: int
|
||||
tok_name: dict[int, str]
|
||||
|
||||
def ISTERMINAL(x: int) -> bool: ...
|
||||
def ISNONTERMINAL(x: int) -> bool: ...
|
||||
def ISEOF(x: int) -> bool: ...
|
||||
@@ -1,55 +0,0 @@
|
||||
import sys
|
||||
from typing import Literal, overload
|
||||
|
||||
if sys.platform != "win32":
|
||||
LOG_ALERT: Literal[1]
|
||||
LOG_AUTH: Literal[32]
|
||||
LOG_AUTHPRIV: Literal[80]
|
||||
LOG_CONS: Literal[2]
|
||||
LOG_CRIT: Literal[2]
|
||||
LOG_CRON: Literal[72]
|
||||
LOG_DAEMON: Literal[24]
|
||||
LOG_DEBUG: Literal[7]
|
||||
LOG_EMERG: Literal[0]
|
||||
LOG_ERR: Literal[3]
|
||||
LOG_INFO: Literal[6]
|
||||
LOG_KERN: Literal[0]
|
||||
LOG_LOCAL0: Literal[128]
|
||||
LOG_LOCAL1: Literal[136]
|
||||
LOG_LOCAL2: Literal[144]
|
||||
LOG_LOCAL3: Literal[152]
|
||||
LOG_LOCAL4: Literal[160]
|
||||
LOG_LOCAL5: Literal[168]
|
||||
LOG_LOCAL6: Literal[176]
|
||||
LOG_LOCAL7: Literal[184]
|
||||
LOG_LPR: Literal[48]
|
||||
LOG_MAIL: Literal[16]
|
||||
LOG_NDELAY: Literal[8]
|
||||
LOG_NEWS: Literal[56]
|
||||
LOG_NOTICE: Literal[5]
|
||||
LOG_NOWAIT: Literal[16]
|
||||
LOG_ODELAY: Literal[4]
|
||||
LOG_PERROR: Literal[32]
|
||||
LOG_PID: Literal[1]
|
||||
LOG_SYSLOG: Literal[40]
|
||||
LOG_USER: Literal[8]
|
||||
LOG_UUCP: Literal[64]
|
||||
LOG_WARNING: Literal[4]
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
LOG_FTP: Literal[88]
|
||||
LOG_INSTALL: Literal[112]
|
||||
LOG_LAUNCHD: Literal[192]
|
||||
LOG_NETINFO: Literal[96]
|
||||
LOG_RAS: Literal[120]
|
||||
LOG_REMOTEAUTH: Literal[104]
|
||||
|
||||
def LOG_MASK(pri: int, /) -> int: ...
|
||||
def LOG_UPTO(pri: int, /) -> int: ...
|
||||
def closelog() -> None: ...
|
||||
def openlog(ident: str = ..., logoption: int = ..., facility: int = ...) -> None: ...
|
||||
def setlogmask(maskpri: int, /) -> int: ...
|
||||
@overload
|
||||
def syslog(priority: int, message: str) -> None: ...
|
||||
@overload
|
||||
def syslog(message: str) -> None: ...
|
||||
@@ -1,80 +0,0 @@
|
||||
from typing import Literal
|
||||
|
||||
# These are not actually bools. See #4669
|
||||
NO: bool
|
||||
YES: bool
|
||||
TRUE: bool
|
||||
FALSE: bool
|
||||
ON: bool
|
||||
OFF: bool
|
||||
N: Literal["n"]
|
||||
S: Literal["s"]
|
||||
W: Literal["w"]
|
||||
E: Literal["e"]
|
||||
NW: Literal["nw"]
|
||||
SW: Literal["sw"]
|
||||
NE: Literal["ne"]
|
||||
SE: Literal["se"]
|
||||
NS: Literal["ns"]
|
||||
EW: Literal["ew"]
|
||||
NSEW: Literal["nsew"]
|
||||
CENTER: Literal["center"]
|
||||
NONE: Literal["none"]
|
||||
X: Literal["x"]
|
||||
Y: Literal["y"]
|
||||
BOTH: Literal["both"]
|
||||
LEFT: Literal["left"]
|
||||
TOP: Literal["top"]
|
||||
RIGHT: Literal["right"]
|
||||
BOTTOM: Literal["bottom"]
|
||||
RAISED: Literal["raised"]
|
||||
SUNKEN: Literal["sunken"]
|
||||
FLAT: Literal["flat"]
|
||||
RIDGE: Literal["ridge"]
|
||||
GROOVE: Literal["groove"]
|
||||
SOLID: Literal["solid"]
|
||||
HORIZONTAL: Literal["horizontal"]
|
||||
VERTICAL: Literal["vertical"]
|
||||
NUMERIC: Literal["numeric"]
|
||||
CHAR: Literal["char"]
|
||||
WORD: Literal["word"]
|
||||
BASELINE: Literal["baseline"]
|
||||
INSIDE: Literal["inside"]
|
||||
OUTSIDE: Literal["outside"]
|
||||
SEL: Literal["sel"]
|
||||
SEL_FIRST: Literal["sel.first"]
|
||||
SEL_LAST: Literal["sel.last"]
|
||||
END: Literal["end"]
|
||||
INSERT: Literal["insert"]
|
||||
CURRENT: Literal["current"]
|
||||
ANCHOR: Literal["anchor"]
|
||||
ALL: Literal["all"]
|
||||
NORMAL: Literal["normal"]
|
||||
DISABLED: Literal["disabled"]
|
||||
ACTIVE: Literal["active"]
|
||||
HIDDEN: Literal["hidden"]
|
||||
CASCADE: Literal["cascade"]
|
||||
CHECKBUTTON: Literal["checkbutton"]
|
||||
COMMAND: Literal["command"]
|
||||
RADIOBUTTON: Literal["radiobutton"]
|
||||
SEPARATOR: Literal["separator"]
|
||||
SINGLE: Literal["single"]
|
||||
BROWSE: Literal["browse"]
|
||||
MULTIPLE: Literal["multiple"]
|
||||
EXTENDED: Literal["extended"]
|
||||
DOTBOX: Literal["dotbox"]
|
||||
UNDERLINE: Literal["underline"]
|
||||
PIESLICE: Literal["pieslice"]
|
||||
CHORD: Literal["chord"]
|
||||
ARC: Literal["arc"]
|
||||
FIRST: Literal["first"]
|
||||
LAST: Literal["last"]
|
||||
BUTT: Literal["butt"]
|
||||
PROJECTING: Literal["projecting"]
|
||||
ROUND: Literal["round"]
|
||||
BEVEL: Literal["bevel"]
|
||||
MITER: Literal["miter"]
|
||||
MOVETO: Literal["moveto"]
|
||||
SCROLL: Literal["scroll"]
|
||||
UNITS: Literal["units"]
|
||||
PAGES: Literal["pages"]
|
||||
@@ -1,28 +0,0 @@
|
||||
import sys
|
||||
from _typeshed import ReadableBuffer
|
||||
from typing import Literal, overload
|
||||
|
||||
if sys.platform == "win32":
|
||||
SND_APPLICATION: Literal[128]
|
||||
SND_FILENAME: Literal[131072]
|
||||
SND_ALIAS: Literal[65536]
|
||||
SND_LOOP: Literal[8]
|
||||
SND_MEMORY: Literal[4]
|
||||
SND_PURGE: Literal[64]
|
||||
SND_ASYNC: Literal[1]
|
||||
SND_NODEFAULT: Literal[2]
|
||||
SND_NOSTOP: Literal[16]
|
||||
SND_NOWAIT: Literal[8192]
|
||||
|
||||
MB_ICONASTERISK: Literal[64]
|
||||
MB_ICONEXCLAMATION: Literal[48]
|
||||
MB_ICONHAND: Literal[16]
|
||||
MB_ICONQUESTION: Literal[32]
|
||||
MB_OK: Literal[0]
|
||||
def Beep(frequency: int, duration: int) -> None: ...
|
||||
# Can actually accept anything ORed with 4, and if not it's definitely str, but that's inexpressible
|
||||
@overload
|
||||
def PlaySound(sound: ReadableBuffer | None, flags: Literal[4]) -> None: ...
|
||||
@overload
|
||||
def PlaySound(sound: str | ReadableBuffer | None, flags: int) -> None: ...
|
||||
def MessageBeep(type: int = 0) -> None: ...
|
||||
@@ -11,24 +11,38 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
red_knot_module_resolver = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
path-slash = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true, features = ["zstd", "deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "testing"]}
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Red Knot
|
||||
|
||||
A work-in-progress multifile module resolver for Ruff.
|
||||
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_module_resolver/vendor/typeshed`. The file `crates/red_knot_module_resolver/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_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.
|
||||
|
||||
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).
|
||||
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! This script should be automatically run at build time
|
||||
//! whenever the script itself changes, or whenever any files
|
||||
//! in `crates/red_knot_module_resolver/vendor/typeshed` change.
|
||||
//! in `crates/red_knot_python_semantic/vendor/typeshed` change.
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
@@ -23,8 +23,21 @@ const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
|
||||
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(CompressionMethod::Zstd)
|
||||
.compression_method(method)
|
||||
.unix_permissions(0o644);
|
||||
|
||||
for entry in walkdir::WalkDir::new(directory_path) {
|
||||
@@ -1,5 +1,5 @@
|
||||
use red_knot_module_resolver::{resolve_module, ModuleName};
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::Db;
|
||||
@@ -11,6 +11,6 @@ use crate::Db;
|
||||
pub(crate) fn builtins_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
let builtins_name =
|
||||
ModuleName::new_static("builtins").expect("Expected 'builtins' to be a valid module name");
|
||||
let builtins_file = resolve_module(db.upcast(), builtins_name)?.file();
|
||||
let builtins_file = resolve_module(db, builtins_name)?.file();
|
||||
Some(global_scope(db, builtins_file))
|
||||
}
|
||||
|
||||
@@ -1,55 +1,22 @@
|
||||
use salsa::DbWithJar;
|
||||
|
||||
use red_knot_module_resolver::Db as ResolverDb;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use crate::builtins::builtins_scope;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
use crate::types::{
|
||||
infer_definition_types, infer_expression_types, infer_scope_types, ClassType, FunctionType,
|
||||
IntersectionType, UnionType,
|
||||
};
|
||||
|
||||
#[salsa::jar(db=Db)]
|
||||
pub struct Jar(
|
||||
ScopeId<'_>,
|
||||
Definition<'_>,
|
||||
Expression<'_>,
|
||||
FunctionType<'_>,
|
||||
ClassType<'_>,
|
||||
UnionType<'_>,
|
||||
IntersectionType<'_>,
|
||||
symbol_table,
|
||||
use_def_map,
|
||||
global_scope,
|
||||
semantic_index,
|
||||
infer_definition_types,
|
||||
infer_expression_types,
|
||||
infer_scope_types,
|
||||
builtins_scope,
|
||||
);
|
||||
|
||||
/// Database giving access to semantic information about a Python program.
|
||||
pub trait Db: SourceDb + ResolverDb + DbWithJar<Jar> + Upcast<dyn ResolverDb> {}
|
||||
#[salsa::db]
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::DebugWithDb;
|
||||
|
||||
use red_knot_module_resolver::{vendored_typeshed_stubs, Db as ResolverDb, Jar as ResolverJar};
|
||||
use crate::module_resolver::vendored_typeshed_stubs;
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast};
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use super::{Db, Jar};
|
||||
use super::Db;
|
||||
|
||||
#[salsa::db(Jar, ResolverJar, SourceJar)]
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
@@ -63,7 +30,7 @@ pub(crate) mod tests {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: vendored_typeshed_stubs().snapshot(),
|
||||
vendored: vendored_typeshed_stubs().clone(),
|
||||
events: std::sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
}
|
||||
@@ -99,6 +66,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
@@ -122,35 +90,16 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn ResolverDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn ResolverDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn ResolverDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl red_knot_module_resolver::Db for TestDb {}
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: salsa::Event) {
|
||||
tracing::trace!("event: {:?}", event.debug(self));
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
impl salsa::ParallelDatabase for TestDb {
|
||||
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||
salsa::Snapshot::new(Self {
|
||||
storage: self.storage.snapshot(),
|
||||
files: self.files.snapshot(),
|
||||
system: self.system.snapshot(),
|
||||
vendored: self.vendored.snapshot(),
|
||||
events: self.events.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,21 @@ use std::hash::BuildHasherDefault;
|
||||
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
pub use db::{Db, Jar};
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, vendored_typeshed_stubs};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings};
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod builtins;
|
||||
mod db;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
mod program;
|
||||
mod python_version;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub mod types;
|
||||
|
||||
@@ -42,7 +42,7 @@ impl ModuleName {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_module_resolver::ModuleName;
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert_eq!(ModuleName::new_static("foo.bar").as_deref(), Some("foo.bar"));
|
||||
/// assert_eq!(ModuleName::new_static(""), None);
|
||||
@@ -68,7 +68,7 @@ impl ModuleName {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_module_resolver::ModuleName;
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().components().collect::<Vec<_>>(), vec!["foo", "bar", "baz"]);
|
||||
/// ```
|
||||
@@ -82,7 +82,7 @@ impl ModuleName {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_module_resolver::ModuleName;
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert_eq!(ModuleName::new_static("foo.bar").unwrap().parent(), Some(ModuleName::new_static("foo").unwrap()));
|
||||
/// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().parent(), Some(ModuleName::new_static("foo.bar").unwrap()));
|
||||
@@ -101,7 +101,7 @@ impl ModuleName {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_module_resolver::ModuleName;
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert!(ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap()));
|
||||
///
|
||||
@@ -133,7 +133,7 @@ impl ModuleName {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_module_resolver::ModuleName;
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert_eq!(&*ModuleName::from_components(["a"]).unwrap(), "a");
|
||||
/// assert_eq!(&*ModuleName::from_components(["a", "b"]).unwrap(), "a.b");
|
||||
@@ -1,19 +1,14 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
pub use db::{Db, Jar};
|
||||
pub use module::{Module, ModuleKind};
|
||||
pub use module_name::ModuleName;
|
||||
pub(crate) use module::Module;
|
||||
pub use resolver::resolve_module;
|
||||
use ruff_db::system::SystemPath;
|
||||
pub use typeshed::{
|
||||
vendored_typeshed_stubs, TypeshedVersionsParseError, TypeshedVersionsParseErrorKind,
|
||||
};
|
||||
pub use typeshed::vendored_typeshed_stubs;
|
||||
|
||||
use crate::resolver::{module_resolution_settings, SearchPathIterator};
|
||||
use crate::Db;
|
||||
use resolver::{module_resolution_settings, SearchPathIterator};
|
||||
|
||||
mod db;
|
||||
mod module;
|
||||
mod module_name;
|
||||
mod path;
|
||||
mod resolver;
|
||||
mod state;
|
||||
@@ -3,9 +3,8 @@ use std::sync::Arc;
|
||||
|
||||
use ruff_db::files::File;
|
||||
|
||||
use crate::db::Db;
|
||||
use super::path::SearchPath;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::path::ModuleSearchPath;
|
||||
|
||||
/// Representation of a Python module.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
@@ -17,7 +16,7 @@ impl Module {
|
||||
pub(crate) fn new(
|
||||
name: ModuleName,
|
||||
kind: ModuleKind,
|
||||
search_path: ModuleSearchPath,
|
||||
search_path: SearchPath,
|
||||
file: File,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -41,7 +40,7 @@ impl Module {
|
||||
}
|
||||
|
||||
/// The search path from which the module was resolved.
|
||||
pub(crate) fn search_path(&self) -> &ModuleSearchPath {
|
||||
pub(crate) fn search_path(&self) -> &SearchPath {
|
||||
&self.inner.search_path
|
||||
}
|
||||
|
||||
@@ -62,22 +61,11 @@ impl std::fmt::Debug for Module {
|
||||
}
|
||||
}
|
||||
|
||||
impl salsa::DebugWithDb<dyn Db> for Module {
|
||||
fn fmt(&self, f: &mut Formatter<'_>, db: &dyn Db) -> std::fmt::Result {
|
||||
f.debug_struct("Module")
|
||||
.field("name", &self.name())
|
||||
.field("kind", &self.kind())
|
||||
.field("file", &self.file().debug(db.upcast()))
|
||||
.field("search_path", &self.search_path())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct ModuleInner {
|
||||
name: ModuleName,
|
||||
kind: ModuleKind,
|
||||
search_path: ModuleSearchPath,
|
||||
search_path: SearchPath,
|
||||
file: File,
|
||||
}
|
||||
|
||||
1098
crates/red_knot_python_semantic/src/module_resolver/path.rs
Normal file
1098
crates/red_knot_python_semantic/src/module_resolver/path.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,22 @@
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use ruff_db::files::{File, FilePath};
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::system::{DirectoryEntry, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
use super::state::ResolverState;
|
||||
use crate::db::Db;
|
||||
use crate::module::{Module, ModuleKind};
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::path::{ModulePathBuf, ModuleSearchPath, SearchPathValidationError};
|
||||
use crate::state::ResolverState;
|
||||
use crate::{Program, PythonVersion, SearchPathSettings};
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
|
||||
let interned_name = internal::ModuleNameIngredient::new(db, module_name);
|
||||
let interned_name = ModuleNameIngredient::new(db, module_name);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
}
|
||||
@@ -28,15 +28,23 @@ pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn resolve_module_query<'db>(
|
||||
db: &'db dyn Db,
|
||||
module_name: internal::ModuleNameIngredient<'db>,
|
||||
module_name: ModuleNameIngredient<'db>,
|
||||
) -> Option<Module> {
|
||||
let name = module_name.name(db);
|
||||
let _span = tracing::trace_span!("resolve_module", %name).entered();
|
||||
|
||||
let (search_path, module_file, kind) = resolve_name(db, name)?;
|
||||
let Some((search_path, module_file, kind)) = resolve_name(db, name) else {
|
||||
tracing::debug!("Module '{name}' not found in the search paths.");
|
||||
return None;
|
||||
};
|
||||
|
||||
let module = Module::new(name.clone(), kind, search_path, module_file);
|
||||
|
||||
tracing::debug!(
|
||||
"Resolved module '{name}' to '{path}'.",
|
||||
path = module_file.path(db)
|
||||
);
|
||||
|
||||
Some(module)
|
||||
}
|
||||
|
||||
@@ -57,6 +65,12 @@ pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option<Module> {
|
||||
file_to_module(db, file)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum SystemOrVendoredPathRef<'a> {
|
||||
System(&'a SystemPath),
|
||||
Vendored(&'a VendoredPath),
|
||||
}
|
||||
|
||||
/// Resolves the module for the file with the given id.
|
||||
///
|
||||
/// Returns `None` if the file is not a module locatable via any of the known search paths.
|
||||
@@ -64,7 +78,11 @@ pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option<Module> {
|
||||
pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
let _span = tracing::trace_span!("file_to_module", ?file).entered();
|
||||
|
||||
let path = file.path(db.upcast());
|
||||
let path = match file.path(db.upcast()) {
|
||||
FilePath::System(system) => SystemOrVendoredPathRef::System(system),
|
||||
FilePath::Vendored(vendored) => SystemOrVendoredPathRef::Vendored(vendored),
|
||||
FilePath::SystemVirtual(_) => return None,
|
||||
};
|
||||
|
||||
let settings = module_resolution_settings(db);
|
||||
|
||||
@@ -72,7 +90,11 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
|
||||
let module_name = loop {
|
||||
let candidate = search_paths.next()?;
|
||||
if let Some(relative_path) = candidate.relativize_path(path) {
|
||||
let relative_path = match path {
|
||||
SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path),
|
||||
SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path),
|
||||
};
|
||||
if let Some(relative_path) = relative_path {
|
||||
break relative_path.to_module_name()?;
|
||||
}
|
||||
};
|
||||
@@ -110,43 +132,58 @@ fn try_resolve_module_resolution_settings(
|
||||
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
workspace_root,
|
||||
src_root,
|
||||
custom_typeshed,
|
||||
site_packages,
|
||||
} = program.search_paths(db.upcast());
|
||||
|
||||
if !extra_paths.is_empty() {
|
||||
tracing::info!("Extra search paths: {extra_paths:?}");
|
||||
}
|
||||
|
||||
if let Some(custom_typeshed) = custom_typeshed {
|
||||
tracing::info!("Custom typeshed directory: {custom_typeshed}");
|
||||
}
|
||||
|
||||
if !extra_paths.is_empty() {
|
||||
tracing::info!("extra search paths: {extra_paths:?}");
|
||||
}
|
||||
|
||||
let system = db.system();
|
||||
let files = db.files();
|
||||
|
||||
let mut static_search_paths = vec![];
|
||||
|
||||
for path in extra_paths {
|
||||
static_search_paths.push(ModuleSearchPath::extra(system, path.to_owned())?);
|
||||
let search_path = SearchPath::extra(system, path.clone())?;
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
search_path.as_system_path().unwrap(),
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
static_search_paths.push(search_path);
|
||||
}
|
||||
|
||||
static_search_paths.push(ModuleSearchPath::first_party(
|
||||
system,
|
||||
workspace_root.to_owned(),
|
||||
)?);
|
||||
static_search_paths.push(SearchPath::first_party(system, src_root.clone())?);
|
||||
|
||||
static_search_paths.push(if let Some(custom_typeshed) = custom_typeshed.as_ref() {
|
||||
ModuleSearchPath::custom_stdlib(db, custom_typeshed.to_owned())?
|
||||
let search_path = SearchPath::custom_stdlib(db, custom_typeshed.clone())?;
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
search_path.as_system_path().unwrap(),
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
search_path
|
||||
} else {
|
||||
ModuleSearchPath::vendored_stdlib()
|
||||
SearchPath::vendored_stdlib()
|
||||
});
|
||||
|
||||
if let Some(site_packages) = site_packages {
|
||||
static_search_paths.push(ModuleSearchPath::site_packages(
|
||||
system,
|
||||
site_packages.to_owned(),
|
||||
)?);
|
||||
let mut site_packages_paths: Vec<_> = Vec::with_capacity(site_packages.len());
|
||||
|
||||
for path in site_packages {
|
||||
let search_path = SearchPath::site_packages(system, path.to_path_buf())?;
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
search_path.as_system_path().unwrap(),
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
site_packages_paths.push(search_path);
|
||||
}
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
|
||||
@@ -174,6 +211,7 @@ fn try_resolve_module_resolution_settings(
|
||||
Ok(ModuleResolutionSettings {
|
||||
target_version,
|
||||
static_search_paths,
|
||||
site_packages_paths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,35 +221,58 @@ pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSetting
|
||||
try_resolve_module_resolution_settings(db).unwrap()
|
||||
}
|
||||
|
||||
/// Collect all dynamic search paths:
|
||||
/// search paths listed in `.pth` files in the `site-packages` directory
|
||||
/// due to editable installations of third-party packages.
|
||||
/// 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
|
||||
/// due to editable installations of third-party packages.
|
||||
///
|
||||
/// The editable-install search paths for the first `site-packages` directory
|
||||
/// should come between the two `site-packages` directories when it comes to
|
||||
/// module-resolution priority.
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn editable_install_resolution_paths(db: &dyn Db) -> Vec<ModuleSearchPath> {
|
||||
// This query needs to be re-executed each time a `.pth` file
|
||||
// is added, modified or removed from the `site-packages` directory.
|
||||
// However, we don't use Salsa queries to read the source text of `.pth` files;
|
||||
// we use the APIs on the `System` trait directly. As such, for now we simply ask
|
||||
// Salsa to recompute this query on each new revision.
|
||||
//
|
||||
// TODO: add some kind of watcher for the `site-packages` directory that looks
|
||||
// for `site-packages/*.pth` files being added/modified/removed; get rid of this.
|
||||
// When doing so, also make the test
|
||||
// `deleting_pth_file_on_which_module_resolution_depends_invalidates_cache()`
|
||||
// more principled!
|
||||
db.report_untracked_read();
|
||||
pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
let ModuleResolutionSettings {
|
||||
target_version: _,
|
||||
static_search_paths,
|
||||
site_packages_paths,
|
||||
} = module_resolution_settings(db);
|
||||
|
||||
let static_search_paths = &module_resolution_settings(db).static_search_paths;
|
||||
let site_packages = static_search_paths
|
||||
let mut dynamic_paths = Vec::new();
|
||||
|
||||
if site_packages_paths.is_empty() {
|
||||
return dynamic_paths;
|
||||
}
|
||||
|
||||
let mut existing_paths: FxHashSet<_> = static_search_paths
|
||||
.iter()
|
||||
.find(|path| path.is_site_packages());
|
||||
.filter_map(|path| path.as_system_path())
|
||||
.map(Cow::Borrowed)
|
||||
.collect();
|
||||
|
||||
let mut dynamic_paths = Vec::default();
|
||||
let files = db.files();
|
||||
let system = db.system();
|
||||
|
||||
if let Some(site_packages) = site_packages {
|
||||
let site_packages = site_packages
|
||||
for site_packages_search_path in site_packages_paths {
|
||||
let site_packages_dir = site_packages_search_path
|
||||
.as_system_path()
|
||||
.expect("Expected site-packages never to be a VendoredPath!");
|
||||
.expect("Expected site package path to be a system path");
|
||||
|
||||
if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let site_packages_root = files
|
||||
.root(db.upcast(), site_packages_dir)
|
||||
.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.
|
||||
// However, we don't use Salsa queries to read the source text of `.pth` files;
|
||||
// we use the APIs on the `System` trait directly. As such, add a dependency on the
|
||||
// site-package directory's revision.
|
||||
site_packages_root.revision(db.upcast());
|
||||
|
||||
dynamic_paths.push(site_packages_search_path.clone());
|
||||
|
||||
// As well as modules installed directly into `site-packages`,
|
||||
// the directory may also contain `.pth` files.
|
||||
@@ -219,30 +280,34 @@ pub(crate) fn editable_install_resolution_paths(db: &dyn Db) -> Vec<ModuleSearch
|
||||
// containing a (relative or absolute) path.
|
||||
// Each of these paths may point to an editable install of a package,
|
||||
// so should be considered an additional search path.
|
||||
let Ok(pth_file_iterator) = PthFileIterator::new(db, site_packages) else {
|
||||
return dynamic_paths;
|
||||
let pth_file_iterator = match PthFileIterator::new(db, site_packages_dir) {
|
||||
Ok(iterator) => iterator,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"Failed to search for editable installation in {site_packages_dir}: {error}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// The Python documentation specifies that `.pth` files in `site-packages`
|
||||
// are processed in alphabetical order, so collecting and then sorting is necessary.
|
||||
// https://docs.python.org/3/library/site.html#module-site
|
||||
let mut all_pth_files: Vec<PthFile> = pth_file_iterator.collect();
|
||||
all_pth_files.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
all_pth_files.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
|
||||
let mut existing_paths: FxHashSet<_> = static_search_paths
|
||||
.iter()
|
||||
.filter_map(|path| path.as_system_path())
|
||||
.map(Cow::Borrowed)
|
||||
.collect();
|
||||
let installations = all_pth_files.iter().flat_map(PthFile::items);
|
||||
|
||||
dynamic_paths.reserve(all_pth_files.len());
|
||||
for installation in installations {
|
||||
if existing_paths.insert(Cow::Owned(installation.clone())) {
|
||||
match SearchPath::editable(system, installation) {
|
||||
Ok(search_path) => {
|
||||
dynamic_paths.push(search_path);
|
||||
}
|
||||
|
||||
for pth_file in &all_pth_files {
|
||||
for installation in pth_file.editable_installations() {
|
||||
if existing_paths.insert(Cow::Owned(
|
||||
installation.as_system_path().unwrap().to_path_buf(),
|
||||
)) {
|
||||
dynamic_paths.push(installation);
|
||||
Err(error) => {
|
||||
tracing::debug!("Skipping editable installation: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,12 +325,12 @@ pub(crate) fn editable_install_resolution_paths(db: &dyn Db) -> Vec<ModuleSearch
|
||||
/// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
|
||||
pub(crate) struct SearchPathIterator<'db> {
|
||||
db: &'db dyn Db,
|
||||
static_paths: std::slice::Iter<'db, ModuleSearchPath>,
|
||||
dynamic_paths: Option<std::slice::Iter<'db, ModuleSearchPath>>,
|
||||
static_paths: std::slice::Iter<'db, SearchPath>,
|
||||
dynamic_paths: Option<std::slice::Iter<'db, SearchPath>>,
|
||||
}
|
||||
|
||||
impl<'db> Iterator for SearchPathIterator<'db> {
|
||||
type Item = &'db ModuleSearchPath;
|
||||
type Item = &'db SearchPath;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let SearchPathIterator {
|
||||
@@ -276,7 +341,7 @@ impl<'db> Iterator for SearchPathIterator<'db> {
|
||||
|
||||
static_paths.next().or_else(|| {
|
||||
dynamic_paths
|
||||
.get_or_insert_with(|| editable_install_resolution_paths(*db).iter())
|
||||
.get_or_insert_with(|| dynamic_resolution_paths(*db).iter())
|
||||
.next()
|
||||
})
|
||||
}
|
||||
@@ -288,7 +353,6 @@ impl<'db> FusedIterator for SearchPathIterator<'db> {}
|
||||
/// One or more lines in a `.pth` file may be a (relative or absolute)
|
||||
/// path that represents an editable installation of a package.
|
||||
struct PthFile<'db> {
|
||||
system: &'db dyn System,
|
||||
path: SystemPathBuf,
|
||||
contents: String,
|
||||
site_packages: &'db SystemPath,
|
||||
@@ -297,9 +361,8 @@ struct PthFile<'db> {
|
||||
impl<'db> PthFile<'db> {
|
||||
/// Yield paths in this `.pth` file that appear to represent editable installations,
|
||||
/// and should therefore be added as module-resolution search paths.
|
||||
fn editable_installations(&'db self) -> impl Iterator<Item = ModuleSearchPath> + 'db {
|
||||
fn items(&'db self) -> impl Iterator<Item = SystemPathBuf> + 'db {
|
||||
let PthFile {
|
||||
system,
|
||||
path: _,
|
||||
contents,
|
||||
site_packages,
|
||||
@@ -318,8 +381,8 @@ impl<'db> PthFile<'db> {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let possible_editable_install = SystemPath::absolute(line, site_packages);
|
||||
ModuleSearchPath::editable(*system, possible_editable_install).ok()
|
||||
|
||||
Some(SystemPath::absolute(line, site_packages))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -368,12 +431,15 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(contents) = db.system().read_to_string(&path) else {
|
||||
continue;
|
||||
let contents = match system.read_to_string(&path) {
|
||||
Ok(contents) => contents,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to read .pth file '{path}': {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
return Some(PthFile {
|
||||
system,
|
||||
path,
|
||||
contents,
|
||||
site_packages,
|
||||
@@ -385,17 +451,23 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
/// Validated and normalized module-resolution settings.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ModuleResolutionSettings {
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
/// These shouldn't ever change unless the config settings themselves change.
|
||||
///
|
||||
/// Note that `site-packages` *is included* as a search path in this sequence,
|
||||
/// but it is also stored separately so that we're able to find editable installs later.
|
||||
static_search_paths: Vec<ModuleSearchPath>,
|
||||
static_search_paths: Vec<SearchPath>,
|
||||
|
||||
/// site-packages paths are not included in the above field:
|
||||
/// if there are multiple site-packages paths, editable installations can appear
|
||||
/// *between* the site-packages paths on `sys.path` at runtime.
|
||||
/// That means we can't know where a second or third `site-packages` path should sit
|
||||
/// in terms of module-resolution priority until we've discovered the editable installs
|
||||
/// for the first `site-packages` path
|
||||
site_packages_paths: Vec<SearchPath>,
|
||||
}
|
||||
|
||||
impl ModuleResolutionSettings {
|
||||
fn target_version(&self) -> TargetVersion {
|
||||
fn target_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
}
|
||||
|
||||
@@ -408,81 +480,33 @@ impl ModuleResolutionSettings {
|
||||
}
|
||||
}
|
||||
|
||||
// The singleton methods generated by salsa are all `pub` instead of `pub(crate)` which triggers
|
||||
// `unreachable_pub`. Work around this by creating a module and allow `unreachable_pub` for it.
|
||||
// Salsa also generates uses to `_db` variables for `interned` which triggers `clippy::used_underscore_binding`. Suppress that too
|
||||
// TODO(micha): Contribute a fix for this upstream where the singleton methods have the same visibility as the struct.
|
||||
#[allow(unreachable_pub, clippy::used_underscore_binding)]
|
||||
pub(crate) mod internal {
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
/// A thin wrapper around `ModuleName` to make it a Salsa ingredient.
|
||||
///
|
||||
/// This is needed because Salsa requires that all query arguments are salsa ingredients.
|
||||
#[salsa::interned]
|
||||
pub(crate) struct ModuleNameIngredient<'db> {
|
||||
#[return_ref]
|
||||
pub(super) name: ModuleName,
|
||||
}
|
||||
/// A thin wrapper around `ModuleName` to make it a Salsa ingredient.
|
||||
///
|
||||
/// This is needed because Salsa requires that all query arguments are salsa ingredients.
|
||||
#[salsa::interned]
|
||||
struct ModuleNameIngredient<'db> {
|
||||
#[return_ref]
|
||||
pub(super) name: ModuleName,
|
||||
}
|
||||
|
||||
/// Modules that are builtin to the Python interpreter itself.
|
||||
///
|
||||
/// When these module names are imported, standard module resolution is bypassed:
|
||||
/// the module name always resolves to the stdlib module,
|
||||
/// even if there's a module of the same name in the workspace root
|
||||
/// (which would normally result in the stdlib module being overridden).
|
||||
///
|
||||
/// TODO(Alex): write a script to generate this list,
|
||||
/// similar to what we do in `crates/ruff_python_stdlib/src/sys.rs`
|
||||
static BUILTIN_MODULES: Lazy<FxHashSet<&str>> = Lazy::new(|| {
|
||||
const BUILTIN_MODULE_NAMES: &[&str] = &[
|
||||
"_abc",
|
||||
"_ast",
|
||||
"_codecs",
|
||||
"_collections",
|
||||
"_functools",
|
||||
"_imp",
|
||||
"_io",
|
||||
"_locale",
|
||||
"_operator",
|
||||
"_signal",
|
||||
"_sre",
|
||||
"_stat",
|
||||
"_string",
|
||||
"_symtable",
|
||||
"_thread",
|
||||
"_tokenize",
|
||||
"_tracemalloc",
|
||||
"_typing",
|
||||
"_warnings",
|
||||
"_weakref",
|
||||
"atexit",
|
||||
"builtins",
|
||||
"errno",
|
||||
"faulthandler",
|
||||
"gc",
|
||||
"itertools",
|
||||
"marshal",
|
||||
"posix",
|
||||
"pwd",
|
||||
"sys",
|
||||
"time",
|
||||
];
|
||||
BUILTIN_MODULE_NAMES.iter().copied().collect()
|
||||
});
|
||||
|
||||
/// Given a module name and a list of search paths in which to lookup modules,
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(ModuleSearchPath, File, ModuleKind)> {
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, ModuleKind)> {
|
||||
let resolver_settings = module_resolution_settings(db);
|
||||
let resolver_state = ResolverState::new(db, resolver_settings.target_version());
|
||||
let is_builtin_module = BUILTIN_MODULES.contains(&name.as_str());
|
||||
let target_version = resolver_settings.target_version();
|
||||
let resolver_state = ResolverState::new(db, target_version);
|
||||
let is_builtin_module =
|
||||
ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str());
|
||||
|
||||
for search_path in resolver_settings.search_paths(db) {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
// the module name always resolves to the stdlib module,
|
||||
// even if there's a module of the same name in the first-party root
|
||||
// (which would normally result in the stdlib module being overridden).
|
||||
if is_builtin_module && !search_path.is_standard_library() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut components = name.components();
|
||||
let module_name = components.next_back()?;
|
||||
|
||||
@@ -493,7 +517,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(ModuleSearchPath, Fil
|
||||
package_path.push(module_name);
|
||||
|
||||
// Must be a `__init__.pyi` or `__init__.py` or it isn't a package.
|
||||
let kind = if package_path.is_directory(search_path, &resolver_state) {
|
||||
let kind = if package_path.is_directory(&resolver_state) {
|
||||
package_path.push("__init__");
|
||||
ModuleKind::Package
|
||||
} else {
|
||||
@@ -501,16 +525,13 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(ModuleSearchPath, Fil
|
||||
};
|
||||
|
||||
// TODO Implement full https://peps.python.org/pep-0561/#type-checker-module-resolution-order resolution
|
||||
if let Some(stub) = package_path
|
||||
.with_pyi_extension()
|
||||
.to_file(search_path, &resolver_state)
|
||||
{
|
||||
if let Some(stub) = package_path.with_pyi_extension().to_file(&resolver_state) {
|
||||
return Some((search_path.clone(), stub, kind));
|
||||
}
|
||||
|
||||
if let Some(module) = package_path
|
||||
.with_py_extension()
|
||||
.and_then(|path| path.to_file(search_path, &resolver_state))
|
||||
.and_then(|path| path.to_file(&resolver_state))
|
||||
{
|
||||
return Some((search_path.clone(), module, kind));
|
||||
}
|
||||
@@ -534,14 +555,14 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(ModuleSearchPath, Fil
|
||||
}
|
||||
|
||||
fn resolve_package<'a, 'db, I>(
|
||||
module_search_path: &ModuleSearchPath,
|
||||
module_search_path: &SearchPath,
|
||||
components: I,
|
||||
resolver_state: &ResolverState<'db>,
|
||||
) -> Result<ResolvedPackage, PackageKind>
|
||||
where
|
||||
I: Iterator<Item = &'a str>,
|
||||
{
|
||||
let mut package_path = module_search_path.as_module_path().clone();
|
||||
let mut package_path = module_search_path.to_module_path();
|
||||
|
||||
// `true` if inside a folder that is a namespace package (has no `__init__.py`).
|
||||
// Namespace packages are special because they can be spread across multiple search paths.
|
||||
@@ -555,12 +576,11 @@ where
|
||||
for folder in components {
|
||||
package_path.push(folder);
|
||||
|
||||
let is_regular_package =
|
||||
package_path.is_regular_package(module_search_path, resolver_state);
|
||||
let is_regular_package = package_path.is_regular_package(resolver_state);
|
||||
|
||||
if is_regular_package {
|
||||
in_namespace_package = false;
|
||||
} else if package_path.is_directory(module_search_path, resolver_state) {
|
||||
} else if package_path.is_directory(resolver_state) {
|
||||
// A directory without an `__init__.py` is a namespace package, continue with the next folder.
|
||||
in_namespace_package = true;
|
||||
} else if in_namespace_package {
|
||||
@@ -593,7 +613,7 @@ where
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ResolvedPackage {
|
||||
path: ModulePathBuf,
|
||||
path: ModulePath,
|
||||
kind: PackageKind,
|
||||
}
|
||||
|
||||
@@ -621,16 +641,17 @@ impl PackageKind {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use internal::ModuleNameIngredient;
|
||||
use ruff_db::files::{system_path_to_file, File, FilePath};
|
||||
use ruff_db::system::{DbWithTestSystem, OsSystem, SystemPath};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::testing::{
|
||||
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
|
||||
};
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::module::ModuleKind;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::module_resolver::module::ModuleKind;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -685,7 +706,7 @@ mod tests {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
@@ -703,7 +724,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -756,7 +777,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
@@ -801,7 +822,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
@@ -845,7 +866,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&[
|
||||
@@ -887,7 +908,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
@@ -911,7 +932,7 @@ mod tests {
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -935,7 +956,7 @@ mod tests {
|
||||
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_vendored_typeshed()
|
||||
.with_target_version(TargetVersion::default())
|
||||
.with_target_version(PythonVersion::default())
|
||||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
@@ -1149,12 +1170,21 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn symlink() -> anyhow::Result<()> {
|
||||
use ruff_db::program::Program;
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::program::Program;
|
||||
use ruff_db::system::{OsSystem, SystemPath};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let root = SystemPath::from_std_path(temp_dir.path()).unwrap();
|
||||
let root = temp_dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.context("Failed to canonicalize temp dir")?;
|
||||
let root = SystemPath::from_std_path(&root).unwrap();
|
||||
db.use_system(OsSystem::new(root));
|
||||
|
||||
let src = root.join("src");
|
||||
@@ -1174,12 +1204,12 @@ mod tests {
|
||||
|
||||
let search_paths = SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages],
|
||||
};
|
||||
|
||||
Program::new(&db, TargetVersion::Py38, search_paths);
|
||||
Program::new(&db, PythonVersion::PY38, search_paths);
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module = resolve_module(&db, ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
@@ -1213,7 +1243,7 @@ mod tests {
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
@@ -1279,6 +1309,7 @@ mod tests {
|
||||
db.memory_file_system()
|
||||
.remove_directory(foo_init_path.parent().unwrap())?;
|
||||
File::sync_path(&mut db, &foo_init_path);
|
||||
File::sync_path(&mut db, foo_init_path.parent().unwrap());
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve");
|
||||
assert_eq!(&src.join("foo.py"), foo_module.file().path(&db));
|
||||
@@ -1300,7 +1331,7 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1309,7 +1340,7 @@ mod tests {
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
Ok(functools_module.file()),
|
||||
system_path_to_file(&db, &stdlib_functools_path)
|
||||
);
|
||||
|
||||
@@ -1321,15 +1352,15 @@ mod tests {
|
||||
.unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let events = db.take_salsa_events();
|
||||
assert_function_query_was_not_run::<resolve_module_query, _, _>(
|
||||
assert_function_query_was_not_run(
|
||||
&db,
|
||||
|res| &res.function,
|
||||
&ModuleNameIngredient::new(&db, functools_module_name.clone()),
|
||||
resolve_module_query,
|
||||
ModuleNameIngredient::new(&db, functools_module_name.clone()),
|
||||
&events,
|
||||
);
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
Ok(functools_module.file()),
|
||||
system_path_to_file(&db, &stdlib_functools_path)
|
||||
);
|
||||
}
|
||||
@@ -1348,14 +1379,14 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
Ok(functools_module.file()),
|
||||
system_path_to_file(&db, stdlib.join("functools.pyi"))
|
||||
);
|
||||
|
||||
@@ -1366,7 +1397,7 @@ mod tests {
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &src);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
Ok(functools_module.file()),
|
||||
system_path_to_file(&db, &src_functools_path)
|
||||
);
|
||||
}
|
||||
@@ -1388,7 +1419,7 @@ mod tests {
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1397,7 +1428,7 @@ mod tests {
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &src);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
Ok(functools_module.file()),
|
||||
system_path_to_file(&db, &src_functools_path)
|
||||
);
|
||||
|
||||
@@ -1410,7 +1441,7 @@ mod tests {
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
Ok(functools_module.file()),
|
||||
system_path_to_file(&db, stdlib.join("functools.pyi"))
|
||||
);
|
||||
}
|
||||
@@ -1573,12 +1604,7 @@ not_a_directory
|
||||
&FilePath::system("/y/src/bar.py")
|
||||
);
|
||||
let events = db.take_salsa_events();
|
||||
assert_function_query_was_not_run::<editable_install_resolution_paths, _, _>(
|
||||
&db,
|
||||
|res| &res.function,
|
||||
&(),
|
||||
&events,
|
||||
);
|
||||
assert_const_function_query_was_not_run(&db, dynamic_resolution_paths, &events);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1607,18 +1633,7 @@ not_a_directory
|
||||
.remove_file(site_packages.join("_foo.pth"))
|
||||
.unwrap();
|
||||
|
||||
// Why are we touching a random file in the path that's been editably installed,
|
||||
// rather than the `.pth` file, when the `.pth` file is the one that has been deleted?
|
||||
// It's because the `.pth` file isn't directly tracked as a dependency by Salsa
|
||||
// currently (we don't use `system_path_to_file()` to get the file, and we don't use
|
||||
// `source_text()` to read the source of the file). Instead of using these APIs which
|
||||
// would automatically add the existence and contents of the file as a Salsa-tracked
|
||||
// dependency, we use `.report_untracked_read()` to force Salsa to re-parse all
|
||||
// `.pth` files on each new "revision". Making a random modification to a tracked
|
||||
// Salsa file forces a new revision.
|
||||
//
|
||||
// TODO: get rid of the `.report_untracked_read()` call...
|
||||
File::sync_path(&mut db, SystemPath::new("/x/src/foo.py"));
|
||||
File::sync_path(&mut db, &site_packages.join("_foo.pth"));
|
||||
|
||||
assert_eq!(resolve_module(&db, foo_module_name.clone()), None);
|
||||
}
|
||||
@@ -1658,13 +1673,62 @@ not_a_directory
|
||||
.with_site_packages_files(&[("_foo.pth", "/src")])
|
||||
.build();
|
||||
|
||||
let search_paths: Vec<&ModuleSearchPath> =
|
||||
let search_paths: Vec<&SearchPath> =
|
||||
module_resolution_settings(&db).search_paths(&db).collect();
|
||||
|
||||
assert!(
|
||||
search_paths.contains(&&ModuleSearchPath::first_party(db.system(), "/src").unwrap())
|
||||
assert!(search_paths.contains(
|
||||
&&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap()
|
||||
));
|
||||
assert!(!search_paths
|
||||
.contains(&&SearchPath::editable(db.system(), SystemPathBuf::from("/src")).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_site_packages_with_editables() {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let venv_site_packages = SystemPathBuf::from("/venv-site-packages");
|
||||
let site_packages_pth = venv_site_packages.join("foo.pth");
|
||||
let system_site_packages = SystemPathBuf::from("/system-site-packages");
|
||||
let editable_install_location = SystemPathBuf::from("/x/y/a.py");
|
||||
let system_site_packages_location = system_site_packages.join("a.py");
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all("/src")
|
||||
.unwrap();
|
||||
db.write_files([
|
||||
(&site_packages_pth, "/x/y"),
|
||||
(&editable_install_location, ""),
|
||||
(&system_site_packages_location, ""),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
Program::new(
|
||||
&db,
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![venv_site_packages, system_site_packages],
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!search_paths.contains(&&ModuleSearchPath::editable(db.system(), "/src").unwrap()));
|
||||
// The editable installs discovered from the `.pth` file in the first `site-packages` directory
|
||||
// take precedence over the second `site-packages` directory...
|
||||
let a_module_name = ModuleName::new_static("a").unwrap();
|
||||
let a_module = resolve_module(&db, a_module_name.clone()).unwrap();
|
||||
assert_eq!(a_module.file().path(&db), &editable_install_location);
|
||||
|
||||
db.memory_file_system()
|
||||
.remove_file(&site_packages_pth)
|
||||
.unwrap();
|
||||
File::sync_path(&mut db, &site_packages_pth);
|
||||
|
||||
// ...But now that the `.pth` file in the first `site-packages` directory has been deleted,
|
||||
// the editable install no longer exists, so the module now resolves to the file in the
|
||||
// second `site-packages` directory
|
||||
let a_module = resolve_module(&db, a_module_name).unwrap();
|
||||
assert_eq!(a_module.file().path(&db), &system_site_packages_location);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
||||
use super::typeshed::LazyTypeshedVersions;
|
||||
use crate::db::Db;
|
||||
use crate::typeshed::LazyTypeshedVersions;
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
pub(crate) struct ResolverState<'db> {
|
||||
pub(crate) db: &'db dyn Db,
|
||||
pub(crate) typeshed_versions: LazyTypeshedVersions<'db>,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl<'db> ResolverState<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, target_version: TargetVersion) -> Self {
|
||||
pub(crate) fn new(db: &'db dyn Db, target_version: PythonVersion) -> Self {
|
||||
Self {
|
||||
db,
|
||||
typeshed_versions: LazyTypeshedVersions::new(),
|
||||
@@ -20,10 +19,6 @@ impl<'db> ResolverState<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn system(&self) -> &dyn System {
|
||||
self.db.system()
|
||||
}
|
||||
|
||||
pub(crate) fn vendored(&self) -> &VendoredFileSystem {
|
||||
self.db.vendored()
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPathBuf;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
/// A test case for the module resolver.
|
||||
///
|
||||
@@ -12,8 +13,11 @@ pub(crate) struct TestCase<T> {
|
||||
pub(crate) db: TestDb,
|
||||
pub(crate) src: SystemPathBuf,
|
||||
pub(crate) stdlib: T,
|
||||
// Most test cases only ever need a single `site-packages` directory,
|
||||
// so this is a single directory instead of a `Vec` of directories,
|
||||
// like it is in `ruff_db::Program`.
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
/// A `(file_name, file_contents)` tuple
|
||||
@@ -95,7 +99,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// to `()`.
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
@@ -114,7 +118,7 @@ impl<T> TestCaseBuilder<T> {
|
||||
}
|
||||
|
||||
/// Specify the target Python version the module resolver should assume
|
||||
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
|
||||
pub(crate) fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self
|
||||
}
|
||||
@@ -141,7 +145,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
@@ -221,9 +225,9 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
target_version,
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -274,9 +278,9 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
target_version,
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub use self::vendored::vendored_typeshed_stubs;
|
||||
pub(crate) use self::versions::{
|
||||
parse_typeshed_versions, LazyTypeshedVersions, TypeshedVersionsQueryResult,
|
||||
pub(super) use self::versions::{
|
||||
parse_typeshed_versions, LazyTypeshedVersions, TypeshedVersionsParseError,
|
||||
TypeshedVersionsQueryResult,
|
||||
};
|
||||
pub use self::versions::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind};
|
||||
|
||||
mod vendored;
|
||||
mod versions;
|
||||
@@ -6,16 +6,15 @@ use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::system::SystemPath;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LazyTypeshedVersions<'db>(OnceCell<&'db TypeshedVersions>);
|
||||
@@ -44,7 +43,7 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
db: &'db dyn Db,
|
||||
module: &ModuleName,
|
||||
stdlib_root: Option<&SystemPath>,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
let versions = self.0.get_or_init(|| {
|
||||
let versions_path = if let Some(system_path) = stdlib_root {
|
||||
@@ -52,7 +51,7 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
} else {
|
||||
return &VENDORED_VERSIONS;
|
||||
};
|
||||
let Some(versions_file) = system_path_to_file(db.upcast(), &versions_path) else {
|
||||
let Ok(versions_file) = system_path_to_file(db.upcast(), &versions_path) else {
|
||||
todo!(
|
||||
"Still need to figure out how to handle VERSIONS files being deleted \
|
||||
from custom typeshed directories! Expected a file to exist at {versions_path}"
|
||||
@@ -64,7 +63,7 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
// Unwrapping here is not correct...
|
||||
parse_typeshed_versions(db, versions_file).as_ref().unwrap()
|
||||
});
|
||||
versions.query_module(module, PyVersion::from(target_version))
|
||||
versions.query_module(module, target_version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +89,7 @@ static VENDORED_VERSIONS: Lazy<TypeshedVersions> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TypeshedVersionsParseError {
|
||||
pub(crate) struct TypeshedVersionsParseError {
|
||||
line_number: Option<NonZeroU16>,
|
||||
reason: TypeshedVersionsParseErrorKind,
|
||||
}
|
||||
@@ -123,7 +122,7 @@ impl std::error::Error for TypeshedVersionsParseError {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum TypeshedVersionsParseErrorKind {
|
||||
pub(super) enum TypeshedVersionsParseErrorKind {
|
||||
TooManyLines(NonZeroUsize),
|
||||
UnexpectedNumberOfColons,
|
||||
InvalidModuleName(String),
|
||||
@@ -178,7 +177,7 @@ impl TypeshedVersions {
|
||||
fn query_module(
|
||||
&self,
|
||||
module: &ModuleName,
|
||||
target_version: PyVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
if let Some(range) = self.exact(module) {
|
||||
if range.contains(target_version) {
|
||||
@@ -323,13 +322,13 @@ impl fmt::Display for TypeshedVersions {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
enum PyVersionRange {
|
||||
AvailableFrom(RangeFrom<PyVersion>),
|
||||
AvailableWithin(RangeInclusive<PyVersion>),
|
||||
AvailableFrom(RangeFrom<PythonVersion>),
|
||||
AvailableWithin(RangeInclusive<PythonVersion>),
|
||||
}
|
||||
|
||||
impl PyVersionRange {
|
||||
#[must_use]
|
||||
fn contains(&self, version: PyVersion) -> bool {
|
||||
fn contains(&self, version: PythonVersion) -> bool {
|
||||
match self {
|
||||
Self::AvailableFrom(inner) => inner.contains(&version),
|
||||
Self::AvailableWithin(inner) => inner.contains(&version),
|
||||
@@ -343,9 +342,14 @@ impl FromStr for PyVersionRange {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s.split('-').map(str::trim);
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(lower), Some(""), None) => Ok(Self::AvailableFrom((lower.parse()?)..)),
|
||||
(Some(lower), Some(""), None) => {
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
Ok(Self::AvailableFrom(lower..))
|
||||
}
|
||||
(Some(lower), Some(upper), None) => {
|
||||
Ok(Self::AvailableWithin((lower.parse()?)..=(upper.parse()?)))
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
let upper = PythonVersion::from_versions_file_string(upper)?;
|
||||
Ok(Self::AvailableWithin(lower..=upper))
|
||||
}
|
||||
_ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens),
|
||||
}
|
||||
@@ -363,74 +367,20 @@ impl fmt::Display for PyVersionRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
struct PyVersion {
|
||||
major: u8,
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
impl FromStr for PyVersion {
|
||||
type Err = TypeshedVersionsParseErrorKind;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
impl PythonVersion {
|
||||
fn from_versions_file_string(s: &str) -> Result<Self, TypeshedVersionsParseErrorKind> {
|
||||
let mut parts = s.split('.').map(str::trim);
|
||||
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
|
||||
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
|
||||
s.to_string(),
|
||||
));
|
||||
};
|
||||
let major = match u8::from_str(major) {
|
||||
Ok(major) => major,
|
||||
Err(err) => {
|
||||
return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err,
|
||||
})
|
||||
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
|
||||
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err: int_parse_error,
|
||||
}
|
||||
};
|
||||
let minor = match u8::from_str(minor) {
|
||||
Ok(minor) => minor,
|
||||
Err(err) => {
|
||||
return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(Self { major, minor })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PyVersion { major, minor } = self;
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for PyVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => PyVersion { major: 3, minor: 7 },
|
||||
TargetVersion::Py38 => PyVersion { major: 3, minor: 8 },
|
||||
TargetVersion::Py39 => PyVersion { major: 3, minor: 9 },
|
||||
TargetVersion::Py310 => PyVersion {
|
||||
major: 3,
|
||||
minor: 10,
|
||||
},
|
||||
TargetVersion::Py311 => PyVersion {
|
||||
major: 3,
|
||||
minor: 11,
|
||||
},
|
||||
TargetVersion::Py312 => PyVersion {
|
||||
major: 3,
|
||||
minor: 12,
|
||||
},
|
||||
TargetVersion::Py313 => PyVersion {
|
||||
major: 3,
|
||||
minor: 13,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,7 +390,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::program::TargetVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -478,34 +427,34 @@ mod tests {
|
||||
|
||||
assert!(versions.contains_exact(&asyncio));
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio, TargetVersion::Py310.into()),
|
||||
versions.query_module(&asyncio, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
|
||||
assert!(versions.contains_exact(&asyncio_staggered));
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio_staggered, TargetVersion::Py38.into()),
|
||||
versions.query_module(&asyncio_staggered, PythonVersion::PY38),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio_staggered, TargetVersion::Py37.into()),
|
||||
versions.query_module(&asyncio_staggered, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
|
||||
assert!(versions.contains_exact(&audioop));
|
||||
assert_eq!(
|
||||
versions.query_module(&audioop, TargetVersion::Py312.into()),
|
||||
versions.query_module(&audioop, PythonVersion::PY312),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
versions.query_module(&audioop, TargetVersion::Py313.into()),
|
||||
versions.query_module(&audioop, PythonVersion::PY313),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeshed_versions_consistent_with_vendored_stubs() {
|
||||
const VERSIONS_DATA: &str = include_str!("../../vendor/typeshed/stdlib/VERSIONS");
|
||||
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();
|
||||
|
||||
@@ -590,15 +539,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&bar));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -610,15 +559,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&foo));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py38.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY38),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
}
|
||||
@@ -630,15 +579,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&bar_baz));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py39.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY39),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -650,15 +599,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(!parsed_versions.contains_exact(&bar_eggs));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -670,11 +619,11 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(!parsed_versions.contains_exact(&spam));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&spam, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&spam, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&spam, TargetVersion::Py313.into()),
|
||||
parsed_versions.query_module(&spam, PythonVersion::PY313),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
46
crates/red_knot_python_semantic/src/program.rs
Normal file
46
crates/red_knot_python_semantic/src/program.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::Db;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use salsa::Durability;
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
pub struct Program {
|
||||
pub target_version: PythonVersion,
|
||||
|
||||
#[return_ref]
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self {
|
||||
Program::builder(settings.target_version, settings.search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct ProgramSettings {
|
||||
pub target_version: PythonVersion,
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
/// Configures the search paths for module resolution.
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Default)]
|
||||
pub struct SearchPathSettings {
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
/// or pyright's stubPath configuration setting.
|
||||
pub extra_paths: Vec<SystemPathBuf>,
|
||||
|
||||
/// The root of the workspace, used for finding first-party modules.
|
||||
pub src_root: SystemPathBuf,
|
||||
|
||||
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub custom_typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub site_packages: Vec<SystemPathBuf>,
|
||||
}
|
||||
62
crates/red_knot_python_semantic/src/python_version.rs
Normal file
62
crates/red_knot_python_semantic/src/python_version.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Representation of a Python version.
|
||||
///
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
pub const PY37: PythonVersion = PythonVersion { major: 3, minor: 7 };
|
||||
pub const PY38: PythonVersion = PythonVersion { major: 3, minor: 8 };
|
||||
pub const PY39: PythonVersion = PythonVersion { major: 3, minor: 9 };
|
||||
pub const PY310: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 10,
|
||||
};
|
||||
pub const PY311: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 11,
|
||||
};
|
||||
pub const PY312: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 12,
|
||||
};
|
||||
pub const PY313: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 13,
|
||||
};
|
||||
|
||||
pub fn free_threaded_build_available(self) -> bool {
|
||||
self >= PythonVersion::PY313
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PythonVersion {
|
||||
fn default() -> Self {
|
||||
Self::PY38
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(&str, &str)> for PythonVersion {
|
||||
type Error = std::num::ParseIntError;
|
||||
|
||||
fn try_from(value: (&str, &str)) -> Result<Self, Self::Error> {
|
||||
let (major, minor) = value;
|
||||
Ok(Self {
|
||||
major: major.parse()?,
|
||||
minor: minor.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PythonVersion { major, minor } = self;
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
@@ -17,6 +18,8 @@ use crate::semantic_index::symbol::{
|
||||
};
|
||||
use crate::Db;
|
||||
|
||||
pub(crate) use self::use_def::UseDefMap;
|
||||
|
||||
pub mod ast_ids;
|
||||
mod builder;
|
||||
pub mod definition;
|
||||
@@ -24,8 +27,6 @@ pub mod expression;
|
||||
pub mod symbol;
|
||||
mod use_def;
|
||||
|
||||
pub(crate) use self::use_def::UseDefMap;
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
|
||||
/// Returns the semantic index for `file`.
|
||||
@@ -33,7 +34,7 @@ type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
/// Prefer using [`symbol_table`] when working with symbols from a single scope.
|
||||
#[salsa::tracked(return_ref, no_eq)]
|
||||
pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
|
||||
let _span = tracing::trace_span!("semantic_index", ?file).entered();
|
||||
let _span = tracing::trace_span!("semantic_index", file = %file.path(db)).entered();
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
|
||||
@@ -47,8 +48,10 @@ pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
|
||||
/// is unchanged.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<SymbolTable> {
|
||||
let _span = tracing::trace_span!("symbol_table", ?scope).entered();
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
let file = scope.file(db);
|
||||
let _span =
|
||||
tracing::trace_span!("symbol_table", scope=?scope.as_id(), file=%file.path(db)).entered();
|
||||
let index = semantic_index(db, file);
|
||||
|
||||
index.symbol_table(scope.file_scope_id(db))
|
||||
}
|
||||
@@ -60,8 +63,10 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
|
||||
/// is unchanged.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseDefMap<'db>> {
|
||||
let _span = tracing::trace_span!("use_def_map", ?scope).entered();
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
let file = scope.file(db);
|
||||
let _span =
|
||||
tracing::trace_span!("use_def_map", scope=?scope.as_id(), file=%file.path(db)).entered();
|
||||
let index = semantic_index(db, file);
|
||||
|
||||
index.use_def_map(scope.file_scope_id(db))
|
||||
}
|
||||
@@ -69,7 +74,7 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseD
|
||||
/// Returns the module global scope of `file`.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
let _span = tracing::trace_span!("global_scope", ?file).entered();
|
||||
let _span = tracing::trace_span!("global_scope", file = %file.path(db)).entered();
|
||||
|
||||
FileScopeId::global().to_scope_id(db, file)
|
||||
}
|
||||
|
||||
@@ -214,30 +214,38 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
fn with_type_params(
|
||||
&mut self,
|
||||
with_params: &WithTypeParams,
|
||||
with_scope: NodeWithScopeRef,
|
||||
type_params: Option<&'db ast::TypeParams>,
|
||||
nested: impl FnOnce(&mut Self) -> FileScopeId,
|
||||
) -> FileScopeId {
|
||||
let type_params = with_params.type_parameters();
|
||||
|
||||
if let Some(type_params) = type_params {
|
||||
let with_scope = match with_params {
|
||||
WithTypeParams::ClassDef { node, .. } => {
|
||||
NodeWithScopeRef::ClassTypeParameters(node)
|
||||
}
|
||||
WithTypeParams::FunctionDef { node, .. } => {
|
||||
NodeWithScopeRef::FunctionTypeParameters(node)
|
||||
}
|
||||
};
|
||||
|
||||
self.push_scope(with_scope);
|
||||
|
||||
for type_param in &type_params.type_params {
|
||||
let name = match type_param {
|
||||
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { name, .. }) => name,
|
||||
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { name, .. }) => name,
|
||||
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { name, .. }) => name,
|
||||
let (name, bound, default) = match type_param {
|
||||
ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
|
||||
range: _,
|
||||
name,
|
||||
bound,
|
||||
default,
|
||||
}) => (name, bound, default),
|
||||
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec {
|
||||
name, default, ..
|
||||
}) => (name, &None, default),
|
||||
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple {
|
||||
name,
|
||||
default,
|
||||
..
|
||||
}) => (name, &None, default),
|
||||
};
|
||||
// TODO create Definition for typevars
|
||||
self.add_or_update_symbol(name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
if let Some(bound) = bound {
|
||||
self.visit_expr(bound);
|
||||
}
|
||||
if let Some(default) = default {
|
||||
self.visit_expr(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,10 +326,11 @@ where
|
||||
self.add_definition(symbol, function_def);
|
||||
|
||||
self.with_type_params(
|
||||
&WithTypeParams::FunctionDef { node: function_def },
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
function_def.type_params.as_deref(),
|
||||
|builder| {
|
||||
builder.visit_parameters(&function_def.parameters);
|
||||
for expr in &function_def.returns {
|
||||
if let Some(expr) = &function_def.returns {
|
||||
builder.visit_annotation(expr);
|
||||
}
|
||||
|
||||
@@ -340,16 +349,20 @@ where
|
||||
self.add_or_update_symbol(class.name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
self.add_definition(symbol, class);
|
||||
|
||||
self.with_type_params(&WithTypeParams::ClassDef { node: class }, |builder| {
|
||||
if let Some(arguments) = &class.arguments {
|
||||
builder.visit_arguments(arguments);
|
||||
}
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::ClassTypeParameters(class),
|
||||
class.type_params.as_deref(),
|
||||
|builder| {
|
||||
if let Some(arguments) = &class.arguments {
|
||||
builder.visit_arguments(arguments);
|
||||
}
|
||||
|
||||
builder.push_scope(NodeWithScopeRef::Class(class));
|
||||
builder.visit_body(&class.body);
|
||||
builder.push_scope(NodeWithScopeRef::Class(class));
|
||||
builder.visit_body(&class.body);
|
||||
|
||||
builder.pop_scope()
|
||||
});
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
}
|
||||
ast::Stmt::Import(node) => {
|
||||
for alias in &node.names {
|
||||
@@ -390,18 +403,12 @@ where
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
// TODO deferred annotation visiting
|
||||
self.visit_expr(&node.annotation);
|
||||
match &node.value {
|
||||
Some(value) => {
|
||||
self.visit_expr(value);
|
||||
self.current_assignment = Some(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.current_assignment = None;
|
||||
}
|
||||
None => {
|
||||
// TODO annotation-only assignments
|
||||
self.visit_expr(&node.target);
|
||||
}
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
self.current_assignment = Some(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.current_assignment = None;
|
||||
}
|
||||
ast::Stmt::If(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
@@ -514,6 +521,14 @@ where
|
||||
self.current_assignment = None;
|
||||
self.visit_expr(&node.value);
|
||||
}
|
||||
ast::Expr::Lambda(lambda) => {
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
self.push_scope(NodeWithScopeRef::Lambda(lambda));
|
||||
self.visit_expr(lambda.body.as_ref());
|
||||
self.pop_scope();
|
||||
}
|
||||
ast::Expr::If(ast::ExprIf {
|
||||
body, test, orelse, ..
|
||||
}) => {
|
||||
@@ -535,20 +550,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
enum WithTypeParams<'node> {
|
||||
ClassDef { node: &'node ast::StmtClassDef },
|
||||
FunctionDef { node: &'node ast::StmtFunctionDef },
|
||||
}
|
||||
|
||||
impl<'node> WithTypeParams<'node> {
|
||||
fn type_parameters(&self) -> Option<&'node ast::TypeParams> {
|
||||
match self {
|
||||
WithTypeParams::ClassDef { node, .. } => node.type_params.as_deref(),
|
||||
WithTypeParams::FunctionDef { node, .. } => node.type_params.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign(&'a ast::StmtAssign),
|
||||
|
||||
@@ -126,6 +126,7 @@ impl<'db> ScopeId<'db> {
|
||||
}
|
||||
NodeWithScopeKind::Function(function)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
|
||||
NodeWithScopeKind::Lambda(_) => "<lambda>",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +297,7 @@ pub(crate) enum NodeWithScopeRef<'a> {
|
||||
Module,
|
||||
Class(&'a ast::StmtClassDef),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
Lambda(&'a ast::ExprLambda),
|
||||
FunctionTypeParameters(&'a ast::StmtFunctionDef),
|
||||
ClassTypeParameters(&'a ast::StmtClassDef),
|
||||
}
|
||||
@@ -315,11 +317,14 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::Function(function) => {
|
||||
NodeWithScopeKind::Function(AstNodeRef::new(module, function))
|
||||
}
|
||||
NodeWithScopeRef::Lambda(lambda) => {
|
||||
NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda))
|
||||
}
|
||||
NodeWithScopeRef::FunctionTypeParameters(function) => {
|
||||
NodeWithScopeKind::FunctionTypeParameters(AstNodeRef::new(module, function))
|
||||
}
|
||||
NodeWithScopeRef::ClassTypeParameters(class) => {
|
||||
NodeWithScopeKind::Class(AstNodeRef::new(module, class))
|
||||
NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,6 +334,7 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::Module => ScopeKind::Module,
|
||||
NodeWithScopeRef::Class(_) => ScopeKind::Class,
|
||||
NodeWithScopeRef::Function(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::Lambda(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::FunctionTypeParameters(_)
|
||||
| NodeWithScopeRef::ClassTypeParameters(_) => ScopeKind::Annotation,
|
||||
}
|
||||
@@ -341,6 +347,9 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::Function(function) => {
|
||||
NodeWithScopeKey::Function(NodeKey::from_node(function))
|
||||
}
|
||||
NodeWithScopeRef::Lambda(lambda) => {
|
||||
NodeWithScopeKey::Lambda(NodeKey::from_node(lambda))
|
||||
}
|
||||
NodeWithScopeRef::FunctionTypeParameters(function) => {
|
||||
NodeWithScopeKey::FunctionTypeParameters(NodeKey::from_node(function))
|
||||
}
|
||||
@@ -359,6 +368,7 @@ pub enum NodeWithScopeKind {
|
||||
ClassTypeParameters(AstNodeRef<ast::StmtClassDef>),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
FunctionTypeParameters(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Lambda(AstNodeRef<ast::ExprLambda>),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -368,4 +378,5 @@ pub(crate) enum NodeWithScopeKey {
|
||||
ClassTypeParameters(NodeKey),
|
||||
Function(NodeKey),
|
||||
FunctionTypeParameters(NodeKey),
|
||||
Lambda(NodeKey),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use red_knot_module_resolver::{resolve_module, Module, ModuleName};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, ExpressionRef, StmtClassDef};
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{resolve_module, Module};
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::{definition_ty, global_symbol_ty_by_name, infer_scope_types, Type};
|
||||
@@ -25,7 +26,7 @@ impl<'db> SemanticModel<'db> {
|
||||
}
|
||||
|
||||
pub fn resolve_module(&self, module_name: ModuleName) -> Option<Module> {
|
||||
resolve_module(self.db.upcast(), module_name)
|
||||
resolve_module(self.db, module_name)
|
||||
}
|
||||
|
||||
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
|
||||
@@ -164,10 +165,11 @@ impl HasTy for ast::Alias {
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::Type;
|
||||
use crate::{HasTy, SemanticModel};
|
||||
|
||||
@@ -175,11 +177,11 @@ mod tests {
|
||||
let db = TestDb::new();
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::Py38,
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
site_packages: vec![],
|
||||
custom_typeshed: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{Db, FxOrderSet};
|
||||
mod display;
|
||||
mod infer;
|
||||
|
||||
pub(crate) use self::infer::{infer_definition_types, infer_expression_types, infer_scope_types};
|
||||
pub(crate) use self::infer::{infer_definition_types, infer_scope_types};
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
pub(crate) fn symbol_ty<'db>(
|
||||
@@ -43,14 +43,14 @@ pub(crate) fn symbol_ty_by_name<'db>(
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty` that looks up a module-global symbol in a file.
|
||||
/// Shorthand for `symbol_ty` that looks up a module-global symbol by name in a file.
|
||||
pub(crate) fn global_symbol_ty_by_name<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
|
||||
symbol_ty_by_name(db, global_scope(db, file), name)
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty` that looks up a symbol in the builtins.
|
||||
///
|
||||
/// Returns `None` if the builtins module isn't available for some reason.
|
||||
/// Returns `Unbound` if the builtins module isn't available for some reason.
|
||||
pub(crate) fn builtins_symbol_ty_by_name<'db>(db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
builtins_scope(db)
|
||||
.map(|builtins| symbol_ty_by_name(db, builtins, name))
|
||||
@@ -130,6 +130,8 @@ pub enum Type<'db> {
|
||||
Union(UnionType<'db>),
|
||||
Intersection(IntersectionType<'db>),
|
||||
IntLiteral(i64),
|
||||
/// A boolean literal, either `True` or `False`.
|
||||
BooleanLiteral(bool),
|
||||
// TODO protocols, callable types, overloads, generics, type vars
|
||||
}
|
||||
|
||||
@@ -157,12 +159,15 @@ impl<'db> Type<'db> {
|
||||
// TODO MRO? get_own_instance_member, get_instance_member
|
||||
todo!("attribute lookup on Instance type")
|
||||
}
|
||||
Type::Union(_) => {
|
||||
// TODO perform the get_member on each type in the union
|
||||
// TODO return the union of those results
|
||||
// TODO if any of those results is `None` then include Unknown in the result union
|
||||
todo!("attribute lookup on Union type")
|
||||
}
|
||||
Type::Union(union) => Type::Union(
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.fold(UnionTypeBuilder::new(db), |builder, element_ty| {
|
||||
builder.add(element_ty.member(db, name))
|
||||
})
|
||||
.build(),
|
||||
),
|
||||
Type::Intersection(_) => {
|
||||
// TODO perform the get_member on each type in the intersection
|
||||
// TODO return the intersection of those results
|
||||
@@ -172,6 +177,17 @@ impl<'db> Type<'db> {
|
||||
// TODO raise error
|
||||
Type::Unknown
|
||||
}
|
||||
Type::BooleanLiteral(_) => Type::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn instance(&self) -> Type<'db> {
|
||||
match self {
|
||||
Type::Any => Type::Any,
|
||||
Type::Unknown => Type::Unknown,
|
||||
Type::Class(class) => Type::Instance(*class),
|
||||
_ => Type::Unknown, // TODO type errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Display for DisplayType<'_> {
|
||||
Type::Unbound => f.write_str("Unbound"),
|
||||
Type::None => f.write_str("None"),
|
||||
Type::Module(file) => {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db.upcast()))
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::Class(class) => write!(f, "Literal[{}]", class.name(self.db)),
|
||||
@@ -35,6 +35,9 @@ impl Display for DisplayType<'_> {
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
Type::IntLiteral(n) => write!(f, "Literal[{n}]"),
|
||||
Type::BooleanLiteral(boolean) => {
|
||||
write!(f, "Literal[{}]", if *boolean { "True" } else { "False" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1
crates/red_knot_python_semantic/vendor/typeshed/source_commit.txt
vendored
Normal file
1
crates/red_knot_python_semantic/vendor/typeshed/source_commit.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
4ef2d66663fc080fefa379e6ae5fc45d4f8b54eb
|
||||
@@ -1,18 +1,18 @@
|
||||
import sys
|
||||
from _typeshed import SupportsWrite
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import Any, Final, Literal
|
||||
from typing import Any, Final
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
__version__: Final[str]
|
||||
|
||||
QUOTE_ALL: Literal[1]
|
||||
QUOTE_MINIMAL: Literal[0]
|
||||
QUOTE_NONE: Literal[3]
|
||||
QUOTE_NONNUMERIC: Literal[2]
|
||||
QUOTE_ALL: Final = 1
|
||||
QUOTE_MINIMAL: Final = 0
|
||||
QUOTE_NONE: Final = 3
|
||||
QUOTE_NONNUMERIC: Final = 2
|
||||
if sys.version_info >= (3, 12):
|
||||
QUOTE_STRINGS: Literal[4]
|
||||
QUOTE_NOTNULL: Literal[5]
|
||||
QUOTE_STRINGS: Final = 4
|
||||
QUOTE_NOTNULL: Final = 5
|
||||
|
||||
# Ideally this would be `QUOTE_ALL | QUOTE_MINIMAL | QUOTE_NONE | QUOTE_NONNUMERIC`
|
||||
# However, using literals in situations like these can cause false-positives (see #7258)
|
||||
@@ -71,7 +71,7 @@ class _CData(metaclass=_CDataMeta):
|
||||
@classmethod
|
||||
def from_address(cls, address: int) -> Self: ...
|
||||
@classmethod
|
||||
def from_param(cls, obj: Any) -> Self | _CArgObject: ...
|
||||
def from_param(cls, value: Any, /) -> Self | _CArgObject: ...
|
||||
@classmethod
|
||||
def in_dll(cls, library: CDLL, name: str) -> Self: ...
|
||||
def __buffer__(self, flags: int, /) -> memoryview: ...
|
||||
@@ -368,11 +368,7 @@ def tparm(
|
||||
) -> bytes: ...
|
||||
def typeahead(fd: int, /) -> None: ...
|
||||
def unctrl(ch: _ChType, /) -> bytes: ...
|
||||
|
||||
if sys.version_info < (3, 12) or sys.platform != "darwin":
|
||||
# The support for macos was dropped in 3.12
|
||||
def unget_wch(ch: int | str, /) -> None: ...
|
||||
|
||||
def unget_wch(ch: int | str, /) -> None: ...
|
||||
def ungetch(ch: _ChType, /) -> None: ...
|
||||
def ungetmouse(id: int, x: int, y: int, z: int, bstate: int, /) -> None: ...
|
||||
def update_lines_cols() -> None: ...
|
||||
@@ -447,13 +443,10 @@ class _CursesWindow:
|
||||
def getch(self) -> int: ...
|
||||
@overload
|
||||
def getch(self, y: int, x: int) -> int: ...
|
||||
if sys.version_info < (3, 12) or sys.platform != "darwin":
|
||||
# The support for macos was dropped in 3.12
|
||||
@overload
|
||||
def get_wch(self) -> int | str: ...
|
||||
@overload
|
||||
def get_wch(self, y: int, x: int) -> int | str: ...
|
||||
|
||||
@overload
|
||||
def get_wch(self) -> int | str: ...
|
||||
@overload
|
||||
def get_wch(self, y: int, x: int) -> int | str: ...
|
||||
@overload
|
||||
def getkey(self) -> str: ...
|
||||
@overload
|
||||
@@ -17,20 +17,20 @@ class DecimalTuple(NamedTuple):
|
||||
digits: tuple[int, ...]
|
||||
exponent: int | Literal["n", "N", "F"]
|
||||
|
||||
ROUND_DOWN: str
|
||||
ROUND_HALF_UP: str
|
||||
ROUND_HALF_EVEN: str
|
||||
ROUND_CEILING: str
|
||||
ROUND_FLOOR: str
|
||||
ROUND_UP: str
|
||||
ROUND_HALF_DOWN: str
|
||||
ROUND_05UP: str
|
||||
HAVE_CONTEXTVAR: bool
|
||||
HAVE_THREADS: bool
|
||||
MAX_EMAX: int
|
||||
MAX_PREC: int
|
||||
MIN_EMIN: int
|
||||
MIN_ETINY: int
|
||||
ROUND_DOWN: Final[str]
|
||||
ROUND_HALF_UP: Final[str]
|
||||
ROUND_HALF_EVEN: Final[str]
|
||||
ROUND_CEILING: Final[str]
|
||||
ROUND_FLOOR: Final[str]
|
||||
ROUND_UP: Final[str]
|
||||
ROUND_HALF_DOWN: Final[str]
|
||||
ROUND_05UP: Final[str]
|
||||
HAVE_CONTEXTVAR: Final[bool]
|
||||
HAVE_THREADS: Final[bool]
|
||||
MAX_EMAX: Final[int]
|
||||
MAX_PREC: Final[int]
|
||||
MIN_EMIN: Final[int]
|
||||
MIN_ETINY: Final[int]
|
||||
|
||||
class DecimalException(ArithmeticError): ...
|
||||
class Clamped(DecimalException): ...
|
||||
@@ -1,5 +1,5 @@
|
||||
from _typeshed import structseq
|
||||
from typing import Final, Literal, SupportsIndex, final
|
||||
from typing import Any, Final, Literal, SupportsIndex, final
|
||||
from typing_extensions import Buffer, Self
|
||||
|
||||
class ChannelError(RuntimeError): ...
|
||||
@@ -72,13 +72,15 @@ class ChannelInfo(structseq[int], tuple[bool, bool, bool, int, int, int, int, in
|
||||
@property
|
||||
def send_released(self) -> bool: ...
|
||||
|
||||
def create() -> ChannelID: ...
|
||||
def create(unboundop: Literal[1, 2, 3]) -> ChannelID: ...
|
||||
def destroy(cid: SupportsIndex) -> None: ...
|
||||
def list_all() -> list[ChannelID]: ...
|
||||
def list_interpreters(cid: SupportsIndex, *, send: bool) -> list[int]: ...
|
||||
def send(cid: SupportsIndex, obj: object, *, blocking: bool = True, timeout: float | None = None) -> None: ...
|
||||
def send_buffer(cid: SupportsIndex, obj: Buffer, *, blocking: bool = True, timeout: float | None = None) -> None: ...
|
||||
def recv(cid: SupportsIndex, default: object = ...) -> object: ...
|
||||
def recv(cid: SupportsIndex, default: object = ...) -> tuple[Any, Literal[1, 2, 3]]: ...
|
||||
def close(cid: SupportsIndex, *, send: bool = False, recv: bool = False) -> None: ...
|
||||
def get_count(cid: SupportsIndex) -> int: ...
|
||||
def get_info(cid: SupportsIndex) -> ChannelInfo: ...
|
||||
def get_channel_defaults(cid: SupportsIndex) -> Literal[1, 2, 3]: ...
|
||||
def release(cid: SupportsIndex, *, send: bool = False, recv: bool = False, force: bool = False) -> None: ...
|
||||
@@ -1,5 +1,5 @@
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TypeVar
|
||||
from typing import Final, TypeVar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_K = TypeVar("_K")
|
||||
@@ -7,15 +7,15 @@ _V = TypeVar("_V")
|
||||
|
||||
__all__ = ["compiler_fixup", "customize_config_vars", "customize_compiler", "get_platform_osx"]
|
||||
|
||||
_UNIVERSAL_CONFIG_VARS: tuple[str, ...] # undocumented
|
||||
_COMPILER_CONFIG_VARS: tuple[str, ...] # undocumented
|
||||
_INITPRE: str # undocumented
|
||||
_UNIVERSAL_CONFIG_VARS: Final[tuple[str, ...]] # undocumented
|
||||
_COMPILER_CONFIG_VARS: Final[tuple[str, ...]] # undocumented
|
||||
_INITPRE: Final[str] # undocumented
|
||||
|
||||
def _find_executable(executable: str, path: str | None = None) -> str | None: ... # undocumented
|
||||
def _read_output(commandstring: str, capture_stderr: bool = False) -> str | None: ... # undocumented
|
||||
def _find_build_tool(toolname: str) -> str: ... # undocumented
|
||||
|
||||
_SYSTEM_VERSION: str | None # undocumented
|
||||
_SYSTEM_VERSION: Final[str | None] # undocumented
|
||||
|
||||
def _get_system_version() -> str: ... # undocumented
|
||||
def _remove_original_values(_config_vars: dict[str, str]) -> None: ... # undocumented
|
||||
119
crates/red_knot_python_semantic/vendor/typeshed/stdlib/_stat.pyi
vendored
Normal file
119
crates/red_knot_python_semantic/vendor/typeshed/stdlib/_stat.pyi
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
SF_APPEND: Final = 0x00040000
|
||||
SF_ARCHIVED: Final = 0x00010000
|
||||
SF_IMMUTABLE: Final = 0x00020000
|
||||
SF_NOUNLINK: Final = 0x00100000
|
||||
SF_SNAPSHOT: Final = 0x00200000
|
||||
|
||||
ST_MODE: Final = 0
|
||||
ST_INO: Final = 1
|
||||
ST_DEV: Final = 2
|
||||
ST_NLINK: Final = 3
|
||||
ST_UID: Final = 4
|
||||
ST_GID: Final = 5
|
||||
ST_SIZE: Final = 6
|
||||
ST_ATIME: Final = 7
|
||||
ST_MTIME: Final = 8
|
||||
ST_CTIME: Final = 9
|
||||
|
||||
S_IFIFO: Final = 0o010000
|
||||
S_IFLNK: Final = 0o120000
|
||||
S_IFREG: Final = 0o100000
|
||||
S_IFSOCK: Final = 0o140000
|
||||
S_IFBLK: Final = 0o060000
|
||||
S_IFCHR: Final = 0o020000
|
||||
S_IFDIR: Final = 0o040000
|
||||
|
||||
# These are 0 on systems that don't support the specific kind of file.
|
||||
# Example: Linux doesn't support door files, so S_IFDOOR is 0 on linux.
|
||||
S_IFDOOR: int
|
||||
S_IFPORT: int
|
||||
S_IFWHT: int
|
||||
|
||||
S_ISUID: Final = 0o4000
|
||||
S_ISGID: Final = 0o2000
|
||||
S_ISVTX: Final = 0o1000
|
||||
|
||||
S_IRWXU: Final = 0o0700
|
||||
S_IRUSR: Final = 0o0400
|
||||
S_IWUSR: Final = 0o0200
|
||||
S_IXUSR: Final = 0o0100
|
||||
|
||||
S_IRWXG: Final = 0o0070
|
||||
S_IRGRP: Final = 0o0040
|
||||
S_IWGRP: Final = 0o0020
|
||||
S_IXGRP: Final = 0o0010
|
||||
|
||||
S_IRWXO: Final = 0o0007
|
||||
S_IROTH: Final = 0o0004
|
||||
S_IWOTH: Final = 0o0002
|
||||
S_IXOTH: Final = 0o0001
|
||||
|
||||
S_ENFMT: Final = 0o2000
|
||||
S_IREAD: Final = 0o0400
|
||||
S_IWRITE: Final = 0o0200
|
||||
S_IEXEC: Final = 0o0100
|
||||
|
||||
UF_APPEND: Final = 0x00000004
|
||||
UF_COMPRESSED: Final = 0x00000020 # OS X 10.6+ only
|
||||
UF_HIDDEN: Final = 0x00008000 # OX X 10.5+ only
|
||||
UF_IMMUTABLE: Final = 0x00000002
|
||||
UF_NODUMP: Final = 0x00000001
|
||||
UF_NOUNLINK: Final = 0x00000010
|
||||
UF_OPAQUE: Final = 0x00000008
|
||||
|
||||
def S_IMODE(mode: int, /) -> int: ...
|
||||
def S_IFMT(mode: int, /) -> int: ...
|
||||
def S_ISBLK(mode: int, /) -> bool: ...
|
||||
def S_ISCHR(mode: int, /) -> bool: ...
|
||||
def S_ISDIR(mode: int, /) -> bool: ...
|
||||
def S_ISDOOR(mode: int, /) -> bool: ...
|
||||
def S_ISFIFO(mode: int, /) -> bool: ...
|
||||
def S_ISLNK(mode: int, /) -> bool: ...
|
||||
def S_ISPORT(mode: int, /) -> bool: ...
|
||||
def S_ISREG(mode: int, /) -> bool: ...
|
||||
def S_ISSOCK(mode: int, /) -> bool: ...
|
||||
def S_ISWHT(mode: int, /) -> bool: ...
|
||||
def filemode(mode: int, /) -> str: ...
|
||||
|
||||
if sys.platform == "win32":
|
||||
IO_REPARSE_TAG_SYMLINK: int
|
||||
IO_REPARSE_TAG_MOUNT_POINT: int
|
||||
IO_REPARSE_TAG_APPEXECLINK: int
|
||||
|
||||
if sys.platform == "win32":
|
||||
FILE_ATTRIBUTE_ARCHIVE: Final = 32
|
||||
FILE_ATTRIBUTE_COMPRESSED: Final = 2048
|
||||
FILE_ATTRIBUTE_DEVICE: Final = 64
|
||||
FILE_ATTRIBUTE_DIRECTORY: Final = 16
|
||||
FILE_ATTRIBUTE_ENCRYPTED: Final = 16384
|
||||
FILE_ATTRIBUTE_HIDDEN: Final = 2
|
||||
FILE_ATTRIBUTE_INTEGRITY_STREAM: Final = 32768
|
||||
FILE_ATTRIBUTE_NORMAL: Final = 128
|
||||
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: Final = 8192
|
||||
FILE_ATTRIBUTE_NO_SCRUB_DATA: Final = 131072
|
||||
FILE_ATTRIBUTE_OFFLINE: Final = 4096
|
||||
FILE_ATTRIBUTE_READONLY: Final = 1
|
||||
FILE_ATTRIBUTE_REPARSE_POINT: Final = 1024
|
||||
FILE_ATTRIBUTE_SPARSE_FILE: Final = 512
|
||||
FILE_ATTRIBUTE_SYSTEM: Final = 4
|
||||
FILE_ATTRIBUTE_TEMPORARY: Final = 256
|
||||
FILE_ATTRIBUTE_VIRTUAL: Final = 65536
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
# Varies by platform.
|
||||
SF_SETTABLE: Final[int]
|
||||
# https://github.com/python/cpython/issues/114081#issuecomment-2119017790
|
||||
# SF_RESTRICTED: Literal[0x00080000]
|
||||
SF_FIRMLINK: Final = 0x00800000
|
||||
SF_DATALESS: Final = 0x40000000
|
||||
|
||||
if sys.platform == "darwin":
|
||||
SF_SUPPORTED: Final = 0x9F0000
|
||||
SF_SYNTHETIC: Final = 0xC0000000
|
||||
|
||||
UF_TRACKED: Final = 0x00000040
|
||||
UF_DATAVAULT: Final = 0x00000080
|
||||
UF_SETTABLE: Final = 0x0000FFFF
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user