Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie Blue
f62ca88077 Rename the msrv job to indicate it tests 2025-07-10 08:06:41 -05:00
66 changed files with 279 additions and 3405 deletions

View File

@@ -385,8 +385,8 @@ jobs:
- name: "Build"
run: cargo build --release --locked
cargo-build-msrv:
name: "cargo build (msrv)"
cargo-test-msrv:
name: "cargo test (msrv)"
runs-on: depot-ubuntu-latest-8
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}

View File

@@ -1,33 +1,5 @@
# Changelog
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.2
### Preview features

9
Cargo.lock generated
View File

@@ -2711,7 +2711,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.3"
version = "0.12.2"
dependencies = [
"anyhow",
"argfile",
@@ -2961,7 +2961,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.3"
version = "0.12.2"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3294,7 +3294,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.3"
version = "0.12.2"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -4152,12 +4152,9 @@ version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"insta",
"regex",
"ruff_db",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"salsa",

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.3/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.2/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.3
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.3"
version = "0.12.2"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -5692,57 +5692,3 @@ class Foo:
"
);
}
#[test_case::test_case("concise")]
#[test_case::test_case("full")]
#[test_case::test_case("json")]
#[test_case::test_case("json-lines")]
#[test_case::test_case("junit")]
#[test_case::test_case("grouped")]
#[test_case::test_case("github")]
#[test_case::test_case("gitlab")]
#[test_case::test_case("pylint")]
#[test_case::test_case("rdjson")]
#[test_case::test_case("azure")]
#[test_case::test_case("sarif")]
fn output_format(output_format: &str) -> Result<()> {
const CONTENT: &str = "\
import os # F401
x = y # F821
match 42: # invalid-syntax
case _: ...
";
let tempdir = TempDir::new()?;
let input = tempdir.path().join("input.py");
fs::write(&input, CONTENT)?;
let snapshot = format!("output_format_{output_format}");
insta::with_settings!({
filters => vec![
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
(ruff_linter::VERSION, "[VERSION]"),
]
}, {
assert_cmd_snapshot!(
snapshot,
Command::new(get_cargo_bin(BIN_NAME))
.args([
"check",
"--no-cache",
"--output-format",
output_format,
"--select",
"F401,F821",
"--target-version",
"py39",
"input.py",
])
.current_dir(&tempdir),
);
});
Ok(())
}

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- azure
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y`
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -1,25 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- concise
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
input.py:2:5: F821 Undefined name `y`
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -1,49 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- full
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1:8: F401 [*] `os` imported but unused
|
1 | import os # F401
| ^^ F401
2 | x = y # F821
3 | match 42: # invalid-syntax
|
= help: Remove unused import: `os`
input.py:2:5: F821 Undefined name `y`
|
1 | import os # F401
2 | x = y # F821
| ^ F821
3 | match 42: # invalid-syntax
4 | case _: ...
|
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
1 | import os # F401
2 | x = y # F821
3 | match 42: # invalid-syntax
| ^^^^^
4 | case _: ...
|
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- github
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused
::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y`
::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -1,60 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- gitlab
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
[
{
"check_name": "F401",
"description": "`os` imported but unused",
"fingerprint": "4dbad37161e65c72",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "input.py"
},
"severity": "major"
},
{
"check_name": "F821",
"description": "Undefined name `y`",
"fingerprint": "7af59862a085230",
"location": {
"lines": {
"begin": 2,
"end": 2
},
"path": "input.py"
},
"severity": "major"
},
{
"check_name": "syntax-error",
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"fingerprint": "e558cec859bb66e8",
"location": {
"lines": {
"begin": 3,
"end": 3
},
"path": "input.py"
},
"severity": "major"
}
]
----- stderr -----

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- grouped
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:
1:8 F401 [*] `os` imported but unused
2:5 F821 Undefined name `y`
3:1 SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 3 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- json-lines
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
----- stderr -----

View File

@@ -1,88 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- json
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
[
{
"cell": null,
"code": "F401",
"end_location": {
"column": 10,
"row": 1
},
"filename": "[TMP]/input.py",
"fix": {
"applicability": "safe",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 2
},
"location": {
"column": 1,
"row": 1
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 1
},
"message": "`os` imported but unused",
"noqa_row": 1,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"cell": null,
"code": "F821",
"end_location": {
"column": 6,
"row": 2
},
"filename": "[TMP]/input.py",
"fix": null,
"location": {
"column": 5,
"row": 2
},
"message": "Undefined name `y`",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/undefined-name"
},
{
"cell": null,
"code": null,
"end_location": {
"column": 6,
"row": 3
},
"filename": "[TMP]/input.py",
"fix": null,
"location": {
"column": 1,
"row": 3
},
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"noqa_row": null,
"url": null
}
]
----- stderr -----

View File

@@ -1,34 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- junit
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="3" failures="3" errors="0">
<testsuite name="[TMP]/input.py" tests="3" disabled="0" errors="0" failures="3" package="org.ruff">
<testcase name="org.ruff.F401" classname="[TMP]/input" line="1" column="8">
<failure message="`os` imported but unused">line 1, col 8, `os` imported but unused</failure>
</testcase>
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
</testcase>
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
</testcase>
</testsuite>
</testsuites>
----- stderr -----

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- pylint
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
input.py:1: [F401] `os` imported but unused
input.py:2: [F821] Undefined name `y`
input.py:3: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
----- stderr -----

View File

@@ -1,103 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- rdjson
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{
"diagnostics": [
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/unused-import",
"value": "F401"
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 10,
"line": 1
},
"start": {
"column": 8,
"line": 1
}
}
},
"message": "`os` imported but unused",
"suggestions": [
{
"range": {
"end": {
"column": 1,
"line": 2
},
"start": {
"column": 1,
"line": 1
}
},
"text": ""
}
]
},
{
"code": {
"url": "https://docs.astral.sh/ruff/rules/undefined-name",
"value": "F821"
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 6,
"line": 2
},
"start": {
"column": 5,
"line": 2
}
}
},
"message": "Undefined name `y`"
},
{
"code": {
"url": null,
"value": null
},
"location": {
"path": "[TMP]/input.py",
"range": {
"end": {
"column": 6,
"line": 3
},
"start": {
"column": 1,
"line": 3
}
}
},
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
}
],
"severity": "warning",
"source": {
"name": "ruff",
"url": "https://docs.astral.sh/ruff"
}
}
----- stderr -----

View File

@@ -1,142 +0,0 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- sarif
- "--select"
- "F401,F821"
- "--target-version"
- py39
- input.py
---
success: false
exit_code: 1
----- stdout -----
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [
{
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 10,
"endLine": 1,
"startColumn": 8,
"startLine": 1
}
}
}
],
"message": {
"text": "`os` imported but unused"
},
"ruleId": "F401"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 6,
"endLine": 2,
"startColumn": 5,
"startLine": 2
}
}
}
],
"message": {
"text": "Undefined name `y`"
},
"ruleId": "F821"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"region": {
"endColumn": 6,
"endLine": 3,
"startColumn": 1,
"startLine": 3
}
}
}
],
"message": {
"text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
},
"ruleId": null
}
],
"tool": {
"driver": {
"informationUri": "https://github.com/astral-sh/ruff",
"name": "ruff",
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
},
"helpUri": "https://docs.astral.sh/ruff/rules/unused-import",
"id": "F401",
"properties": {
"id": "F401",
"kind": "Pyflakes",
"name": "unused-import",
"problem.severity": "error"
},
"shortDescription": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
}
},
{
"fullDescription": {
"text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n"
},
"help": {
"text": "Undefined name `{name}`. {tip}"
},
"helpUri": "https://docs.astral.sh/ruff/rules/undefined-name",
"id": "F821",
"properties": {
"id": "F821",
"kind": "Pyflakes",
"name": "undefined-name",
"problem.severity": "error"
},
"shortDescription": {
"text": "Undefined name `{name}`. {tip}"
}
}
],
"version": "[VERSION]"
}
}
}
],
"version": "2.1.0"
}
----- stderr -----

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.3"
version = "0.12.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -422,35 +422,6 @@ def func(a: dict[str, int]) -> list[dict[str, int]]:
services = a["services"]
return services
# See: https://github.com/astral-sh/ruff/issues/14052
def outer() -> list[object]:
@register
async def inner() -> None:
print(layout)
layout = [...]
return layout
def outer() -> list[object]:
with open("") as f:
async def inner() -> None:
print(layout)
layout = [...]
return layout
def outer() -> list[object]:
def inner():
with open("") as f:
async def inner_inner() -> None:
print(layout)
layout = [...]
return layout
# See: https://github.com/astral-sh/ruff/issues/18411
def f():
(#=

View File

@@ -1,6 +0,0 @@
# Regression test for: https://github.com/astral-sh/ruff/issues/19175
# there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
from typing import TYPE_CHECKING \
if TYPE_CHECKING: import builtins
builtins.print("!")

View File

@@ -4,8 +4,8 @@ use crate::Fix;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return,
flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff,
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
pylint, pyupgrade, refurb, ruff,
};
/// Run lint rules over the [`Binding`]s.
@@ -25,20 +25,11 @@ pub(crate) fn bindings(checker: &Checker) {
Rule::ForLoopWrites,
Rule::CustomTypeVarForSelf,
Rule::PrivateTypeParameter,
Rule::UnnecessaryAssign,
]) {
return;
}
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
if binding.kind.is_function_definition() {
flake8_return::rules::unnecessary_assign(
checker,
binding.statement(checker.semantic()).unwrap(),
);
}
}
if checker.is_rule_enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception()
&& binding.is_unused()

View File

@@ -207,6 +207,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Rule::UnnecessaryReturnNone,
Rule::ImplicitReturnValue,
Rule::ImplicitReturn,
Rule::UnnecessaryAssign,
Rule::SuperfluousElseReturn,
Rule::SuperfluousElseRaise,
Rule::SuperfluousElseContinue,

View File

