Compare commits
3 Commits
0.12.3
...
jack/nonlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f22497bdd | ||
|
|
05cf7c3458 | ||
|
|
664a9a28dc |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -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
9
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -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 -----
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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():
|
||||
(#=
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -670,7 +670,12 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
| SemanticSyntaxErrorKind::InvalidStarExpression
|
||||
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
||||
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
|
||||
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
|
||||
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_)
|
||||
| SemanticSyntaxErrorKind::InvalidNonlocal(_) => {
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -952,6 +952,9 @@ impl Display for SemanticSyntaxError {
|
||||
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
|
||||
write!(f, "name `{name}` is used prior to global declaration")
|
||||
}
|
||||
SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { name, start: _ } => {
|
||||
write!(f, "name `{name}` is used prior to nonlocal declaration")
|
||||
}
|
||||
SemanticSyntaxErrorKind::InvalidStarExpression => {
|
||||
f.write_str("Starred expression cannot be used here")
|
||||
}
|
||||
@@ -977,6 +980,18 @@ impl Display for SemanticSyntaxError {
|
||||
SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
write!(f, "nonlocal declaration not allowed at module level")
|
||||
}
|
||||
SemanticSyntaxErrorKind::NonlocalAndGlobal(name) => {
|
||||
write!(f, "name `{name}` is nonlocal and global")
|
||||
}
|
||||
SemanticSyntaxErrorKind::AnnotatedGlobal(name) => {
|
||||
write!(f, "annotated name `{name}` can't be global")
|
||||
}
|
||||
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
|
||||
write!(f, "annotated name `{name}` can't be nonlocal")
|
||||
}
|
||||
SemanticSyntaxErrorKind::InvalidNonlocal(name) => {
|
||||
write!(f, "no binding for nonlocal `{name}` found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1207,6 +1222,24 @@ pub enum SemanticSyntaxErrorKind {
|
||||
/// [#111123]: https://github.com/python/cpython/issues/111123
|
||||
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
|
||||
|
||||
/// Represents the use of a `nonlocal` variable before its `nonlocal` declaration.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// counter = 0
|
||||
/// def increment():
|
||||
/// print(f"Adding 1 to {counter}")
|
||||
/// nonlocal counter # SyntaxError: name 'counter' is used prior to nonlocal declaration
|
||||
/// counter += 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Known Issues
|
||||
///
|
||||
/// See [`LoadBeforeGlobalDeclaration`][Self::LoadBeforeGlobalDeclaration].
|
||||
LoadBeforeNonlocalDeclaration { name: String, start: TextSize },
|
||||
|
||||
/// Represents the use of a starred expression in an invalid location, such as a `return` or
|
||||
/// `yield` statement.
|
||||
///
|
||||
@@ -1307,6 +1340,41 @@ pub enum SemanticSyntaxErrorKind {
|
||||
|
||||
/// Represents a nonlocal declaration at module level
|
||||
NonlocalDeclarationAtModuleLevel,
|
||||
|
||||
/// Represents the same variable declared as both nonlocal and global
|
||||
NonlocalAndGlobal(String),
|
||||
|
||||
/// Represents a type annotation on a variable that's been declared global
|
||||
AnnotatedGlobal(String),
|
||||
|
||||
/// Represents a type annotation on a variable that's been declared nonlocal
|
||||
AnnotatedNonlocal(String),
|
||||
|
||||
/// Represents a nonlocal declaration with no definition in an enclosing scope
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// nonlocal x # error
|
||||
///
|
||||
/// # Global variables don't count.
|
||||
/// x = 1
|
||||
/// def f():
|
||||
/// nonlocal x # error
|
||||
///
|
||||
/// def f():
|
||||
/// x = 1
|
||||
/// def g():
|
||||
/// nonlocal x # allowed
|
||||
///
|
||||
/// # The definition can come later.
|
||||
/// def f():
|
||||
/// def g():
|
||||
/// nonlocal x # allowed
|
||||
/// x = 1
|
||||
/// ```
|
||||
InvalidNonlocal(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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(§ion_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!(¶m_docs["param1"], "The first parameter description");
|
||||
assert_eq!(
|
||||
¶m_docs["param2"],
|
||||
"The second parameter description\nThis is a continuation of param2 description."
|
||||
);
|
||||
assert_eq!(¶m_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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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(|¶m_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1304,7 +1304,7 @@ scope of the name that was declared `global`, can add a symbol to the global nam
|
||||
def f():
|
||||
global g, h
|
||||
|
||||
g: bool = True
|
||||
g = True
|
||||
|
||||
f()
|
||||
```
|
||||
|
||||
@@ -83,7 +83,7 @@ def f():
|
||||
x = 1
|
||||
def g() -> None:
|
||||
nonlocal x
|
||||
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
|
||||
global x # error: [invalid-syntax] "name `x` is nonlocal and global"
|
||||
x = None
|
||||
```
|
||||
|
||||
@@ -209,5 +209,18 @@ x: int = 1
|
||||
|
||||
def f():
|
||||
global x
|
||||
x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global"
|
||||
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be global"
|
||||
```
|
||||
|
||||
## Global declarations affect the inferred type of the binding
|
||||
|
||||
Even if the `global` declaration isn't used in an assignment, we conservatively assume it could be:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
global x
|
||||
|
||||
# TODO: reveal_type(x) # revealed: Unknown | Literal["1"]
|
||||
```
|
||||
|
||||
@@ -43,3 +43,321 @@ def f():
|
||||
def h():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## The `nonlocal` keyword
|
||||
|
||||
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
|
||||
enclosing scopes. This example isn't a type error, because the inner `x` shadows the outer one:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
def g():
|
||||
x = "hello" # allowed
|
||||
```
|
||||
|
||||
With `nonlocal` it is a type error, because `x` refers to the same place in both scopes:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## Local variable bindings "look ahead" to any assignment in the current scope
|
||||
|
||||
The binding `x = 2` in `g` causes the earlier read of `x` to refer to `g`'s not-yet-initialized
|
||||
binding, rather than to `x = 1` in `f`'s scope:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
if x == 1: # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
x = 2
|
||||
```
|
||||
|
||||
The `nonlocal` keyword makes this example legal (and makes the assignment `x = 2` affect the outer
|
||||
scope):
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
if x == 1:
|
||||
x = 2
|
||||
```
|
||||
|
||||
For the same reason, using the `+=` operator in an inner scope is an error without `nonlocal`
|
||||
(unless you shadow the outer variable first):
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x += 1 # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x = 1
|
||||
x += 1 # allowed, but doesn't affect the outer scope
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x += 1 # allowed, and affects the outer scope
|
||||
```
|
||||
|
||||
## `nonlocal` declarations must match an outer binding
|
||||
|
||||
`nonlocal x` isn't allowed when there's no binding for `x` in an enclosing scope:
|
||||
|
||||
```py
|
||||
def f():
|
||||
def g():
|
||||
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x, y # error: [invalid-syntax] "no binding for nonlocal `y` found"
|
||||
```
|
||||
|
||||
A global `x` doesn't work. The target must be in a function-like scope:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
def g():
|
||||
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||
|
||||
def f():
|
||||
global x
|
||||
def g():
|
||||
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||
```
|
||||
|
||||
A class-scoped `x` also doesn't work:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x = 1
|
||||
@staticmethod
|
||||
def f():
|
||||
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||
```
|
||||
|
||||
However, class-scoped bindings don't break the `nonlocal` chain the way `global` declarations do:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
|
||||
class Foo:
|
||||
x: str = "hello"
|
||||
|
||||
@staticmethod
|
||||
def g():
|
||||
# Skips the class scope and reaches the outer function scope.
|
||||
nonlocal x
|
||||
x = 2 # allowed
|
||||
x = "goodbye" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## `nonlocal` uses the closest binding
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x = 2
|
||||
def h():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||
```
|
||||
|
||||
## `nonlocal` "chaining"
|
||||
|
||||
Multiple `nonlocal` statements can "chain" through nested scopes:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
def h():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
And the `nonlocal` chain can skip over a scope that doesn't bind the variable:
|
||||
|
||||
```py
|
||||
def f1():
|
||||
x = 1
|
||||
def f2():
|
||||
nonlocal x
|
||||
def f3():
|
||||
# No binding; this scope gets skipped.
|
||||
def f4():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
But a `global` statement breaks the chain:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
global x
|
||||
def h():
|
||||
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||
```
|
||||
|
||||
## `nonlocal` bindings respect declared types from the defining scope, even without a binding
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int
|
||||
def g():
|
||||
nonlocal x
|
||||
x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## A complicated mixture of `nonlocal` chaining, empty scopes, class scopes, and the `global` keyword
|
||||
|
||||
```py
|
||||
def f1():
|
||||
# The original bindings of `x`, `y`, and `z` with type declarations.
|
||||
x: int = 1
|
||||
y: int = 2
|
||||
z: int = 3
|
||||
|
||||
def f2():
|
||||
# This scope doesn't touch `x`, `y`, or `z` at all.
|
||||
|
||||
class Foo:
|
||||
# This class scope is totally ignored.
|
||||
x: str = "a"
|
||||
y: str = "b"
|
||||
z: str = "c"
|
||||
|
||||
@staticmethod
|
||||
def f3():
|
||||
# This scope declares `x` nonlocal and `y` as global, and it shadows `z` without
|
||||
# giving it a type declaration.
|
||||
nonlocal x
|
||||
x = 4
|
||||
y = 5
|
||||
global z
|
||||
z = 6
|
||||
|
||||
def f4():
|
||||
# This scope sees `x` from `f1` and `y` from `f3`. It *can't* declare `z`
|
||||
# nonlocal, because of the global statement above, but it *can* load `z` as a
|
||||
# "free" variable, in which case it sees the global value.
|
||||
nonlocal x, y, z # error: [invalid-syntax] "no binding for nonlocal `z` found"
|
||||
x = "string" # error: [invalid-assignment]
|
||||
y = "string" # allowed, because `f3`'s `y` is untyped
|
||||
reveal_type(z) # revealed: Unknown | Literal[6]
|
||||
```
|
||||
|
||||
## TODO: `nonlocal` affects the inferred type in the outer scope
|
||||
|
||||
Without `nonlocal`, `g` can't write to `x`, and the inferred type of `x` in `f`'s scope isn't
|
||||
affected by `g`:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
But with `nonlocal`, `g` could write to `x`, and that affects its inferred type in `f`. That's true
|
||||
regardless of whether `g` actually writes to `x`. With a write:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
x += 1
|
||||
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||
# TODO: should be `Unknown | Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
Without a write:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
# TODO: should be `Unknown | Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Annotating a `nonlocal` binding is a syntax error
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be nonlocal"
|
||||
```
|
||||
|
||||
## Use before `nonlocal`
|
||||
|
||||
Using a name prior to its `nonlocal` declaration in the same scope is a syntax error:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x = 2
|
||||
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
|
||||
```
|
||||
|
||||
This is true even if there are multiple `nonlocal` declarations of the same variable, as long as any
|
||||
of them come after the usage:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x = 2
|
||||
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
nonlocal x
|
||||
x = 2 # allowed
|
||||
```
|
||||
|
||||
## `nonlocal` before outer initialization
|
||||
|
||||
`nonlocal x` works even if `x` isn't bound in the enclosing scope until afterwards:
|
||||
|
||||
```py
|
||||
def f():
|
||||
def g():
|
||||
# This is allowed, because of the subsequent definition of `x`.
|
||||
nonlocal x
|
||||
x = 1
|
||||
```
|
||||
|
||||
@@ -147,8 +147,7 @@ def nonlocal_use():
|
||||
X: Final[int] = 1
|
||||
def inner():
|
||||
nonlocal X
|
||||
# TODO: this should be an error
|
||||
X = 2
|
||||
X = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `X` is not allowed: Reassignment of `Final` symbol"
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
@@ -1421,7 +1421,7 @@ impl RequiresExplicitReExport {
|
||||
/// ```py
|
||||
/// def _():
|
||||
/// x = 1
|
||||
///
|
||||
///
|
||||
/// x = 2
|
||||
///
|
||||
/// if flag():
|
||||
|
||||
@@ -217,9 +217,6 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`].
|
||||
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
||||
|
||||
/// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains.
|
||||
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
|
||||
|
||||
/// Use-def map for each scope in this file.
|
||||
use_def_maps: IndexVec<FileScopeId, ArcUseDefMap<'db>>,
|
||||
|
||||
@@ -308,9 +305,19 @@ impl<'db> SemanticIndex<'db> {
|
||||
symbol: ScopedPlaceId,
|
||||
scope: FileScopeId,
|
||||
) -> bool {
|
||||
self.globals_by_scope
|
||||
.get(&scope)
|
||||
.is_some_and(|globals| globals.contains(&symbol))
|
||||
self.place_table(scope)
|
||||
.place_expr(symbol)
|
||||
.is_marked_global()
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_is_nonlocal_in_scope(
|
||||
&self,
|
||||
symbol: ScopedPlaceId,
|
||||
scope: FileScopeId,
|
||||
) -> bool {
|
||||
self.place_table(scope)
|
||||
.place_expr(symbol)
|
||||
.is_marked_nonlocal()
|
||||
}
|
||||
|
||||
/// Returns the id of the parent scope.
|
||||
|
||||
@@ -83,6 +83,8 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
||||
current_match_case: Option<CurrentMatchCase<'ast>>,
|
||||
/// The name of the first function parameter of the innermost function that we're currently visiting.
|
||||
current_first_parameter_name: Option<&'ast str>,
|
||||
/// Functions defined in the current scope. We walk their bodies at the end of the scope.
|
||||
deferred_function_bodies: Vec<&'ast ast::StmtFunctionDef>,
|
||||
|
||||
/// Per-scope contexts regarding nested `try`/`except` statements
|
||||
try_node_context_stack_manager: TryNodeContextStackManager,
|
||||
@@ -103,7 +105,6 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
||||
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
|
||||
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
@@ -127,6 +128,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
current_assignments: vec![],
|
||||
current_match_case: None,
|
||||
current_first_parameter_name: None,
|
||||
deferred_function_bodies: Vec::new(),
|
||||
try_node_context_stack_manager: TryNodeContextStackManager::default(),
|
||||
|
||||
has_future_annotations: false,
|
||||
@@ -141,7 +143,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
definitions_by_node: FxHashMap::default(),
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
globals_by_scope: FxHashMap::default(),
|
||||
|
||||
imported_modules: FxHashSet::default(),
|
||||
generator_functions: FxHashSet::default(),
|
||||
@@ -349,7 +350,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
popped_scope_id
|
||||
}
|
||||
|
||||
fn current_place_table(&mut self) -> &mut PlaceTableBuilder {
|
||||
fn current_place_table(&self) -> &PlaceTableBuilder {
|
||||
let scope_id = self.current_scope();
|
||||
&self.place_tables[scope_id]
|
||||
}
|
||||
|
||||
fn current_place_table_mut(&mut self) -> &mut PlaceTableBuilder {
|
||||
let scope_id = self.current_scope();
|
||||
&mut self.place_tables[scope_id]
|
||||
}
|
||||
@@ -389,7 +395,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
/// Add a symbol to the place table and the use-def map.
|
||||
/// Return the [`ScopedPlaceId`] that uniquely identifies the symbol in both.
|
||||
fn add_symbol(&mut self, name: Name) -> ScopedPlaceId {
|
||||
let (place_id, added) = self.current_place_table().add_symbol(name);
|
||||
let (place_id, added) = self.current_place_table_mut().add_symbol(name);
|
||||
if added {
|
||||
self.current_use_def_map_mut().add_place(place_id);
|
||||
}
|
||||
@@ -399,7 +405,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
/// Add a place to the place table and the use-def map.
|
||||
/// Return the [`ScopedPlaceId`] that uniquely identifies the place in both.
|
||||
fn add_place(&mut self, place_expr: PlaceExprWithFlags) -> ScopedPlaceId {
|
||||
let (place_id, added) = self.current_place_table().add_place(place_expr);
|
||||
let (place_id, added) = self.current_place_table_mut().add_place(place_expr);
|
||||
if added {
|
||||
self.current_use_def_map_mut().add_place(place_id);
|
||||
}
|
||||
@@ -407,15 +413,15 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
fn mark_place_bound(&mut self, id: ScopedPlaceId) {
|
||||
self.current_place_table().mark_place_bound(id);
|
||||
self.current_place_table_mut().mark_place_bound(id);
|
||||
}
|
||||
|
||||
fn mark_place_declared(&mut self, id: ScopedPlaceId) {
|
||||
self.current_place_table().mark_place_declared(id);
|
||||
self.current_place_table_mut().mark_place_declared(id);
|
||||
}
|
||||
|
||||
fn mark_place_used(&mut self, id: ScopedPlaceId) {
|
||||
self.current_place_table().mark_place_used(id);
|
||||
self.current_place_table_mut().mark_place_used(id);
|
||||
}
|
||||
|
||||
fn add_entry_for_definition_key(&mut self, key: DefinitionNodeKey) -> &mut Definitions<'db> {
|
||||
@@ -1004,8 +1010,83 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_function_body(&mut self, function_def: &'ast ast::StmtFunctionDef) {
|
||||
let ast::StmtFunctionDef {
|
||||
parameters,
|
||||
type_params,
|
||||
returns,
|
||||
body,
|
||||
..
|
||||
} = function_def;
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
type_params.as_deref(),
|
||||
|builder| {
|
||||
builder.visit_parameters(parameters);
|
||||
if let Some(returns) = returns {
|
||||
builder.visit_annotation(returns);
|
||||
}
|
||||
|
||||
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
||||
|
||||
builder.declare_parameters(parameters);
|
||||
|
||||
let mut first_parameter_name = parameters
|
||||
.iter_non_variadic_params()
|
||||
.next()
|
||||
.map(|first_param| first_param.parameter.name.id().as_str());
|
||||
std::mem::swap(
|
||||
&mut builder.current_first_parameter_name,
|
||||
&mut first_parameter_name,
|
||||
);
|
||||
|
||||
builder.visit_scoped_body(body);
|
||||
|
||||
builder.current_first_parameter_name = first_parameter_name;
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Walk the body of a scope, either the global scope or a function scope.
|
||||
///
|
||||
/// When we encounter a (top-level or nested) function definition, we add the function's name
|
||||
/// to the current scope, but we defer walking its body until the end. (See the `FunctionDef`
|
||||
/// branch of `visit_stmt`.) This deferred approach is necessary to be able to check `nonlocal`
|
||||
/// statements as we encounter them, for example:
|
||||
///
|
||||
/// ```py
|
||||
/// def f():
|
||||
/// def g():
|
||||
/// nonlocal x # allowed
|
||||
/// nonlocal y # SyntaxError: no binding for nonlocal 'y' found
|
||||
/// x = 1
|
||||
/// ```
|
||||
///
|
||||
/// See the comments in the `Nonlocal` branch of `visit_stmt`, which relies on this binding
|
||||
/// information being present.
|
||||
fn visit_scoped_body(&mut self, body: &'ast [ast::Stmt]) {
|
||||
debug_assert!(
|
||||
self.deferred_function_bodies.is_empty(),
|
||||
"every function starts with a clean scope",
|
||||
);
|
||||
|
||||
// If this scope contains function definitions, they'll be added to
|
||||
// `self.deferred_function_bodies` as we walk each statement.
|
||||
self.visit_body(body);
|
||||
|
||||
// Now that we've walked all the statements in this scope, walk any deferred function
|
||||
// bodies. This is recursive, so we need to clear out the contents of
|
||||
// `self.deferred_function_bodies` and give each function a fresh list (or else we'll fail
|
||||
// the `debug_assert!` above).
|
||||
let taken_deferred_function_bodies = std::mem::take(&mut self.deferred_function_bodies);
|
||||
for function_def in taken_deferred_function_bodies {
|
||||
self.visit_function_body(function_def);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
||||
self.visit_body(self.module.suite());
|
||||
self.visit_scoped_body(self.module.suite());
|
||||
|
||||
// Pop the root scope
|
||||
self.pop_scope();
|
||||
@@ -1042,7 +1123,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.scopes_by_node.shrink_to_fit();
|
||||
self.generator_functions.shrink_to_fit();
|
||||
self.eager_snapshots.shrink_to_fit();
|
||||
self.globals_by_scope.shrink_to_fit();
|
||||
|
||||
SemanticIndex {
|
||||
place_tables,
|
||||
@@ -1050,7 +1130,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
definitions_by_node: self.definitions_by_node,
|
||||
expressions_by_node: self.expressions_by_node,
|
||||
scope_ids_by_scope: self.scope_ids_by_scope,
|
||||
globals_by_scope: self.globals_by_scope,
|
||||
ast_ids,
|
||||
scopes_by_expression: self.scopes_by_expression,
|
||||
scopes_by_node: self.scopes_by_node,
|
||||
@@ -1084,46 +1163,19 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
let ast::StmtFunctionDef {
|
||||
decorator_list,
|
||||
parameters,
|
||||
type_params,
|
||||
name,
|
||||
returns,
|
||||
body,
|
||||
is_async: _,
|
||||
range: _,
|
||||
node_index: _,
|
||||
..
|
||||
} = function_def;
|
||||
|
||||
// Like Ruff, we don't walk the body of the function here. Instead, we defer it to
|
||||
// the end of the current scope. See `visit_scoped_body`. See also the comments in
|
||||
// the `Nonlocal` branch below about why this deferred visit order is necessary.
|
||||
self.deferred_function_bodies.push(function_def);
|
||||
|
||||
for decorator in decorator_list {
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
type_params.as_deref(),
|
||||
|builder| {
|
||||
builder.visit_parameters(parameters);
|
||||
if let Some(returns) = returns {
|
||||
builder.visit_annotation(returns);
|
||||
}
|
||||
|
||||
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
||||
|
||||
builder.declare_parameters(parameters);
|
||||
|
||||
let mut first_parameter_name = parameters
|
||||
.iter_non_variadic_params()
|
||||
.next()
|
||||
.map(|first_param| first_param.parameter.name.id().as_str());
|
||||
std::mem::swap(
|
||||
&mut builder.current_first_parameter_name,
|
||||
&mut first_parameter_name,
|
||||
);
|
||||
|
||||
builder.visit_body(body);
|
||||
|
||||
builder.current_first_parameter_name = first_parameter_name;
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in parameters
|
||||
@@ -1418,6 +1470,29 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
|
||||
if let ast::Expr::Name(name) = &*node.target {
|
||||
let symbol_id = self.add_symbol(name.id.clone());
|
||||
let symbol = self.current_place_table().place_expr(symbol_id);
|
||||
// Check whether the variable has been declared global.
|
||||
if symbol.is_marked_global() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::AnnotatedGlobal(name.id.as_str().into()),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
// Check whether the variable has been declared nonlocal.
|
||||
if symbol.is_marked_nonlocal() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::AnnotatedNonlocal(
|
||||
name.id.as_str().into(),
|
||||
),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
|
||||
if matches!(
|
||||
*node.target,
|
||||
@@ -1858,8 +1933,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
}) => {
|
||||
for name in names {
|
||||
let symbol_id = self.add_symbol(name.id.clone());
|
||||
let symbol_table = self.current_place_table();
|
||||
let symbol = symbol_table.place_expr(symbol_id);
|
||||
let symbol = self.current_place_table().place_expr(symbol_id);
|
||||
// Check whether the variable has already been accessed in this scope.
|
||||
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration {
|
||||
@@ -1870,11 +1945,112 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
let scope_id = self.current_scope();
|
||||
self.globals_by_scope
|
||||
.entry(scope_id)
|
||||
.or_default()
|
||||
.insert(symbol_id);
|
||||
// Check whether the variable has also been declared nonlocal.
|
||||
if symbol.is_marked_nonlocal() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
self.current_place_table_mut().mark_place_global(symbol_id);
|
||||
}
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
ast::Stmt::Nonlocal(ast::StmtNonlocal {
|
||||
range: _,
|
||||
node_index: _,
|
||||
names,
|
||||
}) => {
|
||||
for name in names {
|
||||
let local_scoped_place_id = self.add_symbol(name.id.clone());
|
||||
let local_place = self.current_place_table().place_expr(local_scoped_place_id);
|
||||
// Check whether the variable has already been accessed in this scope.
|
||||
if local_place.is_bound() || local_place.is_declared() || local_place.is_used()
|
||||
{
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration {
|
||||
name: name.to_string(),
|
||||
start: name.range.start(),
|
||||
},
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
// Check whether the variable has also been declared global.
|
||||
if local_place.is_marked_global() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
// The name is required to exist in an enclosing scope, but that definition
|
||||
// might come later. For example, this is example legal:
|
||||
//
|
||||
// ```py
|
||||
// def f():
|
||||
// def g():
|
||||
// nonlocal x
|
||||
// x = 1
|
||||
// ```
|
||||
//
|
||||
// To handle cases like this, we have to walk `x = 1` before we walk `nonlocal
|
||||
// x`. In other words, walking function bodies must be "deferred" to the end of
|
||||
// the scope where they're defined. See the `FunctionDef` branch above.
|
||||
let name_expr = PlaceExpr::name(name.id.clone());
|
||||
let mut found_matching_definition = false;
|
||||
for enclosing_scope_info in self.scope_stack.iter().rev().skip(1) {
|
||||
let enclosing_scope = &self.scopes[enclosing_scope_info.file_scope_id];
|
||||
if !enclosing_scope.kind().is_function_like() {
|
||||
// Skip over class scopes and the global scope.
|
||||
continue;
|
||||
}
|
||||
let enclosing_place_table =
|
||||
&self.place_tables[enclosing_scope_info.file_scope_id];
|
||||
let Some(enclosing_scoped_place_id) =
|
||||
enclosing_place_table.place_id_by_expr(&name_expr)
|
||||
else {
|
||||
// This name isn't defined in this scope. Keep going.
|
||||
continue;
|
||||
};
|
||||
let enclosing_place =
|
||||
enclosing_place_table.place_expr(enclosing_scoped_place_id);
|
||||
// We've found a definition for this name in an enclosing function-like
|
||||
// scope. Either this definition is the valid place this name refers to, or
|
||||
// else we'll emit a syntax error. Either way, we won't walk any more
|
||||
// enclosing scopes. Note that there are differences here compared to
|
||||
// `infer_place_load`: A regular load (e.g. `print(x)`) is allowed to refer
|
||||
// to a global variable (e.g. `x = 1` in the global scope), and similarly
|
||||
// it's allowed to refer to a variable in an enclosing function that's
|
||||
// declared `global` (e.g. `global x`). However, the `nonlocal` keyword
|
||||
// can't refer to global variables (that's a `SyntaxError`), and it also
|
||||
// can't refer to variables in enclosing functions that are declared
|
||||
// `global` (also a `SyntaxError`).
|
||||
if enclosing_place.is_marked_global() {
|
||||
// A "chain" of `nonlocal` statements is "broken" by a `global`
|
||||
// statement. Stop looping and report that this `nonlocal` statement is
|
||||
// invalid.
|
||||
break;
|
||||
}
|
||||
// We found a definition, and we've checked that that place isn't declared
|
||||
// `global` in its scope, but it's ok if it's `nonlocal`. If a chain of
|
||||
// `nonlocal` statements fails to lead to a valid binding, the outermost
|
||||
// one will be an error; we don't need to report an error for each one.
|
||||
found_matching_definition = true;
|
||||
self.current_place_table_mut()
|
||||
.mark_place_nonlocal(local_scoped_place_id);
|
||||
break;
|
||||
}
|
||||
if !found_matching_definition {
|
||||
// There's no matching definition in an enclosing scope. This `nonlocal`
|
||||
// statement is invalid.
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::InvalidNonlocal(name.to_string()),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
}
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
@@ -1888,7 +2064,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
for target in targets {
|
||||
if let Ok(target) = PlaceExpr::try_from(target) {
|
||||
let place_id = self.add_place(PlaceExprWithFlags::new(target));
|
||||
self.current_place_table().mark_place_used(place_id);
|
||||
self.current_place_table_mut().mark_place_used(place_id);
|
||||
self.delete_binding(place_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -330,6 +330,16 @@ impl PlaceExprWithFlags {
|
||||
self.flags.contains(PlaceFlags::IS_DECLARED)
|
||||
}
|
||||
|
||||
/// Is the place `global` its containing scope?
|
||||
pub fn is_marked_global(&self) -> bool {
|
||||
self.flags.contains(PlaceFlags::MARKED_GLOBAL)
|
||||
}
|
||||
|
||||
/// Is the place `nonlocal` its containing scope?
|
||||
pub fn is_marked_nonlocal(&self) -> bool {
|
||||
self.flags.contains(PlaceFlags::MARKED_NONLOCAL)
|
||||
}
|
||||
|
||||
pub(crate) fn as_name(&self) -> Option<&Name> {
|
||||
self.expr.as_name()
|
||||
}
|
||||
@@ -397,9 +407,7 @@ bitflags! {
|
||||
const IS_USED = 1 << 0;
|
||||
const IS_BOUND = 1 << 1;
|
||||
const IS_DECLARED = 1 << 2;
|
||||
/// TODO: This flag is not yet set by anything
|
||||
const MARKED_GLOBAL = 1 << 3;
|
||||
/// TODO: This flag is not yet set by anything
|
||||
const MARKED_NONLOCAL = 1 << 4;
|
||||
const IS_INSTANCE_ATTRIBUTE = 1 << 5;
|
||||
}
|
||||
@@ -663,7 +671,7 @@ impl PlaceTable {
|
||||
}
|
||||
|
||||
/// Returns the place named `name`.
|
||||
#[allow(unused)] // used in tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExprWithFlags> {
|
||||
let id = self.place_id_by_name(name)?;
|
||||
Some(self.place_expr(id))
|
||||
@@ -814,6 +822,14 @@ impl PlaceTableBuilder {
|
||||
self.table.places[id].insert_flags(PlaceFlags::IS_USED);
|
||||
}
|
||||
|
||||
pub(super) fn mark_place_global(&mut self, id: ScopedPlaceId) {
|
||||
self.table.places[id].insert_flags(PlaceFlags::MARKED_GLOBAL);
|
||||
}
|
||||
|
||||
pub(super) fn mark_place_nonlocal(&mut self, id: ScopedPlaceId) {
|
||||
self.table.places[id].insert_flags(PlaceFlags::MARKED_NONLOCAL);
|
||||
}
|
||||
|
||||
pub(super) fn places(&self) -> impl Iterator<Item = &PlaceExprWithFlags> {
|
||||
self.table.places()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
@@ -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(¶meter.display(self.db), param_name.as_deref())?;
|
||||
|
||||
first = false;
|
||||
join.entry(¶meter.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(¶m_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 }
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
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,
|
||||
};
|
||||
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>(
|
||||
@@ -357,73 +353,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![]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -1562,6 +1564,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let mut bound_ty = ty;
|
||||
|
||||
let global_use_def_map = self.index.use_def_map(FileScopeId::global());
|
||||
let nonlocal_use_def_map;
|
||||
let place_id = binding.place(self.db());
|
||||
let place = place_table.place_expr(place_id);
|
||||
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id);
|
||||
@@ -1572,9 +1575,58 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.place_id_by_expr(&place.expr)
|
||||
{
|
||||
Some(id) => (global_use_def_map.end_of_scope_declarations(id), false),
|
||||
// This case is a syntax error (load before global declaration) but ignore that here
|
||||
// This variable shows up in `global` declarations but doesn't have an explicit
|
||||
// binding in the global scope.
|
||||
None => (use_def.declarations_at_binding(binding), true),
|
||||
}
|
||||
} else if self
|
||||
.index
|
||||
.symbol_is_nonlocal_in_scope(place_id, file_scope_id)
|
||||
{
|
||||
// If we run out of ancestor scopes without finding a definition, we'll fall back to
|
||||
// the local scope. This will also be a syntax error in `infer_nonlocal_statement` (no
|
||||
// binding for `nonlocal` found), but ignore that here.
|
||||
let mut declarations = use_def.declarations_at_binding(binding);
|
||||
let mut is_local = true;
|
||||
// Walk up parent scopes looking for the enclosing scope that has definition of this
|
||||
// name. `ancestor_scopes` includes the current scope, so skip that one.
|
||||
for (enclosing_scope_file_id, enclosing_scope) in
|
||||
self.index.ancestor_scopes(file_scope_id).skip(1)
|
||||
{
|
||||
// Ignore class scopes and the global scope.
|
||||
if !enclosing_scope.kind().is_function_like() {
|
||||
continue;
|
||||
}
|
||||
let enclosing_place_table = self.index.place_table(enclosing_scope_file_id);
|
||||
let Some(enclosing_place_id) = enclosing_place_table.place_id_by_expr(&place.expr)
|
||||
else {
|
||||
// This ancestor scope doesn't have a binding. Keep going.
|
||||
continue;
|
||||
};
|
||||
if self
|
||||
.index
|
||||
.symbol_is_nonlocal_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||
{
|
||||
// The variable is `nonlocal` in this ancestor scope. Keep going.
|
||||
continue;
|
||||
}
|
||||
if self
|
||||
.index
|
||||
.symbol_is_global_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||
{
|
||||
// The variable is `global` in this ancestor scope. This breaks the `nonlocal`
|
||||
// chain, and it's a syntax error in `infer_nonlocal_statement`. Ignore that
|
||||
// here and just bail out of this loop.
|
||||
break;
|
||||
}
|
||||
// We found the closest definition. Note that (unlike in `infer_place_load`) this
|
||||
// does *not* need to be a binding. It could be just `x: int`.
|
||||
nonlocal_use_def_map = self.index.use_def_map(enclosing_scope_file_id);
|
||||
declarations = nonlocal_use_def_map.end_of_scope_declarations(enclosing_place_id);
|
||||
is_local = false;
|
||||
break;
|
||||
}
|
||||
(declarations, is_local)
|
||||
} else {
|
||||
(use_def.declarations_at_binding(binding), true)
|
||||
};
|
||||
@@ -1915,7 +1967,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 +4676,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 +5412,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);
|
||||
|
||||
@@ -5775,13 +5850,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
let current_file = self.file();
|
||||
|
||||
let mut is_nonlocal_binding = false;
|
||||
if let Some(name) = expr.as_name() {
|
||||
let skip_non_global_scopes = place_table
|
||||
.place_id_by_name(name)
|
||||
.is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id));
|
||||
|
||||
if skip_non_global_scopes {
|
||||
return global_symbol(self.db(), self.file(), name);
|
||||
if let Some(symbol_id) = place_table.place_id_by_name(name) {
|
||||
if self.skip_non_global_scopes(file_scope_id, symbol_id) {
|
||||
return global_symbol(self.db(), self.file(), name);
|
||||
}
|
||||
is_nonlocal_binding = self
|
||||
.index
|
||||
.symbol_is_nonlocal_in_scope(symbol_id, file_scope_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5794,7 +5871,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// a local variable or not in function-like scopes. If a variable has any bindings in a
|
||||
// function-like scope, it is considered a local variable; it never references another
|
||||
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
|
||||
if has_bindings_in_this_scope && scope.is_function_like(db) {
|
||||
if has_bindings_in_this_scope && scope.is_function_like(db) && !is_nonlocal_binding {
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()),
|
||||
)),
|
||||
|
||||
@@ -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,
|
||||
>(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(¶ms.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(¶m.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 {}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "scripts"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user