Compare commits

...

33 Commits

Author SHA1 Message Date
Carl Meyer
d91bb060f1 BDD 2024-12-20 09:00:43 -08:00
Carl Meyer
bf0918d72f dot file 2024-12-19 11:13:28 -08:00
Carl Meyer
6870d2c53e use published version of crate 2024-12-18 09:05:47 -08:00
Carl Meyer
54561192dd testing it out 2024-12-17 17:19:48 -08:00
Douglas Creager
91c9168dd7 Handle nested imports correctly in from ... import (#15026)
#14946 fixed our handling of nested imports with the `import` statement,
but didn't touch `from...import` statements.

cf
https://github.com/astral-sh/ruff/issues/14826#issuecomment-2525344515
2024-12-17 14:23:34 -05:00
Micha Reiser
80577a49f8 Upgrade salsa in fuzzer script (#15040) 2024-12-17 18:01:58 +01:00
cake-monotone
f463fa7b7c [red-knot] Narrowing For Truthiness Checks (if x or if not x) (#14687)
## Summary

Fixes #14550.

Add `AlwaysTruthy` and `AlwaysFalsy` types, representing the set of objects whose `__bool__` method can only ever return `True` or `False`, respectively, and narrow `if x` and `if not x` accordingly.


## Test Plan

- New Markdown test for truthiness narrowing `narrow/truthiness.md`
- unit tests in `types.rs` and `builders.rs` (`cargo test --package
red_knot_python_semantic --lib -- types`)
2024-12-17 08:37:07 -08:00
Micha Reiser
c3b6139f39 Upgrade salsa (#15039)
The only code change is that Salsa now requires the `Db` to implement
`Clone` to create "lightweight" snapshots.
2024-12-17 15:50:33 +00:00
InSync
c9fdb1f5e3 [pylint] Preserve original value format (PLR6104) (#14978)
## Summary

Resolves #11672.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-17 16:07:07 +01:00
Alex Waygood
463046ae07 [red-knot] Explicitly test diagnostics are emitted for unresolvable submodule imports (#15035) 2024-12-17 12:55:50 +00:00
Micha Reiser
dcb99cc817 Fix stale File status in tests (#15030)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/15027

The `MemoryFileSystem::write_file` API automatically creates
non-existing ancestor directoryes
but we failed to update the status of the now created ancestor
directories in the `Files` data structure.


## Test Plan

Tested that the case in https://github.com/astral-sh/ruff/issues/15027
now passes regardless of whether the *Simple* case is commented out or
not
2024-12-17 12:45:36 +01:00
InSync
7c2e7cf25e [red-knot] Basic support for other legacy typing aliases (#14998)
## Summary

Resolves #14997.

## Test Plan

Markdown tests.
2024-12-17 09:33:15 +00:00
Wei Lee
867a8f9497 feat(AIR302): extend the following rules (#15015)
## Summary


Airflow 3.0 removes various deprecated functions, members, modules, and
other values. They have been deprecated in 2.x, but the removal causes
incompatibilities that we want to detect. This PR deprecates the
following names.

* `airflow.api_connexion.security.requires_access_dataset` →
`airflow.api_connexion.security.requires_access_asset`
* `airflow.auth.managers.base_auth_manager.is_authorized_dataset` →
`airflow.auth.managers.base_auth_manager.is_authorized_asset`
* `airflow.auth.managers.models.resource_details.DatasetDetails` →
`airflow.auth.managers.models.resource_details.AssetDetails`
* `airflow.lineage.hook.DatasetLineageInfo` →
`airflow.lineage.hook.AssetLineageInfo`
* `airflow.security.permissions.RESOURCE_DATASET` →
`airflow.security.permissions.RESOURCE_ASSET`
* `airflow.www.auth.has_access_dataset` →
`airflow.www.auth.has_access_dataset.has_access_asset`
* remove `airflow.datasets.DatasetAliasEvent`
* `airflow.datasets.Dataset` → `airflow.sdk.definitions.asset.Asset`
* `airflow.Dataset` → `airflow.sdk.definitions.asset.Asset`
* `airflow.datasets.DatasetAlias` →
`airflow.sdk.definitions.asset.AssetAlias`
* `airflow.datasets.DatasetAll` →
`airflow.sdk.definitions.asset.AssetAll`
* `airflow.datasets.DatasetAny` →
`airflow.sdk.definitions.asset.AssetAny`
* `airflow.datasets.metadata` → `airflow.sdk.definitions.asset.metadata`
* `airflow.datasets.expand_alias_to_datasets` →
`airflow.sdk.definitions.asset.expand_alias_to_assets`
* `airflow.datasets.manager.dataset_manager` → `airflow.assets.manager`
* `airflow.datasets.manager.resolve_dataset_manager` →
`airflow.assets.resolve_asset_manager`
* `airflow.datasets.manager.DatasetManager` →
`airflow.assets.AssetManager`
* `airflow.listeners.spec.dataset.on_dataset_created` →
`airflow.listeners.spec.asset.on_asset_created`
* `airflow.listeners.spec.dataset.on_dataset_changed` →
`airflow.listeners.spec.asset.on_asset_changed`
* `airflow.timetables.simple.DatasetTriggeredTimetable` →
`airflow.timetables.simple.AssetTriggeredTimetable`
* `airflow.timetables.datasets.DatasetOrTimeSchedule` →
`airflow.timetables.assets.AssetOrTimeSchedule`
*
`airflow.providers.amazon.auth_manager.avp.entities.AvpEntities.DATASET`
→ `airflow.providers.amazon.auth_manager.avp.entities.AvpEntities.ASSET`
* `airflow.providers.amazon.aws.datasets.s3.create_dataset` →
`airflow.providers.amazon.aws.assets.s3.create_asset`
*
`airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage`
→
`airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage`
* `airflow.providers.amazon.aws.datasets.s3.sanitize_uri` →
`airflow.providers.amazon.aws.assets.s3.sanitize_uri`
*
`airflow.providers.common.io.datasets.file.convert_dataset_to_openlineage`
→ `airflow.providers.common.io.assets.file.convert_asset_to_openlineage`
* `airflow.providers.common.io.datasets.file.sanitize_uri` →
`airflow.providers.common.io.assets.file.sanitize_uri`
* `airflow.providers.common.io.datasets.file.create_dataset` →
`airflow.providers.common.io.assets.file.create_asset`
* `airflow.providers.google.datasets.bigquery.sanitize_uri` →
`airflow.providers.google.assets.bigquery.sanitize_uri`
* `airflow.providers.google.datasets.gcs.create_dataset` →
`airflow.providers.google.assets.gcs.create_asset`
* `airflow.providers.google.datasets.gcs.sanitize_uri` →
`airflow.providers.google.assets.gcs.sanitize_uri`
* `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage`
→ `airflow.providers.google.assets.gcs.convert_asset_to_openlineage`
*
`airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_dataset`
→
`airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset`
* `airflow.providers.openlineage.utils.utils.DatasetInfo` →
`airflow.providers.openlineage.utils.utils.AssetInfo`
* `airflow.providers.openlineage.utils.utils.translate_airflow_dataset`
→ `airflow.providers.openlineage.utils.utils.translate_airflow_asset`
* `airflow.providers.postgres.datasets.postgres.sanitize_uri` →
`airflow.providers.postgres.assets.postgres.sanitize_uri`
* `airflow.providers.mysql.datasets.mysql.sanitize_uri` →
`airflow.providers.mysql.assets.mysql.sanitize_uri`
* `airflow.providers.trino.datasets.trino.sanitize_uri` →
`airflow.providers.trino.assets.trino.sanitize_uri`

In additional to the newly added rules above, the message for
`airflow.contrib.*` and `airflow.subdag.*` has been extended,
`airflow.sensors.external_task.ExternalTaskSensorLink` error has been
fixed and the test fixture has been reorganized

## Test Plan

A test fixture is included in the PR.
2024-12-17 08:32:48 +01:00
w0nder1ng
e22718f25f [perflint] Simplify finding the loop target in PERF401 (#15025)
Fixes #15012.

```python
def f():
    # panics when the code can't find the loop variable
    values = [1, 2, 3]
    result = []
    for i in values:
        result.append(i + 1)
    del i
```

I'm not sure exactly why this test case panics, but I suspect the `del
i` removes the binding from the semantic model's symbols.

I changed the code to search for the correct binding by directly
iterating through the bindings. Since we know exactly which binding we
want, this should find the loop variable without any complications.
2024-12-17 08:30:32 +01:00
Dhruv Manilawala
dcdc6e7c64 [red-knot] Avoid undeclared path when raising conflicting declarations (#14958)
## Summary

This PR updates the logic when raising conflicting declarations
diagnostic to avoid the undeclared path if present.

The conflicting declaration diagnostics is added when there are two or
more declarations in the control flow path of a definition whose type
isn't equivalent to each other. This can be seen in the following
example:

```py
if flag:
	x: int
x = 1  # conflicting-declarations: Unknown, int
```

After this PR, we'd avoid considering "Unknown" as part of the
conflicting declarations. This means we'd still flag it for the
following case:

```py
if flag:
	x: int
else:
	x: str
x = 1  # conflicting-declarations: int, str
```

A solution that's local to the exception control flow was also explored
which required updating the logic for merging the flow snapshot to avoid
considering declarations using a flag. This is preserved here:
https://github.com/astral-sh/ruff/compare/dhruv/control-flow-no-declarations?expand=1.

The main motivation to avoid that is we don't really understand what the
user experience is w.r.t. the Unknown type and the
conflicting-declaration diagnostics. This makes us unsure on what the
right semantics are as to whether that diagnostics should be raised or
not and when to raise them. For now, we've decided to move forward with
this PR and could decide to adopt another solution or remove the
conflicting-declaration diagnostics in the future.

Closes: #13966 

## Test Plan

Update the existing mdtest case. Add an additional case specific to
exception control flow to verify that the diagnostic is not being raised
now.
2024-12-17 09:49:39 +05:30
Douglas Creager
4ddf9228f6 Bind top-most parent when importing nested module (#14946)
When importing a nested module, we were correctly creating a binding for
the top-most parent, but we were binding that to the nested module, not
to that parent module. Moreover, we weren't treating those submodules as
members of their containing parents. This PR addresses both issues, so
that nested imports work as expected.

As discussed in ~Slack~ whatever chat app I find myself in these days
😄, this requires keeping track of which modules have been imported
within the current file, so that when we resolve member access on a
module reference, we can see if that member has been imported as a
submodule. If so, we return the submodule reference immediately, instead
of checking whether the parent module's definition defines the symbol.

This is currently done in a flow insensitive manner. The `SemanticIndex`
now tracks all of the modules that are imported (via `import`, not via
`from...import`). The member access logic mentioned above currently only
considers module imports in the file containing the attribute
expression.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-16 16:15:40 -05:00
Alex Waygood
6d72be2683 Bump zizmor pre-commit hook to the latest version and fix new warnings (#15022) 2024-12-16 17:45:46 +00:00
Alex Waygood
712c886749 Add actionlint as a pre-commit hook (with shellcheck integration) (#15021) 2024-12-16 17:32:49 +00:00
renovate[bot]
50739f91dc Update dependency mdformat-mkdocs to v4 (#15011)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[mdformat-mkdocs](https://redirect.github.com/kyleking/mdformat-mkdocs)
([changelog](https://redirect.github.com/kyleking/mdformat-mkdocs/releases))
| `==3.1.1` -> `==4.0.0` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/mdformat-mkdocs/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mdformat-mkdocs/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mdformat-mkdocs/3.1.1/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mdformat-mkdocs/3.1.1/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>kyleking/mdformat-mkdocs (mdformat-mkdocs)</summary>

###
[`v4.0.0`](https://redirect.github.com/KyleKing/mdformat-mkdocs/releases/tag/v4.0.0)

[Compare
Source](https://redirect.github.com/kyleking/mdformat-mkdocs/compare/v3.1.1...v4.0.0)

#### What's Changed

- fix!: add newline after title for consistency with MKDocs style by
[@&#8203;KyleKing](https://redirect.github.com/KyleKing) in
[https://github.com/KyleKing/mdformat-mkdocs/pull/44](https://redirect.github.com/KyleKing/mdformat-mkdocs/pull/44)

**Full Changelog**:
https://github.com/KyleKing/mdformat-mkdocs/compare/v3.1.1...v4.0.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS41OC4xIiwidXBkYXRlZEluVmVyIjoiMzkuNTguMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Kyle King <KyleKing@users.noreply.github.com>
2024-12-16 22:48:37 +05:30
Dylan
6a5eff6017 [pydocstyle] Skip leading whitespace for D403 (#14963)
This PR introduces three changes to `D403`, which has to do with
capitalizing the first word in a docstring.

1. The diagnostic and fix now skip leading whitespace when determining
what counts as "the first word".
2. The name has been changed to `first-word-uncapitalized` from
`first-line-capitalized`, for both clarity and compliance with our rule
naming policy.
3. The diagnostic message and documentation has been modified slightly
to reflect this.

Closes #14890
2024-12-16 09:09:27 -06:00
renovate[bot]
a623d8f7c4 Update pre-commit dependencies (#15008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-16 11:13:49 +00:00
Dhruv Manilawala
aa429b413f Check diagnostic refresh support from client capability (#15014)
## Summary

Per the LSP spec, the property name is `workspace.diagnostics` with an
`s` at the end but the `lsp-types` dependency uses
`workspace.diagnostic` (without an `s`). Our fork contains this fix
(0f58d62879)
so we should avoid the hardcoded value.

The implication of this is that the client which doesn't support
workspace refresh capability didn't support the [dynamic
configuration](https://docs.astral.sh/ruff/editors/features/#dynamic-configuration)
feature because the server would _always_ send the workspace refresh
request but the client would ignore it. We have a fallback logic to
publish the diagnostics instead:


5f6fc3988b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs (L28-L40)

fixes: #15013 

## Test Plan

### VS Code


https://github.com/user-attachments/assets/61ac8e6f-aa20-41cc-b398-998e1866b5bc

### Neovim



https://github.com/user-attachments/assets/4131e13c-3fba-411c-9bb7-478d26eb8d56
2024-12-16 16:26:40 +05:30
renovate[bot]
425c248232 Update Rust crate colored to v2.2.0 (#15010)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [colored](https://redirect.github.com/mackwic/colored) |
workspace.dependencies | minor | `2.1.0` -> `2.2.0` |

---

### Release Notes

<details>
<summary>mackwic/colored (colored)</summary>

###
[`v2.2.0`](https://redirect.github.com/mackwic/colored/compare/v2.1.0...v2.2.0)

[Compare
Source](https://redirect.github.com/mackwic/colored/compare/v2.1.0...v2.2.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS41OC4xIiwidXBkYXRlZEluVmVyIjoiMzkuNTguMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 08:48:51 +01:00
renovate[bot]
bcd944347d Update dependency monaco-editor to v0.52.2 (#15006) 2024-12-15 20:26:21 -05:00
renovate[bot]
86eff81c6a Update Rust crate thiserror to v2.0.7 (#15005) 2024-12-15 20:26:14 -05:00
renovate[bot]
24ace68560 Update Rust crate serde to v1.0.216 (#15004) 2024-12-15 20:26:08 -05:00
renovate[bot]
b664505d7b Update Rust crate libc to v0.2.168 (#15003) 2024-12-15 20:25:59 -05:00
renovate[bot]
aa575da1e7 Update Rust crate fern to v0.7.1 (#15002) 2024-12-15 20:25:52 -05:00
renovate[bot]
921eb2acb3 Update Rust crate chrono to v0.4.39 (#15001) 2024-12-15 20:25:46 -05:00
renovate[bot]
8665d2dc95 Update Rust crate bstr to v1.11.1 (#15000) 2024-12-15 20:25:39 -05:00
renovate[bot]
1cc27c995c Update NPM Development dependencies (#14999) 2024-12-15 20:25:10 -05:00
renovate[bot]
a93bc2af6b Update dependency ruff to v0.8.3 (#15007) 2024-12-15 20:25:04 -05:00
Alex Waygood
d848182340 Pin mdformat plugins in pre-commit (#14992) 2024-12-15 19:37:45 +00:00
80 changed files with 3285 additions and 1123 deletions

9
.github/actionlint.yaml vendored Normal file
View File

@@ -0,0 +1,9 @@
# Configuration for the actionlint tool, which we run via pre-commit
# to verify the correctness of the syntax in our GitHub Actions workflows.
self-hosted-runner:
# Various runners we use that aren't recognized out-of-the-box by actionlint:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- windows-latest-xlarge

View File

@@ -53,7 +53,7 @@ jobs:
args: --out dist
- name: "Test sdist"
run: |
pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall
pip install dist/"${PACKAGE_NAME}"-*.tar.gz --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload sdist"
@@ -125,7 +125,7 @@ jobs:
args: --release --locked --out dist
- name: "Test wheel - aarch64"
run: |
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
ruff --help
python -m ruff --help
- name: "Upload wheels"
@@ -186,7 +186,7 @@ jobs:
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
run: |
python -m pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
python -m pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
@@ -236,7 +236,7 @@ jobs:
- name: "Test wheel"
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"

View File

@@ -142,6 +142,7 @@ jobs:
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
run: |
# shellcheck disable=SC2046
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
@@ -286,6 +287,8 @@ jobs:
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
run: |
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
# shellcheck disable=SC2046
docker buildx imagetools create \
"${annotations[@]}" \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \

View File

@@ -290,7 +290,9 @@ jobs:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- name: "Install Rust toolchain"
run: rustup default ${{ steps.msrv.outputs.value }}
env:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
@@ -306,7 +308,8 @@ jobs:
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
MSRV: ${{ steps.msrv.outputs.value }}
run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
cargo-fuzz-build:
name: "cargo fuzz build"
@@ -354,16 +357,18 @@ jobs:
name: ruff
path: ruff-to-test
- name: Fuzz
env:
DOWNLOAD_PATH: ${{ steps.download-cached-binary.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
chmod +x "${DOWNLOAD_PATH}/ruff"
(
uvx \
--python=${{ env.PYTHON_VERSION }} \
--python="${PYTHON_VERSION}" \
--from=./python/py-fuzzer \
fuzz \
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
--test-executable="${DOWNLOAD_PATH}/ruff" \
--bin=ruff \
0-500
)
@@ -429,64 +434,72 @@ jobs:
- name: Run `ruff check` stable ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
cat ecosystem-result-check-stable > $GITHUB_STEP_SUMMARY
cat ecosystem-result-check-stable > "$GITHUB_STEP_SUMMARY"
echo "### Linter (stable)" > ecosystem-result
cat ecosystem-result-check-stable >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff check` preview ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
cat ecosystem-result-check-preview > $GITHUB_STEP_SUMMARY
cat ecosystem-result-check-preview > "$GITHUB_STEP_SUMMARY"
echo "### Linter (preview)" >> ecosystem-result
cat ecosystem-result-check-preview >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff format` stable ecosystem check
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
cat ecosystem-result-format-stable > $GITHUB_STEP_SUMMARY
cat ecosystem-result-format-stable > "$GITHUB_STEP_SUMMARY"
echo "### Formatter (stable)" >> ecosystem-result
cat ecosystem-result-format-stable >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff format` preview ecosystem check
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
cat ecosystem-result-format-preview > $GITHUB_STEP_SUMMARY
cat ecosystem-result-format-preview > "$GITHUB_STEP_SUMMARY"
echo "### Formatter (preview)" >> ecosystem-result
cat ecosystem-result-format-preview >> ecosystem-result
echo "" >> ecosystem-result
@@ -541,7 +554,7 @@ jobs:
args: --out dist
- name: "Test wheel"
run: |
pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }}
pip install --force-reinstall --find-links dist "${PACKAGE_NAME}"
ruff --help
python -m ruff --help
- name: "Remove wheels from cache"
@@ -570,13 +583,13 @@ jobs:
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run pre-commit"
run: |
echo '```console' > $GITHUB_STEP_SUMMARY
echo '```console' > "$GITHUB_STEP_SUMMARY"
# Enable color output for pre-commit and remove it for the summary
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always | \
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> $GITHUB_STEP_SUMMARY) >&1
exit_code=${PIPESTATUS[0]}
echo '```' >> $GITHUB_STEP_SUMMARY
exit $exit_code
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
exit_code="${PIPESTATUS[0]}"
echo '```' >> "$GITHUB_STEP_SUMMARY"
exit "$exit_code"
docs:
name: "mkdocs"
@@ -637,7 +650,7 @@ jobs:
- name: "Run checks"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
run: cat target/formatter-ecosystem/stats.txt > "$GITHUB_STEP_SUMMARY"
- name: "Remove checkouts from cache"
run: rm -r target/formatter-ecosystem
@@ -676,11 +689,13 @@ jobs:
just install
- name: Run ruff-lsp tests
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Setup development binary
pip uninstall --yes ruff
chmod +x ${{ steps.ruff-target.outputs.download-path }}/ruff
export PATH=${{ steps.ruff-target.outputs.download-path }}:$PATH
chmod +x "${DOWNLOAD_PATH}/ruff"
export PATH="${DOWNLOAD_PATH}:${PATH}"
ruff version
just test

View File

@@ -46,6 +46,7 @@ jobs:
run: cargo build --locked
- name: Fuzz
run: |
# shellcheck disable=SC2046
(
uvx \
--python=3.12 \

View File

@@ -10,12 +10,11 @@ on:
description: The ecosystem workflow that triggers the workflow run
required: true
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@v7
name: Download pull request number
@@ -30,7 +29,7 @@ jobs:
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@v7
@@ -66,9 +65,9 @@ jobs:
cat pr/ecosystem/ecosystem-result >> comment.txt
echo "" >> comment.txt
echo 'comment<<EOF' >> $GITHUB_OUTPUT
cat comment.txt >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@v3

View File

@@ -44,8 +44,8 @@ jobs:
# Use version as display name for now
display_name="$version"
echo "version=$version" >> $GITHUB_ENV
echo "display_name=$display_name" >> $GITHUB_ENV
echo "version=$version" >> "$GITHUB_ENV"
echo "display_name=$display_name" >> "$GITHUB_ENV"
- name: "Set branch name"
run: |
@@ -55,8 +55,8 @@ jobs:
# characters disallowed in git branch names with hyphens
branch_display_name="$(echo "${display_name}" | tr -c '[:alnum:]._' '-' | tr -s '-')"
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV
echo "timestamp=$timestamp" >> $GITHUB_ENV
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
echo "timestamp=$timestamp" >> "$GITHUB_ENV"
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
@@ -112,7 +112,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
run: |
# set the PR title
pull_request_title="Update ruff documentation for "${display_name}""
pull_request_title="Update ruff documentation for ${display_name}"
# Delete any existing pull requests that are open for this version
# by checking against pull_request_title because the new PR will
@@ -124,10 +124,12 @@ jobs:
git push origin "${branch_name}"
# create the PR
gh pr create --base main --head "${branch_name}" \
--title "$pull_request_title" \
--body "Automated documentation update for "${display_name}"" \
--label "documentation"
gh pr create \
--base=main \
--head="${branch_name}" \
--title="${pull_request_title}" \
--body="Automated documentation update for ${display_name}" \
--label="documentation"
- name: "Merge Pull Request"
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}

View File

@@ -59,7 +59,7 @@ jobs:
run: |
cd ruff
git push --force origin typeshedbot/sync-typeshed
gh pr list --repo $GITHUB_REPOSITORY --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
create-issue-on-failure:

6
.github/zizmor.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
rules:
dangerous-triggers:
ignore:
- pr-comment.yaml

View File

@@ -21,3 +21,11 @@ MD014: false
MD024:
# Allow when nested under different parents e.g. CHANGELOG.md
siblings_only: true
# MD046/code-block-style
#
# Ignore this because it conflicts with the code block style used in content
# tabs of mkdocs-material which is to add a blank line after the content title.
#
# Ref: https://github.com/astral-sh/ruff/pull/15011#issuecomment-2544790854
MD046: false

View File

@@ -26,9 +26,8 @@ repos:
hooks:
- id: mdformat
additional_dependencies:
- mdformat-mkdocs
- mdformat-admon
- mdformat-footnote
- mdformat-mkdocs==4.0.0
- mdformat-footnote==0.1.1
exclude: |
(?x)^(
docs/formatter/black\.md
@@ -59,7 +58,7 @@ repos:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.28.2
rev: v1.28.3
hooks:
- id: typos
@@ -73,7 +72,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.2
rev: v0.8.3
hooks:
- id: ruff-format
- id: ruff
@@ -88,8 +87,10 @@ repos:
- id: prettier
types: [yaml]
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v0.8.0
rev: v0.9.2
hooks:
- id: zizmor
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
@@ -101,5 +102,23 @@ repos:
hooks:
- id: check-github-workflows
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.4
hooks:
- id: actionlint
# `release.yml` is autogenerated by `dist`; issues need to be fixed there
# (https://opensource.axo.dev/cargo-dist/)
exclude: .github/workflows/release.yml
args:
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
- "-ignore=SC2016" # another shellcheck lint: seems to have false positives?
additional_dependencies:
# actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions
# and checks these with shellcheck. This is arguably its most useful feature,
# but the integration only works if shellcheck is installed
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0"
ci:
skip: [cargo-fmt, dev-generate-all]

345
Cargo.lock generated
View File

@@ -30,6 +30,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -203,6 +209,18 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -220,9 +238,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
[[package]]
name = "bstr"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
dependencies = [
"memchr",
"regex-automata 0.4.8",
@@ -314,9 +332,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.38"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -465,12 +483,12 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -835,6 +853,15 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "document-features"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
dependencies = [
"litrs",
]
[[package]]
name = "drop_bomb"
version = "0.1.5"
@@ -923,9 +950,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fern"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"log",
]
@@ -952,6 +979,15 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -985,6 +1021,12 @@ dependencies = [
"libc",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1102,6 +1144,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hugealloc"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "542733215a252c27c674e5d875a910d7de509cb74123d0bdbaa050f871ec84c2"
dependencies = [
"allocator-api2",
"libc",
"sptr",
]
[[package]]
name = "humantime"
version = "2.1.0"
@@ -1444,6 +1497,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_sorted"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357376465c37db3372ef6a00585d336ed3d0f11d4345eef77ebcb05865392b21"
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1521,9 +1580,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.167"
version = "0.2.168"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
[[package]]
name = "libcst"
@@ -1571,6 +1630,12 @@ dependencies = [
"redox_syscall 0.5.3",
]
[[package]]
name = "linear-hashtbl"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e252bb7e9b36739fb2482438638e862241d111c76fae3ebee272454477c63a4f"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -1589,6 +1654,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.4.11"
@@ -1703,6 +1774,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
[[package]]
name = "natord"
version = "1.0.9"
@@ -1856,6 +1933,153 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "oxidd"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f7c2dbbc26b8d2e1ae861401a4bfa1d2e229e39da77ff8f0d64f2b4793e6da"
dependencies = [
"cfg-if",
"document-features",
"oxidd-cache",
"oxidd-core",
"oxidd-derive",
"oxidd-dump",
"oxidd-manager-index",
"oxidd-reorder",
"oxidd-rules-bdd",
"oxidd-rules-mtbdd",
"oxidd-rules-tdd",
"oxidd-rules-zbdd",
"rustc-hash 1.1.0",
]
[[package]]
name = "oxidd-cache"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efcc7923864f2a2525e76e26a3645b65a82e29ac3d3118d6906bad0117eb704e"
dependencies = [
"allocator-api2",
"document-features",
"hugealloc",
"oxidd-core",
"parking_lot",
]
[[package]]
name = "oxidd-core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b47e2c527b7aba7a7c789c7e5300df5a338002b33284c823fd08e1c661d176d6"
dependencies = [
"nanorand",
]
[[package]]
name = "oxidd-derive"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ccd72606db214bdf9b3880a6dedb50990fafdac23a05b65f982b65ac22862ac"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "oxidd-dump"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84ef9fed6a604b8199c988efce9231de372b058cf9501d1b60fa53ed0ea1d43a"
dependencies = [
"bitvec",
"document-features",
"is_sorted",
"memchr",
"oxidd-core",
"rustc-hash 1.1.0",
]
[[package]]
name = "oxidd-manager-index"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd41278c0cd6f0418f78e8ed5a3a062fa31449a929b3a0d03d4c1056af620cdc"
dependencies = [
"bitvec",
"crossbeam-utils",
"linear-hashtbl",
"oxidd-core",
"parking_lot",
"rayon",
"rustc-hash 1.1.0",
]
[[package]]
name = "oxidd-reorder"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222b3b3c71f9c4a5f875c01fda840e25d40d4fa86a5f0636938c17f7adf6c068"
dependencies = [
"flume",
"is_sorted",
"oxidd-core",
"rayon",
"smallvec",
]
[[package]]
name = "oxidd-rules-bdd"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ab7d000b0fb53ddcc282296929f51b2a5215591f6c6aa134a9fca9f133f61b"
dependencies = [
"bitvec",
"document-features",
"oxidd-core",
"oxidd-derive",
"oxidd-dump",
]
[[package]]
name = "oxidd-rules-mtbdd"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9bd51a1a297d98cefd4843cf065aba5cb04b2e78b0623857f0f8babe9f3d421"
dependencies = [
"document-features",
"oxidd-core",
"oxidd-derive",
"oxidd-dump",
]
[[package]]
name = "oxidd-rules-tdd"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "753f378e1d76f1f23afd1a06bbe4f23025ac95d19fb3c2b3ff052c0d3fcd9af3"
dependencies = [
"document-features",
"oxidd-core",
"oxidd-derive",
"oxidd-dump",
]
[[package]]
name = "oxidd-rules-zbdd"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "821482c23511e87b1a9bd7f0454971f4a73b01a7ed16611179d79c5872690dce"
dependencies = [
"bitvec",
"document-features",
"oxidd-core",
"oxidd-derive",
"oxidd-dump",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -2127,6 +2351,29 @@ dependencies = [
"yansi",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
@@ -2161,7 +2408,7 @@ dependencies = [
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
"thiserror 2.0.6",
"thiserror 2.0.7",
"uuid",
]
@@ -2203,6 +2450,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
@@ -2265,6 +2518,9 @@ dependencies = [
"crossbeam",
"ctrlc",
"filetime",
"oxidd",
"oxidd-core",
"oxidd-dump",
"rayon",
"red_knot_python_semantic",
"red_knot_server",
@@ -2314,7 +2570,7 @@ dependencies = [
"static_assertions",
"tempfile",
"test-case",
"thiserror 2.0.6",
"thiserror 2.0.7",
"tracing",
]
@@ -2411,7 +2667,7 @@ dependencies = [
"rustc-hash 2.1.0",
"salsa",
"serde",
"thiserror 2.0.6",
"thiserror 2.0.7",
"toml",
"tracing",
]
@@ -2564,7 +2820,7 @@ dependencies = [
"strum",
"tempfile",
"test-case",
"thiserror 2.0.6",
"thiserror 2.0.7",
"tikv-jemallocator",
"toml",
"tracing",
@@ -2634,7 +2890,7 @@ dependencies = [
"salsa",
"serde",
"tempfile",
"thiserror 2.0.6",
"thiserror 2.0.7",
"tracing",
"tracing-subscriber",
"tracing-tree",
@@ -2786,7 +3042,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"thiserror 2.0.6",
"thiserror 2.0.7",
"toml",
"typed-arena",
"unicode-normalization",
@@ -2820,7 +3076,7 @@ dependencies = [
"serde_json",
"serde_with",
"test-case",
"thiserror 2.0.6",
"thiserror 2.0.7",
"uuid",
]
@@ -2892,7 +3148,7 @@ dependencies = [
"similar",
"smallvec",
"static_assertions",
"thiserror 2.0.6",
"thiserror 2.0.7",
"tracing",
]
@@ -3025,7 +3281,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"thiserror 2.0.6",
"thiserror 2.0.7",
"tracing",
"tracing-subscriber",
]
@@ -3193,7 +3449,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
dependencies = [
"append-only-vec",
"arc-swap",
@@ -3203,6 +3459,7 @@ dependencies = [
"indexmap",
"lazy_static",
"parking_lot",
"rayon",
"rustc-hash 2.1.0",
"salsa-macro-rules",
"salsa-macros",
@@ -3213,12 +3470,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
dependencies = [
"heck",
"proc-macro2",
@@ -3280,9 +3537,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
@@ -3300,9 +3557,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
@@ -3442,6 +3699,15 @@ name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "sptr"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
[[package]]
name = "stable_deref_trait"
@@ -3537,6 +3803,12 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.14.0"
@@ -3623,11 +3895,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.6"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
dependencies = [
"thiserror-impl 2.0.6",
"thiserror-impl 2.0.7",
]
[[package]]
@@ -3643,9 +3915,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.6"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
dependencies = [
"proc-macro2",
"quote",
@@ -4473,6 +4745,15 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "yansi"
version = "1.0.1"

View File

@@ -105,6 +105,7 @@ mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "7.0.0" }
ordermap = { version = "0.5.0" }
oxidd = { version = "0.9.0", features = ["tdd"] }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" }
@@ -118,7 +119,8 @@ 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 = "254c749b02cde2fd29852a7463a33e800b771758" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -24,6 +24,9 @@ colored = { workspace = true }
countme = { workspace = true, features = ["enable"] }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
oxidd = { workspace = true }
oxidd-core = { version = "0.9.0" }
oxidd-dump = { version = "0.4.0" }
rayon = { workspace = true }
salsa = { workspace = true }
tracing = { workspace = true, features = ["release_max_level_debug"] }

View File

@@ -105,8 +105,44 @@ pub enum Command {
Server,
}
use oxidd::bdd::BDDFunction;
use oxidd::ManagerRef;
use oxidd::{BooleanFunction, BooleanFunctionQuant};
use oxidd_core::Manager;
use oxidd_dump::dot::dump_all;
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
pub fn main() -> ExitStatus {
let mgr = oxidd::bdd::new_manager(24, 24, 1);
let (x, y, z) = mgr.with_manager_exclusive(|mgr| {
(
BDDFunction::new_var(mgr).unwrap(),
BDDFunction::new_var(mgr).unwrap(),
BDDFunction::new_var(mgr).unwrap(),
)
});
mgr.with_manager_shared(|manager| {
let inner_func = x
.or(&y.and(&x.not().unwrap()).unwrap())
.unwrap()
.or(&y.not().unwrap().and(&x.not().unwrap()).unwrap())
.unwrap();
let func = z.and(&inner_func).unwrap();
let func = func.restrict(&z).unwrap();
manager.gc();
let file = std::fs::File::create("bdd.dot").expect("could not create `bdd.dot`");
dump_all(
file,
manager,
[(&x, "x"), (&y, "y"), (&z, "z")],
[(&func, "z ^ (x (y ∧ ~x) (~y ∧ ~x))")],
)
.expect("dot export failed");
});
panic!("FOO");
run().unwrap_or_else(|error| {
use std::io::Write;
@@ -279,7 +315,7 @@ impl MainLoop {
while let Ok(message) = self.receiver.recv() {
match message {
MainLoopMessage::CheckWorkspace => {
let db = db.snapshot();
let db = db.clone();
let sender = self.sender.clone();
// Spawn a new task that checks the workspace. This needs to be done in a separate thread

View File

@@ -3,43 +3,59 @@
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
still need to be supported by a type checker.
## Currently unsupported
## Correspondence
Support for most of these symbols is currently a TODO:
All of the following symbols can be mapped one-to-one with the actual type:
```py
import typing
def f(
a: typing.List,
b: typing.List[int],
c: typing.Dict,
d: typing.Dict[int, str],
e: typing.DefaultDict,
f: typing.DefaultDict[str, int],
g: typing.Set,
h: typing.Set[int],
i: typing.FrozenSet,
j: typing.FrozenSet[str],
k: typing.OrderedDict,
l: typing.OrderedDict[int, str],
m: typing.Counter,
n: typing.Counter[int],
list_bare: typing.List,
list_parametrized: typing.List[int],
dict_bare: typing.Dict,
dict_parametrized: typing.Dict[int, str],
set_bare: typing.Set,
set_parametrized: typing.Set[int],
frozen_set_bare: typing.FrozenSet,
frozen_set_parametrized: typing.FrozenSet[str],
chain_map_bare: typing.ChainMap,
chain_map_parametrized: typing.ChainMap[int],
counter_bare: typing.Counter,
counter_parametrized: typing.Counter[int],
default_dict_bare: typing.DefaultDict,
default_dict_parametrized: typing.DefaultDict[str, int],
deque_bare: typing.Deque,
deque_parametrized: typing.Deque[str],
ordered_dict_bare: typing.OrderedDict,
ordered_dict_parametrized: typing.OrderedDict[int, str],
):
reveal_type(a) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(b) # revealed: @Todo(typing.List alias)
reveal_type(c) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(d) # revealed: @Todo(typing.Dict alias)
reveal_type(e) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(f) # revealed: @Todo(typing.DefaultDict[] alias)
reveal_type(g) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(h) # revealed: @Todo(typing.Set alias)
reveal_type(i) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(j) # revealed: @Todo(typing.FrozenSet alias)
reveal_type(k) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(l) # revealed: @Todo(typing.OrderedDict alias)
reveal_type(m) # revealed: @Todo(Unsupported or invalid type in a type expression)
reveal_type(n) # revealed: @Todo(typing.Counter[] alias)
reveal_type(list_bare) # revealed: list
reveal_type(list_parametrized) # revealed: list
reveal_type(dict_bare) # revealed: dict
reveal_type(dict_parametrized) # revealed: dict
reveal_type(set_bare) # revealed: set
reveal_type(set_parametrized) # revealed: set
reveal_type(frozen_set_bare) # revealed: frozenset
reveal_type(frozen_set_parametrized) # revealed: frozenset
reveal_type(chain_map_bare) # revealed: ChainMap
reveal_type(chain_map_parametrized) # revealed: ChainMap
reveal_type(counter_bare) # revealed: Counter
reveal_type(counter_parametrized) # revealed: Counter
reveal_type(default_dict_bare) # revealed: defaultdict
reveal_type(default_dict_parametrized) # revealed: defaultdict
reveal_type(deque_bare) # revealed: deque
reveal_type(deque_parametrized) # revealed: deque
reveal_type(ordered_dict_bare) # revealed: OrderedDict
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict
```
## Inheritance
@@ -49,35 +65,63 @@ The aliases can be inherited from. Some of these are still partially or wholly T
```py
import typing
class A(typing.Dict): ...
####################
### Built-ins
class ListSubclass(typing.List): ...
# TODO: should have `Generic`, should not have `Unknown`
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[dict], Unknown, Literal[object]]
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
reveal_type(ListSubclass.__mro__)
class B(typing.List): ...
class DictSubclass(typing.Dict): ...
# TODO: should have `Generic`, should not have `Unknown`
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[list], Unknown, Literal[object]]
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
reveal_type(DictSubclass.__mro__)
class C(typing.Set): ...
class SetSubclass(typing.Set): ...
# TODO: should have `Generic`, should not have `Unknown`
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[set], Unknown, Literal[object]]
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
reveal_type(SetSubclass.__mro__)
class D(typing.FrozenSet): ...
class FrozenSetSubclass(typing.FrozenSet): ...
# TODO: should have `Generic`, should not have `Unknown`
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[frozenset], Unknown, Literal[object]]
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
reveal_type(FrozenSetSubclass.__mro__)
class E(typing.DefaultDict): ...
####################
### `collections`
reveal_type(E.__mro__) # revealed: tuple[Literal[E], @Todo(Support for more typing aliases as base classes), Literal[object]]
class ChainMapSubclass(typing.ChainMap): ...
class F(typing.OrderedDict): ...
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
reveal_type(ChainMapSubclass.__mro__)
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for more typing aliases as base classes), Literal[object]]
class CounterSubclass(typing.Counter): ...
class G(typing.Counter): ...
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]]
reveal_type(CounterSubclass.__mro__)
reveal_type(G.__mro__) # revealed: tuple[Literal[G], @Todo(Support for more typing aliases as base classes), Literal[object]]
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -51,7 +51,7 @@ class D(TypeIs): ... # error: [invalid-base]
class E(Concatenate): ... # error: [invalid-base]
class F(Callable): ...
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for more typing aliases as base classes), Literal[object]]
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
```
## Subscriptability

View File

@@ -67,6 +67,6 @@ def _(flag: bool):
def __call__(self) -> int: ...
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
# error: "Object of type `Literal[__call__] | Literal[1]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: int | Unknown
```

View File

@@ -19,14 +19,17 @@ def _(flag: bool):
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Partial declarations
## Incompatible declarations for 2 (out of 3) types
```py
def _(flag: bool):
if flag:
def _(flag1: bool, flag2: bool):
if flag1:
x: str
elif flag2:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
# Here, the declared type for `x` is `int | str | Unknown`.
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Incompatible declarations with bad assignment
@@ -42,3 +45,31 @@ def _(flag: bool):
# error: [invalid-assignment]
x = b"foo"
```
## No errors
Currently, we avoid raising the conflicting-declarations for the following cases:
### Partial declarations
```py
def _(flag: bool):
if flag:
x: int
x = 1
```
### Partial declarations in try-except
Refer to <https://github.com/astral-sh/ruff/issues/13966>
```py
def _():
try:
x: int = 1
except:
x = 2
x = 3
```

View File

@@ -25,3 +25,82 @@ reveal_type(D) # revealed: Literal[C]
```py path=b.py
class C: ...
```
## Nested
```py
import a.b
reveal_type(a.b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Deeply nested
```py
import a.b.c
reveal_type(a.b.c.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b/__init__.py
```
```py path=a/b/c.py
class C: ...
```
## Nested with rename
```py
import a.b as b
reveal_type(b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Deeply nested with rename
```py
import a.b.c as c
reveal_type(c.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b/__init__.py
```
```py path=a/b/c.py
class C: ...
```
## Unresolvable submodule imports
```py
# Topmost component resolvable, submodule not resolvable:
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
# Topmost component unresolvable:
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```
```py path=a/__init__.py
```

View File

@@ -7,3 +7,25 @@ from import bar # error: [invalid-syntax]
reveal_type(bar) # revealed: Unknown
```
## Invalid nested module import
TODO: This is correctly flagged as an error, but we could clean up the diagnostics that we report.
```py
# TODO: No second diagnostic
# error: [invalid-syntax] "Expected ',', found '.'"
# error: [unresolved-import] "Module `a` has no member `c`"
from a import b.c
# TODO: Should these be inferred as Unknown?
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.c) # revealed: Literal[1]
```
```py path=a/__init__.py
```
```py path=a/b.py
c = 1
```

View File

@@ -121,23 +121,44 @@ X = 42
```
```py path=package/bar.py
# TODO: support submodule imports
from . import foo # error: [unresolved-import]
from . import foo
y = foo.X
# TODO: should be `Literal[42]`
reveal_type(y) # revealed: Unknown
reveal_type(foo.X) # revealed: Literal[42]
```
## Non-existent + bare to module
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
nor an attribute of `package`.
```py path=package/__init__.py
```
```py path=package/bar.py
# TODO: support submodule imports
from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown
```
## Import submodule from self
We don't currently consider `from...import` statements when building up the `imported_modules` set
in the semantic index. When accessing an attribute of a module, we only consider it a potential
submodule when that submodule name appears in the `imported_modules` set. That means that submodules
that are imported via `from...import` are not visible to our type inference if you also access that
submodule via the attribute on its parent package.
```py path=package/__init__.py
```
```py path=package/foo.py
X = 42
```
```py path=package/bar.py
from . import foo
import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```

View File

@@ -0,0 +1,100 @@
# Tracking imported modules
These tests depend on how we track which modules have been imported. There are currently two
characteristics of our module tracking that can lead to inaccuracies:
- Imports are tracked on a per-file basis. At runtime, importing a submodule in one file makes that
submodule globally available via any reference to the containing package. We will flag an error
if a file tries to access a submodule without there being an import of that submodule _in that
same file_.
This is a purposeful decision, and not one we plan to change. If a module wants to re-export some
other module that it imports, there are ways to do that (tested below) that are blessed by the
typing spec and that are visible to our file-scoped import tracking.
- Imports are tracked flow-insensitively: submodule accesses are allowed and resolved if that
submodule is imported _anywhere in the file_. This handles the common case where all imports are
grouped at the top of the file, and is easiest to implement. We might revisit this decision and
track submodule imports flow-sensitively, in which case we will have to update the assertions in
some of these tests.
## Import submodule later in file
This test highlights our flow-insensitive analysis, since we access the `a.b` submodule before it
has been imported.
```py
import a
# Would be an error with flow-sensitive tracking
reveal_type(a.b.C) # revealed: Literal[C]
import a.b
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Rename a re-export
This test highlights how import tracking is local to each file, but specifically to the file where a
containing module is first referenced. This allows the main module to see that `q.a` contains a
submodule `b`, even though `a.b` is never imported in the main module.
```py
from q import a, b
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.C) # revealed: Literal[C]
reveal_type(a.b) # revealed: <module 'a.b'>
reveal_type(a.b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
```py path=q.py
import a as a
import a.b as b
```
## Attribute overrides submodule
Technically, either a submodule or a non-module attribute could shadow the other, depending on the
ordering of when the submodule is loaded relative to the parent module's `__init__.py` file being
evaluated. We have chosen to always have the submodule take priority. (This matches pyright's
current behavior, and opposite of mypy's current behavior.)
```py
import sub.b
import attr.b
# In the Python interpreter, `attr.b` is Literal[1]
reveal_type(sub.b) # revealed: <module 'sub.b'>
reveal_type(attr.b) # revealed: <module 'attr.b'>
```
```py path=sub/__init__.py
b = 1
```
```py path=sub/b.py
```
```py path=attr/__init__.py
from . import b as _
b = 1
```
```py path=attr/b.py
```

View File

@@ -0,0 +1,221 @@
# Narrowing For Truthiness Checks (`if x` or `if not x`)
## Value Literals
```py
def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]:
return 0
x = foo()
if x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
else:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
if not x:
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
if x and not x:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
if not (x and not x):
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if x or not x:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
else:
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
```
## Function Literals
Basically functions are always truthy.
```py
def flag() -> bool:
return True
def foo(hello: int) -> bytes:
return b""
def bar(world: str, *args, **kwargs) -> float:
return 0.0
x = foo if flag() else bar
if x:
reveal_type(x) # revealed: Literal[foo, bar]
else:
reveal_type(x) # revealed: Never
```
## Mutable Truthiness
### Truthiness of Instances
The boolean value of an instance is not always consistent. For example, `__bool__` can be customized
to return random values, or in the case of a `list()`, the result depends on the number of elements
in the list. Therefore, these types should not be narrowed by `if x` or `if not x`.
```py
class A: ...
class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
if x or not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
```
### Truthiness of Types
Also, types may not be Truthy. This is because `__bool__` can be customized via a metaclass.
Although this is a very rare case, we may consider metaclass checks in the future to handle this
more accurately.
```py
def flag() -> bool:
return True
x = int if flag() else str
reveal_type(x) # revealed: Literal[int, str]
if x:
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
else:
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
```
## Determined Truthiness
Some custom classes can have a boolean value that is consistently determined as either `True` or
`False`, regardless of the instance's state. This is achieved by defining a `__bool__` method that
always returns a fixed value.
These types can always be fully narrowed in boolean contexts, as shown below:
```py
class T:
def __bool__(self) -> Literal[True]:
return True
class F:
def __bool__(self) -> Literal[False]:
return False
t = T()
if t:
reveal_type(t) # revealed: T
else:
reveal_type(t) # revealed: Never
f = F()
if f:
reveal_type(f) # revealed: Never
else:
reveal_type(f) # revealed: F
```
## Narrowing Complex Intersection and Union
```py
class A: ...
class B: ...
def flag() -> bool:
return True
def instance() -> A | B:
return A()
def literals() -> Literal[0, 42, "", "hello"]:
return 42
x = instance()
y = literals()
if isinstance(x, str) and not isinstance(x, B):
reveal_type(x) # revealed: A & str & ~B
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
```
## Narrowing Multiple Variables
```py
def f(x: Literal[0, 1], y: Literal["", "hello"]):
if x and y and not x and not y:
reveal_type(x) # revealed: Never
reveal_type(y) # revealed: Never
else:
# ~(x or not x) and ~(y or not y)
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal["", "hello"]
if (x or not x) and (y and not y):
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Never
else:
# ~(x or not x) or ~(y and not y)
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal["", "hello"]
```
## ControlFlow Merging
After merging control flows, when we take the union of all constraints applied in each branch, we
should return to the original state.
```py
class A: ...
x = A()
if x and not x:
y = x
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
else:
y = x
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
```

View File

@@ -61,10 +61,8 @@ class B: ...
```py path=a/test.py
import a.b
# TODO: no diagnostic
# error: [unresolved-attribute]
def f(c: type[a.b.C]):
reveal_type(c) # revealed: @Todo(unsupported type[X] special form)
reveal_type(c) # revealed: type[C]
```
```py path=a/__init__.py

View File

@@ -27,6 +27,7 @@ pub(crate) mod tests {
use ruff_db::{Db as SourceDb, Upcast};
#[salsa::db]
#[derive(Clone)]
pub(crate) struct TestDb {
storage: salsa::Storage<Self>,
files: Files,

View File

@@ -186,6 +186,26 @@ impl ModuleName {
self.0.push('.');
self.0.push_str(other);
}
/// Returns an iterator of this module name and all of its parent modules.
///
/// # Examples
///
/// ```
/// use red_knot_python_semantic::ModuleName;
///
/// assert_eq!(
/// ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::<Vec<_>>(),
/// vec![
/// ModuleName::new_static("foo.bar.baz").unwrap(),
/// ModuleName::new_static("foo.bar").unwrap(),
/// ModuleName::new_static("foo").unwrap(),
/// ],
/// );
/// ```
pub fn ancestors(&self) -> impl Iterator<Item = Self> {
std::iter::successors(Some(self.clone()), Self::parent)
}
}
impl Deref for ModuleName {

View File

@@ -7,7 +7,7 @@ use super::path::SearchPath;
use crate::module_name::ModuleName;
/// Representation of a Python module.
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Module {
inner: Arc<ModuleInner>,
}
@@ -61,7 +61,7 @@ impl std::fmt::Debug for Module {
}
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Hash)]
struct ModuleInner {
name: ModuleName,
kind: ModuleKind,

View File

@@ -73,6 +73,15 @@ enum SystemOrVendoredPathRef<'a> {
Vendored(&'a VendoredPath),
}
impl std::fmt::Display for SystemOrVendoredPathRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SystemOrVendoredPathRef::System(system) => system.fmt(f),
SystemOrVendoredPathRef::Vendored(vendored) => vendored.fmt(f),
}
}
}
/// 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.

View File

@@ -1,13 +1,14 @@
use std::iter::FusedIterator;
use std::sync::Arc;
use rustc_hash::{FxBuildHasher, FxHashMap};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_index::{IndexSlice, IndexVec};
use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds;
use crate::semantic_index::builder::SemanticIndexBuilder;
@@ -60,6 +61,22 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
index.symbol_table(scope.file_scope_id(db))
}
/// Returns the set of modules that are imported anywhere in `file`.
///
/// This set only considers `import` statements, not `from...import` statements, because:
///
/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is
/// therefore imported) without looking outside the content of this file. (We could turn this
/// into a _potentially_ imported modules set, but that would change how it's used in our type
/// inference logic.)
///
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
/// knowing the name of the current module, and whether it's a package.
#[salsa::tracked]
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
semantic_index(db, file).imported_modules.clone()
}
/// Returns the use-def map for a specific `scope`.
///
/// Using [`use_def_map`] over [`semantic_index`] has the advantage that
@@ -116,6 +133,9 @@ pub(crate) struct SemanticIndex<'db> {
/// changing a file invalidates all dependents.
ast_ids: IndexVec<FileScopeId, AstIds>,
/// The set of modules that are imported anywhere within this file.
imported_modules: Arc<FxHashSet<ModuleName>>,
/// Flags about the global scope (code usage impacting inference)
has_future_annotations: bool,
}

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use except_handlers::TryNodeContextStackManager;
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_db::files::File;
use ruff_db::parsed::ParsedModule;
@@ -12,6 +12,7 @@ use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
use ruff_python_ast::{BoolOp, Expr};
use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::definition::{
@@ -79,6 +80,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
imported_modules: FxHashSet<ModuleName>,
}
impl<'db> SemanticIndexBuilder<'db> {
@@ -105,6 +107,8 @@ impl<'db> SemanticIndexBuilder<'db> {
scopes_by_node: FxHashMap::default(),
definitions_by_node: FxHashMap::default(),
expressions_by_node: FxHashMap::default(),
imported_modules: FxHashSet::default(),
};
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
@@ -558,6 +562,7 @@ impl<'db> SemanticIndexBuilder<'db> {
scopes_by_expression: self.scopes_by_expression,
scopes_by_node: self.scopes_by_node,
use_def_maps,
imported_modules: Arc::new(self.imported_modules),
has_future_annotations: self.has_future_annotations,
}
}
@@ -661,6 +666,12 @@ where
}
ast::Stmt::Import(node) => {
for alias in &node.names {
// Mark the imported module, and all of its parents, as being imported in this
// file.
if let Some(module_name) = ModuleName::new(&alias.name) {
self.imported_modules.extend(module_name.ancestors());
}
let symbol_name = if let Some(asname) = &alias.asname {
asname.id.clone()
} else {

View File

@@ -17,6 +17,7 @@ pub(crate) enum CoreStdlibModule {
Sys,
#[allow(dead_code)]
Abc, // currently only used in tests
Collections,
}
impl CoreStdlibModule {
@@ -29,6 +30,7 @@ impl CoreStdlibModule {
Self::TypingExtensions => "typing_extensions",
Self::Sys => "sys",
Self::Abc => "abc",
Self::Collections => "collections",
}
}

View File

@@ -14,13 +14,14 @@ pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
};
pub(crate) use self::signatures::Signature;
use crate::module_resolver::file_to_module;
use crate::module_name::ModuleName;
use crate::module_resolver::{file_to_module, resolve_module};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
global_scope, semantic_index, symbol_table, use_def_map, BindingWithConstraints,
BindingWithConstraintsIterator, DeclarationsIterator,
global_scope, imported_modules, semantic_index, symbol_table, use_def_map,
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
};
use crate::stdlib::{
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
@@ -291,8 +292,8 @@ type DeclaredTypeResult<'db> = Result<Type<'db>, (Type<'db>, Box<[Type<'db>]>)>;
/// `Ok(declared_type)`. If there are conflicting declarations, returns
/// `Err((union_of_declared_types, conflicting_declared_types))`.
///
/// If undeclared is a possibility, `undeclared_ty` type will be part of the return type (and may
/// conflict with other declarations.)
/// If undeclared is a possibility, `undeclared_ty` type will be part of the return type but it
/// will not be considered to be conflicting with any other types.
///
/// # Panics
/// Will panic if there are no declarations and no `undeclared_ty` is provided. This is a logic
@@ -303,27 +304,31 @@ fn declarations_ty<'db>(
declarations: DeclarationsIterator<'_, 'db>,
undeclared_ty: Option<Type<'db>>,
) -> DeclaredTypeResult<'db> {
let decl_types = declarations.map(|declaration| declaration_ty(db, declaration));
let mut declaration_types = declarations.map(|declaration| declaration_ty(db, declaration));
let mut all_types = undeclared_ty.into_iter().chain(decl_types);
let first = all_types.next().expect(
"declarations_ty must not be called with zero declarations and no may-be-undeclared",
);
let Some(first) = declaration_types.next() else {
if let Some(undeclared_ty) = undeclared_ty {
// Short-circuit to return the undeclared type if there are no declarations.
return Ok(undeclared_ty);
}
panic!("declarations_ty must not be called with zero declarations and no undeclared_ty");
};
let mut conflicting: Vec<Type<'db>> = vec![];
let declared_ty = if let Some(second) = all_types.next() {
let mut builder = UnionBuilder::new(db).add(first);
for other in [second].into_iter().chain(all_types) {
if !first.is_equivalent_to(db, other) {
conflicting.push(other);
}
builder = builder.add(other);
let mut builder = UnionBuilder::new(db).add(first);
for other in declaration_types {
if !first.is_equivalent_to(db, other) {
conflicting.push(other);
}
builder.build()
} else {
first
};
builder = builder.add(other);
}
// Avoid considering the undeclared type for the conflicting declaration diagnostics. It
// should still be part of the declared type.
if let Some(undeclared_ty) = undeclared_ty {
builder = builder.add(undeclared_ty);
}
let declared_ty = builder.build();
if conflicting.is_empty() {
Ok(declared_ty)
} else {
@@ -413,7 +418,7 @@ pub enum Type<'db> {
/// A specific function object
FunctionLiteral(FunctionType<'db>),
/// A specific module object
ModuleLiteral(File),
ModuleLiteral(ModuleLiteralType<'db>),
/// A specific class object
ClassLiteral(ClassLiteralType<'db>),
// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`.
@@ -426,6 +431,11 @@ pub enum Type<'db> {
Union(UnionType<'db>),
/// The set of objects in all of the types in the intersection
Intersection(IntersectionType<'db>),
/// Represents objects whose `__bool__` method is deterministic:
/// - `AlwaysTruthy`: `__bool__` always returns `True`
/// - `AlwaysFalsy`: `__bool__` always returns `False`
AlwaysTruthy,
AlwaysFalsy,
/// An integer literal
IntLiteral(i64),
/// A boolean literal, either `True` or `False`.
@@ -446,6 +456,10 @@ pub enum Type<'db> {
}
impl<'db> Type<'db> {
pub const fn is_unknown(&self) -> bool {
matches!(self, Type::Unknown)
}
pub const fn is_never(&self) -> bool {
matches!(self, Type::Never)
}
@@ -475,15 +489,19 @@ impl<'db> Type<'db> {
matches!(self, Type::ClassLiteral(..))
}
pub const fn into_module_literal(self) -> Option<File> {
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self {
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
}
pub const fn into_module_literal(self) -> Option<ModuleLiteralType<'db>> {
match self {
Type::ModuleLiteral(file) => Some(file),
Type::ModuleLiteral(module) => Some(module),
_ => None,
}
}
#[track_caller]
pub fn expect_module_literal(self) -> File {
pub fn expect_module_literal(self) -> ModuleLiteralType<'db> {
self.into_module_literal()
.expect("Expected a Type::ModuleLiteral variant")
}
@@ -704,6 +722,15 @@ impl<'db> Type<'db> {
.all(|&neg_ty| self.is_disjoint_from(db, neg_ty))
}
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
(left, Type::AlwaysFalsy) => matches!(left.bool(db), Truthiness::AlwaysFalse),
(left, Type::AlwaysTruthy) => matches!(left.bool(db), Truthiness::AlwaysTrue),
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
target.is_equivalent_to(db, KnownClass::Object.to_instance(db))
}
// All `StringLiteral` types are a subtype of `LiteralString`.
(Type::StringLiteral(_), Type::LiteralString) => true,
@@ -1092,6 +1119,16 @@ impl<'db> Type<'db> {
false
}
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
matches!(ty.bool(db), Truthiness::AlwaysFalse)
}
(Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => {
// Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`.
matches!(ty.bool(db), Truthiness::AlwaysTrue)
}
(Type::KnownInstance(left), right) => {
left.instance_fallback(db).is_disjoint_from(db, right)
}
@@ -1225,7 +1262,9 @@ impl<'db> Type<'db> {
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::KnownInstance(_) => true,
| Type::KnownInstance(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => true,
Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)),
Type::ClassLiteral(_) | Type::Instance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
@@ -1327,6 +1366,7 @@ impl<'db> Type<'db> {
//
false
}
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
}
}
@@ -1380,6 +1420,11 @@ impl<'db> Type<'db> {
| KnownClass::ModuleType
| KnownClass::FunctionType
| KnownClass::SpecialForm
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
| KnownClass::Deque
| KnownClass::OrderedDict
| KnownClass::StdlibAlias
| KnownClass::TypeVar,
) => false,
@@ -1392,7 +1437,9 @@ impl<'db> Type<'db> {
| Type::Todo(_)
| Type::Union(..)
| Type::Intersection(..)
| Type::LiteralString => false,
| Type::LiteralString
| Type::AlwaysTruthy
| Type::AlwaysFalsy => false,
}
}
@@ -1418,7 +1465,7 @@ impl<'db> Type<'db> {
// TODO: attribute lookup on function type
todo_type!().into()
}
Type::ModuleLiteral(file) => {
Type::ModuleLiteral(module_ref) => {
// `__dict__` is a very special member that is never overridden by module globals;
// we should always look it up directly as an attribute on `types.ModuleType`,
// never in the global scope of the module.
@@ -1428,7 +1475,30 @@ impl<'db> Type<'db> {
.member(db, "__dict__");
}
let global_lookup = symbol(db, global_scope(db, *file), name);
// If the file that originally imported the module has also imported a submodule
// named [name], then the result is (usually) that submodule, even if the module
// also defines a (non-module) symbol with that name.
//
// Note that technically, either the submodule or the non-module symbol could take
// priority, depending on the ordering of when the submodule is loaded relative to
// the parent module's `__init__.py` file being evaluated. That said, we have
// chosen to always have the submodule take priority. (This matches pyright's
// current behavior, and opposite of mypy's current behavior.)
if let Some(submodule_name) = ModuleName::new(name) {
let importing_file = module_ref.importing_file(db);
let imported_submodules = imported_modules(db, importing_file);
let mut full_submodule_name = module_ref.module(db).name().clone();
full_submodule_name.extend(&submodule_name);
if imported_submodules.contains(&full_submodule_name) {
if let Some(submodule) = resolve_module(db, &full_submodule_name) {
let submodule_ty = Type::module_literal(db, importing_file, submodule);
return Symbol::Type(submodule_ty, Boundness::Bound);
}
}
}
let global_lookup =
symbol(db, global_scope(db, module_ref.module(db).file()), name);
// If it's unbound, check if it's present as an instance on `types.ModuleType`
// or `builtins.object`.
@@ -1537,6 +1607,10 @@ impl<'db> Type<'db> {
// TODO: implement tuple methods
todo_type!().into()
}
Type::AlwaysTruthy | Type::AlwaysFalsy => {
// TODO return `Callable[[], Literal[True/False]]` for `__bool__` access
KnownClass::Object.to_instance(db).member(db, name)
}
&todo @ Type::Todo(_) => todo.into(),
}
}
@@ -1559,6 +1633,8 @@ impl<'db> Type<'db> {
// TODO: see above
Truthiness::Ambiguous
}
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
instance_ty @ Type::Instance(InstanceType { class }) => {
if class.is_known(db, KnownClass::NoneType) {
Truthiness::AlwaysFalse
@@ -1871,7 +1947,9 @@ impl<'db> Type<'db> {
| Type::StringLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_)
| Type::LiteralString => Type::Unknown,
| Type::LiteralString
| Type::AlwaysTruthy
| Type::AlwaysFalsy => Type::Unknown,
}
}
@@ -1892,6 +1970,28 @@ impl<'db> Type<'db> {
// We treat `typing.Type` exactly the same as `builtins.type`:
Type::KnownInstance(KnownInstanceType::Type) => Ok(KnownClass::Type.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Tuple) => Ok(KnownClass::Tuple.to_instance(db)),
// Legacy `typing` aliases
Type::KnownInstance(KnownInstanceType::List) => Ok(KnownClass::List.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Dict) => Ok(KnownClass::Dict.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Set) => Ok(KnownClass::Set.to_instance(db)),
Type::KnownInstance(KnownInstanceType::FrozenSet) => {
Ok(KnownClass::FrozenSet.to_instance(db))
}
Type::KnownInstance(KnownInstanceType::ChainMap) => {
Ok(KnownClass::ChainMap.to_instance(db))
}
Type::KnownInstance(KnownInstanceType::Counter) => {
Ok(KnownClass::Counter.to_instance(db))
}
Type::KnownInstance(KnownInstanceType::DefaultDict) => {
Ok(KnownClass::DefaultDict.to_instance(db))
}
Type::KnownInstance(KnownInstanceType::Deque) => Ok(KnownClass::Deque.to_instance(db)),
Type::KnownInstance(KnownInstanceType::OrderedDict) => {
Ok(KnownClass::OrderedDict.to_instance(db))
}
Type::Union(union) => {
let mut builder = UnionBuilder::new(db);
let mut invalid_expressions = smallvec::SmallVec::default();
@@ -2011,6 +2111,7 @@ impl<'db> Type<'db> {
ClassBase::try_from_ty(db, todo_type!("Intersection meta-type"))
.expect("Type::Todo should be a valid ClassBase"),
),
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
Type::Todo(todo) => Type::subclass_of_base(ClassBase::Todo(*todo)),
}
}
@@ -2153,6 +2254,12 @@ pub enum KnownClass {
TypeVar,
TypeAliasType,
NoDefaultType,
// Collections
ChainMap,
Counter,
DefaultDict,
Deque,
OrderedDict,
// sys
VersionInfo,
}
@@ -2183,6 +2290,11 @@ impl<'db> KnownClass {
Self::TypeVar => "TypeVar",
Self::TypeAliasType => "TypeAliasType",
Self::NoDefaultType => "_NoDefaultType",
Self::ChainMap => "ChainMap",
Self::Counter => "Counter",
Self::DefaultDict => "defaultdict",
Self::Deque => "deque",
Self::OrderedDict => "OrderedDict",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed,
@@ -2246,6 +2358,11 @@ impl<'db> KnownClass {
CoreStdlibModule::TypingExtensions
}
}
Self::ChainMap
| Self::Counter
| Self::DefaultDict
| Self::Deque
| Self::OrderedDict => CoreStdlibModule::Collections,
}
}
@@ -2273,6 +2390,11 @@ impl<'db> KnownClass {
| Self::ModuleType
| Self::FunctionType
| Self::SpecialForm
| Self::ChainMap
| Self::Counter
| Self::DefaultDict
| Self::Deque
| Self::OrderedDict
| Self::StdlibAlias
| Self::BaseException
| Self::BaseExceptionGroup
@@ -2305,6 +2427,11 @@ impl<'db> KnownClass {
"ModuleType" => Self::ModuleType,
"FunctionType" => Self::FunctionType,
"TypeAliasType" => Self::TypeAliasType,
"ChainMap" => Self::ChainMap,
"Counter" => Self::Counter,
"defaultdict" => Self::DefaultDict,
"deque" => Self::Deque,
"OrderedDict" => Self::OrderedDict,
"_Alias" => Self::StdlibAlias,
"_SpecialForm" => Self::SpecialForm,
"_NoDefaultType" => Self::NoDefaultType,
@@ -2336,6 +2463,11 @@ impl<'db> KnownClass {
| Self::Dict
| Self::Slice
| Self::GenericAlias
| Self::ChainMap
| Self::Counter
| Self::DefaultDict
| Self::Deque
| Self::OrderedDict
| Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will
| Self::ModuleType
| Self::VersionInfo
@@ -2371,6 +2503,24 @@ pub enum KnownInstanceType<'db> {
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,
/// The symbol `typing.List` (which can also be found as `typing_extensions.List`)
List,
/// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`)
Dict,
/// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`)
Set,
/// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`)
FrozenSet,
/// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`)
ChainMap,
/// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`)
Counter,
/// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`)
DefaultDict,
/// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`)
Deque,
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
OrderedDict,
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
@@ -2391,15 +2541,6 @@ pub enum KnownInstanceType<'db> {
TypeAlias,
TypeGuard,
TypeIs,
List,
Dict,
DefaultDict,
Set,
FrozenSet,
Counter,
Deque,
ChainMap,
OrderedDict,
ReadOnly,
// TODO: fill this enum out with more special forms, etc.
}
@@ -2860,6 +3001,18 @@ impl KnownFunction {
}
}
#[salsa::interned]
pub struct ModuleLiteralType<'db> {
/// The file in which this module was imported.
///
/// We need this in order to know which submodules should be attached to it as attributes
/// (because the submodules were also imported in this file).
pub importing_file: File,
/// The imported module.
pub module: Module,
}
/// Representation of a runtime class object.
///
/// Does not in itself represent a type,
@@ -3443,6 +3596,8 @@ pub(crate) mod tests {
SubclassOfAbcClass(&'static str),
StdlibModule(CoreStdlibModule),
SliceLiteral(i32, i32, i32),
AlwaysTruthy,
AlwaysFalsy,
}
impl Ty {
@@ -3501,7 +3656,8 @@ pub(crate) mod tests {
.class,
),
Ty::StdlibModule(module) => {
Type::ModuleLiteral(resolve_module(db, &module.name()).unwrap().file())
let module = resolve_module(db, &module.name()).unwrap();
Type::module_literal(db, module.file(), module)
}
Ty::SliceLiteral(start, stop, step) => Type::SliceLiteral(SliceLiteralType::new(
db,
@@ -3509,6 +3665,8 @@ pub(crate) mod tests {
Some(stop),
Some(step),
)),
Ty::AlwaysTruthy => Type::AlwaysTruthy,
Ty::AlwaysFalsy => Type::AlwaysFalsy,
}
}
}
@@ -3647,6 +3805,12 @@ pub(crate) mod tests {
)]
#[test_case(Ty::SliceLiteral(1, 2, 3), Ty::BuiltinInstance("slice"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::Intersection{pos: vec![], neg: vec![Ty::None]})]
#[test_case(Ty::IntLiteral(1), Ty::AlwaysTruthy)]
#[test_case(Ty::IntLiteral(0), Ty::AlwaysFalsy)]
#[test_case(Ty::AlwaysTruthy, Ty::BuiltinInstance("object"))]
#[test_case(Ty::AlwaysFalsy, Ty::BuiltinInstance("object"))]
#[test_case(Ty::Never, Ty::AlwaysTruthy)]
#[test_case(Ty::Never, Ty::AlwaysFalsy)]
fn is_subtype_of(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
@@ -3681,6 +3845,10 @@ pub(crate) mod tests {
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)]
#[test_case(Ty::AbcInstance("ABCMeta"), Ty::SubclassOfBuiltinClass("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::BuiltinClassLiteral("str"))]
#[test_case(Ty::IntLiteral(1), Ty::AlwaysFalsy)]
#[test_case(Ty::IntLiteral(0), Ty::AlwaysTruthy)]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysTruthy)]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysFalsy)]
fn is_not_subtype_of(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
@@ -3815,6 +3983,7 @@ pub(crate) mod tests {
#[test_case(Ty::Tuple(vec![]), Ty::BuiltinClassLiteral("object"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::None)]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::LiteralString)]
#[test_case(Ty::AlwaysFalsy, Ty::AlwaysTruthy)]
fn is_disjoint_from(a: Ty, b: Ty) {
let db = setup_db();
let a = a.into_type(&db);
@@ -3845,6 +4014,8 @@ pub(crate) mod tests {
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)]
#[test_case(Ty::AbcClassLiteral("ABC"), Ty::AbcInstance("ABCMeta"))]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysTruthy)]
#[test_case(Ty::BuiltinInstance("str"), Ty::AlwaysFalsy)]
fn is_not_disjoint_from(a: Ty, b: Ty) {
let db = setup_db();
let a = a.into_type(&db);

View File

@@ -30,6 +30,8 @@ use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
use super::Truthiness;
pub(crate) struct UnionBuilder<'db> {
elements: Vec<Type<'db>>,
db: &'db dyn Db,
@@ -243,15 +245,22 @@ impl<'db> InnerIntersectionBuilder<'db> {
}
} else {
// ~Literal[True] & bool = Literal[False]
// ~AlwaysTruthy & bool = Literal[False]
if let Type::Instance(InstanceType { class }) = new_positive {
if class.is_known(db, KnownClass::Bool) {
if let Some(&Type::BooleanLiteral(value)) = self
if let Some(new_type) = self
.negative
.iter()
.find(|element| element.is_boolean_literal())
.find(|element| {
element.is_boolean_literal()
| matches!(element, Type::AlwaysFalsy | Type::AlwaysTruthy)
})
.map(|element| {
Type::BooleanLiteral(element.bool(db) != Truthiness::AlwaysTrue)
})
{
*self = Self::default();
self.positive.insert(Type::BooleanLiteral(!value));
self.positive.insert(new_type);
return;
}
}
@@ -318,15 +327,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
// simplify the representation.
self.add_positive(db, ty);
}
// ~Literal[True] & bool = Literal[False]
Type::BooleanLiteral(bool)
if self
.positive
.iter()
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
// bool & ~Literal[True] = Literal[False]
// bool & ~AlwaysTruthy = Literal[False]
Type::BooleanLiteral(_) | Type::AlwaysFalsy | Type::AlwaysTruthy
if self.positive.contains(&KnownClass::Bool.to_instance(db)) =>
{
*self = Self::default();
self.positive.insert(Type::BooleanLiteral(!bool));
self.positive.insert(Type::BooleanLiteral(
new_negative.bool(db) != Truthiness::AlwaysTrue,
));
}
_ => {
let mut to_remove = SmallVec::<[usize; 1]>::new();
@@ -380,7 +389,7 @@ mod tests {
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
@@ -997,42 +1006,43 @@ mod tests {
assert_eq!(ty, expected);
}
#[test_case(true)]
#[test_case(false)]
fn build_intersection_simplify_split_bool(bool_value: bool) {
#[test_case(Type::BooleanLiteral(true))]
#[test_case(Type::BooleanLiteral(false))]
#[test_case(Type::AlwaysTruthy)]
#[test_case(Type::AlwaysFalsy)]
fn build_intersection_simplify_split_bool(t_splitter: Type) {
let db = setup_db();
let t_bool = KnownClass::Bool.to_instance(&db);
let t_boolean_literal = Type::BooleanLiteral(bool_value);
let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue;
// We add t_object in various orders (in first or second position) in
// the tests below to ensure that the boolean simplification eliminates
// everything from the intersection, not just `bool`.
let t_object = KnownClass::Object.to_instance(&db);
let t_bool = KnownClass::Bool.to_instance(&db);
let ty = IntersectionBuilder::new(&db)
.add_positive(t_object)
.add_positive(t_bool)
.add_negative(t_boolean_literal)
.add_negative(t_splitter)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db)
.add_positive(t_bool)
.add_positive(t_object)
.add_negative(t_boolean_literal)
.add_negative(t_splitter)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db)
.add_positive(t_object)
.add_negative(t_boolean_literal)
.add_negative(t_splitter)
.add_positive(t_bool)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db)
.add_negative(t_boolean_literal)
.add_negative(t_splitter)
.add_positive(t_object)
.add_positive(t_bool)
.build();

View File

@@ -70,7 +70,9 @@ impl<'db> ClassBase<'db> {
| Type::Tuple(_)
| Type::SliceLiteral(_)
| Type::ModuleLiteral(_)
| Type::SubclassOf(_) => None,
| Type::SubclassOf(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => None,
Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
@@ -112,15 +114,24 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::FrozenSet => {
Self::try_from_ty(db, KnownClass::FrozenSet.to_class_literal(db))
}
KnownInstanceType::Callable
| KnownInstanceType::ChainMap
| KnownInstanceType::Counter
| KnownInstanceType::DefaultDict
| KnownInstanceType::Deque
| KnownInstanceType::OrderedDict => Self::try_from_ty(
db,
todo_type!("Support for more typing aliases as base classes"),
),
KnownInstanceType::ChainMap => {
Self::try_from_ty(db, KnownClass::ChainMap.to_class_literal(db))
}
KnownInstanceType::Counter => {
Self::try_from_ty(db, KnownClass::Counter.to_class_literal(db))
}
KnownInstanceType::DefaultDict => {
Self::try_from_ty(db, KnownClass::DefaultDict.to_class_literal(db))
}
KnownInstanceType::Deque => {
Self::try_from_ty(db, KnownClass::Deque.to_class_literal(db))
}
KnownInstanceType::OrderedDict => {
Self::try_from_ty(db, KnownClass::OrderedDict.to_class_literal(db))
}
KnownInstanceType::Callable => {
Self::try_from_ty(db, todo_type!("Support for Callable as a base class"))
}
},
}
}

View File

@@ -79,8 +79,8 @@ impl Display for DisplayRepresentation<'_> {
// `[Type::Todo]`'s display should be explicit that is not a valid display of
// any other type
Type::Todo(todo) => write!(f, "@Todo{todo}"),
Type::ModuleLiteral(file) => {
write!(f, "<module '{:?}'>", file.path(self.db))
Type::ModuleLiteral(module) => {
write!(f, "<module '{}'>", module.module(self.db).name())
}
// TODO functions and classes should display using a fully qualified name
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
@@ -140,6 +140,8 @@ impl Display for DisplayRepresentation<'_> {
}
f.write_str("]")
}
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
}
}
}

View File

@@ -124,7 +124,6 @@ pub(crate) fn infer_definition_types<'db>(
let file = definition.file(db);
let _span = tracing::trace_span!(
"infer_definition_types",
definition = ?definition.as_id(),
range = ?definition.kind(db).range(),
file = %file.path(db)
)
@@ -866,6 +865,14 @@ impl<'db> TypeInferenceBuilder<'db> {
self.types.bindings.insert(definition, inferred_ty);
}
fn add_unknown_declaration_with_binding(
&mut self,
node: AnyNodeRef,
definition: Definition<'db>,
) {
self.add_declaration_with_binding(node, definition, Type::Unknown, Type::Unknown);
}
fn infer_module(&mut self, module: &ast::ModModule) {
self.infer_body(&module.body);
}
@@ -2116,22 +2123,45 @@ impl<'db> TypeInferenceBuilder<'db> {
let ast::Alias {
range: _,
name,
asname: _,
asname,
} = alias;
let module_ty = if let Some(module_name) = ModuleName::new(name) {
if let Some(module) = self.module_ty_from_name(&module_name) {
module
} else {
self.diagnostics.add_unresolved_module(alias, 0, Some(name));
Type::Unknown
}
} else {
// The name of the module being imported
let Some(full_module_name) = ModuleName::new(name) else {
tracing::debug!("Failed to resolve import due to invalid syntax");
Type::Unknown
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
self.add_declaration_with_binding(alias.into(), definition, module_ty, module_ty);
// Resolve the module being imported.
let Some(full_module_ty) = self.module_ty_from_name(&full_module_name) else {
self.diagnostics.add_unresolved_module(alias, 0, Some(name));
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
let binding_ty = if asname.is_some() {
// If we are renaming the imported module via an `as` clause, then we bind the resolved
// module's type to that name, even if that module is nested.
full_module_ty
} else if full_module_name.contains('.') {
// If there's no `as` clause and the imported module is nested, we're not going to bind
// the resolved module itself into the current scope; we're going to bind the top-most
// parent package of that module.
let topmost_parent_name =
ModuleName::new(full_module_name.components().next().unwrap()).unwrap();
let Some(topmost_parent_ty) = self.module_ty_from_name(&topmost_parent_name) else {
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
topmost_parent_ty
} else {
// If there's no `as` clause and the imported module isn't nested, then the imported
// module _is_ what we bind into the current scope.
full_module_ty
};
self.add_declaration_with_binding(alias.into(), definition, binding_ty, binding_ty);
}
fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) {
@@ -2211,12 +2241,6 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO:
// - Absolute `*` imports (`from collections import *`)
// - Relative `*` imports (`from ...foo import *`)
// - Submodule imports (`from collections import abc`,
// where `abc` is a submodule of the `collections` package)
//
// For the last item, see the currently skipped tests
// `follow_relative_import_bare_to_module()` and
// `follow_nonexistent_import_bare_to_module()`.
let ast::StmtImportFrom { module, level, .. } = import_from;
let module = module.as_deref();
@@ -2239,46 +2263,13 @@ impl<'db> TypeInferenceBuilder<'db> {
.ok_or(ModuleNameResolutionError::InvalidSyntax)
};
let ty = match module_name {
Ok(module_name) => {
if let Some(module_ty) = self.module_ty_from_name(&module_name) {
let ast::Alias {
range: _,
name,
asname: _,
} = alias;
match module_ty.member(self.db, &ast::name::Name::new(&name.id)) {
Symbol::Type(ty, boundness) => {
if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_IMPORT,
AnyNodeRef::Alias(alias),
format_args!("Member `{name}` of module `{module_name}` is possibly unbound", ),
);
}
ty
}
Symbol::Unbound => {
self.diagnostics.add_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias),
format_args!("Module `{module_name}` has no member `{name}`",),
);
Type::Unknown
}
}
} else {
self.diagnostics
.add_unresolved_module(import_from, *level, module);
Type::Unknown
}
}
let module_name = match module_name {
Ok(module_name) => module_name,
Err(ModuleNameResolutionError::InvalidSyntax) => {
tracing::debug!("Failed to resolve import due to invalid syntax");
// Invalid syntax diagnostics are emitted elsewhere.
Type::Unknown
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
}
Err(ModuleNameResolutionError::TooManyDots) => {
tracing::debug!(
@@ -2287,7 +2278,8 @@ impl<'db> TypeInferenceBuilder<'db> {
);
self.diagnostics
.add_unresolved_module(import_from, *level, module);
Type::Unknown
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
}
Err(ModuleNameResolutionError::UnknownCurrentModule) => {
tracing::debug!(
@@ -2297,10 +2289,72 @@ impl<'db> TypeInferenceBuilder<'db> {
);
self.diagnostics
.add_unresolved_module(import_from, *level, module);
Type::Unknown
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
}
};
let Some(module_ty) = self.module_ty_from_name(&module_name) else {
self.diagnostics
.add_unresolved_module(import_from, *level, module);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
let ast::Alias {
range: _,
name,
asname: _,
} = alias;
// Check if the symbol being imported is a submodule. This won't get handled by the
// `Type::member` call below because it relies on the semantic index's `imported_modules`
// set. The semantic index does not include information about `from...import` statements
// because there are two things it cannot determine while only inspecting the content of
// the current file:
//
// - whether the imported symbol is an attribute or submodule
// - whether the containing file is in a module or a package (needed to correctly resolve
// relative imports)
//
// The first would be solvable by making it a _potentially_ imported modules set. The
// second is not.
//
// Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute
// check here when inferring types for a `from...import` statement.
if let Some(submodule_name) = ModuleName::new(name) {
let mut full_submodule_name = module_name.clone();
full_submodule_name.extend(&submodule_name);
if let Some(submodule_ty) = self.module_ty_from_name(&full_submodule_name) {
self.add_declaration_with_binding(
alias.into(),
definition,
submodule_ty,
submodule_ty,
);
return;
}
}
// Otherwise load the requested attribute from the module.
let Symbol::Type(ty, boundness) = module_ty.member(self.db, name) else {
self.diagnostics.add_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias),
format_args!("Module `{module_name}` has no member `{name}`",),
);
self.add_unknown_declaration_with_binding(alias.into(), definition);
return;
};
if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_IMPORT,
AnyNodeRef::Alias(alias),
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
);
}
self.add_declaration_with_binding(alias.into(), definition, ty, ty);
}
@@ -2316,7 +2370,8 @@ impl<'db> TypeInferenceBuilder<'db> {
}
fn module_ty_from_name(&self, module_name: &ModuleName) -> Option<Type<'db>> {
resolve_module(self.db, module_name).map(|module| Type::ModuleLiteral(module.file()))
resolve_module(self.db, module_name)
.map(|module| Type::module_literal(self.db, self.file, module))
}
fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> {
@@ -4904,42 +4959,45 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_expression(arguments_slice);
todo_type!("Callable types")
}
// TODO: Generics
KnownInstanceType::ChainMap => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.ChainMap alias")
KnownClass::ChainMap.to_instance(self.db)
}
KnownInstanceType::OrderedDict => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.OrderedDict alias")
KnownClass::OrderedDict.to_instance(self.db)
}
KnownInstanceType::Dict => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.Dict alias")
KnownClass::Dict.to_instance(self.db)
}
KnownInstanceType::List => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.List alias")
KnownClass::List.to_instance(self.db)
}
KnownInstanceType::DefaultDict => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.DefaultDict[] alias")
KnownClass::DefaultDict.to_instance(self.db)
}
KnownInstanceType::Counter => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.Counter[] alias")
KnownClass::Counter.to_instance(self.db)
}
KnownInstanceType::Set => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.Set alias")
KnownClass::Set.to_instance(self.db)
}
KnownInstanceType::FrozenSet => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.FrozenSet alias")
KnownClass::FrozenSet.to_instance(self.db)
}
KnownInstanceType::Deque => {
self.infer_type_expression(arguments_slice);
todo_type!("typing.Deque alias")
KnownClass::Deque.to_instance(self.db)
}
KnownInstanceType::ReadOnly => {
self.infer_type_expression(arguments_slice);
todo_type!("Required[] type qualifier")

View File

@@ -196,6 +196,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
is_positive: bool,
) -> Option<NarrowingConstraints<'db>> {
match expression_node {
ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)),
ast::Expr::Compare(expr_compare) => {
self.evaluate_expr_compare(expr_compare, expression, is_positive)
}
@@ -254,6 +255,31 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
fn evaluate_expr_name(
&mut self,
expr_name: &ast::ExprName,
is_positive: bool,
) -> NarrowingConstraints<'db> {
let ast::ExprName { id, .. } = expr_name;
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
let mut constraints = NarrowingConstraints::default();
constraints.insert(
symbol,
if is_positive {
Type::AlwaysFalsy.negate(self.db)
} else {
Type::AlwaysTruthy.negate(self.db)
},
);
constraints
}
fn evaluate_expr_compare(
&mut self,
expr_compare: &ast::ExprCompare,

View File

@@ -75,6 +75,8 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
Ty::AbcClassLiteral("ABCMeta"),
Ty::SubclassOfAbcClass("ABC"),
Ty::SubclassOfAbcClass("ABCMeta"),
Ty::AlwaysTruthy,
Ty::AlwaysFalsy,
])
.unwrap()
.clone()