@@ -5,7 +5,6 @@ use ruff_python_ast::Stmt;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_codegen::Stylist;
use ruff_python_parser::{TokenKind, Tokens};
use ruff_python_trivia::is_python_whitespace;
use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
use ruff_text_size::{Ranged, TextSize};
@@ -307,7 +306,7 @@ fn match_semicolon(s: &str) -> Option<TextSize> {
fn match_continuation(s: &str) -> Option<TextSize> {
for (offset, c) in s.char_indices() {
match c {
_ if is_python_whitespace(c) => continue,
' ' | '\t' => continue,
'\\' => return Some(TextSize::try_from(offset).unwrap()),
_ => break,
}

View File

@@ -539,21 +539,7 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt:
}
/// RET504
pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
let Stmt::FunctionDef(function_def) = function_stmt else {
return;
};
let Some(stack) = create_stack(checker, function_def) else {
return;
};
if !result_exists(&stack.returns) {
return;
}
let Some(function_scope) = checker.semantic().function_scope(function_def) else {
return;
};
fn unnecessary_assign(checker: &Checker, stack: &Stack) {
for (assign, return_, stmt) in &stack.assignment_return {
// Identify, e.g., `return x`.
let Some(value) = return_.value.as_ref() else {
@@ -597,22 +583,6 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
continue;
}
let Some(assigned_binding) = function_scope
.get(assigned_id)
.map(|binding_id| checker.semantic().binding(binding_id))
else {
continue;
};
// Check if there's any reference made to `assigned_binding` in another scope, e.g, nested
// functions. If there is, ignore them.
if assigned_binding
.references()
.map(|reference_id| checker.semantic().reference(reference_id))
.any(|reference| reference.scope_id() != assigned_binding.scope)
{
continue;
}
let mut diagnostic = checker.report_diagnostic(
UnnecessaryAssign {
name: assigned_id.to_string(),
@@ -695,21 +665,24 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) {
}
}
fn create_stack<'a>(
checker: &'a Checker,
function_def: &'a ast::StmtFunctionDef,
) -> Option<Stack<'a>> {
let ast::StmtFunctionDef { body, .. } = function_def;
/// Run all checks from the `flake8-return` plugin.
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let ast::StmtFunctionDef {
decorator_list,
returns,
body,
..
} = function_def;
// Find the last statement in the function.
let Some(last_stmt) = body.last() else {
// Skip empty functions.
return None;
return;
};
// Skip functions that consist of a single return statement.
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
return None;
return;
}
// Traverse the function body, to collect the stack.
@@ -723,29 +696,9 @@ fn create_stack<'a>(
// Avoid false positives for generators.
if stack.is_generator {
return None;
return;
}
Some(stack)
}
/// Run all checks from the `flake8-return` plugin, but `RET504` which is ran
/// after the semantic model is fully built.
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let ast::StmtFunctionDef {
decorator_list,
returns,
body,
..
} = function_def;
let Some(stack) = create_stack(checker, function_def) else {
return;
};
let Some(last_stmt) = body.last() else {
return;
};
if checker.any_rule_enabled(&[
Rule::SuperfluousElseReturn,
Rule::SuperfluousElseRaise,
@@ -768,6 +721,10 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
if checker.is_rule_enabled(Rule::ImplicitReturn) {
implicit_return(checker, function_def, last_stmt);
}
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
unnecessary_assign(checker, &stack);
}
} else {
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
// Skip functions that have a return annotation that is not `None`.

View File

@@ -247,6 +247,8 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
422 | services = a["services"]
423 | return services
| ^^^^^^^^ RET504
424 |
425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
= help: Remove unnecessary assignment
@@ -258,46 +260,46 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
423 |- return services
422 |+ return a["services"]
424 423 |
425 424 |
426 425 | # See: https://github.com/astral-sh/ruff/issues/14052
425 424 | # See: https://github.com/astral-sh/ruff/issues/18411
426 425 | def f():
RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
RET504.py:429:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
456 | (#=
457 | x) = 1
458 | return x
427 | (#=
428 | x) = 1
429 | return x
| ^ RET504
459 |
460 | def f():
430 |
431 | def f():
|
= help: Remove unnecessary assignment
Unsafe fix
453 453 |
454 454 | # See: https://github.com/astral-sh/ruff/issues/18411
455 455 | def f():
456 |- (#=
457 |- x) = 1
458 |- return x
456 |+ return 1
459 457 |
460 458 | def f():
461 459 | x = (1
424 424 |
425 425 | # See: https://github.com/astral-sh/ruff/issues/18411
426 426 | def f():
427 |- (#=
428 |- x) = 1
429 |- return x
427 |+ return 1
430 428 |
431 429 | def f():
432 430 | x = (1
RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
RET504.py:434:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
461 | x = (1
462 | )
463 | return x
432 | x = (1
433 | )
434 | return x
| ^ RET504
|
= help: Remove unnecessary assignment
Unsafe fix
458 458 | return x
459 459 |
460 460 | def f():
461 |- x = (1
461 |+ return (1
462 462 | )
463 |- return x
429 429 | return x
430 430 |
431 431 | def f():
432 |- x = (1
432 |+ return (1
433 433 | )
434 |- return x

View File

@@ -36,7 +36,6 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_9.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("whitespace.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_1.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TC001.py"))]

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
whitespace.py:5:26: TC004 [*] Move import `builtins` out of type-checking block. Import is used for more than type hinting.
|
3 | from typing import TYPE_CHECKING \
4 |
5 | if TYPE_CHECKING: import builtins
| ^^^^^^^^ TC004
6 | builtins.print("!")
|
= help: Move out of type-checking block
Unsafe fix
1 1 | # Regression test for: https://github.com/astral-sh/ruff/issues/19175
2 2 | # there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
3 |-from typing import TYPE_CHECKING \
3 |+from typing import TYPE_CHECKING; import builtins \
4 4 |
5 |-if TYPE_CHECKING: import builtins
5 |+if TYPE_CHECKING: pass
6 6 | builtins.print("!")

View File

@@ -2094,20 +2094,6 @@ impl<'a> SemanticModel<'a> {
None
})
}
/// Finds and returns the [`Scope`] corresponding to a given [`ast::StmtFunctionDef`].
///
/// This method searches all scopes created by a function definition, comparing the
/// [`TextRange`] of the provided `function_def` with the the range of the function
/// associated with the scope.
pub fn function_scope(&self, function_def: &ast::StmtFunctionDef) -> Option<&Scope> {
self.scopes.iter().find(|scope| {
let Some(function) = scope.kind.as_function() else {
return false;
};
function.range() == function_def.range()
})
}
}
pub struct ShadowedBinding {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.12.3"
version = "0.12.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }

3
crates/ty/docs/cli.md generated
View File

@@ -84,8 +84,7 @@ over all configuration files.</p>
<li><code>3.11</code></li>
<li><code>3.12</code></li>
<li><code>3.13</code></li>
</ul></dd><dt id="ty-check--quiet"><a href="#ty-check--quiet"><code>--quiet</code></a></dt><dd><p>Use quiet output</p>
</dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
</ul></dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
</dd><dt id="ty-check--typeshed"><a href="#ty-check--typeshed"><code>--typeshed</code></a>, <code>--custom-typeshed-dir</code> <i>path</i></dt><dd><p>Custom directory to use for stdlib typeshed stubs</p>
</dd><dt id="ty-check--verbose"><a href="#ty-check--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output (or <code>-vv</code> and <code>-vvv</code> for more verbose output)</p>
</dd><dt id="ty-check--warn"><a href="#ty-check--warn"><code>--warn</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'warn'. Can be specified multiple times.</p>

View File

@@ -1,13 +1,12 @@
mod args;
mod logging;
mod printer;
mod python_version;
mod version;
pub use args::Cli;
use ty_static::EnvVars;
use std::fmt::Write;
use std::io::{self, BufWriter, Write, stdout};
use std::process::{ExitCode, Termination};
use anyhow::Result;
@@ -15,7 +14,6 @@ use std::sync::Mutex;
use crate::args::{CheckCommand, Command, TerminalColor};
use crate::logging::setup_tracing;
use crate::printer::Printer;
use anyhow::{Context, anyhow};
use clap::{CommandFactory, Parser};
use colored::Colorize;
@@ -27,7 +25,7 @@ use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::watch::ProjectWatcher;
use ty_project::{Db, watch};
use ty_project::{Db, DummyReporter, Reporter, watch};
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_server::run_server;
@@ -44,8 +42,6 @@ pub fn run() -> anyhow::Result<ExitStatus> {
Command::Check(check_args) => run_check(check_args),
Command::Version => version().map(|()| ExitStatus::Success),
Command::GenerateShellCompletion { shell } => {
use std::io::stdout;
shell.generate(&mut Cli::command(), &mut stdout());
Ok(ExitStatus::Success)
}
@@ -53,7 +49,7 @@ pub fn run() -> anyhow::Result<ExitStatus> {
}
pub(crate) fn version() -> Result<()> {
let mut stdout = Printer::default().stream_for_requested_summary().lock();
let mut stdout = BufWriter::new(io::stdout().lock());
let version_info = crate::version::version();
writeln!(stdout, "ty {}", &version_info)?;
Ok(())
@@ -65,8 +61,6 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let verbosity = args.verbosity.level();
let _guard = setup_tracing(verbosity, args.color.unwrap_or_default())?;
let printer = Printer::default().with_verbosity(verbosity);
tracing::warn!(
"ty is pre-release software and not ready for production use. \
Expect to encounter bugs, missing features, and fatal errors.",
@@ -131,8 +125,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
}
let project_options_overrides = ProjectOptionsOverrides::new(config_file, options);
let (main_loop, main_loop_cancellation_token) =
MainLoop::new(project_options_overrides, printer);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(project_options_overrides);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -150,7 +143,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
main_loop.run(&mut db)?
};
let mut stdout = printer.stream_for_requested_summary().lock();
let mut stdout = stdout().lock();
match std::env::var(EnvVars::TY_MEMORY_REPORT).as_deref() {
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
@@ -199,16 +192,12 @@ struct MainLoop {
/// The file system watcher, if running in watch mode.
watcher: Option<ProjectWatcher>,
/// Interface for displaying information to the user.
printer: Printer,
project_options_overrides: ProjectOptionsOverrides,
}
impl MainLoop {
fn new(
project_options_overrides: ProjectOptionsOverrides,
printer: Printer,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
@@ -218,7 +207,6 @@ impl MainLoop {
receiver,
watcher: None,
project_options_overrides,
printer,
},
MainLoopCancellationToken { sender },
)
@@ -235,24 +223,32 @@ impl MainLoop {
// Do not show progress bars with `--watch`, indicatif does not seem to
// handle cancelling independent progress bars very well.
// TODO(zanieb): We can probably use `MultiProgress` to handle this case in the future.
self.printer = self.printer.with_no_progress();
self.run(db)?;
self.run_with_progress::<DummyReporter>(db)?;
Ok(ExitStatus::Success)
}
fn run(self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
self.run_with_progress::<IndicatifReporter>(db)
}
fn run_with_progress<R>(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
let result = self.main_loop(db);
let result = self.main_loop::<R>(db);
tracing::debug!("Exiting main loop");
result
}
fn main_loop(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
fn main_loop<R>(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus>
where
R: Reporter + Default + 'static,
{
// Schedule the first check.
tracing::debug!("Starting main loop");
@@ -268,7 +264,7 @@ impl MainLoop {
// to prevent blocking the main loop here.
rayon::spawn(move || {
match salsa::Cancelled::catch(|| {
let mut reporter = IndicatifReporter::from(self.printer);
let mut reporter = R::default();
db.check_with_reporter(&mut reporter)
}) {
Ok(result) => {
@@ -303,12 +299,10 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
let mut stdout = stdout().lock();
if result.is_empty() {
writeln!(
self.printer.stream_for_success_summary(),
"{}",
"All checks passed!".green().bold()
)?;
writeln!(stdout, "{}", "All checks passed!".green().bold())?;
if self.watcher.is_none() {
return Ok(ExitStatus::Success);
@@ -317,19 +311,14 @@ impl MainLoop {
let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
for diagnostic in result {
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
if stdout.is_enabled() {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
}
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
max_severity = max_severity.max(diagnostic.severity());
}
writeln!(
self.printer.stream_for_failure_summary(),
stdout,
"Found {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
@@ -389,53 +378,27 @@ impl MainLoop {
}
/// A progress reporter for `ty check`.
enum IndicatifReporter {
/// A constructed reporter that is not yet ready, contains the target for the progress bar.
Pending(indicatif::ProgressDrawTarget),
/// A reporter that is ready, containing a progress bar to report to.
///
/// Initialization of the bar is deferred to [`ty_project::ProgressReporter::set_files`] so we
/// do not initialize the bar too early as it may take a while to collect the number of files to
/// process and we don't want to display an empty "0/0" bar.
Initialized(indicatif::ProgressBar),
}
#[derive(Default)]
struct IndicatifReporter(Option<indicatif::ProgressBar>);
impl From<Printer> for IndicatifReporter {
fn from(printer: Printer) -> Self {
Self::Pending(printer.progress_target())
}
}
impl ty_project::ProgressReporter for IndicatifReporter {
impl ty_project::Reporter for IndicatifReporter {
fn set_files(&mut self, files: usize) {
let target = match std::mem::replace(
self,
IndicatifReporter::Pending(indicatif::ProgressDrawTarget::hidden()),
) {
Self::Pending(target) => target,
Self::Initialized(_) => panic!("The progress reporter should only be initialized once"),
};
let bar = indicatif::ProgressBar::with_draw_target(Some(files as u64), target);
bar.set_style(
let progress = indicatif::ProgressBar::new(files as u64);
progress.set_style(
indicatif::ProgressStyle::with_template(
"{msg:8.dim} {bar:60.green/dim} {pos}/{len} files",
)
.unwrap()
.progress_chars("--"),
);
bar.set_message("Checking");
*self = Self::Initialized(bar);
progress.set_message("Checking");
self.0 = Some(progress);
}
fn report_file(&self, _file: &ruff_db::files::File) {
match self {
IndicatifReporter::Initialized(progress_bar) => {
progress_bar.inc(1);
}
IndicatifReporter::Pending(_) => {
panic!("`report_file` called before `set_files`")
}
if let Some(ref progress_bar) = self.0 {
progress_bar.inc(1);
}
}
}

View File

@@ -24,32 +24,15 @@ pub(crate) struct Verbosity {
help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)",
action = clap::ArgAction::Count,
global = true,
overrides_with = "quiet",
)]
verbose: u8,
#[arg(
long,
help = "Use quiet output",
action = clap::ArgAction::Count,
global = true,
overrides_with = "verbose",
)]
quiet: u8,
}
impl Verbosity {
/// Returns the verbosity level based on the number of `-v` and `-q` flags.
/// Returns the verbosity level based on the number of `-v` flags.
///
/// Returns `None` if the user did not specify any verbosity flags.
pub(crate) fn level(&self) -> VerbosityLevel {
// `--quiet` and `--verbose` are mutually exclusive in Clap, so we can just check one first.
match self.quiet {
0 => {}
_ => return VerbosityLevel::Quiet,
// TODO(zanieb): Add support for `-qq` with a "silent" mode
}
match self.verbose {
0 => VerbosityLevel::Default,
1 => VerbosityLevel::Verbose,
@@ -59,14 +42,9 @@ impl Verbosity {
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum VerbosityLevel {
/// Quiet output. Only shows Ruff and ty events up to the [`ERROR`](tracing::Level::ERROR).
/// Silences output except for summary information.
Quiet,
/// Default output level. Only shows Ruff and ty events up to the [`WARN`](tracing::Level::WARN).
#[default]
Default,
/// Enables verbose output. Emits Ruff and ty events up to the [`INFO`](tracing::Level::INFO).
@@ -84,7 +62,6 @@ pub(crate) enum VerbosityLevel {
impl VerbosityLevel {
const fn level_filter(self) -> LevelFilter {
match self {
VerbosityLevel::Quiet => LevelFilter::ERROR,
VerbosityLevel::Default => LevelFilter::WARN,
VerbosityLevel::Verbose => LevelFilter::INFO,
VerbosityLevel::ExtraVerbose => LevelFilter::DEBUG,

View File

@@ -1,172 +0,0 @@
use std::io::StdoutLock;
use indicatif::ProgressDrawTarget;
use crate::logging::VerbosityLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Printer {
verbosity: VerbosityLevel,
no_progress: bool,
}
impl Printer {
#[must_use]
pub(crate) fn with_no_progress(self) -> Self {
Self {
verbosity: self.verbosity,
no_progress: true,
}
}
#[must_use]
pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self {
Self {
verbosity,
no_progress: self.no_progress,
}
}
/// Return the [`ProgressDrawTarget`] for this printer.
pub(crate) fn progress_target(self) -> ProgressDrawTarget {
if self.no_progress {
return ProgressDrawTarget::hidden();
}
match self.verbosity {
VerbosityLevel::Quiet => ProgressDrawTarget::hidden(),
VerbosityLevel::Default => ProgressDrawTarget::stderr(),
// Hide the progress bar when in verbose mode.
// Otherwise, it gets interleaved with log messages.
VerbosityLevel::Verbose => ProgressDrawTarget::hidden(),
VerbosityLevel::ExtraVerbose => ProgressDrawTarget::hidden(),
VerbosityLevel::Trace => ProgressDrawTarget::hidden(),
}
}
/// Return the [`Stdout`] stream for important messages.
///
/// Unlike [`Self::stdout_general`], the returned stream will be enabled when
/// [`VerbosityLevel::Quiet`] is used.
fn stdout_important(self) -> Stdout {
match self.verbosity {
VerbosityLevel::Quiet => Stdout::enabled(),
VerbosityLevel::Default => Stdout::enabled(),
VerbosityLevel::Verbose => Stdout::enabled(),
VerbosityLevel::ExtraVerbose => Stdout::enabled(),
VerbosityLevel::Trace => Stdout::enabled(),
}
}
/// Return the [`Stdout`] stream for general messages.
///
/// The returned stream will be disabled when [`VerbosityLevel::Quiet`] is used.
fn stdout_general(self) -> Stdout {
match self.verbosity {
VerbosityLevel::Quiet => Stdout::disabled(),
VerbosityLevel::Default => Stdout::enabled(),
VerbosityLevel::Verbose => Stdout::enabled(),
VerbosityLevel::ExtraVerbose => Stdout::enabled(),
VerbosityLevel::Trace => Stdout::enabled(),
}
}
/// Return the [`Stdout`] stream for a summary message that was explicitly requested by the
/// user.
///
/// For example, in `ty version` the user has requested the version information and we should
/// display it even if [`VerbosityLevel::Quiet`] is used. Or, in `ty check`, if the
/// `TY_MEMORY_REPORT` variable has been set, we should display the memory report because the
/// user has opted-in to display.
pub(crate) fn stream_for_requested_summary(self) -> Stdout {
self.stdout_important()
}
/// Return the [`Stdout`] stream for a summary message on failure.
///
/// For example, in `ty check`, this would be used for the message indicating the number of
/// diagnostics found. The failure summary should capture information that is not reflected in
/// the exit code.
pub(crate) fn stream_for_failure_summary(self) -> Stdout {
self.stdout_important()
}
/// Return the [`Stdout`] stream for a summary message on success.
///
/// For example, in `ty check`, this would be used for the message indicating that no diagnostic
/// were found. The success summary does not capture important information for users that have
/// opted-in to [`VerbosityLevel::Quiet`].
pub(crate) fn stream_for_success_summary(self) -> Stdout {
self.stdout_general()
}
/// Return the [`Stdout`] stream for detailed messages.
///
/// For example, in `ty check`, this would be used for the diagnostic output.
pub(crate) fn stream_for_details(self) -> Stdout {
self.stdout_general()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StreamStatus {
Enabled,
Disabled,
}
#[derive(Debug)]
pub(crate) struct Stdout {
status: StreamStatus,
lock: Option<StdoutLock<'static>>,
}
impl Stdout {
fn enabled() -> Self {
Self {
status: StreamStatus::Enabled,
lock: None,
}
}
fn disabled() -> Self {
Self {
status: StreamStatus::Disabled,
lock: None,
}
}
pub(crate) fn lock(mut self) -> Self {
match self.status {
StreamStatus::Enabled => {
// Drop the previous lock first, to avoid deadlocking
self.lock.take();
self.lock = Some(std::io::stdout().lock());
}
StreamStatus::Disabled => self.lock = None,
}
self
}
fn handle(&mut self) -> Box<dyn std::io::Write + '_> {
match self.lock.as_mut() {
Some(lock) => Box::new(lock),
None => Box::new(std::io::stdout()),
}
}
pub(crate) fn is_enabled(&self) -> bool {
matches!(self.status, StreamStatus::Enabled)
}
}
impl std::fmt::Write for Stdout {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
match self.status {
StreamStatus::Enabled => {
let _ = write!(self.handle(), "{s}");
Ok(())
}
StreamStatus::Disabled => Ok(()),
}
}
}

View File

@@ -14,64 +14,6 @@ use std::{
};
use tempfile::TempDir;
#[test]
fn test_quiet_output() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "x: int = 1")?;
// By default, we emit an "all checks passed" message
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// With `quiet`, the message is not displayed
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
// By default, we emit a diagnostic
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
--> test.py:1:1
|
1 | x: int = 'foo'
| ^
|
info: rule `invalid-assignment` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
// With `quiet`, the diagnostic is not displayed, just the summary message
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
success: false
exit_code: 1
----- stdout -----
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;

View File

@@ -15,12 +15,9 @@ bitflags = { workspace = true }
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ty_python_semantic = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
smallvec = { workspace = true }

View File

@@ -1,664 +0,0 @@
//! Docstring parsing utilities for language server features.
//!
//! This module provides functionality for extracting structured information from
//! Python docstrings, including parameter documentation for signature help.
//! Supports Google-style, NumPy-style, and reST/Sphinx-style docstrings.
//! There are no formal specifications for any of these formats, so the parsing
//! logic needs to be tolerant of variations.
use regex::Regex;
use ruff_python_trivia::leading_indentation;
use ruff_source_file::UniversalNewlines;
use std::collections::HashMap;
use std::sync::LazyLock;
// Static regex instances to avoid recompilation
static GOOGLE_SECTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^\s*(Args|Arguments|Parameters)\s*:\s*$")
.expect("Google section regex should be valid")
});
static GOOGLE_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:\s*(.+)")
.expect("Google parameter regex should be valid")
});
static NUMPY_SECTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^\s*Parameters\s*$").expect("NumPy section regex should be valid")
});
static NUMPY_UNDERLINE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*-+\s*$").expect("NumPy underline regex should be valid"));
static REST_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\s*:param\s+(?:(\w+)\s+)?(\w+)\s*:\s*(.+)")
.expect("reST parameter regex should be valid")
});
/// Extract parameter documentation from popular docstring formats.
/// Returns a map of parameter names to their documentation.
pub fn get_parameter_documentation(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
// Google-style docstrings
param_docs.extend(extract_google_style_params(docstring));
// NumPy-style docstrings
param_docs.extend(extract_numpy_style_params(docstring));
// reST/Sphinx-style docstrings
param_docs.extend(extract_rest_style_params(docstring));
param_docs
}
/// Extract parameter documentation from Google-style docstrings.
fn extract_google_style_params(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
let mut in_args_section = false;
let mut current_param: Option<String> = None;
let mut current_doc = String::new();
for line_obj in docstring.universal_newlines() {
let line = line_obj.as_str();
if GOOGLE_SECTION_REGEX.is_match(line) {
in_args_section = true;
continue;
}
if in_args_section {
// Check if we hit another section (starts with a word followed by colon at line start)
if !line.starts_with(' ') && !line.starts_with('\t') && line.contains(':') {
if let Some(colon_pos) = line.find(':') {
let section_name = line[..colon_pos].trim();
// If this looks like another section, stop processing args
if !section_name.is_empty()
&& section_name
.chars()
.all(|c| c.is_alphabetic() || c.is_whitespace())
{
// Check if this is a known section name
let known_sections = [
"Returns", "Return", "Raises", "Yields", "Yield", "Examples",
"Example", "Note", "Notes", "Warning", "Warnings",
];
if known_sections.contains(&section_name) {
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_args_section = false;
continue;
}
}
}
}
if let Some(captures) = GOOGLE_PARAM_REGEX.captures(line) {
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Start new parameter
if let (Some(param), Some(desc)) = (captures.get(1), captures.get(3)) {
current_param = Some(param.as_str().to_string());
current_doc = desc.as_str().to_string();
}
} else if line.starts_with(' ') || line.starts_with('\t') {
// This is a continuation of the current parameter documentation
if current_param.is_some() {
if !current_doc.is_empty() {
current_doc.push('\n');
}
current_doc.push_str(line.trim());
}
} else {
// This is a line that doesn't start with whitespace and isn't a parameter
// It might be a section or other content, so stop processing args
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_args_section = false;
}
}
}
// Don't forget the last parameter
if let Some(param_name) = current_param {
param_docs.insert(param_name, current_doc.trim().to_string());
}
param_docs
}
/// Calculate the indentation level of a line (number of leading whitespace characters)
fn get_indentation_level(line: &str) -> usize {
leading_indentation(line).len()
}
/// Extract parameter documentation from NumPy-style docstrings.
fn extract_numpy_style_params(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
let mut lines = docstring
.universal_newlines()
.map(|line| line.as_str())
.peekable();
let mut in_params_section = false;
let mut found_underline = false;
let mut current_param: Option<String> = None;
let mut current_doc = String::new();
let mut base_param_indent: Option<usize> = None;
let mut base_content_indent: Option<usize> = None;
while let Some(line) = lines.next() {
if NUMPY_SECTION_REGEX.is_match(line) {
// Check if the next line is an underline
if let Some(next_line) = lines.peek() {
if NUMPY_UNDERLINE_REGEX.is_match(next_line) {
in_params_section = true;
found_underline = false;
base_param_indent = None;
base_content_indent = None;
continue;
}
}
}
if in_params_section && !found_underline {
if NUMPY_UNDERLINE_REGEX.is_match(line) {
found_underline = true;
continue;
}
}
if in_params_section && found_underline {
let current_indent = get_indentation_level(line);
let trimmed = line.trim();
// Skip empty lines
if trimmed.is_empty() {
continue;
}
// Check if we hit another section
if current_indent == 0 {
if let Some(next_line) = lines.peek() {
if NUMPY_UNDERLINE_REGEX.is_match(next_line) {
// This is another section
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_params_section = false;
continue;
}
}
}
// Determine if this could be a parameter line
let could_be_param = if let Some(base_indent) = base_param_indent {
// We've seen parameters before - check if this matches the expected parameter indentation
current_indent == base_indent
} else {
// First potential parameter - check if it has reasonable indentation and content
current_indent > 0
&& (trimmed.contains(':')
|| trimmed.chars().all(|c| c.is_alphanumeric() || c == '_'))
};
if could_be_param {
// Check if this could be a section header by looking at the next line
if let Some(next_line) = lines.peek() {
if NUMPY_UNDERLINE_REGEX.is_match(next_line) {
// This is a section header, not a parameter
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_params_section = false;
continue;
}
}
// Set base indentation levels on first parameter
if base_param_indent.is_none() {
base_param_indent = Some(current_indent);
}
// Handle parameter with type annotation (param : type)
if trimmed.contains(':') {
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Extract parameter name and description
let parts: Vec<&str> = trimmed.splitn(2, ':').collect();
if parts.len() == 2 {
let param_name = parts[0].trim();
// Extract just the parameter name (before any type info)
let param_name = param_name.split_whitespace().next().unwrap_or(param_name);
current_param = Some(param_name.to_string());
current_doc.clear(); // Description comes on following lines, not on this line
}
} else {
// Handle parameter without type annotation
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// This line is the parameter name
current_param = Some(trimmed.to_string());
current_doc.clear();
}
} else if current_param.is_some() {
// Determine if this is content for the current parameter
let is_content = if let Some(base_content) = base_content_indent {
// We've seen content before - check if this matches expected content indentation
current_indent >= base_content
} else {
// First potential content line - should be more indented than parameter
if let Some(base_param) = base_param_indent {
current_indent > base_param
} else {
// Fallback: any indented content
current_indent > 0
}
};
if is_content {
// Set base content indentation on first content line
if base_content_indent.is_none() {
base_content_indent = Some(current_indent);
}
// This is a continuation of the current parameter documentation
if !current_doc.is_empty() {
current_doc.push('\n');
}
current_doc.push_str(trimmed);
} else {
// This line doesn't match our expected indentation patterns
// Save current parameter and stop processing
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
in_params_section = false;
}
}
}
}
// Don't forget the last parameter
if let Some(param_name) = current_param {
param_docs.insert(param_name, current_doc.trim().to_string());
}
param_docs
}
/// Extract parameter documentation from reST/Sphinx-style docstrings.
fn extract_rest_style_params(docstring: &str) -> HashMap<String, String> {
let mut param_docs = HashMap::new();
let mut current_param: Option<String> = None;
let mut current_doc = String::new();
for line_obj in docstring.universal_newlines() {
let line = line_obj.as_str();
if let Some(captures) = REST_PARAM_REGEX.captures(line) {
// Save previous parameter if exists
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Extract parameter name and description
if let (Some(param_match), Some(desc_match)) = (captures.get(2), captures.get(3)) {
current_param = Some(param_match.as_str().to_string());
current_doc = desc_match.as_str().to_string();
}
} else if current_param.is_some() {
let trimmed = line.trim();
// Check if this is a new section - stop processing if we hit section headers
if trimmed == "Parameters" || trimmed == "Args" || trimmed == "Arguments" {
// Save current param and stop processing
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
break;
}
// Check if this is another directive line starting with ':'
if trimmed.starts_with(':') {
// This is a new directive, save current param
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
// Let the next iteration handle this directive
continue;
}
// Check if this is a continuation line (indented)
if line.starts_with(" ") && !trimmed.is_empty() {
// This is a continuation line
if !current_doc.is_empty() {
current_doc.push('\n');
}
current_doc.push_str(trimmed);
} else if !trimmed.is_empty() && !line.starts_with(' ') && !line.starts_with('\t') {
// This is a non-indented line - likely end of the current parameter
if let Some(param_name) = current_param.take() {
param_docs.insert(param_name, current_doc.trim().to_string());
current_doc.clear();
}
break;
}
}
}
// Don't forget the last parameter
if let Some(param_name) = current_param {
param_docs.insert(param_name, current_doc.trim().to_string());
}
param_docs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_google_style_parameter_documentation() {
let docstring = r#"
This is a function description.
Args:
param1 (str): The first parameter description
param2 (int): The second parameter description
This is a continuation of param2 description.
param3: A parameter without type annotation
Returns:
str: The return value description
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(&param_docs["param1"], "The first parameter description");
assert_eq!(
&param_docs["param2"],
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(&param_docs["param3"], "A parameter without type annotation");
}
#[test]
fn test_numpy_style_parameter_documentation() {
let docstring = r#"
This is a function description.
Parameters
----------
param1 : str
The first parameter description
param2 : int
The second parameter description
This is a continuation of param2 description.
param3
A parameter without type annotation
Returns
-------
str
The return value description
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_no_parameter_documentation() {
let docstring = r#"
This is a simple function description without parameter documentation.
"#;
let param_docs = get_parameter_documentation(docstring);
assert!(param_docs.is_empty());
}
#[test]
fn test_mixed_style_parameter_documentation() {
let docstring = r#"
This is a function description.
Args:
param1 (str): Google-style parameter
param2 (int): Another Google-style parameter
Parameters
----------
param3 : bool
NumPy-style parameter
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"Google-style parameter"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"Another Google-style parameter"
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"NumPy-style parameter"
);
}
#[test]
fn test_rest_style_parameter_documentation() {
let docstring = r#"
This is a function description.
:param str param1: The first parameter description
:param int param2: The second parameter description
This is a continuation of param2 description.
:param param3: A parameter without type annotation
:returns: The return value description
:rtype: str
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_mixed_style_with_rest_parameter_documentation() {
let docstring = r#"
This is a function description.
Args:
param1 (str): Google-style parameter
:param int param2: reST-style parameter
:param param3: Another reST-style parameter
Parameters
----------
param4 : bool
NumPy-style parameter
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 4);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"Google-style parameter"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"reST-style parameter"
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"Another reST-style parameter"
);
assert_eq!(
param_docs.get("param4").expect("param4 should exist"),
"NumPy-style parameter"
);
}
#[test]
fn test_numpy_style_with_different_indentation() {
let docstring = r#"
This is a function description.
Parameters
----------
param1 : str
The first parameter description
param2 : int
The second parameter description
This is a continuation of param2 description.
param3
A parameter without type annotation
Returns
-------
str
The return value description
"#;
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_numpy_style_with_tabs_and_mixed_indentation() {
// Using raw strings to avoid tab/space conversion issues in the test
let docstring = "
This is a function description.
Parameters
----------
\tparam1 : str
\t\tThe first parameter description
\tparam2 : int
\t\tThe second parameter description
\t\tThis is a continuation of param2 description.
\tparam3
\t\tA parameter without type annotation
";
let param_docs = get_parameter_documentation(docstring);
assert_eq!(param_docs.len(), 3);
assert_eq!(
param_docs.get("param1").expect("param1 should exist"),
"The first parameter description"
);
assert_eq!(
param_docs.get("param2").expect("param2 should exist"),
"The second parameter description\nThis is a continuation of param2 description."
);
assert_eq!(
param_docs.get("param3").expect("param3 should exist"),
"A parameter without type annotation"
);
}
#[test]
fn test_universal_newlines() {
// Test with Windows-style line endings (\r\n)
let docstring_windows = "This is a function description.\r\n\r\nArgs:\r\n param1 (str): The first parameter\r\n param2 (int): The second parameter\r\n";
// Test with old Mac-style line endings (\r)
let docstring_mac = "This is a function description.\r\rArgs:\r param1 (str): The first parameter\r param2 (int): The second parameter\r";
// Test with Unix-style line endings (\n) - should work the same
let docstring_unix = "This is a function description.\n\nArgs:\n param1 (str): The first parameter\n param2 (int): The second parameter\n";
let param_docs_windows = get_parameter_documentation(docstring_windows);
let param_docs_mac = get_parameter_documentation(docstring_mac);
let param_docs_unix = get_parameter_documentation(docstring_unix);
// All should produce the same results
assert_eq!(param_docs_windows.len(), 2);
assert_eq!(param_docs_mac.len(), 2);
assert_eq!(param_docs_unix.len(), 2);
assert_eq!(
param_docs_windows.get("param1"),
Some(&"The first parameter".to_string())
);
assert_eq!(
param_docs_mac.get("param1"),
Some(&"The first parameter".to_string())
);
assert_eq!(
param_docs_unix.get("param1"),
Some(&"The first parameter".to_string())
);
}
}

View File

@@ -1,17 +1,14 @@
mod completion;
mod db;
mod docstring;
mod find_node;
mod goto;
mod hover;
mod inlay_hints;
mod markup;
mod semantic_tokens;
mod signature_help;
pub use completion::completion;
pub use db::Db;
pub use docstring::get_parameter_documentation;
pub use goto::goto_type_definition;
pub use hover::hover;
pub use inlay_hints::inlay_hints;
@@ -19,7 +16,6 @@ pub use markup::MarkupKind;
pub use semantic_tokens::{
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
};
pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, signature_help};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, TextRange};

View File

@@ -1,687 +0,0 @@
//! This module handles the "signature help" request in the language server
//! protocol. This request is typically issued by a client when the user types
//! an open parenthesis and starts to enter arguments for a function call.
//! The signature help provides information that the editor displays to the
//! user about the target function signature including parameter names,
//! types, and documentation. It supports multiple signatures for union types
//! and overloads.
use crate::{Db, docstring::get_parameter_documentation, find_node::covering_node};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::{CallSignatureDetails, call_signature_details};
// Limitations of the current implementation:
// TODO - If the target function is declared in a stub file but defined (implemented)
// in a source file, the documentation will not reflect the a docstring that appears
// only in the implementation. To do this, we'll need to map the function or
// method in the stub to the implementation and extract the docstring from there.
/// Information about a function parameter
#[derive(Debug, Clone)]
pub struct ParameterDetails {
/// The parameter name (e.g., "param1")
pub name: String,
/// The parameter label in the signature (e.g., "param1: str")
pub label: String,
/// Documentation specific to the parameter, typically extracted from the
/// function's docstring
pub documentation: Option<String>,
}
/// Information about a function signature
#[derive(Debug, Clone)]
pub struct SignatureDetails {
/// Text representation of the full signature (including input parameters and return type).
pub label: String,
/// Documentation for the signature, typically from the function's docstring.
pub documentation: Option<String>,
/// Information about each of the parameters in left-to-right order.
pub parameters: Vec<ParameterDetails>,
/// Index of the parameter that corresponds to the argument where the
/// user's cursor is currently positioned.
pub active_parameter: Option<usize>,
}
/// Signature help information for function calls
#[derive(Debug, Clone)]
pub struct SignatureHelpInfo {
/// Information about each of the signatures for the function call. We
/// need to handle multiple because of unions, overloads, and composite
/// calls like constructors (which invoke both __new__ and __init__).
pub signatures: Vec<SignatureDetails>,
/// Index of the "active signature" which is the first signature where
/// all arguments that are currently present in the code map to parameters.
pub active_signature: Option<usize>,
}
/// Signature help information for function calls at the given position
pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<SignatureHelpInfo> {
let parsed = parsed_module(db, file).load(db);
// Get the call expression at the given position.
let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?;
// Get signature details from the semantic analyzer.
let signature_details: Vec<CallSignatureDetails<'_>> =
call_signature_details(db, file, call_expr);
if signature_details.is_empty() {
return None;
}
// Find the active signature - the first signature where all arguments map to parameters.
let active_signature_index = find_active_signature_from_details(&signature_details);
// Convert to SignatureDetails objects.
let signatures: Vec<SignatureDetails> = signature_details
.into_iter()
.map(|details| {
create_signature_details_from_call_signature_details(db, &details, current_arg_index)
})
.collect();
Some(SignatureHelpInfo {
signatures,
active_signature: active_signature_index,
})
}
/// Returns the innermost call expression that contains the specified offset
/// and the index of the argument that the offset maps to.
fn get_call_expr(
parsed: &ruff_db::parsed::ParsedModuleRef,
offset: TextSize,
) -> Option<(&ast::ExprCall, usize)> {
// Create a range from the offset for the covering_node function.
let range = TextRange::new(offset, offset);
// Find the covering node at the given position that is a function call.
let covering_node = covering_node(parsed.syntax().into(), range)
.find_first(|node| matches!(node, AnyNodeRef::ExprCall(_)))
.ok()?;
// Get the function call expression.
let AnyNodeRef::ExprCall(call_expr) = covering_node.node() else {
return None;
};
// Determine which argument corresponding to the current cursor location.
let current_arg_index = get_argument_index(call_expr, offset);
Some((call_expr, current_arg_index))
}
/// Determine which argument is associated with the specified offset.
/// Returns zero if not within any argument.
fn get_argument_index(call_expr: &ast::ExprCall, offset: TextSize) -> usize {
let mut current_arg = 0;
for (i, arg) in call_expr.arguments.arguments_source_order().enumerate() {
if offset <= arg.end() {
return i;
}
current_arg = i + 1;
}
current_arg
}
/// Create signature details from `CallSignatureDetails`.
fn create_signature_details_from_call_signature_details(
db: &dyn crate::Db,
details: &CallSignatureDetails,
current_arg_index: usize,
) -> SignatureDetails {
let signature_label = details.label.clone();
let documentation = get_callable_documentation(db, details.definition);
// Translate the argument index to parameter index using the mapping.
let active_parameter =
if details.argument_to_parameter_mapping.is_empty() && current_arg_index == 0 {
Some(0)
} else {
details
.argument_to_parameter_mapping
.get(current_arg_index)
.and_then(|&param_index| param_index)
.or({
// If we can't find a mapping for this argument, but we have a current
// argument index, use that as the active parameter if it's within bounds.
if current_arg_index < details.parameter_label_offsets.len() {
Some(current_arg_index)
} else {
None
}
})
};
SignatureDetails {
label: signature_label.clone(),
documentation: Some(documentation),
parameters: create_parameters_from_offsets(
&details.parameter_label_offsets,
&signature_label,
db,
details.definition,
&details.parameter_names,
),
active_parameter,
}
}
/// Determine appropriate documentation for a callable type based on its original type.
fn get_callable_documentation(db: &dyn crate::Db, definition: Option<Definition>) -> String {
// TODO: If the definition is located within a stub file and no docstring
// is present, try to map the symbol to an implementation file and extract
// the docstring from that location.
if let Some(definition) = definition {
definition.docstring(db).unwrap_or_default()
} else {
String::new()
}
}
/// Create `ParameterDetails` objects from parameter label offsets.
fn create_parameters_from_offsets(
parameter_offsets: &[TextRange],
signature_label: &str,
db: &dyn crate::Db,
definition: Option<Definition>,
parameter_names: &[String],
) -> Vec<ParameterDetails> {
// Extract parameter documentation from the function's docstring if available.
let param_docs = if let Some(definition) = definition {
let docstring = definition.docstring(db);
docstring
.map(|doc| get_parameter_documentation(&doc))
.unwrap_or_default()
} else {
std::collections::HashMap::new()
};
parameter_offsets
.iter()
.enumerate()
.map(|(i, offset)| {
// Extract the parameter label from the signature string.
let start = usize::from(offset.start());
let end = usize::from(offset.end());
let label = signature_label
.get(start..end)
.unwrap_or("unknown")
.to_string();
// Get the parameter name for documentation lookup.
let param_name = parameter_names.get(i).map(String::as_str).unwrap_or("");
ParameterDetails {
name: param_name.to_string(),
label,
documentation: param_docs.get(param_name).cloned(),
}
})
.collect()
}
/// Find the active signature index from `CallSignatureDetails`.
/// The active signature is the first signature where all arguments present in the call
/// have valid mappings to parameters (i.e., none of the mappings are None).
fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]) -> Option<usize> {
let first = signature_details.first()?;
// If there are no arguments in the mapping, just return the first signature.
if first.argument_to_parameter_mapping.is_empty() {
return Some(0);
}
// First, try to find a signature where all arguments have valid parameter mappings.
let perfect_match = signature_details.iter().position(|details| {
// Check if all arguments have valid parameter mappings (i.e., are not None).
details
.argument_to_parameter_mapping
.iter()
.all(Option::is_some)
});
if let Some(index) = perfect_match {
return Some(index);
}
// If no perfect match, find the signature with the most valid argument mappings.
let (best_index, _) = signature_details
.iter()
.enumerate()
.max_by_key(|(_, details)| {
details
.argument_to_parameter_mapping
.iter()
.filter(|mapping| mapping.is_some())
.count()
})?;
Some(best_index)
}
#[cfg(test)]
mod tests {
use crate::signature_help::SignatureHelpInfo;
use crate::tests::{CursorTest, cursor_test};
#[test]
fn signature_help_basic_function_call() {
let test = cursor_test(
r#"
def example_function(param1: str, param2: int) -> str:
"""This is a docstring for the example function.
Args:
param1: The first parameter as a string
param2: The second parameter as an integer
Returns:
A formatted string combining both parameters
"""
return f"{param1}: {param2}"
result = example_function(<CURSOR>
"#,
);
// Test that signature help is provided
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("param1") && signature.label.contains("param2"));
// Verify that the docstring is extracted and included in the documentation
let expected_docstring = concat!(
"This is a docstring for the example function.\n",
" \n",
" Args:\n",
" param1: The first parameter as a string\n",
" param2: The second parameter as an integer\n",
" \n",
" Returns:\n",
" A formatted string combining both parameters\n",
" "
);
assert_eq!(
signature.documentation,
Some(expected_docstring.to_string())
);
assert_eq!(result.active_signature, Some(0));
assert_eq!(signature.active_parameter, Some(0));
}
#[test]
fn signature_help_method_call() {
let test = cursor_test(
r#"
class MyClass:
def my_method(self, arg1: str, arg2: bool) -> None:
pass
obj = MyClass()
obj.my_method(arg2=True, arg1=<CURSOR>
"#,
);
// Test that signature help is provided for method calls
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("arg1") && signature.label.contains("arg2"));
assert_eq!(result.active_signature, Some(0));
// Check the active parameter from the active signature
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(0));
}
}
#[test]
fn signature_help_nested_function_calls() {
let test = cursor_test(
r#"
def outer(a: int) -> int:
return a * 2
def inner(b: str) -> str:
return b.upper()
result = outer(inner(<CURSOR>
"#,
);
// Test that signature help focuses on the innermost function call
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("str") || signature.label.contains("->"));
assert_eq!(result.active_signature, Some(0));
assert_eq!(signature.active_parameter, Some(0));
}
#[test]
fn signature_help_union_callable() {
let test = cursor_test(
r#"
import random
def func_a(x: int) -> int:
return x
def func_b(y: str) -> str:
return y
if random.random() > 0.5:
f = func_a
else:
f = func_b
f(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 2);
let signature = &result.signatures[0];
assert_eq!(signature.label, "(x: int) -> int");
assert_eq!(signature.parameters.len(), 1);
// Check parameter information
let param = &signature.parameters[0];
assert_eq!(param.label, "x: int");
assert_eq!(param.name, "x");
// Validate the second signature (from func_b)
let signature_b = &result.signatures[1];
assert_eq!(signature_b.label, "(y: str) -> str");
assert_eq!(signature_b.parameters.len(), 1);
// Check parameter information for the second signature
let param_b = &signature_b.parameters[0];
assert_eq!(param_b.label, "y: str");
assert_eq!(param_b.name, "y");
assert_eq!(result.active_signature, Some(0));
// Check the active parameter from the active signature
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(0));
}
}
#[test]
fn signature_help_overloaded_function() {
let test = cursor_test(
r#"
from typing import overload
@overload
def process(value: int) -> str: ...
@overload
def process(value: str) -> int: ...
def process(value):
if isinstance(value, int):
return str(value)
else:
return len(value)
result = process(<CURSOR>
"#,
);
// Test that signature help is provided for overloaded functions
let result = test.signature_help().expect("Should have signature help");
// We should have signatures for the overloads
assert_eq!(result.signatures.len(), 2);
assert_eq!(result.active_signature, Some(0));
// Check the active parameter from the active signature
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(0));
}
// Validate the first overload: process(value: int) -> str
let signature1 = &result.signatures[0];
assert_eq!(signature1.label, "(value: int) -> str");
assert_eq!(signature1.parameters.len(), 1);
let param1 = &signature1.parameters[0];
assert_eq!(param1.label, "value: int");
assert_eq!(param1.name, "value");
// Validate the second overload: process(value: str) -> int
let signature2 = &result.signatures[1];
assert_eq!(signature2.label, "(value: str) -> int");
assert_eq!(signature2.parameters.len(), 1);
let param2 = &signature2.parameters[0];
assert_eq!(param2.label, "value: str");
assert_eq!(param2.name, "value");
}
#[test]
fn signature_help_class_constructor() {
let test = cursor_test(
r#"
class Point:
"""A simple point class representing a 2D coordinate."""
def __init__(self, x: int, y: int):
"""Initialize a point with x and y coordinates.
Args:
x: The x-coordinate
y: The y-coordinate
"""
self.x = x
self.y = y
point = Point(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have exactly one signature for the constructor
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
// Validate the constructor signature
assert_eq!(signature.label, "(x: int, y: int) -> Point");
assert_eq!(signature.parameters.len(), 2);
// Validate the first parameter (x: int)
let param_x = &signature.parameters[0];
assert_eq!(param_x.label, "x: int");
assert_eq!(param_x.name, "x");
assert_eq!(param_x.documentation, Some("The x-coordinate".to_string()));
// Validate the second parameter (y: int)
let param_y = &signature.parameters[1];
assert_eq!(param_y.label, "y: int");
assert_eq!(param_y.name, "y");
assert_eq!(param_y.documentation, Some("The y-coordinate".to_string()));
// Should have the __init__ method docstring as documentation (not the class docstring)
let expected_docstring = "Initialize a point with x and y coordinates.\n \n Args:\n x: The x-coordinate\n y: The y-coordinate\n ";
assert_eq!(
signature.documentation,
Some(expected_docstring.to_string())
);
}
#[test]
fn signature_help_callable_object() {
let test = cursor_test(
r#"
class Multiplier:
def __call__(self, x: int) -> int:
return x * 2
multiplier = Multiplier()
result = multiplier(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have a signature for the callable object
assert!(!result.signatures.is_empty());
let signature = &result.signatures[0];
// Should provide signature help for the callable
assert!(signature.label.contains("int") || signature.label.contains("->"));
}
#[test]
fn signature_help_subclass_of_constructor() {
let test = cursor_test(
r#"
from typing import Type
def create_instance(cls: Type[list]) -> list:
return cls(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have a signature
assert!(!result.signatures.is_empty());
let signature = &result.signatures[0];
// Should have empty documentation for now
assert_eq!(signature.documentation, Some(String::new()));
}
#[test]
fn signature_help_parameter_label_offsets() {
let test = cursor_test(
r#"
def test_function(param1: str, param2: int, param3: bool) -> str:
return f"{param1}: {param2}, {param3}"
result = test_function(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert_eq!(signature.parameters.len(), 3);
// Check that we have parameter labels
for (i, param) in signature.parameters.iter().enumerate() {
let expected_param_spec = match i {
0 => "param1: str",
1 => "param2: int",
2 => "param3: bool",
_ => panic!("Unexpected parameter index"),
};
assert_eq!(param.label, expected_param_spec);
}
}
#[test]
fn signature_help_active_signature_selection() {
// This test verifies that the algorithm correctly selects the first signature
// where all arguments present in the call have valid parameter mappings.
let test = cursor_test(
r#"
from typing import overload
@overload
def process(value: int) -> str: ...
@overload
def process(value: str, flag: bool) -> int: ...
def process(value, flag=None):
if isinstance(value, int):
return str(value)
elif flag is not None:
return len(value) if flag else 0
else:
return len(value)
# Call with two arguments - should select the second overload
result = process("hello", True<CURSOR>)
"#,
);
let result = test.signature_help().expect("Should have signature help");
// Should have signatures for the overloads.
assert!(!result.signatures.is_empty());
// Check that we have an active signature and parameter
if let Some(active_sig_index) = result.active_signature {
let active_signature = &result.signatures[active_sig_index];
assert_eq!(active_signature.active_parameter, Some(1));
}
}
#[test]
fn signature_help_parameter_documentation() {
let test = cursor_test(
r#"
def documented_function(param1: str, param2: int) -> str:
"""This is a function with parameter documentation.
Args:
param1: The first parameter description
param2: The second parameter description
"""
return f"{param1}: {param2}"
result = documented_function(<CURSOR>
"#,
);
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert_eq!(signature.parameters.len(), 2);
// Check that parameter documentation is extracted
let param1 = &signature.parameters[0];
assert_eq!(
param1.documentation,
Some("The first parameter description".to_string())
);
let param2 = &signature.parameters[1];
assert_eq!(
param2.documentation,
Some("The second parameter description".to_string())
);
}
impl CursorTest {
fn signature_help(&self) -> Option<SignatureHelpInfo> {
crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset)
}
}
}

View File

@@ -5,7 +5,7 @@ use std::{cmp, fmt};
use crate::metadata::settings::file_settings;
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
use crate::{ProgressReporter, Project, ProjectMetadata};
use crate::{Project, ProjectMetadata, Reporter};
use ruff_db::Db as SourceDb;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
@@ -87,7 +87,7 @@ impl ProjectDatabase {
}
/// Checks all open files in the project and its dependencies, using the given reporter.
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
pub fn check_with_reporter(&self, reporter: &mut dyn Reporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, CheckMode::OpenFiles, reporter)
}
@@ -95,7 +95,7 @@ impl ProjectDatabase {
/// Check the project with the given mode.
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter);
self.project().check(self, mode, reporter)
}

View File

@@ -113,7 +113,7 @@ pub struct Project {
}
/// A progress reporter.
pub trait ProgressReporter: Send + Sync {
pub trait Reporter: Send + Sync {
/// Initialize the reporter with the number of files.
fn set_files(&mut self, files: usize);
@@ -121,11 +121,11 @@ pub trait ProgressReporter: Send + Sync {
fn report_file(&self, file: &File);
}
/// A no-op implementation of [`ProgressReporter`].
/// A no-op implementation of [`Reporter`].
#[derive(Default)]
pub struct DummyReporter;
impl ProgressReporter for DummyReporter {
impl Reporter for DummyReporter {
fn set_files(&mut self, _files: usize) {}
fn report_file(&self, _file: &File) {}
}
@@ -212,7 +212,7 @@ impl Project {
self,
db: &ProjectDatabase,
mode: CheckMode,
mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>,
mut reporter: AssertUnwindSafe<&mut dyn Reporter>,
) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check");
let _span = project_span.enter();
@@ -257,11 +257,8 @@ impl Project {
tracing::debug_span!(parent: project_span, "check_file", ?file);
let _entered = check_file_span.entered();
let result = check_file_impl(&db, file);
file_diagnostics
.lock()
.unwrap()
.extend(result.iter().map(Clone::clone));
let result = self.check_file_impl(&db, file);
file_diagnostics.lock().unwrap().extend(result);
reporter.report_file(&file);
});
@@ -288,7 +285,7 @@ impl Project {
return Vec::new();
}
check_file_impl(db, file).iter().map(Clone::clone).collect()
self.check_file_impl(db, file)
}
/// Opens a file in the project.
@@ -469,73 +466,71 @@ impl Project {
self.set_file_set(db).to(IndexedFiles::lazy());
}
}
}
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
fn check_file_impl(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
// Abort checking if there are IO errors.
let source = source_text(db, file);
// Abort checking if there are IO errors.
let source = source_text(db, file);
if let Some(read_error) = source.read_error() {
diagnostics.push(
IOErrorDiagnostic {
file: Some(file),
error: read_error.clone().into(),
}
.to_diagnostic(),
);
return diagnostics.into_boxed_slice();
}
let parsed = parsed_module(db, file);
let parsed_ref = parsed.load(db);
diagnostics.extend(
parsed_ref
.errors()
.iter()
.map(|error| Diagnostic::invalid_syntax(file, &error.error, error)),
);
diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| {
let mut error = Diagnostic::invalid_syntax(file, error, error);
add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax");
error
}));
{
let db = AssertUnwindSafe(db);
match catch(&**db, file, || check_types(*db, file)) {
Ok(Some(type_check_diagnostics)) => {
diagnostics.extend(type_check_diagnostics);
}
Ok(None) => {}
Err(diagnostic) => diagnostics.push(diagnostic),
if let Some(read_error) = source.read_error() {
diagnostics.push(
IOErrorDiagnostic {
file: Some(file),
error: read_error.clone().into(),
}
.to_diagnostic(),
);
return diagnostics;
}
let parsed = parsed_module(db, file);
let parsed_ref = parsed.load(db);
diagnostics.extend(
parsed_ref
.errors()
.iter()
.map(|error| Diagnostic::invalid_syntax(file, &error.error, error)),
);
diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| {
let mut error = Diagnostic::invalid_syntax(file, error, error);
add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax");
error
}));
{
let db = AssertUnwindSafe(db);
match catch(&**db, file, || check_types(*db, file)) {
Ok(Some(type_check_diagnostics)) => {
diagnostics.extend(type_check_diagnostics.into_iter().cloned());
}
Ok(None) => {}
Err(diagnostic) => diagnostics.push(diagnostic),
}
}
if self
.open_fileset(db)
.is_none_or(|files| !files.contains(&file))
{
// Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed.
parsed.clear();
}
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.primary_span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics
}
if db
.project()
.open_fileset(db)
.is_none_or(|files| !files.contains(&file))
{
// Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed.
parsed.clear();
}
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.primary_span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics.into_boxed_slice()
}
#[derive(Debug)]
@@ -706,8 +701,8 @@ where
#[cfg(test)]
mod tests {
use crate::Db;
use crate::ProjectMetadata;
use crate::check_file_impl;
use crate::db::tests::TestDb;
use ruff_db::Db as _;
use ruff_db::files::system_path_to_file;
@@ -746,8 +741,9 @@ mod tests {
assert_eq!(source_text(&db, file).as_str(), "");
assert_eq!(
check_file_impl(&db, file)
.iter()
db.project()
.check_file_impl(&db, file)
.into_iter()
.map(|diagnostic| diagnostic.primary_message().to_string())
.collect::<Vec<_>>(),
vec!["Failed to read file: No such file or directory".to_string()]
@@ -762,8 +758,9 @@ mod tests {
assert_eq!(source_text(&db, file).as_str(), "");
assert_eq!(
check_file_impl(&db, file)
.iter()
db.project()
.check_file_impl(&db, file)
.into_iter()
.map(|diagnostic| diagnostic.primary_message().to_string())
.collect::<Vec<_>>(),
vec![] as Vec<String>

View File

@@ -1,7 +1,7 @@
use std::ops::Deref;
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange};
@@ -57,45 +57,6 @@ impl<'db> Definition<'db> {
pub fn focus_range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> FileRange {
FileRange::new(self.file(db), self.kind(db).target_range(module))
}
/// Extract a docstring from this definition, if applicable.
/// This method returns a docstring for function and class definitions.
/// The docstring is extracted from the first statement in the body if it's a string literal.
pub fn docstring(self, db: &'db dyn Db) -> Option<String> {
let file = self.file(db);
let module = parsed_module(db, file).load(db);
let kind = self.kind(db);
match kind {
DefinitionKind::Function(function_def) => {
let function_node = function_def.node(&module);
docstring_from_body(&function_node.body)
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
}
DefinitionKind::Class(class_def) => {
let class_node = class_def.node(&module);
docstring_from_body(&class_node.body)
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
}
_ => None,
}
}
}
/// Extract a docstring from a function or class body.
fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
let stmt = body.first()?;
// Require the docstring to be a standalone expression.
let ast::Stmt::Expr(ast::StmtExpr {
value,
range: _,
node_index: _,
}) = stmt
else {
return None;
};
// Only match string literals.
value.as_string_literal_expr()
}
/// One or more [`Definition`]s.

View File

@@ -1,5 +1,5 @@
use infer::nearest_enclosing_class;
use itertools::{Either, Itertools};
use itertools::Either;
use ruff_db::parsed::parsed_module;
use std::slice::Iter;
@@ -46,9 +46,7 @@ use crate::types::generics::{
GenericContext, PartialSpecialization, Specialization, walk_generic_context,
walk_partial_specialization, walk_specialization,
};
pub use crate::types::ide_support::{
CallSignatureDetails, all_members, call_signature_details, definition_kind_for_name,
};
pub use crate::types::ide_support::{all_members, definition_kind_for_name};
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
@@ -90,7 +88,8 @@ mod definition;
#[cfg(test)]
mod property_tests;
pub fn check_types(db: &dyn Db, file: File) -> Vec<Diagnostic> {
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
let _span = tracing::trace_span!("check_types", ?file).entered();
tracing::debug!("Checking file '{path}'", path = file.path(db));
@@ -112,7 +111,7 @@ pub fn check_types(db: &dyn Db, file: File) -> Vec<Diagnostic> {
check_suppressions(db, file, &mut diagnostics);
diagnostics.into_vec()
diagnostics
}
/// Infer the type of a binding.
@@ -7536,23 +7535,6 @@ impl<'db> ModuleLiteralType<'db> {
self._importing_file(db)
}
fn available_submodule_attributes(&self, db: &'db dyn Db) -> impl Iterator<Item = Name> {
self.importing_file(db)
.into_iter()
.flat_map(|file| imported_modules(db, file))
.filter_map(|submodule_name| submodule_name.relative_to(self.module(db).name()))
.filter_map(|relative_submodule| relative_submodule.components().next().map(Name::from))
}
fn resolve_submodule(self, db: &'db dyn Db, name: &str) -> Option<Type<'db>> {
let importing_file = self.importing_file(db)?;
let relative_submodule_name = ModuleName::new(name)?;
let mut absolute_submodule_name = self.module(db).name().clone();
absolute_submodule_name.extend(&relative_submodule_name);
let submodule = resolve_module(db, &absolute_submodule_name)?;
Some(Type::module_literal(db, importing_file, &submodule))
}
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
// `__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`,
@@ -7572,9 +7554,17 @@ impl<'db> ModuleLiteralType<'db> {
// 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, but is the opposite of mypy's current behavior.)
if self.available_submodule_attributes(db).contains(name) {
if let Some(submodule) = self.resolve_submodule(db, name) {
return Place::bound(submodule).into();
if let Some(importing_file) = self.importing_file(db) {
if let Some(submodule_name) = ModuleName::new(name) {
let imported_submodules = imported_modules(db, importing_file);
let mut full_submodule_name = self.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) {
return Place::bound(Type::module_literal(db, importing_file, &submodule))
.into();
}
}
}
}

View File

@@ -3,7 +3,7 @@ use super::{Signature, Type};
use crate::Db;
mod arguments;
pub(crate) mod bind;
mod bind;
pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments};
pub(super) use bind::{Binding, Bindings, CallableBinding};

View File

@@ -2,7 +2,6 @@ use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
use itertools::{Either, Itertools};
use ruff_python_ast as ast;
use crate::Db;
use crate::types::KnownClass;
@@ -15,26 +14,6 @@ use super::Type;
pub(crate) struct CallArguments<'a>(Vec<Argument<'a>>);
impl<'a> CallArguments<'a> {
/// Create `CallArguments` from AST arguments
pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self {
arguments
.arguments_source_order()
.map(|arg_or_keyword| match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
_ => Argument::Positional,
},
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
if let Some(arg) = arg {
Argument::Keyword(&arg.id)
} else {
Argument::Keywords
}
}
})
.collect()
}
/// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front
/// of this argument list. (If `bound_self` is none, we return the argument list
/// unmodified.)

View File

@@ -2109,7 +2109,7 @@ impl<'db> Binding<'db> {
}
}
pub(crate) fn match_parameters(
fn match_parameters(
&mut self,
arguments: &CallArguments<'_>,
argument_forms: &mut [Option<ParameterForm>],
@@ -2267,12 +2267,6 @@ impl<'db> Binding<'db> {
self.parameter_tys = parameter_tys;
self.errors = errors;
}
/// Returns a vector where each index corresponds to an argument position,
/// and the value is the parameter index that argument maps to (if any).
pub(crate) fn argument_to_parameter_mapping(&self) -> &[Option<usize>] {
&self.argument_parameters
}
}
#[derive(Clone, Debug)]

View File

@@ -678,7 +678,6 @@ impl<'db> ClassType<'db> {
if let Some(signature) = signature {
let synthesized_signature = |signature: &Signature<'db>| {
Signature::new(signature.parameters().clone(), Some(correct_return_type))
.with_definition(signature.definition())
.bind_self()
};

View File

@@ -1598,10 +1598,6 @@ impl TypeCheckDiagnostics {
self.diagnostics.shrink_to_fit();
}
pub(crate) fn into_vec(self) -> Vec<Diagnostic> {
self.diagnostics
}
pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> {
self.diagnostics.iter()
}

View File

@@ -5,7 +5,6 @@ use std::fmt::{self, Display, Formatter, Write};
use ruff_db::display::FormatterJoinExtension;
use ruff_python_ast::str::{Quote, TripleQuotes};
use ruff_python_literal::escape::AsciiEscape;
use ruff_text_size::{TextRange, TextSize};
use crate::types::class::{ClassLiteral, ClassType, GenericAlias};
use crate::types::function::{FunctionType, OverloadLiteral};
@@ -558,193 +557,46 @@ pub(crate) struct DisplaySignature<'db> {
db: &'db dyn Db,
}
impl DisplaySignature<'_> {
/// Get detailed display information including component ranges
pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails {
let mut writer = SignatureWriter::Details(SignatureDetailsWriter::new());
self.write_signature(&mut writer).unwrap();
match writer {
SignatureWriter::Details(details) => details.finish(),
SignatureWriter::Formatter(_) => unreachable!("Expected Details variant"),
}
}
/// Internal method to write signature with the signature writer
fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result {
// Opening parenthesis
writer.write_char('(')?;
impl Display for DisplaySignature<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_char('(')?;
if self.parameters.is_gradual() {
// We represent gradual form as `...` in the signature, internally the parameters still
// contain `(*args, **kwargs)` parameters.
writer.write_str("...")?;
f.write_str("...")?;
} else {
let mut star_added = false;
let mut needs_slash = false;
let mut first = true;
let mut join = f.join(", ");
for parameter in self.parameters.as_slice() {
// Handle special separators
if !star_added && parameter.is_keyword_only() {
if !first {
writer.write_str(", ")?;
}
writer.write_char('*')?;
join.entry(&'*');
star_added = true;
first = false;
}
if parameter.is_positional_only() {
needs_slash = true;
} else if needs_slash {
if !first {
writer.write_str(", ")?;
}
writer.write_char('/')?;
join.entry(&'/');
needs_slash = false;
first = false;
}
// Add comma before parameter if not first
if !first {
writer.write_str(", ")?;
}
// Write parameter with range tracking
let param_name = parameter.display_name();
writer.write_parameter(&parameter.display(self.db), param_name.as_deref())?;
first = false;
join.entry(&parameter.display(self.db));
}
if needs_slash {
if !first {
writer.write_str(", ")?;
}
writer.write_char('/')?;
join.entry(&'/');
}
join.finish()?;
}
// Closing parenthesis
writer.write_char(')')?;
// Return type
let return_ty = self.return_ty.unwrap_or_else(Type::unknown);
writer.write_return_type(&return_ty.display(self.db))?;
Ok(())
write!(
f,
") -> {}",
self.return_ty.unwrap_or(Type::unknown()).display(self.db)
)
}
}
impl Display for DisplaySignature<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut writer = SignatureWriter::Formatter(f);
self.write_signature(&mut writer)
}
}
/// Writer for building signature strings with different output targets
enum SignatureWriter<'a, 'b> {
/// Write directly to a formatter (for Display trait)
Formatter(&'a mut Formatter<'b>),
/// Build a string with range tracking (for `to_string_parts`)
Details(SignatureDetailsWriter),
}
/// Writer that builds a string with range tracking
struct SignatureDetailsWriter {
label: String,
parameter_ranges: Vec<TextRange>,
parameter_names: Vec<String>,
}
impl SignatureDetailsWriter {
fn new() -> Self {
Self {
label: String::new(),
parameter_ranges: Vec::new(),
parameter_names: Vec::new(),
}
}
fn finish(self) -> SignatureDisplayDetails {
SignatureDisplayDetails {
label: self.label,
parameter_ranges: self.parameter_ranges,
parameter_names: self.parameter_names,
}
}
}
impl SignatureWriter<'_, '_> {
fn write_char(&mut self, c: char) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => f.write_char(c),
SignatureWriter::Details(details) => {
details.label.push(c);
Ok(())
}
}
}
fn write_str(&mut self, s: &str) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => f.write_str(s),
SignatureWriter::Details(details) => {
details.label.push_str(s);
Ok(())
}
}
}
fn write_parameter<T: Display>(&mut self, param: &T, param_name: Option<&str>) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => param.fmt(f),
SignatureWriter::Details(details) => {
let param_start = details.label.len();
let param_display = param.to_string();
details.label.push_str(&param_display);
// Use TextSize::try_from for safe conversion, falling back to empty range on overflow
let start = TextSize::try_from(param_start).unwrap_or_default();
let length = TextSize::try_from(param_display.len()).unwrap_or_default();
details.parameter_ranges.push(TextRange::at(start, length));
// Store the parameter name if available
if let Some(name) = param_name {
details.parameter_names.push(name.to_string());
} else {
details.parameter_names.push(String::new());
}
Ok(())
}
}
}
fn write_return_type<T: Display>(&mut self, return_ty: &T) -> fmt::Result {
match self {
SignatureWriter::Formatter(f) => write!(f, " -> {return_ty}"),
SignatureWriter::Details(details) => {
let return_display = format!(" -> {return_ty}");
details.label.push_str(&return_display);
Ok(())
}
}
}
}
/// Details about signature display components, including ranges for parameters and return type
#[derive(Debug, Clone)]
pub(crate) struct SignatureDisplayDetails {
/// The full signature string
pub label: String,
/// Ranges for each parameter within the label
pub parameter_ranges: Vec<TextRange>,
/// Names of the parameters in order
pub parameter_names: Vec<String>,
}
impl<'db> Parameter<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> {
DisplayParameter { param: self, db }

View File

@@ -1,20 +1,17 @@
use std::cmp::Ordering;
use crate::module_resolver::resolve_module;
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId;
use crate::semantic_index::{
attribute_scopes, global_scope, place_table, semantic_index, use_def_map,
attribute_scopes, global_scope, imported_modules, place_table, semantic_index, use_def_map,
};
use crate::types::call::CallArguments;
use crate::types::signatures::Signature;
use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type};
use crate::{Db, HasType, NameKind, SemanticModel};
use crate::{Db, NameKind};
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_text_size::TextRange;
use rustc_hash::FxHashSet;
pub(crate) fn all_declarations_and_bindings<'db>(
@@ -200,14 +197,26 @@ impl<'db> AllMembers<'db> {
});
}
self.members
.extend(literal.available_submodule_attributes(db).filter_map(
|submodule_name| {
let ty = literal.resolve_submodule(db, &submodule_name)?;
let name = submodule_name.clone();
let module_name = module.name();
self.members.extend(
literal
.importing_file(db)
.into_iter()
.flat_map(|file| imported_modules(db, file))
.filter_map(|submodule_name| {
let module = resolve_module(db, submodule_name)?;
let ty = Type::module_literal(db, file, &module);
Some((submodule_name, ty))
})
.filter_map(|(submodule_name, ty)| {
let relative = submodule_name.relative_to(module_name)?;
Some((relative, ty))
})
.filter_map(|(relative_submodule_name, ty)| {
let name = Name::from(relative_submodule_name.components().next()?);
Some(Member { name, ty })
},
));
}),
);
}
}
}
@@ -357,73 +366,3 @@ pub fn definition_kind_for_name<'db>(
None
}
/// Details about a callable signature for IDE support.
#[derive(Debug, Clone)]
pub struct CallSignatureDetails<'db> {
/// The signature itself
pub signature: Signature<'db>,
/// The display label for this signature (e.g., "(param1: str, param2: int) -> str")
pub label: String,
/// Label offsets for each parameter in the signature string.
/// Each range specifies the start position and length of a parameter label
/// within the full signature string.
pub parameter_label_offsets: Vec<TextRange>,
/// The names of the parameters in the signature, in order.
/// This provides easy access to parameter names for documentation lookup.
pub parameter_names: Vec<String>,
/// The definition where this callable was originally defined (useful for
/// extracting docstrings).
pub definition: Option<Definition<'db>>,
/// Mapping from argument indices to parameter indices. This helps
/// determine which parameter corresponds to which argument position.
pub argument_to_parameter_mapping: Vec<Option<usize>>,
}
/// Extract signature details from a function call expression.
/// This function analyzes the callable being invoked and returns zero or more
/// `CallSignatureDetails` objects, each representing one possible signature
/// (in case of overloads or union types).
pub fn call_signature_details<'db>(
db: &'db dyn Db,
file: File,
call_expr: &ast::ExprCall,
) -> Vec<CallSignatureDetails<'db>> {
let model = SemanticModel::new(db, file);
let func_type = call_expr.func.inferred_type(&model);
// Use into_callable to handle all the complex type conversions
if let Some(callable_type) = func_type.into_callable(db) {
let call_arguments = CallArguments::from_arguments(&call_expr.arguments);
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
// Extract signature details from all callable bindings
bindings
.into_iter()
.flat_map(std::iter::IntoIterator::into_iter)
.map(|binding| {
let signature = &binding.signature;
let display_details = signature.display(db).to_string_parts();
let parameter_label_offsets = display_details.parameter_ranges.clone();
let parameter_names = display_details.parameter_names.clone();
CallSignatureDetails {
signature: signature.clone(),
label: display_details.label,
parameter_label_offsets,
parameter_names,
definition: signature.definition(),
argument_to_parameter_mapping: binding.argument_to_parameter_mapping().to_vec(),
}
})
.collect()
} else {
// Type is not callable, return empty signatures
vec![]
}
}

View File

@@ -84,7 +84,9 @@ use crate::semantic_index::place::{
use crate::semantic_index::{
ApplicableConstraints, EagerSnapshotResult, SemanticIndex, place_table, semantic_index,
};
use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallArguments, CallError};
use crate::types::call::{
Argument, Binding, Bindings, CallArgumentTypes, CallArguments, CallError,
};
use crate::types::class::{CodeGeneratorKind, MetaclassErrorKind, SliceLiteral};
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
@@ -1915,7 +1917,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_parameters(type_params);
if let Some(arguments) = class.arguments.as_deref() {
let call_arguments = CallArguments::from_arguments(arguments);
let call_arguments = Self::parse_arguments(arguments);
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, call_arguments, &argument_forms);
}
@@ -4624,6 +4626,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(expression)
}
fn parse_arguments(arguments: &ast::Arguments) -> CallArguments<'_> {
arguments
.arguments_source_order()
.map(|arg_or_keyword| {
match arg_or_keyword {
ast::ArgOrKeyword::Arg(arg) => match arg {
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
// TODO diagnostic if after a keyword argument
_ => Argument::Positional,
},
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
if let Some(arg) = arg {
Argument::Keyword(&arg.id)
} else {
// TODO diagnostic if not last
Argument::Keywords
}
}
}
})
.collect()
}
fn infer_argument_types<'a>(
&mut self,
ast_arguments: &ast::Arguments,
@@ -5337,7 +5362,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// We don't call `Type::try_call`, because we want to perform type inference on the
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let call_arguments = CallArguments::from_arguments(arguments);
let call_arguments = Self::parse_arguments(arguments);
let callable_type = self.infer_maybe_standalone_expression(func);
@@ -10010,7 +10035,7 @@ mod tests {
}
#[track_caller]
fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) {
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
let messages: Vec<&str> = diagnostics
.iter()
.map(Diagnostic::primary_message)
@@ -10023,7 +10048,7 @@ mod tests {
let file = system_path_to_file(db, filename).unwrap();
let diagnostics = check_types(db, file);
assert_diagnostic_messages(&diagnostics, expected);
assert_diagnostic_messages(diagnostics, expected);
}
#[test]

View File

@@ -213,7 +213,7 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
}
/// The signature of one of the overloads of a callable.
#[derive(Clone, Debug, salsa::Update, get_size2::GetSize)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
pub struct Signature<'db> {
/// The generic context for this overload, if it is generic.
pub(crate) generic_context: Option<GenericContext<'db>>,
@@ -223,10 +223,6 @@ pub struct Signature<'db> {
/// to its own generic context.
pub(crate) inherited_generic_context: Option<GenericContext<'db>>,
/// The original definition associated with this function, if available.
/// This is useful for locating and extracting docstring information for the signature.
pub(crate) definition: Option<Definition<'db>>,
/// Parameters, in source order.
///
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
@@ -269,7 +265,6 @@ impl<'db> Signature<'db> {
Self {
generic_context: None,
inherited_generic_context: None,
definition: None,
parameters,
return_ty,
}
@@ -283,7 +278,6 @@ impl<'db> Signature<'db> {
Self {
generic_context,
inherited_generic_context: None,
definition: None,
parameters,
return_ty,
}
@@ -294,7 +288,6 @@ impl<'db> Signature<'db> {
Signature {
generic_context: None,
inherited_generic_context: None,
definition: None,
parameters: Parameters::gradual_form(),
return_ty: Some(signature_type),
}
@@ -307,7 +300,6 @@ impl<'db> Signature<'db> {
Signature {
generic_context: None,
inherited_generic_context: None,
definition: None,
parameters: Parameters::todo(),
return_ty: Some(signature_type),
}
@@ -340,7 +332,6 @@ impl<'db> Signature<'db> {
Self {
generic_context: generic_context.or(legacy_generic_context),
inherited_generic_context,
definition: Some(definition),
parameters,
return_ty,
}
@@ -360,7 +351,6 @@ impl<'db> Signature<'db> {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
// Parameters are at contravariant position, so the variance is flipped.
parameters: self.parameters.materialize(db, variance.flip()),
return_ty: Some(
@@ -383,7 +373,6 @@ impl<'db> Signature<'db> {
inherited_generic_context: self
.inherited_generic_context
.map(|ctx| ctx.normalized_impl(db, visitor)),
definition: self.definition,
parameters: self
.parameters
.iter()
@@ -403,7 +392,6 @@ impl<'db> Signature<'db> {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
parameters: self.parameters.apply_type_mapping(db, type_mapping),
return_ty: self
.return_ty
@@ -434,16 +422,10 @@ impl<'db> Signature<'db> {
&self.parameters
}
/// Return the definition associated with this signature, if any.
pub(crate) fn definition(&self) -> Option<Definition<'db>> {
self.definition
}
pub(crate) fn bind_self(&self) -> Self {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
parameters: Parameters::new(self.parameters().iter().skip(1).cloned()),
return_ty: self.return_ty,
}
@@ -917,33 +899,6 @@ impl<'db> Signature<'db> {
true
}
/// Create a new signature with the given definition.
pub(crate) fn with_definition(self, definition: Option<Definition<'db>>) -> Self {
Self { definition, ..self }
}
}
// Manual implementations of PartialEq, Eq, and Hash that exclude the definition field
// since the definition is not relevant for type equality/equivalence
impl PartialEq for Signature<'_> {
fn eq(&self, other: &Self) -> bool {
self.generic_context == other.generic_context
&& self.inherited_generic_context == other.inherited_generic_context
&& self.parameters == other.parameters
&& self.return_ty == other.return_ty
}
}
impl Eq for Signature<'_> {}
impl std::hash::Hash for Signature<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.generic_context.hash(state);
self.inherited_generic_context.hash(state);
self.parameters.hash(state);
self.return_ty.hash(state);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]

View File

@@ -2,14 +2,14 @@
use self::schedule::spawn_main_loop;
use crate::PositionEncoding;
use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session};
use crate::session::{AllOptions, ClientOptions, Session};
use lsp_server::Connection;
use lsp_types::{
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
InlayHintOptions, InlayHintServerCapabilities, MessageType, SemanticTokensLegend,
SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities,
SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
};
use std::num::NonZeroUsize;
use std::panic::PanicHookInfo;
@@ -54,8 +54,7 @@ impl Server {
let client_capabilities = init_params.capabilities;
let position_encoding = Self::find_best_position_encoding(&client_capabilities);
let server_capabilities =
Self::server_capabilities(position_encoding, global_options.diagnostic_mode());
let server_capabilities = Self::server_capabilities(position_encoding);
let connection = connection.initialize_finish(
id,
@@ -169,17 +168,13 @@ impl Server {
.unwrap_or_default()
}
fn server_capabilities(
position_encoding: PositionEncoding,
diagnostic_mode: DiagnosticMode,
) -> ServerCapabilities {
fn server_capabilities(position_encoding: PositionEncoding) -> ServerCapabilities {
ServerCapabilities {
position_encoding: Some(position_encoding.into()),
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
inter_file_dependencies: true,
// TODO: Dynamically register for workspace diagnostics.
workspace_diagnostics: diagnostic_mode.is_workspace(),
workspace_diagnostics: true,
..Default::default()
})),
text_document_sync: Some(TextDocumentSyncCapability::Options(
@@ -191,11 +186,6 @@ impl Server {
)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
retrigger_characters: Some(vec![")".to_string()]),
work_done_progress_options: lsp_types::WorkDoneProgressOptions::default(),
}),
inlay_hint_provider: Some(lsp_types::OneOf::Right(
InlayHintServerCapabilities::Options(InlayHintOptions::default()),
)),

View File

@@ -58,9 +58,6 @@ pub(super) fn request(req: server::Request) -> Task {
>(
req, BackgroundSchedule::Worker
),
requests::SignatureHelpRequestHandler::METHOD => background_document_request_task::<
requests::SignatureHelpRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::CompletionRequestHandler::METHOD => background_document_request_task::<
requests::CompletionRequestHandler,
>(

View File

@@ -6,7 +6,6 @@ mod inlay_hints;
mod semantic_tokens;
mod semantic_tokens_range;
mod shutdown;
mod signature_help;
mod workspace_diagnostic;
pub(super) use completion::CompletionRequestHandler;
@@ -17,5 +16,4 @@ pub(super) use inlay_hints::InlayHintRequestHandler;
pub(super) use semantic_tokens::SemanticTokensRequestHandler;
pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler;
pub(super) use shutdown::ShutdownHandler;
pub(super) use signature_help::SignatureHelpRequestHandler;
pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler;

View File

@@ -1,145 +0,0 @@
use std::borrow::Cow;
use crate::DocumentSnapshot;
use crate::document::{PositionEncoding, PositionExt};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::client::Client;
use lsp_types::request::SignatureHelpRequest;
use lsp_types::{
Documentation, ParameterInformation, ParameterLabel, SignatureHelp, SignatureHelpParams,
SignatureInformation, Url,
};
use ruff_db::source::{line_index, source_text};
use ty_ide::signature_help;
use ty_project::ProjectDatabase;
pub(crate) struct SignatureHelpRequestHandler;
impl RequestHandler for SignatureHelpRequestHandler {
type RequestType = SignatureHelpRequest;
}
impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler {
fn document_url(params: &SignatureHelpParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document_position_params.text_document.uri)
}
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
_client: &Client,
params: SignatureHelpParams,
) -> crate::server::Result<Option<SignatureHelp>> {
if snapshot.client_settings().is_language_services_disabled() {
return Ok(None);
}
let Some(file) = snapshot.file(db) else {
tracing::debug!("Failed to resolve file for {:?}", params);
return Ok(None);
};
let source = source_text(db, file);
let line_index = line_index(db, file);
let offset = params.text_document_position_params.position.to_text_size(
&source,
&line_index,
snapshot.encoding(),
);
// Extract signature help capabilities from the client
let resolved_capabilities = snapshot.resolved_client_capabilities();
let Some(signature_help_info) = signature_help(db, file, offset) else {
return Ok(None);
};
// Compute active parameter from the active signature
let active_parameter = signature_help_info
.active_signature
.and_then(|s| signature_help_info.signatures.get(s))
.and_then(|sig| sig.active_parameter)
.and_then(|p| u32::try_from(p).ok());
// Convert from IDE types to LSP types
let signatures = signature_help_info
.signatures
.into_iter()
.map(|sig| {
let parameters = sig
.parameters
.into_iter()
.map(|param| {
let label = if resolved_capabilities.signature_label_offset_support {
// Find the parameter's offset in the signature label
if let Some(start) = sig.label.find(&param.label) {
let encoding = snapshot.encoding();
// Convert byte offsets to character offsets based on negotiated encoding
let start_char_offset = match encoding {
PositionEncoding::UTF8 => start,
PositionEncoding::UTF16 => {
sig.label[..start].encode_utf16().count()
}
PositionEncoding::UTF32 => sig.label[..start].chars().count(),
};
let end_char_offset = match encoding {
PositionEncoding::UTF8 => start + param.label.len(),
PositionEncoding::UTF16 => sig.label
[..start + param.label.len()]
.encode_utf16()
.count(),
PositionEncoding::UTF32 => {
sig.label[..start + param.label.len()].chars().count()
}
};
let start_u32 =
u32::try_from(start_char_offset).unwrap_or(u32::MAX);
let end_u32 = u32::try_from(end_char_offset).unwrap_or(u32::MAX);
ParameterLabel::LabelOffsets([start_u32, end_u32])
} else {
ParameterLabel::Simple(param.label)
}
} else {
ParameterLabel::Simple(param.label)
};
ParameterInformation {
label,
documentation: param.documentation.map(Documentation::String),
}
})
.collect();
let active_parameter = if resolved_capabilities.signature_active_parameter_support {
sig.active_parameter.and_then(|p| u32::try_from(p).ok())
} else {
None
};
SignatureInformation {
label: sig.label,
documentation: sig.documentation.map(Documentation::String),
parameters: Some(parameters),
active_parameter,
}
})
.collect();
let signature_help = SignatureHelp {
signatures,
active_signature: signature_help_info
.active_signature
.and_then(|s| u32::try_from(s).ok()),
active_parameter,
};
Ok(Some(signature_help))
}
}
impl RetriableRequestHandler for SignatureHelpRequestHandler {}

View File

@@ -16,7 +16,7 @@ use ty_project::{ProjectDatabase, ProjectMetadata};
pub(crate) use self::capabilities::ResolvedClientCapabilities;
pub use self::index::DocumentQuery;
pub(crate) use self::options::{AllOptions, ClientOptions, DiagnosticMode};
pub(crate) use self::options::{AllOptions, ClientOptions};
pub(crate) use self::settings::ClientSettings;
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
use crate::session::request_queue::RequestQueue;

View File

@@ -22,12 +22,6 @@ pub(crate) struct ResolvedClientCapabilities {
/// Whether the client supports multiline semantic tokens
pub(crate) semantic_tokens_multiline_support: bool,
/// Whether the client supports signature label offsets in signature help
pub(crate) signature_label_offset_support: bool,
/// Whether the client supports per-signature active parameter in signature help
pub(crate) signature_active_parameter_support: bool,
}
impl ResolvedClientCapabilities {
@@ -101,34 +95,6 @@ impl ResolvedClientCapabilities {
.and_then(|semantic_tokens| semantic_tokens.multiline_token_support)
.unwrap_or(false);
let signature_label_offset_support = client_capabilities
.text_document
.as_ref()
.and_then(|text_document| {
text_document
.signature_help
.as_ref()?
.signature_information
.as_ref()?
.parameter_information
.as_ref()?
.label_offset_support
})
.unwrap_or_default();
let signature_active_parameter_support = client_capabilities
.text_document
.as_ref()
.and_then(|text_document| {
text_document
.signature_help
.as_ref()?
.signature_information
.as_ref()?
.active_parameter_support
})
.unwrap_or_default();
Self {
code_action_deferred_edit_resolution: code_action_data_support
&& code_action_edit_resolution,
@@ -140,8 +106,6 @@ impl ResolvedClientCapabilities {
type_definition_link_support: declaration_link_support,
hover_prefer_markdown,
semantic_tokens_multiline_support,
signature_label_offset_support,
signature_active_parameter_support,
}
}
}

View File

@@ -25,10 +25,6 @@ impl GlobalOptions {
pub(crate) fn into_settings(self) -> ClientSettings {
self.client.into_settings()
}
pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode {
self.client.diagnostic_mode.unwrap_or_default()
}
}
/// This is a direct representation of the workspace settings schema, which inherits the schema of

View File

@@ -340,7 +340,7 @@ fn run_test(
Err(failures) => return Some(failures),
};
diagnostics.extend(type_diagnostics);
diagnostics.extend(type_diagnostics.into_iter().cloned());
diagnostics.sort_by(|left, right| {
left.rendering_sort_key(db)
.cmp(&right.rendering_sort_key(db))

View File

@@ -14,7 +14,6 @@ use ruff_notebook::Notebook;
use ruff_python_formatter::formatted_file;
use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextSize};
use ty_ide::signature_help;
use ty_ide::{MarkupKind, goto_type_definition, hover, inlay_hints};
use ty_project::ProjectMetadata;
use ty_project::metadata::options::Options;
@@ -386,51 +385,6 @@ impl Workspace {
Ok(result)
}
#[wasm_bindgen(js_name = "signatureHelp")]
pub fn signature_help(
&self,
file_id: &FileHandle,
position: Position,
) -> Result<Option<SignatureHelp>, Error> {
let source = source_text(&self.db, file_id.file);
let index = line_index(&self.db, file_id.file);
let offset = position.to_text_size(&source, &index, self.position_encoding)?;
let Some(signature_help_info) = signature_help(&self.db, file_id.file, offset) else {
return Ok(None);
};
let signatures = signature_help_info
.signatures
.into_iter()
.map(|sig| {
let parameters = sig
.parameters
.into_iter()
.map(|param| ParameterInformation {
label: param.label,
documentation: param.documentation,
})
.collect();
SignatureInformation {
label: sig.label,
documentation: sig.documentation,
parameters,
active_parameter: sig.active_parameter.and_then(|p| u32::try_from(p).ok()),
}
})
.collect();
Ok(Some(SignatureHelp {
signatures,
active_signature: signature_help_info
.active_signature
.and_then(|s| u32::try_from(s).ok()),
}))
}
}
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
@@ -795,35 +749,6 @@ pub struct SemanticToken {
pub range: Range,
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct SignatureHelp {
#[wasm_bindgen(getter_with_clone)]
pub signatures: Vec<SignatureInformation>,
pub active_signature: Option<u32>,
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct SignatureInformation {
#[wasm_bindgen(getter_with_clone)]
pub label: String,
#[wasm_bindgen(getter_with_clone)]
pub documentation: Option<String>,
#[wasm_bindgen(getter_with_clone)]
pub parameters: Vec<ParameterInformation>,
pub active_parameter: Option<u32>,
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct ParameterInformation {
#[wasm_bindgen(getter_with_clone)]
pub label: String,
#[wasm_bindgen(getter_with_clone)]
pub documentation: Option<String>,
}
#[wasm_bindgen]
impl SemanticToken {
pub fn kinds() -> Vec<String> {

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.12.3-alpine
name: ghcr.io/astral-sh/ruff:0.12.2-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.3
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.3
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.3
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.3
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -152,8 +152,7 @@ class PlaygroundServer
languages.DocumentFormattingEditProvider,
languages.CompletionItemProvider,
languages.DocumentSemanticTokensProvider,
languages.DocumentRangeSemanticTokensProvider,
languages.SignatureHelpProvider
languages.DocumentRangeSemanticTokensProvider
{
private typeDefinitionProviderDisposable: IDisposable;
private editorOpenerDisposable: IDisposable;
@@ -163,7 +162,6 @@ class PlaygroundServer
private completionDisposable: IDisposable;
private semanticTokensDisposable: IDisposable;
private rangeSemanticTokensDisposable: IDisposable;
private signatureHelpDisposable: IDisposable;
constructor(
private monaco: Monaco,
@@ -193,13 +191,9 @@ class PlaygroundServer
this.editorOpenerDisposable = monaco.editor.registerEditorOpener(this);
this.formatDisposable =
monaco.languages.registerDocumentFormattingEditProvider("python", this);
this.signatureHelpDisposable =
monaco.languages.registerSignatureHelpProvider("python", this);
}
triggerCharacters: string[] = ["."];
signatureHelpTriggerCharacters: string[] = ["(", ","];
signatureHelpRetriggerCharacters: string[] = [")"];
getLegend(): languages.SemanticTokensLegend {
return {
@@ -298,61 +292,6 @@ class PlaygroundServer
resolveCompletionItem: undefined;
provideSignatureHelp(
model: editor.ITextModel,
position: Position,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: CancellationToken,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_context: languages.SignatureHelpContext,
): languages.ProviderResult<languages.SignatureHelpResult> {
const selectedFile = this.props.files.selected;
if (selectedFile == null) {
return;
}
const selectedHandle = this.props.files.handles[selectedFile];
if (selectedHandle == null) {
return;
}
const signatureHelp = this.props.workspace.signatureHelp(
selectedHandle,
new TyPosition(position.lineNumber, position.column),
);
if (signatureHelp == null) {
return undefined;
}
return {
dispose() {},
value: {
signatures: signatureHelp.signatures.map((sig) => ({
label: sig.label,
documentation: sig.documentation
? { value: sig.documentation }
: undefined,
parameters: sig.parameters.map((param) => ({
label: param.label,
documentation: param.documentation
? { value: param.documentation }
: undefined,
})),
activeParameter: sig.active_parameter,
})),
activeSignature: signatureHelp.active_signature ?? 0,
activeParameter:
signatureHelp.active_signature != null
? (signatureHelp.signatures[signatureHelp.active_signature]
?.active_parameter ?? 0)
: 0,
},
};
}
provideInlayHints(
_model: editor.ITextModel,
range: Range,
@@ -630,7 +569,6 @@ class PlaygroundServer
this.rangeSemanticTokensDisposable.dispose();
this.semanticTokensDisposable.dispose();
this.completionDisposable.dispose();
this.signatureHelpDisposable.dispose();
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.12.3"
version = "0.12.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.12.3"
version = "0.12.2"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]