View File

@@ -91,11 +91,11 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
let db = match path {
AnySystemPath::System(path) => {
match session.workspace_db_for_path(path.as_std_path()) {
Some(db) => db.snapshot(),
None => session.default_workspace_db().snapshot(),
Some(db) => db.clone(),
None => session.default_workspace_db().clone(),
}
}
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().snapshot(),
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().clone(),
};
let Some(snapshot) = session.take_snapshot(url) else {

View File

@@ -9,6 +9,7 @@ use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
#[salsa::db]
#[derive(Clone)]
pub(crate) struct Db {
workspace_root: SystemPathBuf,
storage: salsa::Storage<Self>,

View File

@@ -21,6 +21,7 @@ pub trait Db: SemanticDb + Upcast<dyn SemanticDb> {
}
#[salsa::db]
#[derive(Clone)]
pub struct RootDatabase {
workspace: Option<Workspace>,
storage: salsa::Storage<RootDatabase>,
@@ -80,17 +81,6 @@ impl RootDatabase {
{
Cancelled::catch(|| f(self))
}
#[must_use]
pub fn snapshot(&self) -> Self {
Self {
workspace: self.workspace,
storage: self.storage.clone(),
files: self.files.snapshot(),
system: Arc::clone(&self.system),
rule_selection: Arc::clone(&self.rule_selection),
}
}
}
impl Upcast<dyn SemanticDb> for RootDatabase {
@@ -184,6 +174,7 @@ pub(crate) mod tests {
use crate::DEFAULT_LINT_REGISTRY;
#[salsa::db]
#[derive(Clone)]
pub(crate) struct TestDb {
storage: salsa::Storage<Self>,
events: Arc<std::sync::Mutex<Vec<Event>>>,

View File

@@ -195,13 +195,13 @@ impl Workspace {
let result = Arc::new(std::sync::Mutex::new(Vec::new()));
let inner_result = Arc::clone(&result);
let db = db.snapshot();
let db = db.clone();
let workspace_span = workspace_span.clone();
rayon::scope(move |scope| {
for file in &files {
let result = inner_result.clone();
let db = db.snapshot();
let db = db.clone();
let workspace_span = workspace_span.clone();
scope.spawn(move |_| {

View File

@@ -81,7 +81,7 @@ pub(crate) fn analyze_graph(
// Collect and resolve the imports for each file.
let result = Arc::new(Mutex::new(Vec::new()));
let inner_result = Arc::clone(&result);
let db = db.snapshot();
let db = db.clone();
rayon::scope(move |scope| {
for resolved_file in paths {
@@ -137,7 +137,7 @@ pub(crate) fn analyze_graph(
continue;
};
let db = db.snapshot();
let db = db.clone();
let glob_resolver = glob_resolver.clone();
let root = root.clone();
let result = inner_result.clone();

View File

@@ -31,15 +31,10 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
@@ -47,7 +42,6 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
];

View File

@@ -48,7 +48,7 @@ pub fn vendored_path_to_file(
}
/// Lookup table that maps [file paths](`FilePath`) to salsa interned [`File`] instances.
#[derive(Default)]
#[derive(Default, Clone)]
pub struct Files {
inner: Arc<FilesInner>,
}
@@ -253,13 +253,6 @@ impl Files {
root.set_revision(db).to(FileRevision::now());
}
}
#[must_use]
pub fn snapshot(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl std::fmt::Debug for Files {

View File

@@ -48,13 +48,13 @@ mod tests {
///
/// Uses an in memory filesystem and it stubs out the vendored files by default.
#[salsa::db]
#[derive(Default)]
#[derive(Default, Clone)]
pub(crate) struct TestDb {
storage: salsa::Storage<Self>,
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
}
impl TestDb {

View File

@@ -175,19 +175,26 @@ pub trait DbWithTestSystem: Db + Sized {
///
/// # Panics
/// If the system isn't using the memory file system.
fn write_file(
&mut self,
path: impl AsRef<SystemPath>,
content: impl ToString,
) -> crate::system::Result<()> {
fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
let path = path.as_ref();
let result = self
.test_system()
.memory_file_system()
.write_file(path, content);
let memory_fs = self.test_system().memory_file_system();
let sync_ancestors = path
.parent()
.is_some_and(|parent| !memory_fs.exists(parent));
let result = memory_fs.write_file(path, content);
if result.is_ok() {
File::sync_path(self, path);
// Sync the ancestor paths if the path's parent
// directory didn't exist before.
if sync_ancestors {
for ancestor in path.ancestors() {
File::sync_path(self, ancestor);
}
}
}
result

View File

@@ -16,7 +16,7 @@ static EMPTY_VENDORED: std::sync::LazyLock<VendoredFileSystem> = std::sync::Lazy
});
#[salsa::db]
#[derive(Default)]
#[derive(Default, Clone)]
pub struct ModuleDb {
storage: salsa::Storage<Self>,
files: Files,
@@ -55,17 +55,6 @@ impl ModuleDb {
Ok(db)
}
/// Create a snapshot of the current database.
#[must_use]
pub fn snapshot(&self) -> Self {
Self {
storage: self.storage.clone(),
system: self.system.clone(),
files: self.files.snapshot(),
rule_selection: Arc::clone(&self.rule_selection),
}
}
}
impl Upcast<dyn SourceDb> for ModuleDb {

View File

@@ -1,36 +1,80 @@
from airflow import PY36, PY37, PY38, PY39, PY310, PY311, PY312
from airflow.triggers.external_task import TaskStateTrigger
from airflow.api_connexion.security import requires_access
from airflow import (
PY36,
PY37,
PY38,
PY39,
PY310,
PY311,
PY312,
Dataset as DatasetFromRoot,
)
from airflow.api_connexion.security import requires_access, requires_access_dataset
from airflow.auth.managers.base_auth_manager import is_authorized_dataset
from airflow.auth.managers.models.resource_details import DatasetDetails
from airflow.configuration import (
as_dict,
get,
getboolean,
getfloat,
getint,
has_option,
remove_option,
as_dict,
set,
)
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.metrics.validators import AllowListValidator
from airflow.metrics.validators import BlockListValidator
from airflow.operators.subdag import SubDagOperator
from airflow.sensors.external_task import ExternalTaskSensorLink
from airflow.datasets import (
Dataset,
DatasetAlias,
DatasetAliasEvent,
DatasetAll,
DatasetAny,
expand_alias_to_datasets,
)
from airflow.datasets.metadata import Metadata
from airflow.datasets.manager import (
DatasetManager,
dataset_manager,
resolve_dataset_manager,
)
from airflow.lineage.hook import DatasetLineageInfo
from airflow.listeners.spec.dataset import on_dataset_changed, on_dataset_created
from airflow.metrics.validators import AllowListValidator, BlockListValidator
from airflow.operators import dummy_operator
from airflow.operators.bash_operator import BashOperator
from airflow.operators.branch_operator import BaseBranchOperator
from airflow.operators.dummy import EmptyOperator, DummyOperator
from airflow.operators import dummy_operator
from airflow.operators.dummy import DummyOperator, EmptyOperator
from airflow.operators.email_operator import EmailOperator
from airflow.operators.subdag import SubDagOperator
from airflow.providers.amazon.auth_manager.avp.entities import AvpEntities
from airflow.providers.amazon.aws.datasets import s3
from airflow.providers.common.io.datasets import file as common_io_file
from airflow.providers.fab.auth_manager import fab_auth_manager
from airflow.providers.google.datasets import bigquery, gcs
from airflow.providers.mysql.datasets import mysql
from airflow.providers.openlineage.utils.utils import (
DatasetInfo,
translate_airflow_dataset,
)
from airflow.providers.postgres.datasets import postgres
from airflow.providers.trino.datasets import trino
from airflow.secrets.local_filesystem import get_connection, load_connections
from airflow.security.permissions import RESOURCE_DATASET
from airflow.sensors.base_sensor_operator import BaseSensorOperator
from airflow.sensors.date_time_sensor import DateTimeSensor
from airflow.sensors.external_task import (
ExternalTaskSensorLink as ExternalTaskSensorLinkFromExternalTask,
)
from airflow.sensors.external_task_sensor import (
ExternalTaskMarker,
ExternalTaskSensor,
ExternalTaskSensorLink,
ExternalTaskSensorLink as ExternalTaskSensorLinkFromExternalTaskSensor,
)
from airflow.sensors.time_delta_sensor import TimeDeltaSensor
from airflow.secrets.local_filesystem import get_connection, load_connections
from airflow.timetables.datasets import DatasetOrTimeSchedule
from airflow.timetables.simple import DatasetTriggeredTimetable
from airflow.triggers.external_task import TaskStateTrigger
from airflow.utils import dates
from airflow.utils.dag_cycle_tester import test_cycle
from airflow.utils.dates import (
date_range,
datetime_to_nano,
@@ -44,70 +88,168 @@ from airflow.utils.decorators import apply_defaults
from airflow.utils.file import TemporaryDirectory, mkdirs
from airflow.utils.helpers import chain, cross_downstream
from airflow.utils.state import SHUTDOWN, terminating_states
from airflow.utils.dag_cycle_tester import test_cycle
from airflow.utils.trigger_rule import TriggerRule
from airflow.www.auth import has_access
from airflow.www.auth import has_access, has_access_dataset
from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key
# airflow root
PY36, PY37, PY38, PY39, PY310, PY311, PY312
DatasetFromRoot
# airflow.api_connexion.security
requires_access, requires_access_dataset
# airflow.auth.managers
is_authorized_dataset
DatasetDetails
# airflow.configuration
get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
# airflow.contrib.*
AWSAthenaHook
TaskStateTrigger
requires_access
# airflow.datasets
Dataset
DatasetAlias
DatasetAliasEvent
DatasetAll
DatasetAny
expand_alias_to_datasets
Metadata
AllowListValidator
BlockListValidator
# airflow.datasets.manager
DatasetManager, dataset_manager, resolve_dataset_manager
# airflow.lineage.hook
DatasetLineageInfo
# airflow.listeners.spec.dataset
on_dataset_changed, on_dataset_created
# airflow.metrics.validators
AllowListValidator, BlockListValidator
# airflow.operators.dummy_operator
dummy_operator.EmptyOperator
dummy_operator.DummyOperator
# airflow.operators.bash_operator
BashOperator
# airflow.operators.branch_operator
BaseBranchOperator
# airflow.operators.dummy
EmptyOperator, DummyOperator
# airflow.operators.email_operator
EmailOperator
# airflow.operators.subdag.*
SubDagOperator
# airflow.providers.amazon
AvpEntities.DATASET
s3.create_dataset
s3.convert_dataset_to_openlineage
s3.sanitize_uri
# airflow.providers.common.io
common_io_file.convert_dataset_to_openlineage
common_io_file.create_dataset
common_io_file.sanitize_uri
# airflow.providers.fab
fab_auth_manager.is_authorized_dataset
# airflow.providers.google
bigquery.sanitize_uri
gcs.create_dataset
gcs.sanitize_uri
gcs.convert_dataset_to_openlineage
# airflow.providers.mysql
mysql.sanitize_uri
# airflow.providers.openlineage
DatasetInfo, translate_airflow_dataset
# airflow.providers.postgres
postgres.sanitize_uri
# airflow.providers.trino
trino.sanitize_uri
# airflow.secrets
get_connection, load_connections
# airflow.security.permissions
RESOURCE_DATASET
# airflow.sensors.base_sensor_operator
BaseSensorOperator
# airflow.sensors.date_time_sensor
DateTimeSensor
# airflow.sensors.external_task
ExternalTaskSensorLinkFromExternalTask
# airflow.sensors.external_task_sensor
ExternalTaskMarker
ExternalTaskSensor
ExternalTaskSensorLinkFromExternalTaskSensor
# airflow.sensors.time_delta_sensor
TimeDeltaSensor
# airflow.timetables
DatasetOrTimeSchedule
DatasetTriggeredTimetable
# airflow.triggers.external_task
TaskStateTrigger
# airflow.utils.date
dates.date_range
dates.days_ago
date_range
days_ago
infer_time_unit
parse_execution_date
round_time
scale_time_units
infer_time_unit
# This one was not deprecated.
datetime_to_nano
dates.datetime_to_nano
get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set
get_connection, load_connections
ExternalTaskSensorLink
BashOperator
BaseBranchOperator
EmptyOperator, DummyOperator
dummy_operator.EmptyOperator
dummy_operator.DummyOperator
EmailOperator
BaseSensorOperator
DateTimeSensor
(ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink)
TimeDeltaSensor
# airflow.utils.dag_cycle_tester
test_cycle
# airflow.utils.decorators
apply_defaults
TemporaryDirectory
mkdirs
# airflow.utils.file
TemporaryDirectory, mkdirs
chain
cross_downstream
# airflow.utils.helpers
chain, cross_downstream
SHUTDOWN
terminating_states
# airflow.utils.state
SHUTDOWN, terminating_states
# airflow.utils.trigger_rule
TriggerRule.DUMMY
TriggerRule.NONE_FAILED_OR_SKIPPED
test_cycle
# airflow.www.auth
has_access
has_access_dataset
# airflow.www.utils
get_sensitive_variables_fields, should_hide_value_for_key

View File

@@ -244,3 +244,10 @@ def f():
for a in values:
result.append(a + 1) # PERF401
result = []
def f():
values = [1, 2, 3]
result = []
for i in values:
result.append(i + 1) # Ok
del i

View File

@@ -31,3 +31,13 @@ def single_word():
def single_word_no_dot():
"""singleword"""
def first_word_lots_of_whitespace():
"""
here is the start of my docstring!
What do you think?
"""

View File

@@ -50,6 +50,50 @@ class T:
obj = T()
obj.a = obj.a + 1
a = a+-1
# Regression tests for https://github.com/astral-sh/ruff/issues/11672
test = 0x5
test = test + 0xBA
test2 = b""
test2 = test2 + b"\000"
test3 = ""
test3 = test3 + ( a := R""
f"oo" )
test4 = []
test4 = test4 + ( e
for e in
range(10)
)
test5 = test5 + (
4
*
10
)
test6 = test6 + \
(
4
*
10
)
test7 = \
100 \
+ test7
test8 = \
886 \
+ \
\
test8
# OK
a_list[0] = a_list[:] * 3
index = a_number = a_number + 1

View File

@@ -46,7 +46,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::EndsInPeriod,
Rule::EndsInPunctuation,
Rule::EscapeSequenceInDocstring,
Rule::FirstLineCapitalized,
Rule::FirstWordUncapitalized,
Rule::FitsOnOneLine,
Rule::IndentWithSpaces,
Rule::MultiLineSummaryFirstLine,
@@ -277,7 +277,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if checker.enabled(Rule::NoSignature) {
pydocstyle::rules::no_signature(checker, &docstring);
}
if checker.enabled(Rule::FirstLineCapitalized) {
if checker.enabled(Rule::FirstWordUncapitalized) {
pydocstyle::rules::capitalized(checker, &docstring);
}
if checker.enabled(Rule::DocstringStartsWithThis) {

View File

@@ -566,7 +566,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::EndsInPeriod),
(Pydocstyle, "401") => (RuleGroup::Stable, rules::pydocstyle::rules::NonImperativeMood),
(Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::NoSignature),
(Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstLineCapitalized),
(Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstWordUncapitalized),
(Pydocstyle, "404") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringStartsWithThis),
(Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::CapitalizeSectionName),
(Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterSectionName),

View File

@@ -160,15 +160,23 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
.semantic()
.resolve_qualified_name(expr)
.and_then(|qualname| match qualname.segments() {
["airflow", "triggers", "external_task", "TaskStateTrigger"] => {
Some((qualname.to_string(), Replacement::None))
}
["airflow", "api_connexion", "security", "requires_access"] => Some((
qualname.to_string(),
Replacement::Name(
"airflow.api_connexion.security.requires_access_*".to_string(),
),
)),
["airflow", "api_connexion", "security", "requires_access_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.api_connexion.security.requires_access_asset".to_string()),
)),
["airflow", "triggers", "external_task", "TaskStateTrigger"] => {
Some((qualname.to_string(), Replacement::None))
}
["airflow", "security", "permissions", "RESOURCE_DATASET"] => Some((
qualname.to_string(),
Replacement::Name("airflow.security.permissions.RESOURCE_ASSET".to_string()),
)),
// airflow.PY\d{1,2}
["airflow", "PY36"] => Some((
qualname.to_string(),
@@ -231,8 +239,22 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
qualname.to_string(),
Replacement::Name("airflow.configuration.conf.set".to_string()),
)),
// airflow.auth.managers
["airflow", "auth", "managers", "models", "resource_details", "DatasetDetails"] => Some((
qualname.to_string(),
Replacement::Name("airflow.auth.managers.models.resource_details.AssetDetails".to_string()),
)),
["airflow", "auth", "managers", "base_auth_manager", "is_authorized_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.auth.managers.base_auth_manager.is_authorized_asset".to_string()),
)),
// airflow.contrib.*
["airflow", "contrib", ..] => Some((qualname.to_string(), Replacement::None)),
["airflow", "contrib", ..] => Some((qualname.to_string(),
Replacement::Message(
"The whole `airflow.contrib` module has been removed."
.to_string(),
),
)),
// airflow.metrics.validators
["airflow", "metrics", "validators", "AllowListValidator"] => Some((
qualname.to_string(),
@@ -246,13 +268,86 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
"airflow.metrics.validators.PatternBlockListValidator".to_string(),
),
)),
// airflow.operators
["airflow", "operators", "subdag", ..] => {
// airflow.datasets
["airflow", "Dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sdk.definitions.asset.Asset".to_string()),
)),
["airflow", "datasets", "DatasetAliasEvent"] => {
Some((qualname.to_string(), Replacement::None))
}
["airflow.sensors.external_task.ExternalTaskSensorLink"] => Some((
["airflow", "datasets", "Dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sensors.external_task.ExternalDagLin".to_string()),
Replacement::Name("airflow.sdk.definitions.asset.Asset".to_string()),
)),
["airflow", "datasets", "DatasetAlias"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sdk.definitions.asset.AssetAlias".to_string()),
)),
["airflow", "datasets", "DatasetAll"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sdk.definitions.asset.AssetAll".to_string()),
)),
["airflow", "datasets", "DatasetAny"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sdk.definitions.asset.AssetAny".to_string()),
)),
["airflow", "datasets", "expand_alias_to_datasets"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sdk.definitions.asset.expand_alias_to_assets".to_string()),
)),
["airflow", "datasets", "metadata", "Metadata"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sdk.definitions.asset.metadata.Metadata".to_string()),
)),
// airflow.datasets.manager
["airflow", "datasets", "manager", "dataset_manager"] => Some((
qualname.to_string(),
Replacement::Name("airflow.assets.manager".to_string()),
)),
["airflow", "datasets", "manager", "resolve_dataset_manager"] => Some((
qualname.to_string(),
Replacement::Name("airflow.assets.resolve_asset_manager".to_string()),
)),
["airflow", "datasets.manager", "DatasetManager"] => Some((
qualname.to_string(),
Replacement::Name("airflow.assets.AssetManager".to_string()),
)),
// airflow.listeners.spec
["airflow", "listeners", "spec", "dataset", "on_dataset_created"] => Some((
qualname.to_string(),
Replacement::Name("airflow.listeners.spec.asset.on_asset_created".to_string()),
)),
["airflow", "listeners", "spec", "dataset", "on_dataset_changed"] => Some((
qualname.to_string(),
Replacement::Name("airflow.listeners.spec.asset.on_asset_changed".to_string()),
)),
// airflow.timetables
["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => Some((
qualname.to_string(),
Replacement::Name("airflow.timetables.assets.AssetOrTimeSchedule".to_string()),
)),
["airflow", "timetables", "simple", "DatasetTriggeredTimetable"] => Some((
qualname.to_string(),
Replacement::Name("airflow.timetables.simple.AssetTriggeredTimetable".to_string()),
)),
// airflow.lineage.hook
["airflow", "lineage", "hook", "DatasetLineageInfo"] => Some((
qualname.to_string(),
Replacement::Name("airflow.lineage.hook.AssetLineageInfo".to_string()),
)),
// airflow.operators
["airflow", "operators", "subdag", ..] => {
Some((
qualname.to_string(),
Replacement::Message(
"The whole `airflow.subdag` module has been removed.".to_string(),
),
))
},
["airflow", "sensors", "external_task", "ExternalTaskSensorLink"] => Some((
qualname.to_string(),
Replacement::Name("airflow.sensors.external_task.ExternalDagLink".to_string()),
)),
["airflow", "operators", "bash_operator", "BashOperator"] => Some((
qualname.to_string(),
@@ -305,7 +400,7 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
["airflow", "sensors", "external_task_sensor", "ExternalTaskSensorLink"] => Some((
qualname.to_string(),
Replacement::Name(
"airflow.sensors.external_task.ExternalTaskSensorLink".to_string(),
"airflow.sensors.external_task.ExternalDagLink".to_string(),
),
)),
["airflow", "sensors", "time_delta_sensor", "TimeDeltaSensor"] => Some((
@@ -354,7 +449,6 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
qualname.to_string(),
Replacement::Name("pendulum.today('UTC').add(days=-N, ...)".to_string()),
)),
// airflow.utils.helpers
["airflow", "utils", "helpers", "chain"] => Some((
qualname.to_string(),
@@ -394,6 +488,10 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
qualname.to_string(),
Replacement::Name("airflow.www.auth.has_access_*".to_string()),
)),
["airflow", "www", "auth", "has_access_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.www.auth.has_access_dataset.has_access_asset".to_string()),
)),
["airflow", "www", "utils", "get_sensitive_variables_fields"] => Some((
qualname.to_string(),
Replacement::Name(
@@ -407,6 +505,82 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
"airflow.utils.log.secrets_masker.should_hide_value_for_key".to_string(),
),
)),
// airflow.providers.amazon
["airflow", "providers", "amazon", "aws", "datasets", "s3", "create_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.amazon.aws.assets.s3.create_asset".to_string()),
)),
["airflow", "providers", "amazon", "aws", "datasets", "s3", "convert_dataset_to_openlineage"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.amazon.aws.assets.s3.convert_asset_to_openlineage".to_string()),
)),
["airflow", "providers", "amazon", "aws", "datasets", "s3", "sanitize_uri"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.amazon.aws.assets.s3.sanitize_uri".to_string()),
)),
["airflow", "providers", "amazon", "auth_manager", "avp", "entities", "AvpEntities", "DATASET"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.amazon.auth_manager.avp.entities.AvpEntities.ASSET".to_string()),
)),
// airflow.providers.common.io
["airflow", "providers", "common", "io", "datasets", "file", "create_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.common.io.assets.file.create_asset".to_string()),
)),
["airflow", "providers", "common", "io", "datasets", "file", "convert_dataset_to_openlineage"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.common.io.assets.file.convert_asset_to_openlineage".to_string()),
)),
["airflow", "providers", "common", "io", "datasets", "file", "sanitize_uri"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.common.io.assets.file.sanitize_uri".to_string()),
)),
// airflow.providers.fab
["airflow", "providers", "fab", "auth_manager", "fab_auth_manager", "is_authorized_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset".to_string()),
)),
// airflow.providers.google
["airflow", "providers", "google", "datasets", "bigquery", "create_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.google.assets.bigquery.create_asset".to_string()),
)),
["airflow", "providers", "google", "datasets", "gcs", "create_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.google.assets.gcs.create_asset".to_string()),
)),
["airflow", "providers", "google", "datasets", "gcs", "convert_dataset_to_openlineage"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.google.assets.gcs.convert_asset_to_openlineage".to_string()),
)),
["airflow", "providers", "google", "datasets", "gcs", "sanitize_uri"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.google.assets.gcs.sanitize_uri".to_string()),
)),
// airflow.providers.mysql
["airflow", "providers", "mysql", "datasets", "mysql", "sanitize_uri"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.mysql.assets.mysql.sanitize_uri".to_string()),
)),
// airflow.providers.postgres
["airflow", "providers", "postgres", "datasets", "postgres", "sanitize_uri"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.postgres.assets.postgres.sanitize_uri".to_string()),
)),
// airflow.providers.openlineage
["airflow", "providers", "openlineage", "utils", "utils", "DatasetInfo"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.openlineage.utils.utils.AssetInfo".to_string()),
)),
["airflow", "providers", "openlineage", "utils", "utils", "translate_airflow_dataset"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.openlineage.utils.utils.translate_airflow_asset".to_string()),
)),
// airflow.providers.trino
["airflow", "providers", "trino", "datasets", "trino", "sanitize_uri"] => Some((
qualname.to_string(),
Replacement::Name("airflow.providers.trino.assets.trino.sanitize_uri".to_string()),
)),
_ => None,
});
if let Some((deprecated, replacement)) = result {

View File

@@ -237,31 +237,12 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, for_stmt: &ast::S
// filtered = [x for x in y]
// print(x)
// ```
let last_target_binding = checker
let target_binding = checker
.semantic()
.lookup_symbol(for_stmt_target_id)
.expect("for loop target must exist");
let target_binding = {
let mut bindings = [last_target_binding].into_iter().chain(
checker
.semantic()
.shadowed_bindings(checker.semantic().scope_id, last_target_binding)
.filter_map(|shadowed| shadowed.same_scope().then_some(shadowed.shadowed_id())),
);
bindings
.find_map(|binding_id| {
let binding = checker.semantic().binding(binding_id);
binding
.statement(checker.semantic())
.and_then(ast::Stmt::as_for_stmt)
.is_some_and(|stmt| stmt.range == for_stmt.range)
.then_some(binding)
})
.expect("for target binding must exist")
};
.bindings
.iter()
.find(|binding| for_stmt.target.range() == binding.range)
.unwrap();
// If any references to the loop target variable are after the loop,
// then converting it into a comprehension would cause a NameError
if target_binding

View File

@@ -484,3 +484,5 @@ PERF401.py:245:13: PERF401 [*] Use `list.extend` to create a transformed list
245 |- result.append(a + 1) # PERF401
244 |+ result.extend(a + 1 for a in values) # PERF401
246 245 | result = []
247 246 |
248 247 | def f():

View File

@@ -32,8 +32,8 @@ mod tests {
#[test_case(Rule::EndsInPeriod, Path::new("D400_415.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D400_415.py"))]
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"))]
#[test_case(Rule::FirstLineCapitalized, Path::new("D403.py"))]
#[test_case(Rule::FirstWordUncapitalized, Path::new("D.py"))]
#[test_case(Rule::FirstWordUncapitalized, Path::new("D403.py"))]
#[test_case(Rule::FitsOnOneLine, Path::new("D.py"))]
#[test_case(Rule::IndentWithSpaces, Path::new("D.py"))]
#[test_case(Rule::UndocumentedMagicMethod, Path::new("D.py"))]

View File

@@ -10,8 +10,8 @@ use crate::docstrings::Docstring;
/// Checks for docstrings that do not start with a capital letter.
///
/// ## Why is this bad?
/// The first character in a docstring should be capitalized for, grammatical
/// correctness and consistency.
/// The first non-whitespace character in a docstring should be
/// capitalized for grammatical correctness and consistency.
///
/// ## Example
/// ```python
@@ -30,16 +30,16 @@ use crate::docstrings::Docstring;
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
#[derive(ViolationMetadata)]
pub(crate) struct FirstLineCapitalized {
pub(crate) struct FirstWordUncapitalized {
first_word: String,
capitalized_word: String,
}
impl AlwaysFixableViolation for FirstLineCapitalized {
impl AlwaysFixableViolation for FirstWordUncapitalized {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"First word of the first line should be capitalized: `{}` -> `{}`",
"First word of the docstring should be capitalized: `{}` -> `{}`",
self.first_word, self.capitalized_word
)
}
@@ -59,7 +59,8 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) {
}
let body = docstring.body();
let first_word = body.split_once(' ').map_or_else(
let trim_start_body = body.trim_start();
let first_word = trim_start_body.split_once(' ').map_or_else(
|| {
// If the docstring is a single word, trim the punctuation marks because
// it makes the ASCII test below fail.
@@ -91,8 +92,10 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) {
let capitalized_word = uppercase_first_char.to_string() + first_word_chars.as_str();
let leading_whitespace_len = body.text_len() - trim_start_body.text_len();
let mut diagnostic = Diagnostic::new(
FirstLineCapitalized {
FirstWordUncapitalized {
first_word: first_word.to_string(),
capitalized_word: capitalized_word.to_string(),
},
@@ -101,7 +104,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
capitalized_word,
TextRange::at(body.start(), first_word.text_len()),
TextRange::at(body.start() + leading_whitespace_len, first_word.text_len()),
)));
checker.diagnostics.push(diagnostic);

View File

@@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
snapshot_kind: text
---
D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` -> `This`
D403.py:2:5: D403 [*] First word of the docstring should be capitalized: `this` -> `This`
|
1 | def bad_function():
2 | """this docstring is not capitalized"""
@@ -20,7 +19,7 @@ D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this`
4 4 | def good_function():
5 5 | """This docstring is capitalized."""
D403.py:30:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword`
D403.py:30:5: D403 [*] First word of the docstring should be capitalized: `singleword` -> `Singleword`
|
29 | def single_word():
30 | """singleword."""
@@ -40,11 +39,13 @@ D403.py:30:5: D403 [*] First word of the first line should be capitalized: `sing
32 32 | def single_word_no_dot():
33 33 | """singleword"""
D403.py:33:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword`
D403.py:33:5: D403 [*] First word of the docstring should be capitalized: `singleword` -> `Singleword`
|
32 | def single_word_no_dot():
33 | """singleword"""
| ^^^^^^^^^^^^^^^^ D403
34 |
35 | def first_word_lots_of_whitespace():
|
= help: Capitalize `singleword` to `Singleword`
@@ -54,3 +55,32 @@ D403.py:33:5: D403 [*] First word of the first line should be capitalized: `sing
32 32 | def single_word_no_dot():
33 |- """singleword"""
33 |+ """Singleword"""
34 34 |
35 35 | def first_word_lots_of_whitespace():
36 36 | """
D403.py:36:5: D403 [*] First word of the docstring should be capitalized: `here` -> `Here`
|
35 | def first_word_lots_of_whitespace():
36 | """
| _____^
37 | |
38 | |
39 | |
40 | | here is the start of my docstring!
41 | |
42 | | What do you think?
43 | | """
| |_______^ D403
|
= help: Capitalize `here` to `Here`
Safe fix
37 37 |
38 38 |
39 39 |
40 |- here is the start of my docstring!
40 |+ Here is the start of my docstring!
41 41 |
42 42 | What do you think?
43 43 | """

View File

@@ -1,10 +1,10 @@
use ast::{Expr, StmtAugAssign};
use ast::Expr;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::Operator;
use ruff_python_codegen::Generator;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{AstNode, ExprBinOp, ExpressionRef, Operator};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -103,11 +103,12 @@ pub(crate) fn non_augmented_assignment(checker: &mut Checker, assign: &ast::Stmt
if ComparableExpr::from(target) == ComparableExpr::from(&value.left) {
let mut diagnostic = Diagnostic::new(NonAugmentedAssignment { operator }, assign.range());
diagnostic.set_fix(Fix::unsafe_edit(augmented_assignment(
checker.generator(),
checker,
target,
value.op,
operator,
&value.right,
assign.range(),
value,
assign.range,
)));
checker.diagnostics.push(diagnostic);
return;
@@ -121,11 +122,12 @@ pub(crate) fn non_augmented_assignment(checker: &mut Checker, assign: &ast::Stmt
{
let mut diagnostic = Diagnostic::new(NonAugmentedAssignment { operator }, assign.range());
diagnostic.set_fix(Fix::unsafe_edit(augmented_assignment(
checker.generator(),
checker,
target,
value.op,
operator,
&value.left,
assign.range(),
value,
assign.range,
)));
checker.diagnostics.push(diagnostic);
}
@@ -135,21 +137,30 @@ pub(crate) fn non_augmented_assignment(checker: &mut Checker, assign: &ast::Stmt
///
/// For example, given `x = x + 1`, the fix would be `x += 1`.
fn augmented_assignment(
generator: Generator,
checker: &Checker,
target: &Expr,
operator: Operator,
operator: AugmentedOperator,
right_operand: &Expr,
original_expr: &ExprBinOp,
range: TextRange,
) -> Edit {
Edit::range_replacement(
generator.stmt(&ast::Stmt::AugAssign(StmtAugAssign {
range: TextRange::default(),
target: Box::new(target.clone()),
op: operator,
value: Box::new(right_operand.clone()),
})),
range,
)
let locator = checker.locator();
let right_operand_ref = ExpressionRef::from(right_operand);
let parent = original_expr.as_any_node_ref();
let comment_ranges = checker.comment_ranges();
let source = checker.source();
let right_operand_range =
parenthesized_range(right_operand_ref, parent, comment_ranges, source)
.unwrap_or(right_operand.range());
let right_operand_expr = locator.slice(right_operand_range);
let target_expr = locator.slice(target);
let new_content = format!("{target_expr} {operator} {right_operand_expr}");
Edit::range_replacement(new_content, range)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -269,7 +269,7 @@ non_augmented_assignment.py:29:1: PLR6104 [*] Use `&=` to perform an augmented a
27 27 | to_cube = to_cube**to_cube
28 28 | timeDiffSeconds = timeDiffSeconds % 60
29 |-flags = flags & 0x1
29 |+flags &= 1
29 |+flags &= 0x1
30 30 | flags = flags | 0x1
31 31 | flags = flags ^ 0x1
32 32 | flags = flags << 1
@@ -290,7 +290,7 @@ non_augmented_assignment.py:30:1: PLR6104 [*] Use `|=` to perform an augmented a
28 28 | timeDiffSeconds = timeDiffSeconds % 60
29 29 | flags = flags & 0x1
30 |-flags = flags | 0x1
30 |+flags |= 1
30 |+flags |= 0x1
31 31 | flags = flags ^ 0x1
32 32 | flags = flags << 1
33 33 | flags = flags >> 1
@@ -311,7 +311,7 @@ non_augmented_assignment.py:31:1: PLR6104 [*] Use `^=` to perform an augmented a
29 29 | flags = flags & 0x1
30 30 | flags = flags | 0x1
31 |-flags = flags ^ 0x1
31 |+flags ^= 1
31 |+flags ^= 0x1
32 32 | flags = flags << 1
33 33 | flags = flags >> 1
34 34 | mat1 = mat1 @ mat2
@@ -495,7 +495,7 @@ non_augmented_assignment.py:42:1: PLR6104 [*] Use `*=` to perform an augmented a
40 40 | a_list[:] = a_list[:] * 3
41 41 |
42 |-index = index * (index + 10)
42 |+index *= index + 10
42 |+index *= (index + 10)
43 43 |
44 44 |
45 45 | class T:
@@ -524,8 +524,6 @@ non_augmented_assignment.py:51:1: PLR6104 [*] Use `+=` to perform an augmented a
50 | obj = T()
51 | obj.a = obj.a + 1
| ^^^^^^^^^^^^^^^^^ PLR6104
52 |
53 | # OK
|
= help: Replace with augmented assignment
@@ -536,5 +534,213 @@ non_augmented_assignment.py:51:1: PLR6104 [*] Use `+=` to perform an augmented a
51 |-obj.a = obj.a + 1
51 |+obj.a += 1
52 52 |
53 53 | # OK
54 54 | a_list[0] = a_list[:] * 3
53 53 |
54 54 | a = a+-1
non_augmented_assignment.py:54:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
54 | a = a+-1
| ^^^^^^^^ PLR6104
55 |
56 | # Regression tests for https://github.com/astral-sh/ruff/issues/11672
|
= help: Replace with augmented assignment
Unsafe fix
51 51 | obj.a = obj.a + 1
52 52 |
53 53 |
54 |-a = a+-1
54 |+a += -1
55 55 |
56 56 | # Regression tests for https://github.com/astral-sh/ruff/issues/11672
57 57 | test = 0x5
non_augmented_assignment.py:58:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
56 | # Regression tests for https://github.com/astral-sh/ruff/issues/11672
57 | test = 0x5
58 | test = test + 0xBA
| ^^^^^^^^^^^^^^^^^^ PLR6104
59 |
60 | test2 = b""
|
= help: Replace with augmented assignment
Unsafe fix
55 55 |
56 56 | # Regression tests for https://github.com/astral-sh/ruff/issues/11672
57 57 | test = 0x5
58 |-test = test + 0xBA
58 |+test += 0xBA
59 59 |
60 60 | test2 = b""
61 61 | test2 = test2 + b"\000"
non_augmented_assignment.py:61:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
60 | test2 = b""
61 | test2 = test2 + b"\000"
| ^^^^^^^^^^^^^^^^^^^^^^^ PLR6104
62 |
63 | test3 = ""
|
= help: Replace with augmented assignment
Unsafe fix
58 58 | test = test + 0xBA
59 59 |
60 60 | test2 = b""
61 |-test2 = test2 + b"\000"
61 |+test2 += b"\000"
62 62 |
63 63 | test3 = ""
64 64 | test3 = test3 + ( a := R""
non_augmented_assignment.py:64:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
63 | test3 = ""
64 | / test3 = test3 + ( a := R""
65 | | f"oo" )
| |__________________________________^ PLR6104
66 |
67 | test4 = []
|
= help: Replace with augmented assignment
Unsafe fix
61 61 | test2 = test2 + b"\000"
62 62 |
63 63 | test3 = ""
64 |-test3 = test3 + ( a := R""
64 |+test3 += ( a := R""
65 65 | f"oo" )
66 66 |
67 67 | test4 = []
non_augmented_assignment.py:68:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
67 | test4 = []
68 | / test4 = test4 + ( e
69 | | for e in
70 | | range(10)
71 | | )
| |___________________^ PLR6104
72 |
73 | test5 = test5 + (
|
= help: Replace with augmented assignment
Unsafe fix
65 65 | f"oo" )
66 66 |
67 67 | test4 = []
68 |-test4 = test4 + ( e
68 |+test4 += ( e
69 69 | for e in
70 70 | range(10)
71 71 | )
non_augmented_assignment.py:73:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
71 | )
72 |
73 | / test5 = test5 + (
74 | | 4
75 | | *
76 | | 10
77 | | )
| |_^ PLR6104
78 |
79 | test6 = test6 + \
|
= help: Replace with augmented assignment
Unsafe fix
70 70 | range(10)
71 71 | )
72 72 |
73 |-test5 = test5 + (
73 |+test5 += (
74 74 | 4
75 75 | *
76 76 | 10
non_augmented_assignment.py:79:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
77 | )
78 |
79 | / test6 = test6 + \
80 | | (
81 | | 4
82 | | *
83 | | 10
84 | | )
| |_________^ PLR6104
85 |
86 | test7 = \
|
= help: Replace with augmented assignment
Unsafe fix
76 76 | 10
77 77 | )
78 78 |
79 |-test6 = test6 + \
80 |- (
79 |+test6 += (
81 80 | 4
82 81 | *
83 82 | 10
non_augmented_assignment.py:86:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
84 | )
85 |
86 | / test7 = \
87 | | 100 \
88 | | + test7
| |___________^ PLR6104
89 |
90 | test8 = \
|
= help: Replace with augmented assignment
Unsafe fix
83 83 | 10
84 84 | )
85 85 |
86 |-test7 = \
87 |- 100 \
88 |- + test7
86 |+test7 += 100
89 87 |
90 88 | test8 = \
91 89 | 886 \
non_augmented_assignment.py:90:1: PLR6104 [*] Use `+=` to perform an augmented assignment directly
|
88 | + test7
89 |
90 | / test8 = \
91 | | 886 \
92 | | + \
93 | | \
94 | | test8
| |_________^ PLR6104
|
= help: Replace with augmented assignment
Unsafe fix
87 87 | 100 \
88 88 | + test7
89 89 |
90 |-test8 = \
91 |- 886 \
92 |- + \
93 |- \
94 |- test8
90 |+test8 += 886
95 91 |
96 92 |
97 93 | # OK

View File

@@ -249,13 +249,13 @@ pub enum SimpleTokenKind {
/// `;`
Semi,
/// '/'
/// `/`
Slash,
/// '*'
/// `*`
Star,
/// `.`.
/// `.`
Dot,
/// `+`

View File

@@ -37,18 +37,12 @@ impl ResolvedClientCapabilities {
.and_then(|workspace_edit| workspace_edit.document_changes)
.unwrap_or_default();
let workspace_refresh = true;
// TODO(jane): Once the bug involving workspace.diagnostic(s) deserialization has been fixed,
// uncomment this.
/*
let workspace_refresh = client_capabilities
.workspace
.as_ref()
.and_then(|workspace| workspace.diagnostic.as_ref())
.and_then(|workspace| workspace.diagnostics.as_ref())
.and_then(|diagnostic| diagnostic.refresh_support)
.unwrap_or_default();
*/
let pull_diagnostics = client_capabilities
.text_document

View File

@@ -95,6 +95,7 @@ Similar to Ruff's CLI, the Ruff Language Server fully supports Jupyter Notebook
capabilities available to Python files.
!!! note
Unlike [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) and similar to the Ruff's CLI, the
native language server requires user to explicitly include the Jupyter Notebook files in the set
of files to lint and format. Refer to the [Jupyter Notebook discovery](https://docs.astral.sh/ruff/configuration/#jupyter-notebook-discovery)

View File

@@ -22,6 +22,7 @@ The Ruff Language Server was available first in Ruff [v0.4.5](https://astral.sh/
in beta and stabilized in Ruff [v0.5.3](https://github.com/astral-sh/ruff/releases/tag/0.5.3).
!!! note
This is the documentation for Ruff's built-in language server written in Rust (`ruff server`).
If you are looking for the documentation for the `ruff-lsp` language server, please refer to the
[README](https://github.com/astral-sh/ruff-lsp) of the `ruff-lsp` repository.

View File

@@ -4,6 +4,7 @@ While `ruff server` supports the same feature set as [`ruff-lsp`](https://github
`ruff server` may require changes to your Ruff or language server configuration.
!!! note
The [VS Code extension](https://github.com/astral-sh/ruff-vscode) settings include documentation to indicate which
settings are supported by `ruff server`. As such, this migration guide is primarily targeted at editors that lack
explicit documentation for `ruff server` settings, such as Helix or Neovim.
@@ -20,6 +21,7 @@ Several `ruff-lsp` settings are not supported by `ruff server`. These are, as fo
- `path`
!!! note
Some of these settings, like `interpreter` and `path`, are still accepted by the VS Code
extension. `path`, in particular, can be used to specify a dedicated binary to use when
initializing `ruff server`. But the language server itself will no longer accept such settings.

View File

@@ -23,6 +23,7 @@ behavior of the Ruff CLI.
**Example usage**:
=== "VS Code"
```json
{
"ruff.configuration": "~/path/to/ruff.toml"
@@ -30,6 +31,7 @@ behavior of the Ruff CLI.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -41,6 +43,7 @@ behavior of the Ruff CLI.
```
=== "Zed"
```json
{
"lsp": {
@@ -72,6 +75,7 @@ configuration is prioritized over `ruff.toml` and `pyproject.toml` files.
**Example usage**:
=== "VS Code"
```json
{
"ruff.configurationPreference": "filesystemFirst"
@@ -79,6 +83,7 @@ configuration is prioritized over `ruff.toml` and `pyproject.toml` files.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -90,6 +95,7 @@ configuration is prioritized over `ruff.toml` and `pyproject.toml` files.
```
=== "Zed"
```json
{
"lsp": {
@@ -116,6 +122,7 @@ documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details.
**Example usage**:
=== "VS Code"
```json
{
"ruff.exclude": ["**/tests/**"]
@@ -123,6 +130,7 @@ documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -134,6 +142,7 @@ documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details.
```
=== "Zed"
```json
{
"lsp": {
@@ -159,6 +168,7 @@ The line length to use for the linter and formatter.
**Example usage**:
=== "VS Code"
```json
{
"ruff.lineLength": 100
@@ -166,6 +176,7 @@ The line length to use for the linter and formatter.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -177,6 +188,7 @@ The line length to use for the linter and formatter.
```
=== "Zed"
```json
{
"lsp": {
@@ -202,6 +214,7 @@ Whether to register the server as capable of handling `source.fixAll` code actio
**Example usage**:
=== "VS Code"
```json
{
"ruff.fixAll": false
@@ -209,6 +222,7 @@ Whether to register the server as capable of handling `source.fixAll` code actio
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -220,6 +234,7 @@ Whether to register the server as capable of handling `source.fixAll` code actio
```
=== "Zed"
```json
{
"lsp": {
@@ -245,6 +260,7 @@ Whether to register the server as capable of handling `source.organizeImports` c
**Example usage**:
=== "VS Code"
```json
{
"ruff.organizeImports": false
@@ -252,6 +268,7 @@ Whether to register the server as capable of handling `source.organizeImports` c
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -263,6 +280,7 @@ Whether to register the server as capable of handling `source.organizeImports` c
```
=== "Zed"
```json
{
"lsp": {
@@ -290,6 +308,7 @@ Whether to show syntax error diagnostics.
**Example usage**:
=== "VS Code"
```json
{
"ruff.showSyntaxErrors": false
@@ -297,6 +316,7 @@ Whether to show syntax error diagnostics.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -308,6 +328,7 @@ Whether to show syntax error diagnostics.
```
=== "Zed"
```json
{
"lsp": {
@@ -333,6 +354,7 @@ The log level to use for the server.
**Example usage**:
=== "VS Code"
```json
{
"ruff.logLevel": "debug"
@@ -340,6 +362,7 @@ The log level to use for the server.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -351,6 +374,7 @@ The log level to use for the server.
```
=== "Zed"
```json
{
"lsp": {
@@ -378,6 +402,7 @@ If not set, logs will be written to stderr.
**Example usage**:
=== "VS Code"
```json
{
"ruff.logFile": "~/path/to/ruff.log"
@@ -385,6 +410,7 @@ If not set, logs will be written to stderr.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -396,6 +422,7 @@ If not set, logs will be written to stderr.
```
=== "Zed"
```json
{
"lsp": {
@@ -425,6 +452,7 @@ Whether to display Quick Fix actions to disable rules via `noqa` suppression com
**Example usage**:
=== "VS Code"
```json
{
"ruff.codeAction.disableRuleComment.enable": false
@@ -432,6 +460,7 @@ Whether to display Quick Fix actions to disable rules via `noqa` suppression com
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -447,6 +476,7 @@ Whether to display Quick Fix actions to disable rules via `noqa` suppression com
```
=== "Zed"
```json
{
"lsp": {
@@ -476,6 +506,7 @@ Whether to display Quick Fix actions to autofix violations.
**Example usage**:
=== "VS Code"
```json
{
"ruff.codeAction.fixViolation.enable": false
@@ -483,6 +514,7 @@ Whether to display Quick Fix actions to autofix violations.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -498,6 +530,7 @@ Whether to display Quick Fix actions to autofix violations.
```
=== "Zed"
```json
{
"lsp": {
@@ -531,6 +564,7 @@ Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter
**Example usage**:
=== "VS Code"
```json
{
"ruff.lint.enable": false
@@ -538,6 +572,7 @@ Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -551,6 +586,7 @@ Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter
```
=== "Zed"
```json
{
"lsp": {
@@ -580,6 +616,7 @@ Whether to enable Ruff's preview mode when linting.
**Example usage**:
=== "VS Code"
```json
{
"ruff.lint.preview": true
@@ -587,6 +624,7 @@ Whether to enable Ruff's preview mode when linting.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -600,6 +638,7 @@ Whether to enable Ruff's preview mode when linting.
```
=== "Zed"
```json
{
"lsp": {
@@ -627,6 +666,7 @@ Rules to enable by default. See [the documentation](https://docs.astral.sh/ruff/
**Example usage**:
=== "VS Code"
```json
{
"ruff.lint.select": ["E", "F"]
@@ -634,6 +674,7 @@ Rules to enable by default. See [the documentation](https://docs.astral.sh/ruff/
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -647,6 +688,7 @@ Rules to enable by default. See [the documentation](https://docs.astral.sh/ruff/
```
=== "Zed"
```json
{
"lsp": {
@@ -674,6 +716,7 @@ Rules to enable in addition to those in [`lint.select`](#select).
**Example usage**:
=== "VS Code"
```json
{
"ruff.lint.extendSelect": ["W"]
@@ -681,6 +724,7 @@ Rules to enable in addition to those in [`lint.select`](#select).
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -694,6 +738,7 @@ Rules to enable in addition to those in [`lint.select`](#select).
```
=== "Zed"
```json
{
"lsp": {
@@ -721,6 +766,7 @@ Rules to disable by default. See [the documentation](https://docs.astral.sh/ruff
**Example usage**:
=== "VS Code"
```json
{
"ruff.lint.ignore": ["E4", "E7"]
@@ -728,6 +774,7 @@ Rules to disable by default. See [the documentation](https://docs.astral.sh/ruff
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -741,6 +788,7 @@ Rules to disable by default. See [the documentation](https://docs.astral.sh/ruff
```
=== "Zed"
```json
{
"lsp": {
@@ -768,6 +816,7 @@ Rules to disable in addition to those in [`lint.ignore`](#ignore).
**Example usage**:
=== "VS Code"
```json
{
"ruff.lint.extendIgnore": ["W1"]
@@ -775,6 +824,7 @@ Rules to disable in addition to those in [`lint.ignore`](#ignore).
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -788,6 +838,7 @@ Rules to disable in addition to those in [`lint.ignore`](#ignore).
```
=== "Zed"
```json
{
"lsp": {
@@ -819,6 +870,7 @@ Whether to enable Ruff's preview mode when formatting.
**Example usage**:
=== "VS Code"
```json
{
"ruff.format.preview": true
@@ -826,6 +878,7 @@ Whether to enable Ruff's preview mode when formatting.
```
=== "Neovim"
```lua
require('lspconfig').ruff.setup {
init_options = {
@@ -839,6 +892,7 @@ Whether to enable Ruff's preview mode when formatting.
```
=== "Zed"
```json
{
"lsp": {

View File

@@ -8,11 +8,13 @@ regardless of editor, there are several settings which have changed or are no lo
the [migration guide](./migration.md) for more.
!!! note
The setup instructions provided below are on a best-effort basis. If you encounter any issues
while setting up the Ruff in an editor, please [open an issue](https://github.com/astral-sh/ruff/issues/new)
for assistance and help in improving this documentation.
!!! tip
Regardless of the editor, it is recommended to disable the older language server
([`ruff-lsp`](https://github.com/astral-sh/ruff-lsp)) to prevent any conflicts.
@@ -45,6 +47,7 @@ require('lspconfig').ruff.setup({
```
!!! note
If the installed version of `nvim-lspconfig` includes the changes from
[neovim/nvim-lspconfig@`70d1c2c`](https://github.com/neovim/nvim-lspconfig/commit/70d1c2c31a88af4b36019dc1551be16bffb8f9db),
you will need to use Ruff version `0.5.3` or later.
@@ -242,6 +245,7 @@ language-servers = ["ruff", "pylsp"]
```
!!! note
Support for multiple language servers for a language is only available in Helix version
[`23.10`](https://github.com/helix-editor/helix/blob/master/CHANGELOG.md#2310-2023-10-24) and later.
@@ -287,6 +291,7 @@ logLevel = "debug"
You can also divert Ruff's logs to a separate file with the [`logFile`](./settings.md#logfile) setting.
!!! note
Setting `RUFF_TRACE=verbose` does not enable Helix's verbose mode by itself. You'll need to run
Helix with `-v` for verbose logging.
@@ -313,6 +318,7 @@ See [LSP Client documentation](https://docs.kde.org/stable5/en/kate/kate/kate-ap
on how to configure the server from there.
!!! important
Kate's LSP Client plugin does not support multiple servers for the same language. As a
workaround, you can use the [`python-lsp-server`](https://github.com/python-lsp/python-lsp-server)
along with the [`python-lsp-ruff`](https://github.com/python-lsp/python-lsp-ruff) plugin to
@@ -431,6 +437,7 @@ under the [`lsp.ruff.initialization_options.settings`](https://zed.dev/docs/conf
```
!!! note
Support for multiple formatters for a given language is only available in Zed version
`0.146.0` and later.
@@ -438,6 +445,7 @@ You can configure Ruff to format Python code on-save by registering the Ruff for
and enabling the [`format_on_save`](https://zed.dev/docs/configuring-zed#format-on-save) setting:
=== "Zed 0.146.0+"
```json
{
"languages": {
@@ -460,6 +468,7 @@ You can configure Ruff to fix lint violations and/or organize imports on-save by
`source.fixAll.ruff` and `source.organizeImports.ruff` code actions respectively:
=== "Zed 0.146.0+"
```json
{
"languages": {
@@ -485,12 +494,14 @@ Taken together, you can configure Ruff to format, fix, and organize imports on-s
following `settings.json`:
!!! note
For this configuration, it is important to use the correct order of the code action and
formatter language server settings. The code actions should be defined before the formatter to
ensure that the formatter takes care of any remaining style issues after the code actions have
been applied.
=== "Zed 0.146.0+"
```json
{
"languages": {

View File

@@ -1,11 +1,10 @@
PyYAML==6.0.2
ruff==0.8.2
ruff==0.8.3
mkdocs==1.6.1
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff
mkdocs-redirects==1.2.2
mdformat==0.7.19
mdformat-mkdocs==3.1.1
mdformat-admon==2.0.6
mdformat-mkdocs==4.0.0
# Using a commit from pygments main branch so we get
# https://github.com/pygments/pygments/pull/2773 before it's been released
pygments @ git+https://github.com/pygments/pygments.git@67b460fdde6d9a00342b5102b37b3a8399f0e8ef

View File

@@ -1,11 +1,10 @@
PyYAML==6.0.2
ruff==0.8.2
ruff==0.8.3
mkdocs==1.6.1
mkdocs-material==9.5.38
mkdocs-redirects==1.2.2
mdformat==0.7.19
mdformat-mkdocs==3.1.1
mdformat-admon==2.0.6
mdformat-mkdocs==4.0.0
# Using a commit from pygments main branch so we get
# https://github.com/pygments/pygments/pull/2773 before it's been released
pygments @ git+https://github.com/pygments/pygments.git@67b460fdde6d9a00342b5102b37b3a8399f0e8ef

View File

@@ -2,8 +2,8 @@
name = "ruff-fuzz"
version = "0.0.0"
authors = [
"Charlie Marsh <charlie.r.marsh@gmail.com>",
"Addison Crump <research@addisoncrump.info>",
"Charlie Marsh <charlie.r.marsh@gmail.com>",
"Addison Crump <research@addisoncrump.info>",
]
publish = false
edition = "2021"
@@ -25,11 +25,11 @@ ruff_python_ast = { path = "../crates/ruff_python_ast" }
ruff_python_codegen = { path = "../crates/ruff_python_codegen" }
ruff_python_parser = { path = "../crates/ruff_python_parser" }
ruff_source_file = { path = "../crates/ruff_source_file" }
ruff_python_formatter = { path = "../crates/ruff_python_formatter"}
ruff_python_formatter = { path = "../crates/ruff_python_formatter" }
ruff_text_size = { path = "../crates/ruff_text_size" }
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
similar = { version = "2.5.0" }
tracing = { version = "0.1.40" }

View File

@@ -22,12 +22,13 @@ use ruff_python_parser::{parse_unchecked, Mode};
///
/// Uses an in memory filesystem and it stubs out the vendored files by default.
#[salsa::db]
#[derive(Clone)]
struct TestDb {
storage: salsa::Storage<Self>,
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
events: std::sync::Arc<Mutex<Vec<salsa::Event>>>,
rule_selection: std::sync::Arc<RuleSelection>,
}

View File

@@ -16,7 +16,7 @@
"@cloudflare/workers-types": "^4.20230801.0",
"miniflare": "^3.20230801.1",
"typescript": "^5.1.6",
"wrangler": "3.93.0"
"wrangler": "3.95.0"
}
},
"node_modules/@cloudflare/kv-asset-handler": {
@@ -118,9 +118,9 @@
}
},
"node_modules/@cloudflare/workers-shared": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.10.0.tgz",
"integrity": "sha512-j3EwZBc9ctavmFVOQT1gqztRO/Plx4ZR0LMEEOif+5YoCcuD1P7/NEjlODPMc5a1w+8+7A/H+Ci8Ihd55+x0Zw==",
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.11.0.tgz",
"integrity": "sha512-A+lQ8xp7992qSeMmuQ0ssL6CPmm+ZmAv6Ddikan0n1jjpMAic+97l7xtVIsswSn9iLMFPYQ9uNN/8Fl0AgARIQ==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
@@ -1453,14 +1453,14 @@
}
},
"node_modules/wrangler": {
"version": "3.93.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.93.0.tgz",
"integrity": "sha512-+wfxjOrtm6YgDS+NdJkB6aiBIS3ED97mNRQmfrEShRJW4pVo4sWY6oQ1FsGT+j4tGHplrTbWCE6U5yTgjNW/lw==",
"version": "3.95.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.95.0.tgz",
"integrity": "sha512-3w5852i3FNyDz421K2Qk4v5L8jjwegO5O8E1+VAQmjnm82HFNxpIRUBq0bmM7CTLvOPI/Jjcmj/eAWjQBL7QYg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"@cloudflare/kv-asset-handler": "0.3.4",
"@cloudflare/workers-shared": "0.10.0",
"@cloudflare/workers-shared": "0.11.0",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"blake3-wasm": "^2.1.5",

View File

@@ -5,7 +5,7 @@
"@cloudflare/workers-types": "^4.20230801.0",
"miniflare": "^3.20230801.1",
"typescript": "^5.1.6",
"wrangler": "3.93.0"
"wrangler": "3.95.0"
},
"private": true,
"scripts": {

View File

@@ -1219,25 +1219,27 @@
}
},
"node_modules/@types/react-dom": {
"version": "19.0.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.1.tgz",
"integrity": "sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==",
"version": "19.0.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
"integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
"dev": true,
"dependencies": {
"@types/react": "*"
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz",
"integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz",
"integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.17.0",
"@typescript-eslint/type-utils": "8.17.0",
"@typescript-eslint/utils": "8.17.0",
"@typescript-eslint/visitor-keys": "8.17.0",
"@typescript-eslint/scope-manager": "8.18.0",
"@typescript-eslint/type-utils": "8.18.0",
"@typescript-eslint/utils": "8.18.0",
"@typescript-eslint/visitor-keys": "8.18.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1252,24 +1254,21 @@
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz",
"integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz",
"integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==",
"dev": true,
"license": "MITClause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.17.0",
"@typescript-eslint/types": "8.17.0",
"@typescript-eslint/typescript-estree": "8.17.0",
"@typescript-eslint/visitor-keys": "8.17.0",
"@typescript-eslint/scope-manager": "8.18.0",
"@typescript-eslint/types": "8.18.0",
"@typescript-eslint/typescript-estree": "8.18.0",
"@typescript-eslint/visitor-keys": "8.18.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1280,22 +1279,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz",
"integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz",
"integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.17.0",
"@typescript-eslint/visitor-keys": "8.17.0"
"@typescript-eslint/types": "8.18.0",
"@typescript-eslint/visitor-keys": "8.18.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1306,13 +1302,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz",
"integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz",
"integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.17.0",
"@typescript-eslint/utils": "8.17.0",
"@typescript-eslint/typescript-estree": "8.18.0",
"@typescript-eslint/utils": "8.18.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1324,19 +1321,16 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz",
"integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz",
"integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -1346,13 +1340,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz",
"integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz",
"integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.17.0",
"@typescript-eslint/visitor-keys": "8.17.0",
"@typescript-eslint/types": "8.18.0",
"@typescript-eslint/visitor-keys": "8.18.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1367,22 +1362,21 @@
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"peerDependencies": {
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz",
"integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz",
"integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.17.0",
"@typescript-eslint/types": "8.17.0",
"@typescript-eslint/typescript-estree": "8.17.0"
"@typescript-eslint/scope-manager": "8.18.0",
"@typescript-eslint/types": "8.18.0",
"@typescript-eslint/typescript-estree": "8.18.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1392,21 +1386,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz",
"integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz",
"integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.17.0",
"@typescript-eslint/types": "8.18.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@@ -1422,6 +1413,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -3936,9 +3928,10 @@
}
},
"node_modules/monaco-editor": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw=="
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
@@ -4753,6 +4746,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},

View File

@@ -1,44 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import mdformat
if TYPE_CHECKING:
import argparse
from markdown_it import MarkdownIt
from mdformat.renderer import RenderContext, RenderTreeNode
class NoEscapeTextPlugin:
r"""Overrides the default text formatting behavior of mdformat.
By default mdformat will escape any markdown special character found in a
text block, e.g., <. Some of these characters are found in our
documentation, and when escaped (i.e. \<) will be rendered incorrectly by
mkdocs, i.e., the backslash will appear in the render. Because our only
purpose in using mdformat is to manage the line-breaks, it makes sense to
override its text formatting behavior.
"""
def __init__(self: NoEscapeTextPlugin) -> None:
self.POSTPROCESSORS = {"text": NoEscapeTextPlugin.text}
self.RENDERERS = {}
@staticmethod
def add_cli_options(parser: argparse.ArgumentParser) -> None:
pass
@staticmethod
def update_mdit(mdit: MarkdownIt) -> None:
pass
@staticmethod
def text(_text: str, node: RenderTreeNode, _context: RenderContext) -> str:
return node.content
def add_no_escape_text_plugin() -> None:
"""Add NoEscapeTextPlugin to the list of mdformat extensions."""
mdformat.plugins.PARSER_EXTENSIONS["no-escape-text"] = NoEscapeTextPlugin()

View File

@@ -14,8 +14,6 @@ from typing import NamedTuple
import mdformat
import yaml
from _mdformat_utils import add_no_escape_text_plugin
class Section(NamedTuple):
"""A section to include in the MkDocs documentation."""
@@ -229,12 +227,11 @@ def main() -> None:
f.write(clean_file_content(file_content, title))
add_no_escape_text_plugin()
for rule_doc in Path("docs/rules").glob("*.md"):
# Format rules docs. This has to be completed before adding the meta description
# otherwise the meta description will be formatted in a way that mkdocs does not
# support.
mdformat.file(rule_doc, extensions=["mkdocs", "admon", "no-escape-text"])
mdformat.file(rule_doc, extensions=["mkdocs"])
generate_rule_metadata(rule_doc)