Compare commits
2 Commits
0.11.9
...
micha/more
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f78c03c5dc | ||
|
|
1fa5e1fbbc |
@@ -5,7 +5,7 @@ exclude: |
|
||||
.github/workflows/release.yml|
|
||||
crates/ty_vendored/vendor/.*|
|
||||
crates/ty_project/resources/.*|
|
||||
crates/ty/docs/(configuration|rules|cli).md|
|
||||
crates/ty/docs/rules.md|
|
||||
crates/ruff_benchmark/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,37 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.9
|
||||
|
||||
### Preview features
|
||||
|
||||
- Default to latest supported Python version for version-related syntax errors ([#17529](https://github.com/astral-sh/ruff/pull/17529))
|
||||
- Implement deferred annotations for Python 3.14 ([#17658](https://github.com/astral-sh/ruff/pull/17658))
|
||||
- \[`airflow`\] Fix `SQLTableCheckOperator` typo (`AIR302`) ([#17946](https://github.com/astral-sh/ruff/pull/17946))
|
||||
- \[`airflow`\] Remove `airflow.utils.dag_parsing_context.get_parsing_context` (`AIR301`) ([#17852](https://github.com/astral-sh/ruff/pull/17852))
|
||||
- \[`airflow`\] Skip attribute check in try catch block (`AIR301`) ([#17790](https://github.com/astral-sh/ruff/pull/17790))
|
||||
- \[`flake8-bandit`\] Mark tuples of string literals as trusted input in `S603` ([#17801](https://github.com/astral-sh/ruff/pull/17801))
|
||||
- \[`isort`\] Check full module path against project root(s) when categorizing first-party imports ([#16565](https://github.com/astral-sh/ruff/pull/16565))
|
||||
- \[`ruff`\] Add new rule `in-empty-collection` (`RUF060`) ([#16480](https://github.com/astral-sh/ruff/pull/16480))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix missing `combine` call for `lint.typing-extensions` setting ([#17823](https://github.com/astral-sh/ruff/pull/17823))
|
||||
- \[`flake8-async`\] Fix module name in `ASYNC110`, `ASYNC115`, and `ASYNC116` fixes ([#17774](https://github.com/astral-sh/ruff/pull/17774))
|
||||
- \[`pyupgrade`\] Add spaces between tokens as necessary to avoid syntax errors in `UP018` autofix ([#17648](https://github.com/astral-sh/ruff/pull/17648))
|
||||
- \[`refurb`\] Fix false positive for float and complex numbers in `FURB116` ([#17661](https://github.com/astral-sh/ruff/pull/17661))
|
||||
- [parser] Flag single unparenthesized generator expr with trailing comma in arguments. ([#17893](https://github.com/astral-sh/ruff/pull/17893))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add instructions on how to upgrade to a newer Rust version ([#17928](https://github.com/astral-sh/ruff/pull/17928))
|
||||
- Update code of conduct email address ([#17875](https://github.com/astral-sh/ruff/pull/17875))
|
||||
- Add fix safety sections to `PLC2801`, `PLR1722`, and `RUF013` ([#17825](https://github.com/astral-sh/ruff/pull/17825), [#17826](https://github.com/astral-sh/ruff/pull/17826), [#17759](https://github.com/astral-sh/ruff/pull/17759))
|
||||
- Add link to `check-typed-exception` from `S110` and `S112` ([#17786](https://github.com/astral-sh/ruff/pull/17786))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Allow passing a virtual environment to `ruff analyze graph` ([#17743](https://github.com/astral-sh/ruff/pull/17743))
|
||||
|
||||
## 0.11.8
|
||||
|
||||
### Preview features
|
||||
|
||||
46
Cargo.lock
generated
46
Cargo.lock
generated
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -487,7 +487,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1771,15 +1771,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
|
||||
dependencies = [
|
||||
"unicode-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@@ -2558,7 +2549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.9"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2592,7 +2583,6 @@ dependencies = [
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
@@ -2711,7 +2701,6 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"markdown",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
"regex",
|
||||
@@ -2720,7 +2709,6 @@ dependencies = [
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_notebook",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
@@ -2737,7 +2725,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
"ty",
|
||||
"ty_project",
|
||||
"url",
|
||||
]
|
||||
@@ -2799,7 +2786,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.9"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2889,13 +2876,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_options_metadata"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
@@ -3134,7 +3114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.9"
|
||||
version = "0.11.8"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3182,7 +3162,6 @@ dependencies = [
|
||||
"ruff_graph",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_semantic",
|
||||
@@ -3260,7 +3239,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=af69cc11146352ec2eb6d972537e99c473ac3748#af69cc11146352ec2eb6d972537e99c473ac3748"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3283,12 +3262,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=af69cc11146352ec2eb6d972537e99c473ac3748#af69cc11146352ec2eb6d972537e99c473ac3748"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=af69cc11146352ec2eb6d972537e99c473ac3748#af69cc11146352ec2eb6d972537e99c473ac3748"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -4036,7 +4015,6 @@ dependencies = [
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_macros",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_text_size",
|
||||
@@ -4243,12 +4221,6 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-id"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -4625,7 +4597,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -23,7 +23,6 @@ ruff_index = { path = "crates/ruff_index" }
|
||||
ruff_linter = { path = "crates/ruff_linter" }
|
||||
ruff_macros = { path = "crates/ruff_macros" }
|
||||
ruff_notebook = { path = "crates/ruff_notebook" }
|
||||
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
|
||||
ruff_python_ast = { path = "crates/ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
|
||||
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
|
||||
@@ -38,7 +37,6 @@ ruff_source_file = { path = "crates/ruff_source_file" }
|
||||
ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
ty = { path = "crates/ty" }
|
||||
ty_ide = { path = "crates/ty_ide" }
|
||||
ty_project = { path = "crates/ty_project", default-features = false }
|
||||
ty_python_semantic = { path = "crates/ty_python_semantic" }
|
||||
@@ -126,7 +124,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "af69cc11146352ec2eb6d972537e99c473ac3748" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -184,7 +182,7 @@ wild = { version = "2" }
|
||||
zip = { version = "0.6.6", default-features = false }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["getrandom", "ruff_options_metadata"]
|
||||
ignored = ["getrandom"]
|
||||
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -149,8 +149,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.11.9/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.9/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.8/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.8/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,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.11.9
|
||||
rev: v0.11.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.11.9"
|
||||
version = "0.11.8"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -20,7 +20,6 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] }
|
||||
ruff_linter = { workspace = true, features = ["clap"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true, features = ["serde"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
@@ -22,12 +22,12 @@ use ruff_linter::settings::types::{
|
||||
PythonVersion, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
@@ -2,8 +2,10 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
|
||||
use itertools::Itertools;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::{
|
||||
options::Options,
|
||||
options_base::{OptionField, OptionSet, OptionsMetadata, Visit},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
|
||||
@@ -2,8 +2,8 @@ use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::args::HelpFormat;
|
||||
|
||||
use ruff_options_metadata::OptionsMetadata;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
#[expect(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {
|
||||
|
||||
@@ -277,7 +277,7 @@ impl std::panic::RefUnwindSafe for Files {}
|
||||
#[salsa::input]
|
||||
pub struct File {
|
||||
/// The path of the file (immutable).
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub path: FilePath,
|
||||
|
||||
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::Db;
|
||||
#[salsa::input(debug)]
|
||||
pub struct FileRoot {
|
||||
/// The path of a root is guaranteed to never change.
|
||||
#[returns(deref)]
|
||||
pub path: SystemPathBuf,
|
||||
#[return_ref]
|
||||
path_buf: SystemPathBuf,
|
||||
|
||||
/// The kind of the root at the time of its creation.
|
||||
kind_at_time_of_creation: FileRootKind,
|
||||
@@ -32,6 +32,10 @@ pub struct FileRoot {
|
||||
}
|
||||
|
||||
impl FileRoot {
|
||||
pub fn path(self, db: &dyn Db) -> &SystemPath {
|
||||
self.path_buf(db)
|
||||
}
|
||||
|
||||
pub fn durability(self, db: &dyn Db) -> salsa::Durability {
|
||||
self.kind_at_time_of_creation(db).durability()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::Db;
|
||||
/// reflected in the changed AST offsets.
|
||||
/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires
|
||||
/// for determining if a query result is unchanged.
|
||||
#[salsa::tracked(returns(ref), no_eq)]
|
||||
#[salsa::tracked(return_ref, no_eq)]
|
||||
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
let _span = tracing::trace_span!("parsed_module", ?file).entered();
|
||||
|
||||
|
||||
@@ -11,14 +11,12 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ty = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["schemars"] }
|
||||
ruff = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_formatter = { workspace = true }
|
||||
ruff_linter = { workspace = true, features = ["schemars"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_codegen = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
@@ -33,7 +31,6 @@ imara-diff = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
libcst = { workspace = true }
|
||||
markdown = { version = "1.0.0" }
|
||||
pretty_assertions = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
|
||||
generate_ty_options, generate_ty_rules, generate_ty_schema,
|
||||
generate_cli_help, generate_docs, generate_json_schema, generate_ty_rules, generate_ty_schema,
|
||||
};
|
||||
|
||||
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
|
||||
@@ -41,8 +40,6 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
generate_docs::main(&generate_docs::Args {
|
||||
dry_run: args.mode.is_dry_run(),
|
||||
})?;
|
||||
generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?;
|
||||
generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?;
|
||||
generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Generate CLI help.
|
||||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, str};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Generate Markdown documentation for applicable rules.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write as _;
|
||||
@@ -12,8 +13,8 @@ use strum::IntoEnumIterator;
|
||||
|
||||
use ruff_diagnostics::FixAvailability;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
use itertools::Itertools;
|
||||
use std::fmt::Write;
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_python_trivia::textwrap;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
|
||||
pub(crate) fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -11,8 +11,8 @@ use strum::IntoEnumIterator;
|
||||
use ruff_diagnostics::FixAvailability;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
|
||||
use ruff_options_metadata::OptionsMetadata;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
//! Generate a Markdown-compatible reference for the ty command-line interface.
|
||||
use std::cmp::max;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Command, CommandFactory};
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::StrComparison;
|
||||
|
||||
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
use ty::Cli;
|
||||
|
||||
const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"];
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let reference_string = generate();
|
||||
let filename = "crates/ty/docs/cli.md";
|
||||
let reference_path = PathBuf::from(ROOT_DIR).join(filename);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{reference_string}");
|
||||
}
|
||||
Mode::Check => {
|
||||
match std::fs::read_to_string(reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &reference_string);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
bail!("{filename} not found, please run `{REGENERATE_ALL_COMMAND}`");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Write => match std::fs::read_to_string(&reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
println!("Updating: {filename}");
|
||||
std::fs::write(reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
println!("Updating: {filename}");
|
||||
std::fs::write(reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut ty = Cli::command();
|
||||
|
||||
// It is very important to build the command before beginning inspection or subcommands
|
||||
// will be missing all of the propagated options.
|
||||
ty.build();
|
||||
|
||||
let mut parents = Vec::new();
|
||||
|
||||
output.push_str("# CLI Reference\n\n");
|
||||
generate_command(&mut output, &ty, &mut parents);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[allow(clippy::format_push_string)]
|
||||
fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) {
|
||||
if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the command header.
|
||||
let name = if parents.is_empty() {
|
||||
command.get_name().to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{} {}",
|
||||
parents.iter().map(|cmd| cmd.get_name()).join(" "),
|
||||
command.get_name()
|
||||
)
|
||||
};
|
||||
|
||||
// Display the top-level `ty` command at the same level as its children
|
||||
let level = max(2, parents.len() + 1);
|
||||
output.push_str(&format!("{} {name}\n\n", "#".repeat(level)));
|
||||
|
||||
// Display the command description.
|
||||
if let Some(about) = command.get_long_about().or_else(|| command.get_about()) {
|
||||
output.push_str(&about.to_string());
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
// Display the usage
|
||||
{
|
||||
// This appears to be the simplest way to get rendered usage from Clap,
|
||||
// it is complicated to render it manually. It's annoying that it
|
||||
// requires a mutable reference but it doesn't really matter.
|
||||
let mut command = command.clone();
|
||||
output.push_str("<h3 class=\"cli-reference\">Usage</h3>\n\n");
|
||||
output.push_str(&format!(
|
||||
"```\n{}\n```",
|
||||
command
|
||||
.render_usage()
|
||||
.to_string()
|
||||
.trim_start_matches("Usage: "),
|
||||
));
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
if command.get_name() == "help" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a list of child commands
|
||||
let mut subcommands = command.get_subcommands().peekable();
|
||||
let has_subcommands = subcommands.peek().is_some();
|
||||
if has_subcommands {
|
||||
output.push_str("<h3 class=\"cli-reference\">Commands</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for subcommand in subcommands {
|
||||
if subcommand.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
let subcommand_name = format!("{name} {}", subcommand.get_name());
|
||||
output.push_str(&format!(
|
||||
"<dt><a href=\"#{}\"><code>{subcommand_name}</code></a></dt>",
|
||||
subcommand_name.replace(' ', "-")
|
||||
));
|
||||
if let Some(about) = subcommand.get_about() {
|
||||
output.push_str(&format!(
|
||||
"<dd>{}</dd>\n",
|
||||
markdown::to_html(&about.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Do not display options for commands with children
|
||||
if !has_subcommands {
|
||||
let name_key = name.replace(' ', "-");
|
||||
|
||||
// Display positional arguments
|
||||
let mut arguments = command
|
||||
.get_positionals()
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.peekable();
|
||||
|
||||
if arguments.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Arguments</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for arg in arguments {
|
||||
let id = format!("{name_key}--{}", arg.get_id());
|
||||
output.push_str(&format!("<dt id=\"{id}\">"));
|
||||
output.push_str(&format!(
|
||||
"<a href=\"#{id}\"<code>{}</code></a>",
|
||||
arg.get_id().to_string().to_uppercase(),
|
||||
));
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Display options and flags
|
||||
let mut options = command
|
||||
.get_arguments()
|
||||
.filter(|arg| !arg.is_positional())
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.sorted_by_key(|arg| arg.get_id())
|
||||
.peekable();
|
||||
|
||||
if options.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Options</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
for opt in options {
|
||||
let Some(long) = opt.get_long() else { continue };
|
||||
let id = format!("{name_key}--{long}");
|
||||
|
||||
output.push_str(&format!("<dt id=\"{id}\">"));
|
||||
output.push_str(&format!("<a href=\"#{id}\"><code>--{long}</code></a>"));
|
||||
for long_alias in opt.get_all_aliases().into_iter().flatten() {
|
||||
output.push_str(&format!(", <code>--{long_alias}</code>"));
|
||||
}
|
||||
if let Some(short) = opt.get_short() {
|
||||
output.push_str(&format!(", <code>-{short}</code>"));
|
||||
}
|
||||
for short_alias in opt.get_all_short_aliases().into_iter().flatten() {
|
||||
output.push_str(&format!(", <code>-{short_alias}</code>"));
|
||||
}
|
||||
|
||||
// Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts`
|
||||
if opt
|
||||
.get_num_args()
|
||||
.unwrap_or_else(|| 1.into())
|
||||
.takes_values()
|
||||
{
|
||||
if let Some(values) = opt.get_value_names() {
|
||||
for value in values {
|
||||
output.push_str(&format!(
|
||||
" <i>{}</i>",
|
||||
value.to_lowercase().replace('_', "-")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
emit_env_option(opt, output);
|
||||
emit_default_option(opt, output);
|
||||
emit_possible_options(opt, output);
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>");
|
||||
}
|
||||
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
parents.push(command);
|
||||
|
||||
// Recurse to all of the subcommands.
|
||||
for subcommand in command.get_subcommands() {
|
||||
generate_command(output, subcommand, parents);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
fn emit_env_option(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_env_set() {
|
||||
return;
|
||||
}
|
||||
if let Some(env) = opt.get_env() {
|
||||
output.push_str(&markdown::to_html(&format!(
|
||||
"May also be set with the `{}` environment variable.",
|
||||
env.to_string_lossy()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_default_option(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_default_value_set() || !opt.get_num_args().expect("built").takes_values() {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = opt.get_default_values();
|
||||
if !values.is_empty() {
|
||||
let value = format!(
|
||||
"\n[default: {}]",
|
||||
opt.get_default_values()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.join(",")
|
||||
);
|
||||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_possible_options(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_possible_values_set() {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = opt.get_possible_values();
|
||||
if !values.is_empty() {
|
||||
let value = format!(
|
||||
"\nPossible values:\n{}",
|
||||
values
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_hide_set())
|
||||
.map(|value| {
|
||||
let name = value.get_name();
|
||||
value.get_help().map_or_else(
|
||||
|| format!(" - `{name}`"),
|
||||
|help| format!(" - `{name}`: {help}"),
|
||||
)
|
||||
})
|
||||
.collect_vec()
|
||||
.join("\n"),
|
||||
);
|
||||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_cli_reference_is_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
|
||||
|
||||
use anyhow::bail;
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::StrComparison;
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ty_project::metadata::Options;
|
||||
|
||||
use crate::{
|
||||
generate_all::{Mode, REGENERATE_ALL_COMMAND},
|
||||
ROOT_DIR,
|
||||
};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
/// Write the generated table to stdout (rather than to `crates/ty/docs/configuration.md`).
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
|
||||
let mut output = String::new();
|
||||
let file_name = "crates/ty/docs/configuration.md";
|
||||
let markdown_path = PathBuf::from(ROOT_DIR).join(file_name);
|
||||
|
||||
generate_set(
|
||||
&mut output,
|
||||
Set::Toplevel(Options::metadata()),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{output}");
|
||||
}
|
||||
Mode::Check => {
|
||||
let current = std::fs::read_to_string(&markdown_path)?;
|
||||
if output == current {
|
||||
println!("Up-to-date: {file_name}",);
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &output);
|
||||
bail!("{file_name} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}",);
|
||||
}
|
||||
}
|
||||
Mode::Write => {
|
||||
let current = std::fs::read_to_string(&markdown_path)?;
|
||||
if current == output {
|
||||
println!("Up-to-date: {file_name}",);
|
||||
} else {
|
||||
println!("Updating: {file_name}",);
|
||||
std::fs::write(markdown_path, output.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
match &set {
|
||||
Set::Toplevel(_) => {
|
||||
output.push_str("# Configuration\n");
|
||||
}
|
||||
Set::Named { name, .. } => {
|
||||
let title = parents
|
||||
.iter()
|
||||
.filter_map(|set| set.name())
|
||||
.chain(std::iter::once(name.as_str()))
|
||||
.join(".");
|
||||
writeln!(output, "## `{title}`\n",).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(documentation) = set.metadata().documentation() {
|
||||
output.push_str(documentation);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let mut visitor = CollectOptionsVisitor::default();
|
||||
set.metadata().record(&mut visitor);
|
||||
|
||||
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
|
||||
|
||||
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
|
||||
parents.push(set);
|
||||
|
||||
// Generate the fields.
|
||||
for (name, field) in &fields {
|
||||
emit_field(output, name, field, parents.as_slice());
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
// Generate all the sub-sets.
|
||||
for (set_name, sub_set) in &sets {
|
||||
generate_set(
|
||||
output,
|
||||
Set::Named {
|
||||
name: set_name.to_string(),
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
enum Set {
|
||||
Toplevel(OptionSet),
|
||||
Named { name: String, set: OptionSet },
|
||||
}
|
||||
|
||||
impl Set {
|
||||
fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Set::Toplevel(_) => None,
|
||||
Set::Named { name, .. } => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &OptionSet {
|
||||
match self {
|
||||
Set::Toplevel(set) => set,
|
||||
Set::Named { set, .. } => set,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
|
||||
let header_level = if parents.is_empty() { "###" } else { "####" };
|
||||
|
||||
let _ = writeln!(output, "{header_level} [`{name}`]");
|
||||
|
||||
output.push('\n');
|
||||
|
||||
if let Some(deprecated) = &field.deprecated {
|
||||
output.push_str("!!! warning \"Deprecated\"\n");
|
||||
output.push_str(" This option has been deprecated");
|
||||
|
||||
if let Some(since) = deprecated.since {
|
||||
write!(output, " in {since}").unwrap();
|
||||
}
|
||||
|
||||
output.push('.');
|
||||
|
||||
if let Some(message) = deprecated.message {
|
||||
writeln!(output, " {message}").unwrap();
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output.push_str(field.doc);
|
||||
output.push_str("\n\n");
|
||||
let _ = writeln!(output, "**Default value**: `{}`", field.default);
|
||||
output.push('\n');
|
||||
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
|
||||
output.push('\n');
|
||||
output.push_str("**Example usage** (`pyproject.toml`):\n\n");
|
||||
output.push_str(&format_example(
|
||||
&format_header(
|
||||
field.scope,
|
||||
field.example,
|
||||
parents,
|
||||
ConfigurationFile::PyprojectToml,
|
||||
),
|
||||
field.example,
|
||||
));
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
fn format_example(header: &str, content: &str) -> String {
|
||||
if header.is_empty() {
|
||||
format!("```toml\n{content}\n```\n",)
|
||||
} else {
|
||||
format!("```toml\n{header}\n{content}\n```\n",)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the TOML header for the example usage for a given option.
|
||||
///
|
||||
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
|
||||
fn format_header(
|
||||
scope: Option<&str>,
|
||||
example: &str,
|
||||
parents: &[Set],
|
||||
configuration: ConfigurationFile,
|
||||
) -> String {
|
||||
let tool_parent = match configuration {
|
||||
ConfigurationFile::PyprojectToml => Some("tool.ty"),
|
||||
ConfigurationFile::TyToml => None,
|
||||
};
|
||||
|
||||
let header = tool_parent
|
||||
.into_iter()
|
||||
.chain(parents.iter().filter_map(|parent| parent.name()))
|
||||
.chain(scope)
|
||||
.join(".");
|
||||
|
||||
// Ex) `[[tool.ty.xx]]`
|
||||
if example.starts_with(&format!("[[{header}")) {
|
||||
return String::new();
|
||||
}
|
||||
// Ex) `[tool.ty.rules]`
|
||||
if example.starts_with(&format!("[{header}")) {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
if header.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("[{header}]")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
groups: Vec<(String, OptionSet)>,
|
||||
fields: Vec<(String, OptionField)>,
|
||||
}
|
||||
|
||||
impl Visit for CollectOptionsVisitor {
|
||||
fn record_set(&mut self, name: &str, group: OptionSet) {
|
||||
self.groups.push((name.to_owned(), group));
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
self.fields.push((name.to_owned(), field));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum ConfigurationFile {
|
||||
PyprojectToml,
|
||||
#[expect(dead_code)]
|
||||
TyToml,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_configuration_markdown_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Generates the rules table for ty
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//!
|
||||
//! Within the ruff repository you can run it with `cargo dev`.
|
||||
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use ruff::{args::GlobalConfigArgs, check};
|
||||
@@ -17,8 +15,6 @@ mod generate_docs;
|
||||
mod generate_json_schema;
|
||||
mod generate_options;
|
||||
mod generate_rules_table;
|
||||
mod generate_ty_cli_reference;
|
||||
mod generate_ty_options;
|
||||
mod generate_ty_rules;
|
||||
mod generate_ty_schema;
|
||||
mod print_ast;
|
||||
@@ -52,7 +48,6 @@ enum Command {
|
||||
GenerateTyRules(generate_ty_rules::Args),
|
||||
/// Generate a Markdown-compatible listing of configuration options.
|
||||
GenerateOptions,
|
||||
GenerateTyOptions(generate_ty_options::Args),
|
||||
/// Generate CLI help.
|
||||
GenerateCliHelp(generate_cli_help::Args),
|
||||
/// Generate Markdown docs.
|
||||
@@ -97,7 +92,6 @@ fn main() -> Result<ExitCode> {
|
||||
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
|
||||
Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?,
|
||||
Command::GenerateOptions => println!("{}", generate_options::generate()),
|
||||
Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?,
|
||||
Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
|
||||
Command::GenerateDocs(args) => generate_docs::main(&args)?,
|
||||
Command::PrintAST(args) => print_ast::main(&args)?,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Print the AST for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Print the `LibCST` CST for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Print the token stream for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Run round-trip source code generation on a given Python or Jupyter notebook file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -88,8 +88,8 @@ impl Db for ModuleDb {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.9"
|
||||
version = "0.11.8"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -86,8 +86,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl ruff_options_metadata::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn ruff_options_metadata::Visit) {
|
||||
impl crate::options_base::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn crate::options_base::Visit) {
|
||||
#(#output);*
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => (visit.record_set(#kebab_name, ruff_options_metadata::OptionSet::of::<#path>()))
|
||||
ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>()))
|
||||
))
|
||||
}
|
||||
_ => Err(syn::Error::new(
|
||||
@@ -214,14 +214,14 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
|
||||
let note = quote_option(deprecated.note);
|
||||
let since = quote_option(deprecated.since);
|
||||
|
||||
quote!(Some(ruff_options_metadata::Deprecated { since: #since, message: #note }))
|
||||
quote!(Some(crate::options_base::Deprecated { since: #since, message: #note }))
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => {
|
||||
visit.record_field(#kebab_name, ruff_options_metadata::OptionField{
|
||||
visit.record_field(#kebab_name, crate::options_base::OptionField{
|
||||
doc: &#doc,
|
||||
default: &#default,
|
||||
value_type: &#value_type,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "ruff_options_metadata"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -60,7 +60,6 @@ impl PythonVersion {
|
||||
}
|
||||
|
||||
pub const fn latest_ty() -> Self {
|
||||
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
|
||||
Self::PY313
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.9"
|
||||
version = "0.11.8"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -18,7 +18,6 @@ ruff_formatter = { workspace = true }
|
||||
ruff_graph = { workspace = true, features = ["serde", "schemars"] }
|
||||
ruff_linter = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true, features = ["serde"] }
|
||||
ruff_python_semantic = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod options;
|
||||
pub mod pyproject;
|
||||
pub mod resolver;
|
||||
|
||||
pub mod options_base;
|
||||
mod settings;
|
||||
|
||||
pub use settings::{FileResolverSettings, FormatterSettings, Settings};
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::options_base::{OptionsMetadata, Visit};
|
||||
use crate::settings::LineEnding;
|
||||
use ruff_formatter::IndentStyle;
|
||||
use ruff_graph::Direction;
|
||||
@@ -31,7 +32,6 @@ use ruff_linter::settings::types::{
|
||||
};
|
||||
use ruff_linter::{warn_user_once, RuleSelector};
|
||||
use ruff_macros::{CombineOptions, OptionsMetadata};
|
||||
use ruff_options_metadata::{OptionsMetadata, Visit};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle};
|
||||
use ruff_python_semantic::NameImports;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
/// Visits [`OptionsMetadata`].
|
||||
@@ -39,8 +42,8 @@ where
|
||||
}
|
||||
|
||||
/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(::serde::Serialize), serde(untagged))]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OptionEntry {
|
||||
/// A single option.
|
||||
Field(OptionField),
|
||||
@@ -99,7 +102,7 @@ impl OptionSet {
|
||||
/// ### Test for the existence of a child option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// struct WithOptions;
|
||||
///
|
||||
@@ -122,7 +125,7 @@ impl OptionSet {
|
||||
/// ### Test for the existence of a nested option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// struct Root;
|
||||
///
|
||||
@@ -173,7 +176,7 @@ impl OptionSet {
|
||||
/// ### Find a child option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// struct WithOptions;
|
||||
///
|
||||
@@ -198,7 +201,7 @@ impl OptionSet {
|
||||
/// ### Find a nested option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// static HARD_TABS: OptionField = OptionField {
|
||||
/// doc: "Use hard tabs for indentation and spaces for alignment.",
|
||||
@@ -342,14 +345,51 @@ impl Display for OptionSet {
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializeVisitor<'a> {
|
||||
entries: &'a mut BTreeMap<String, OptionField>,
|
||||
}
|
||||
|
||||
impl Visit for SerializeVisitor<'_> {
|
||||
fn record_set(&mut self, name: &str, set: OptionSet) {
|
||||
// Collect the entries of the set.
|
||||
let mut entries = BTreeMap::new();
|
||||
let mut visitor = SerializeVisitor {
|
||||
entries: &mut entries,
|
||||
};
|
||||
set.record(&mut visitor);
|
||||
|
||||
// Insert the set into the entries.
|
||||
for (key, value) in entries {
|
||||
self.entries.insert(format!("{name}.{key}"), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
self.entries.insert(name.to_string(), field);
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for OptionSet {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut entries = BTreeMap::new();
|
||||
let mut visitor = SerializeVisitor {
|
||||
entries: &mut entries,
|
||||
};
|
||||
self.record(&mut visitor);
|
||||
entries.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for OptionSet {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
|
||||
pub struct OptionField {
|
||||
pub doc: &'static str,
|
||||
/// Ex) `"false"`
|
||||
@@ -362,8 +402,7 @@ pub struct OptionField {
|
||||
pub deprecated: Option<Deprecated>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
|
||||
pub struct Deprecated {
|
||||
pub since: Option<&'static str>,
|
||||
pub message: Option<&'static str>,
|
||||
@@ -393,48 +432,3 @@ impl Display for OptionField {
|
||||
writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use super::{OptionField, OptionSet, Visit};
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
impl Serialize for OptionSet {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut entries = BTreeMap::new();
|
||||
let mut visitor = SerializeVisitor {
|
||||
entries: &mut entries,
|
||||
};
|
||||
self.record(&mut visitor);
|
||||
entries.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializeVisitor<'a> {
|
||||
entries: &'a mut BTreeMap<String, OptionField>,
|
||||
}
|
||||
|
||||
impl Visit for SerializeVisitor<'_> {
|
||||
fn record_set(&mut self, name: &str, set: OptionSet) {
|
||||
// Collect the entries of the set.
|
||||
let mut entries = BTreeMap::new();
|
||||
let mut visitor = SerializeVisitor {
|
||||
entries: &mut entries,
|
||||
};
|
||||
set.record(&mut visitor);
|
||||
|
||||
// Insert the set into the entries.
|
||||
for (key, value) in entries {
|
||||
self.entries.insert(format!("{name}.{key}"), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
self.entries.insert(name.to_string(), field);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
# CLI Reference
|
||||
|
||||
## ty
|
||||
|
||||
An extremely fast Python type checker.
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty <COMMAND>
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Commands</h3>
|
||||
|
||||
<dl class="cli-reference"><dt><a href="#ty-check"><code>ty check</code></a></dt><dd><p>Check a project for type errors</p></dd>
|
||||
<dt><a href="#ty-server"><code>ty server</code></a></dt><dd><p>Start the language server</p></dd>
|
||||
<dt><a href="#ty-version"><code>ty version</code></a></dt><dd><p>Display ty's version</p></dd>
|
||||
<dt><a href="#ty-help"><code>ty help</code></a></dt><dd><p>Print this message or the help of the given subcommand(s)</p></dd>
|
||||
</dl>
|
||||
|
||||
## ty check
|
||||
|
||||
Check a project for type errors
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty check [OPTIONS] [PATH]...
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Arguments</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-check--paths"><a href="#ty-check--paths"<code>PATHS</code></a></dt><dd><p>List of files or directories to check [default: the project root]</p>
|
||||
</dd></dl>
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-check--color"><a href="#ty-check--color"><code>--color</code></a> <i>when</i></dt><dd><p>Control when colored output is used</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>auto</code>: Display colors if the output goes to an interactive terminal</li>
|
||||
<li><code>always</code>: Always display colors</li>
|
||||
<li><code>never</code>: Never display colors</li>
|
||||
</ul></dd><dt id="ty-check--config"><a href="#ty-check--config"><code>--config</code></a>, <code>-c</code> <i>config-option</i></dt><dd><p>A TOML <code><KEY> = <VALUE></code> pair</p>
|
||||
</dd><dt id="ty-check--error"><a href="#ty-check--error"><code>--error</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'error'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--error-on-warning"><a href="#ty-check--error-on-warning"><code>--error-on-warning</code></a></dt><dd><p>Use exit code 1 if there are any warning-level diagnostics</p>
|
||||
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times)</p>
|
||||
</dd><dt id="ty-check--help"><a href="#ty-check--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help (see a summary with '-h')</p>
|
||||
</dd><dt id="ty-check--ignore"><a href="#ty-check--ignore"><code>--ignore</code></a> <i>rule</i></dt><dd><p>Disables the rule. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints</li>
|
||||
<li><code>concise</code>: Print diagnostics concisely, one per line</li>
|
||||
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
|
||||
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
|
||||
</dd><dt id="ty-check--python"><a href="#ty-check--python"><code>--python</code></a> <i>path</i></dt><dd><p>Path to the Python installation from which ty resolves type information and third-party dependencies.</p>
|
||||
<p>If not specified, ty will look at the <code>VIRTUAL_ENV</code> environment variable.</p>
|
||||
<p>ty will search in the path's <code>site-packages</code> directories for type information and third-party imports.</p>
|
||||
<p>This option is commonly used to specify the path to a virtual environment.</p>
|
||||
</dd><dt id="ty-check--python-platform"><a href="#ty-check--python-platform"><code>--python-platform</code></a>, <code>--platform</code> <i>platform</i></dt><dd><p>Target platform to assume when resolving types.</p>
|
||||
<p>This is used to specialize the type of <code>sys.platform</code> and will affect the visibility of platform-specific functions and attributes. If the value is set to <code>all</code>, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.</p>
|
||||
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>3.7</code></li>
|
||||
<li><code>3.8</code></li>
|
||||
<li><code>3.9</code></li>
|
||||
<li><code>3.10</code></li>
|
||||
<li><code>3.11</code></li>
|
||||
<li><code>3.12</code></li>
|
||||
<li><code>3.13</code></li>
|
||||
</ul></dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
|
||||
</dd><dt id="ty-check--typeshed"><a href="#ty-check--typeshed"><code>--typeshed</code></a>, <code>--custom-typeshed-dir</code> <i>path</i></dt><dd><p>Custom directory to use for stdlib typeshed stubs</p>
|
||||
</dd><dt id="ty-check--verbose"><a href="#ty-check--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output (or <code>-vv</code> and <code>-vvv</code> for more verbose output)</p>
|
||||
</dd><dt id="ty-check--warn"><a href="#ty-check--warn"><code>--warn</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'warn'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--watch"><a href="#ty-check--watch"><code>--watch</code></a>, <code>-W</code></dt><dd><p>Watch files for changes and recheck files related to the changed files</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty server
|
||||
|
||||
Start the language server
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty server
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-server--help"><a href="#ty-server--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty version
|
||||
|
||||
Display ty's version
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty version
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-version--help"><a href="#ty-version--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty generate-shell-completion
|
||||
|
||||
Generate shell completion
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty generate-shell-completion <SHELL>
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Arguments</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-generate-shell-completion--shell"><a href="#ty-generate-shell-completion--shell"<code>SHELL</code></a></dt></dl>
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-generate-shell-completion--help"><a href="#ty-generate-shell-completion--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty help
|
||||
|
||||
Print this message or the help of the given subcommand(s)
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty help [COMMAND]
|
||||
```
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
# Configuration
|
||||
#### [`respect-ignore-files`]
|
||||
|
||||
Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
`.gitignore`, `.git/info/exclude`, and global `gitignore` files.
|
||||
Enabled by default.
|
||||
|
||||
**Default value**: `true`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty]
|
||||
respect-ignore-files = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`rules`]
|
||||
|
||||
Configures the enabled rules and their severity.
|
||||
|
||||
See [the rules documentation](https://github.com/astral-sh/ruff/blob/main/crates/ty/docs/rules.md) for a list of all available rules.
|
||||
|
||||
Valid severities are:
|
||||
|
||||
* `ignore`: Disable the rule.
|
||||
* `warn`: Enable the rule and create a warning diagnostic.
|
||||
* `error`: Enable the rule and create an error diagnostic.
|
||||
ty will exit with a non-zero code if any error diagnostics are emitted.
|
||||
|
||||
**Default value**: `{...}`
|
||||
|
||||
**Type**: `dict[RuleName, "ignore" | "warn" | "error"]`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
division-by-zero = "ignore"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `environment`
|
||||
|
||||
#### [`extra-paths`]
|
||||
|
||||
List of user-provided paths that should take first priority in the module resolution.
|
||||
Examples in other type checkers are mypy's `MYPYPATH` environment variable,
|
||||
or pyright's `stubPath` configuration setting.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `list[str]`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
extra-paths = ["~/shared/my-search-path"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`python`]
|
||||
|
||||
Path to the Python installation from which ty resolves type information and third-party dependencies.
|
||||
|
||||
ty will search in the path's `site-packages` directories for type information and
|
||||
third-party imports.
|
||||
|
||||
This option is commonly used to specify the path to a virtual environment.
|
||||
|
||||
**Default value**: `null`
|
||||
|
||||
**Type**: `str`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
python = "./.venv"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`python-platform`]
|
||||
|
||||
Specifies the target platform that will be used to analyze the source code.
|
||||
If specified, ty will understand conditions based on comparisons with `sys.platform`, such
|
||||
as are commonly found in typeshed to reflect the differing contents of the standard library across platforms.
|
||||
|
||||
If no platform is specified, ty will use the current platform:
|
||||
- `win32` for Windows
|
||||
- `darwin` for macOS
|
||||
- `android` for Android
|
||||
- `ios` for iOS
|
||||
- `linux` for everything else
|
||||
|
||||
**Default value**: `<current-platform>`
|
||||
|
||||
**Type**: `"win32" | "darwin" | "android" | "ios" | "linux" | str`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
# Tailor type stubs and conditionalized type definitions to windows.
|
||||
python-platform = "win32"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`python-version`]
|
||||
|
||||
Specifies the version of Python that will be used to analyze the source code.
|
||||
The version should be specified as a string in the format `M.m` where `M` is the major version
|
||||
and `m` is the minor (e.g. `"3.0"` or `"3.6"`).
|
||||
If a version is provided, ty will generate errors if the source code makes use of language features
|
||||
that are not supported in that version.
|
||||
It will also understand conditionals based on comparisons with `sys.version_info`, such
|
||||
as are commonly found in typeshed to reflect the differing contents of the standard
|
||||
library across Python versions.
|
||||
|
||||
**Default value**: `"3.13"`
|
||||
|
||||
**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`typeshed`]
|
||||
|
||||
Optional path to a "typeshed" directory on disk for us to use for standard-library types.
|
||||
If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
bundled as a zip file in the binary
|
||||
|
||||
**Default value**: `null`
|
||||
|
||||
**Type**: `str`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
typeshed = "/path/to/custom/typeshed"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `src`
|
||||
|
||||
#### [`root`]
|
||||
|
||||
The root(s) of the project, used for finding first-party modules.
|
||||
|
||||
**Default value**: `[".", "./src"]`
|
||||
|
||||
**Type**: `list[str]`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.src]
|
||||
root = ["./app"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `terminal`
|
||||
|
||||
#### [`error-on-warning`]
|
||||
|
||||
Use exit code 1 if there are any warning-level diagnostics.
|
||||
|
||||
Defaults to `false`.
|
||||
|
||||
**Default value**: `false`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.terminal]
|
||||
# Error if ty emits any warning-level diagnostics.
|
||||
error-on-warning = true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`output-format`]
|
||||
|
||||
The format to use for printing diagnostic messages.
|
||||
|
||||
Defaults to `full`.
|
||||
|
||||
**Default value**: `full`
|
||||
|
||||
**Type**: `full | concise`
|
||||
|
||||
**Example usage** (`pyproject.toml`):
|
||||
|
||||
```toml
|
||||
[tool.ty.terminal]
|
||||
output-format = "concise"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
use crate::logging::Verbosity;
|
||||
use crate::python_version::PythonVersion;
|
||||
use clap::error::ErrorKind;
|
||||
use clap::{ArgAction, ArgMatches, Error, Parser};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ty_project::combine::Combine;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf, ValueSource};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use ty_python_semantic::lint;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, name = "ty", about = "An extremely fast Python type checker.")]
|
||||
#[command(long_version = crate::version::version())]
|
||||
pub struct Cli {
|
||||
pub(crate) struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Command,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub(crate) enum Command {
|
||||
/// Check a project for type errors.
|
||||
@@ -89,9 +86,6 @@ pub(crate) struct CheckCommand {
|
||||
#[clap(flatten)]
|
||||
pub(crate) rules: RulesArg,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) config: ConfigsArg,
|
||||
|
||||
/// The format to use for printing diagnostic messages.
|
||||
#[arg(long)]
|
||||
pub(crate) output_format: Option<OutputFormat>,
|
||||
@@ -146,7 +140,7 @@ impl CheckCommand {
|
||||
.no_respect_ignore_files
|
||||
.then_some(false)
|
||||
.or(self.respect_ignore_files);
|
||||
let options = Options {
|
||||
Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: self
|
||||
.python_version
|
||||
@@ -172,9 +166,7 @@ impl CheckCommand {
|
||||
rules,
|
||||
respect_ignore_files,
|
||||
..Default::default()
|
||||
};
|
||||
// Merge with options passed in via --config
|
||||
options.combine(self.config.into_options().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,55 +299,3 @@ pub(crate) enum TerminalColor {
|
||||
/// Never display colors.
|
||||
Never,
|
||||
}
|
||||
/// A TOML `<KEY> = <VALUE>` pair
|
||||
/// (such as you might find in a `ty.toml` configuration file)
|
||||
/// overriding a specific configuration option.
|
||||
/// Overrides of individual settings using this option always take precedence
|
||||
/// over all configuration files.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConfigsArg(Option<Options>);
|
||||
|
||||
impl clap::FromArgMatches for ConfigsArg {
|
||||
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
|
||||
let combined = matches
|
||||
.get_many::<String>("config")
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|s| {
|
||||
Options::from_toml_str(s, ValueSource::Cli)
|
||||
.map_err(|err| Error::raw(ErrorKind::InvalidValue, err.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.reduce(|acc, item| item.combine(acc));
|
||||
Ok(Self(combined))
|
||||
}
|
||||
|
||||
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
|
||||
self.0 = Self::from_arg_matches(matches)?.0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl clap::Args for ConfigsArg {
|
||||
fn augment_args(cmd: clap::Command) -> clap::Command {
|
||||
cmd.arg(
|
||||
clap::Arg::new("config")
|
||||
.short('c')
|
||||
.long("config")
|
||||
.value_name("CONFIG_OPTION")
|
||||
.help("A TOML `<KEY> = <VALUE>` pair")
|
||||
.action(ArgAction::Append),
|
||||
)
|
||||
}
|
||||
|
||||
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigsArg {
|
||||
pub(crate) fn into_options(self) -> Option<Options> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
mod args;
|
||||
mod logging;
|
||||
mod python_version;
|
||||
mod version;
|
||||
|
||||
pub use args::Cli;
|
||||
|
||||
use std::io::{self, stdout, BufWriter, Write};
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::Upcast;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use ty_project::metadata::options::Options;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{watch, Db};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_server::run_server;
|
||||
|
||||
pub fn run() -> anyhow::Result<ExitStatus> {
|
||||
setup_rayon();
|
||||
|
||||
let args = wild::args_os();
|
||||
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
|
||||
.context("Failed to read CLI arguments from file")?;
|
||||
let args = Cli::parse_from(args);
|
||||
|
||||
match args.command {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
Command::Version => version().map(|()| ExitStatus::Success),
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Cli::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn version() -> Result<()> {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
let version_info = crate::version::version();
|
||||
writeln!(stdout, "ty {}", &version_info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
set_colored_override(args.color);
|
||||
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
tracing::debug!("Version: {}", version::version());
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cwd = {
|
||||
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
|
||||
SystemPathBuf::from_path_buf(cwd)
|
||||
.map_err(|path| {
|
||||
anyhow!(
|
||||
"The current working directory `{}` contains non-Unicode characters. ty only supports Unicode paths.",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let project_path = args
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| {
|
||||
if project.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(project, &cwd))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided project path `{project}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
|
||||
let check_paths: Vec<_> = args
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
|
||||
if !check_paths.is_empty() {
|
||||
db.project().set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
ctrlc::set_handler(move || {
|
||||
let mut lock = main_loop_cancellation_token.lock().unwrap();
|
||||
|
||||
if let Some(token) = lock.take() {
|
||||
token.stop();
|
||||
}
|
||||
})?;
|
||||
|
||||
let exit_status = if watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db)?
|
||||
};
|
||||
|
||||
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
|
||||
|
||||
std::mem::forget(db);
|
||||
|
||||
if exit_zero {
|
||||
Ok(ExitStatus::Success)
|
||||
} else {
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ExitStatus {
|
||||
/// Checking was successful and there were no errors.
|
||||
Success = 0,
|
||||
|
||||
/// Checking was successful but there were errors.
|
||||
Failure = 1,
|
||||
|
||||
/// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...)
|
||||
Error = 2,
|
||||
|
||||
/// Internal ty error (panic, or any other error that isn't due to the user using the
|
||||
/// program incorrectly or transient environment errors).
|
||||
InternalError = 101,
|
||||
}
|
||||
|
||||
impl Termination for ExitStatus {
|
||||
fn report(self) -> ExitCode {
|
||||
ExitCode::from(self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
struct MainLoop {
|
||||
/// Sender that can be used to send messages to the main loop.
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
|
||||
/// Receiver for the messages sent **to** the main loop.
|
||||
receiver: crossbeam_channel::Receiver<MainLoopMessage>,
|
||||
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<ProjectWatcher>,
|
||||
|
||||
cli_options: Options,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
Self {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
}
|
||||
|
||||
fn watch(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
tracing::debug!("Starting watch mode");
|
||||
let sender = self.sender.clone();
|
||||
let watcher = watch::directory_watcher(move |event| {
|
||||
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
|
||||
})?;
|
||||
|
||||
self.watcher = Some(ProjectWatcher::new(watcher, db));
|
||||
|
||||
self.run(db)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn run(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
|
||||
let result = self.main_loop(db);
|
||||
|
||||
tracing::debug!("Exiting main loop");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
// Schedule the first check.
|
||||
tracing::debug!("Starting main loop");
|
||||
|
||||
let mut revision = 0u64;
|
||||
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the project. This needs to be done in a separate thread
|
||||
// to prevent blocking the main loop here.
|
||||
rayon::spawn(move || {
|
||||
match db.check() {
|
||||
Ok(result) => {
|
||||
// Send the result back to the main loop for printing.
|
||||
sender
|
||||
.send(MainLoopMessage::CheckCompleted { result, revision })
|
||||
.unwrap();
|
||||
}
|
||||
Err(cancelled) => {
|
||||
tracing::debug!("Check has been cancelled: {cancelled:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MainLoopMessage::CheckCompleted {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format)
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize());
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
||||
let mut stdout = stdout().lock();
|
||||
|
||||
if result.is_empty() {
|
||||
writeln!(stdout, "{}", "All checks passed!".green().bold())?;
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
} else {
|
||||
let mut max_severity = Severity::Info;
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
diagnostic.display(&db.upcast(), &display_config)
|
||||
)?;
|
||||
|
||||
max_severity = max_severity.max(diagnostic.severity());
|
||||
}
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
|
||||
if max_severity.is_fatal() {
|
||||
tracing::warn!("A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.");
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(match max_severity {
|
||||
Severity::Info => ExitStatus::Success,
|
||||
Severity::Warning => {
|
||||
if terminal_settings.error_on_warning {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
}
|
||||
}
|
||||
Severity::Error => ExitStatus::Failure,
|
||||
Severity::Fatal => ExitStatus::InternalError,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::trace!("Counts after last check:\n{}", countme::get_all());
|
||||
}
|
||||
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes, Some(&self.cli_options));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
}
|
||||
MainLoopMessage::Exit => {
|
||||
// Cancel any pending queries and wait for them to complete.
|
||||
// TODO: Don't use Salsa internal APIs
|
||||
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
|
||||
let _ = db.zalsa_mut();
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Waiting for next main loop message.");
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MainLoopCancellationToken {
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
}
|
||||
|
||||
impl MainLoopCancellationToken {
|
||||
fn stop(self) {
|
||||
self.sender.send(MainLoopMessage::Exit).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Message sent from the orchestrator to the main loop.
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Diagnostic>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn set_colored_override(color: Option<TerminalColor>) {
|
||||
let Some(color) = color else {
|
||||
return;
|
||||
};
|
||||
|
||||
match color {
|
||||
TerminalColor::Auto => {
|
||||
colored::control::unset_override();
|
||||
}
|
||||
TerminalColor::Always => {
|
||||
colored::control::set_override(true);
|
||||
}
|
||||
TerminalColor::Never => {
|
||||
colored::control::set_override(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the global rayon thread pool to never use more than `TY_MAX_PARALLELISM` threads.
|
||||
fn setup_rayon() {
|
||||
ThreadPoolBuilder::default()
|
||||
.num_threads(max_parallelism().get())
|
||||
// Use a reasonably large stack size to avoid running into stack overflows too easily. The
|
||||
// size was chosen in such a way as to still be able to handle large expressions involving
|
||||
// binary operators (x + x + … + x) both during the AST walk in semantic index building as
|
||||
// well as during type checking. Using this stack size, we can handle handle expressions
|
||||
// that are several times larger than the corresponding limits in existing type checkers.
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.build_global()
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,13 +1,40 @@
|
||||
use std::io::{self, stdout, BufWriter, Write};
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{Args, CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use std::io;
|
||||
use ty::{run, ExitStatus};
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::Upcast;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use ty_project::metadata::options::Options;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{watch, Db};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_server::run_server;
|
||||
|
||||
mod args;
|
||||
mod logging;
|
||||
mod python_version;
|
||||
mod version;
|
||||
|
||||
pub fn main() -> ExitStatus {
|
||||
setup_rayon();
|
||||
|
||||
run().unwrap_or_else(|error| {
|
||||
use io::Write;
|
||||
use std::io::Write;
|
||||
|
||||
// Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
|
||||
let mut stderr = io::stderr().lock();
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
|
||||
// This communicates that this isn't a linter error but ty itself hard-errored for
|
||||
// some reason (e.g. failed to resolve the configuration)
|
||||
@@ -31,3 +58,368 @@ pub fn main() -> ExitStatus {
|
||||
ExitStatus::Error
|
||||
})
|
||||
}
|
||||
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let args = wild::args_os();
|
||||
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
|
||||
.context("Failed to read CLI arguments from file")?;
|
||||
let args = Args::parse_from(args);
|
||||
|
||||
match args.command {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
Command::Version => version().map(|()| ExitStatus::Success),
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn version() -> Result<()> {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
let version_info = crate::version::version();
|
||||
writeln!(stdout, "ty {}", &version_info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
set_colored_override(args.color);
|
||||
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
tracing::debug!("Version: {}", version::version());
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cwd = {
|
||||
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
|
||||
SystemPathBuf::from_path_buf(cwd)
|
||||
.map_err(|path| {
|
||||
anyhow!(
|
||||
"The current working directory `{}` contains non-Unicode characters. ty only supports Unicode paths.",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let project_path = args
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| {
|
||||
if project.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(project, &cwd))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided project path `{project}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
|
||||
let check_paths: Vec<_> = args
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
|
||||
if !check_paths.is_empty() {
|
||||
db.project().set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
ctrlc::set_handler(move || {
|
||||
let mut lock = main_loop_cancellation_token.lock().unwrap();
|
||||
|
||||
if let Some(token) = lock.take() {
|
||||
token.stop();
|
||||
}
|
||||
})?;
|
||||
|
||||
let exit_status = if watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db)?
|
||||
};
|
||||
|
||||
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
|
||||
|
||||
std::mem::forget(db);
|
||||
|
||||
if exit_zero {
|
||||
Ok(ExitStatus::Success)
|
||||
} else {
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ExitStatus {
|
||||
/// Checking was successful and there were no errors.
|
||||
Success = 0,
|
||||
|
||||
/// Checking was successful but there were errors.
|
||||
Failure = 1,
|
||||
|
||||
/// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...)
|
||||
Error = 2,
|
||||
|
||||
/// Internal ty error (panic, or any other error that isn't due to the user using the
|
||||
/// program incorrectly or transient environment errors).
|
||||
InternalError = 101,
|
||||
}
|
||||
|
||||
impl Termination for ExitStatus {
|
||||
fn report(self) -> ExitCode {
|
||||
ExitCode::from(self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
struct MainLoop {
|
||||
/// Sender that can be used to send messages to the main loop.
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
|
||||
/// Receiver for the messages sent **to** the main loop.
|
||||
receiver: crossbeam_channel::Receiver<MainLoopMessage>,
|
||||
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<ProjectWatcher>,
|
||||
|
||||
cli_options: Options,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
Self {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
}
|
||||
|
||||
fn watch(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
tracing::debug!("Starting watch mode");
|
||||
let sender = self.sender.clone();
|
||||
let watcher = watch::directory_watcher(move |event| {
|
||||
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
|
||||
})?;
|
||||
|
||||
self.watcher = Some(ProjectWatcher::new(watcher, db));
|
||||
|
||||
self.run(db)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn run(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
|
||||
let result = self.main_loop(db);
|
||||
|
||||
tracing::debug!("Exiting main loop");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
// Schedule the first check.
|
||||
tracing::debug!("Starting main loop");
|
||||
|
||||
let mut revision = 0u64;
|
||||
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the project. This needs to be done in a separate thread
|
||||
// to prevent blocking the main loop here.
|
||||
rayon::spawn(move || {
|
||||
match db.check() {
|
||||
Ok(result) => {
|
||||
// Send the result back to the main loop for printing.
|
||||
sender
|
||||
.send(MainLoopMessage::CheckCompleted { result, revision })
|
||||
.unwrap();
|
||||
}
|
||||
Err(cancelled) => {
|
||||
tracing::debug!("Check has been cancelled: {cancelled:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MainLoopMessage::CheckCompleted {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format)
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize());
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
||||
let mut stdout = stdout().lock();
|
||||
|
||||
if result.is_empty() {
|
||||
writeln!(stdout, "All checks passed!")?;
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
} else {
|
||||
let mut max_severity = Severity::Info;
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
diagnostic.display(&db.upcast(), &display_config)
|
||||
)?;
|
||||
|
||||
max_severity = max_severity.max(diagnostic.severity());
|
||||
}
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
|
||||
if max_severity.is_fatal() {
|
||||
tracing::warn!("A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.");
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(match max_severity {
|
||||
Severity::Info => ExitStatus::Success,
|
||||
Severity::Warning => {
|
||||
if terminal_settings.error_on_warning {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
}
|
||||
}
|
||||
Severity::Error => ExitStatus::Failure,
|
||||
Severity::Fatal => ExitStatus::InternalError,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::trace!("Counts after last check:\n{}", countme::get_all());
|
||||
}
|
||||
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes, Some(&self.cli_options));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
}
|
||||
MainLoopMessage::Exit => {
|
||||
// Cancel any pending queries and wait for them to complete.
|
||||
// TODO: Don't use Salsa internal APIs
|
||||
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
|
||||
let _ = db.zalsa_mut();
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Waiting for next main loop message.");
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MainLoopCancellationToken {
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
}
|
||||
|
||||
impl MainLoopCancellationToken {
|
||||
fn stop(self) {
|
||||
self.sender.send(MainLoopMessage::Exit).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Message sent from the orchestrator to the main loop.
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Diagnostic>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn set_colored_override(color: Option<TerminalColor>) {
|
||||
let Some(color) = color else {
|
||||
return;
|
||||
};
|
||||
|
||||
match color {
|
||||
TerminalColor::Auto => {
|
||||
colored::control::unset_override();
|
||||
}
|
||||
TerminalColor::Always => {
|
||||
colored::control::set_override(true);
|
||||
}
|
||||
TerminalColor::Never => {
|
||||
colored::control::set_override(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the global rayon thread pool to never use more than `TY_MAX_PARALLELISM` threads.
|
||||
fn setup_rayon() {
|
||||
ThreadPoolBuilder::default()
|
||||
.num_threads(max_parallelism().get())
|
||||
// Use a reasonably large stack size to avoid running into stack overflows too easily. The
|
||||
// size was chosen in such a way as to still be able to handle large expressions involving
|
||||
// binary operators (x + x + … + x) both during the AST walk in semantic index building as
|
||||
// well as during type checking. Using this stack size, we can handle handle expressions
|
||||
// that are several times larger than the corresponding limits in existing type checkers.
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.build_global()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -197,12 +197,12 @@ fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:5:13
|
||||
--> test.py:5:1
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^ `Literal["linux"]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal["linux"]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -215,12 +215,12 @@ fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:5:13
|
||||
--> test.py:5:1
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^ `LiteralString`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `LiteralString`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -711,11 +711,11 @@ fn exit_code_only_info() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:3:13
|
||||
--> test.py:3:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^ `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -741,11 +741,11 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:3:13
|
||||
--> test.py:3:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^ `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -1219,7 +1219,7 @@ fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
|
||||
info[revealed-type] test.py:5:1: Revealed type: `Literal["hello"]`
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
@@ -1248,12 +1248,12 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:4:13
|
||||
--> test.py:4:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1...
|
||||
4 | reveal_type(total)
|
||||
| ^^^^^ `Literal[2000]`
|
||||
| ^^^^^^^^^^^^^^^^^^ `Literal[2000]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -1338,128 +1338,6 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
// Long flag
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// Short flag
|
||||
assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-reference: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files(vec![
|
||||
(
|
||||
"knot.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
])?;
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(1)")?;
|
||||
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: TOML parse error at line 1, column 1
|
||||
|
|
||||
1 | bad-option=true
|
||||
| ^^^^^^^^^^
|
||||
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `respect-ignore-files`
|
||||
|
||||
|
||||
Usage: ty <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
_temp_dir: TempDir,
|
||||
_settings_scope: SettingsBindDropGuard,
|
||||
|
||||
@@ -120,8 +120,8 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -15,7 +15,6 @@ license.workspace = true
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["cache", "serde"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["serde"] }
|
||||
ruff_python_formatter = { workspace = true, optional = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
@@ -45,7 +44,11 @@ insta = { workspace = true, features = ["redactions", "ron"] }
|
||||
[features]
|
||||
default = ["zstd"]
|
||||
deflate = ["ty_vendored/deflate"]
|
||||
schemars = ["dep:schemars", "ruff_db/schemars", "ty_python_semantic/schemars"]
|
||||
schemars = [
|
||||
"dep:schemars",
|
||||
"ruff_db/schemars",
|
||||
"ty_python_semantic/schemars",
|
||||
]
|
||||
zstd = ["ty_vendored/zstd"]
|
||||
format = ["ruff_python_formatter"]
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ impl SemanticDb for ProjectDatabase {
|
||||
project.is_file_open(self, file)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,21 +60,21 @@ pub struct Project {
|
||||
///
|
||||
/// Setting the open files to a non-`None` value changes `check` to only check the
|
||||
/// open files rather than all files in the project.
|
||||
#[returns(as_deref)]
|
||||
#[return_ref]
|
||||
#[default]
|
||||
open_fileset: Option<Arc<FxHashSet<File>>>,
|
||||
|
||||
/// The first-party files of this project.
|
||||
#[default]
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
file_set: IndexedFiles,
|
||||
|
||||
/// The metadata describing the project, including the unresolved options.
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub metadata: ProjectMetadata,
|
||||
|
||||
/// The resolved project settings.
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub settings: Settings,
|
||||
|
||||
/// The paths that should be included when checking this project.
|
||||
@@ -98,11 +98,11 @@ pub struct Project {
|
||||
/// in an IDE when the user only wants to check the open tabs. This could be modeled
|
||||
/// with `included_paths` too but it would require an explicit walk dir step that's simply unnecessary.
|
||||
#[default]
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
included_paths_list: Vec<SystemPathBuf>,
|
||||
|
||||
/// Diagnostics that were generated when resolving the project settings.
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
settings_diagnostics: Vec<OptionDiagnostic>,
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ impl Project {
|
||||
/// This is a salsa query to prevent re-computing queries if other, unrelated
|
||||
/// settings change. For example, we don't want that changing the terminal settings
|
||||
/// invalidates any type checking queries.
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked]
|
||||
pub fn rules(self, db: &dyn Db) -> Arc<RuleSelection> {
|
||||
self.settings(db).to_rules()
|
||||
}
|
||||
@@ -157,7 +157,7 @@ impl Project {
|
||||
self.set_settings(db).to(settings);
|
||||
}
|
||||
|
||||
if self.settings_diagnostics(db) != settings_diagnostics {
|
||||
if self.settings_diagnostics(db) != &settings_diagnostics {
|
||||
self.set_settings_diagnostics(db).to(settings_diagnostics);
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ impl Project {
|
||||
/// This can be useful to check arbitrary files, but it isn't something we recommend.
|
||||
/// We should try to support this use case but it's okay if there are some limitations around it.
|
||||
fn included_paths_or_root(self, db: &dyn Db) -> &[SystemPathBuf] {
|
||||
match self.included_paths_list(db) {
|
||||
match &**self.included_paths_list(db) {
|
||||
[] => std::slice::from_ref(&self.metadata(db).root),
|
||||
paths => paths,
|
||||
}
|
||||
@@ -292,7 +292,7 @@ impl Project {
|
||||
|
||||
/// Returns the open files in the project or `None` if the entire project should be checked.
|
||||
pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet<File>> {
|
||||
self.open_fileset(db)
|
||||
self.open_fileset(db).as_deref()
|
||||
}
|
||||
|
||||
/// Sets the open files in the project.
|
||||
|
||||
@@ -8,7 +8,7 @@ use ty_python_semantic::ProgramSettings;
|
||||
use crate::combine::Combine;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
|
||||
use crate::metadata::value::ValueSource;
|
||||
pub use options::Options;
|
||||
use options::Options;
|
||||
use options::TyTomlError;
|
||||
|
||||
mod configuration_file;
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::Db;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::{Combine, OptionsMetadata};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -14,63 +14,31 @@ use ty_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPath
|
||||
|
||||
use super::settings::{Settings, TerminalSettings};
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
/// The options for the project.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Options {
|
||||
/// Configures the type checking environment.
|
||||
#[option_group]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<EnvironmentOptions>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option_group]
|
||||
pub src: Option<SrcOptions>,
|
||||
|
||||
/// Configures the enabled rules and their severity.
|
||||
///
|
||||
/// See [the rules documentation](https://github.com/astral-sh/ruff/blob/main/crates/ty/docs/rules.md) for a list of all available rules.
|
||||
///
|
||||
/// Valid severities are:
|
||||
///
|
||||
/// * `ignore`: Disable the rule.
|
||||
/// * `warn`: Enable the rule and create a warning diagnostic.
|
||||
/// * `error`: Enable the rule and create an error diagnostic.
|
||||
/// ty will exit with a non-zero code if any error diagnostics are emitted.
|
||||
/// Configures the enabled lints and their severity.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"{...}"#,
|
||||
value_type = r#"dict[RuleName, "ignore" | "warn" | "error"]"#,
|
||||
example = r#"
|
||||
[tool.ty.rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
division-by-zero = "ignore"
|
||||
"#
|
||||
)]
|
||||
pub rules: Option<Rules>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option_group]
|
||||
pub terminal: Option<TerminalOptions>,
|
||||
|
||||
/// Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
/// `.gitignore`, `.git/info/exclude`, and global `gitignore` files.
|
||||
/// Enabled by default.
|
||||
#[option(
|
||||
default = r#"true"#,
|
||||
value_type = r#"bool"#,
|
||||
example = r#"
|
||||
respect-ignore-files = false
|
||||
"#
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub respect_ignore_files: Option<bool>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
|
||||
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
|
||||
let _guard = ValueSourceGuard::new(source, true);
|
||||
let options = toml::from_str(content)?;
|
||||
Ok(options)
|
||||
@@ -258,33 +226,22 @@ impl Options {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct EnvironmentOptions {
|
||||
/// Specifies the version of Python that will be used to analyze the source code.
|
||||
/// The version should be specified as a string in the format `M.m` where `M` is the major version
|
||||
/// and `m` is the minor (e.g. `"3.0"` or `"3.6"`).
|
||||
/// and `m` is the minor (e.g. "3.0" or "3.6").
|
||||
/// If a version is provided, ty will generate errors if the source code makes use of language features
|
||||
/// that are not supported in that version.
|
||||
/// It will also understand conditionals based on comparisons with `sys.version_info`, such
|
||||
/// as are commonly found in typeshed to reflect the differing contents of the standard
|
||||
/// library across Python versions.
|
||||
/// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#""3.13""#,
|
||||
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>"#,
|
||||
example = r#"
|
||||
python-version = "3.12"
|
||||
"#
|
||||
)]
|
||||
pub python_version: Option<RangedValue<PythonVersion>>,
|
||||
|
||||
/// Specifies the target platform that will be used to analyze the source code.
|
||||
/// If specified, ty will understand conditions based on comparisons with `sys.platform`, such
|
||||
/// as are commonly found in typeshed to reflect the differing contents of the standard library across platforms.
|
||||
/// If specified, ty will tailor its use of type stub files,
|
||||
/// which conditionalize type definitions based on the platform.
|
||||
///
|
||||
/// If no platform is specified, ty will use the current platform:
|
||||
/// - `win32` for Windows
|
||||
@@ -293,40 +250,18 @@ pub struct EnvironmentOptions {
|
||||
/// - `ios` for iOS
|
||||
/// - `linux` for everything else
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"<current-platform>"#,
|
||||
value_type = r#""win32" | "darwin" | "android" | "ios" | "linux" | str"#,
|
||||
example = r#"
|
||||
# Tailor type stubs and conditionalized type definitions to windows.
|
||||
python-platform = "win32"
|
||||
"#
|
||||
)]
|
||||
pub python_platform: Option<RangedValue<PythonPlatform>>,
|
||||
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's `MYPYPATH` environment variable,
|
||||
/// or pyright's `stubPath` configuration setting.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
/// or pyright's stubPath configuration setting.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
extra-paths = ["~/shared/my-search-path"]
|
||||
"#
|
||||
)]
|
||||
pub extra_paths: Option<Vec<RelativePathBuf>>,
|
||||
|
||||
/// Optional path to a "typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"null"#,
|
||||
value_type = "str",
|
||||
example = r#"
|
||||
typeshed = "/path/to/custom/typeshed"
|
||||
"#
|
||||
)]
|
||||
pub typeshed: Option<RelativePathBuf>,
|
||||
|
||||
/// Path to the Python installation from which ty resolves type information and third-party dependencies.
|
||||
@@ -336,31 +271,15 @@ pub struct EnvironmentOptions {
|
||||
///
|
||||
/// This option is commonly used to specify the path to a virtual environment.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"null"#,
|
||||
value_type = "str",
|
||||
example = r#"
|
||||
python = "./.venv"
|
||||
"#
|
||||
)]
|
||||
pub python: Option<RelativePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct SrcOptions {
|
||||
/// The root(s) of the project, used for finding first-party modules.
|
||||
/// The root of the project, used for finding first-party modules.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"[".", "./src"]"#,
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
root = ["./app"]
|
||||
"#
|
||||
)]
|
||||
pub root: Option<RelativePathBuf>,
|
||||
}
|
||||
|
||||
@@ -382,9 +301,7 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TerminalOptions {
|
||||
@@ -392,25 +309,10 @@ pub struct TerminalOptions {
|
||||
///
|
||||
/// Defaults to `full`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"full"#,
|
||||
value_type = "full | concise",
|
||||
example = r#"
|
||||
output-format = "concise"
|
||||
"#
|
||||
)]
|
||||
pub output_format: Option<RangedValue<DiagnosticFormat>>,
|
||||
/// Use exit code 1 if there are any warning-level diagnostics.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[option(
|
||||
default = r#"false"#,
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
# Error if ty emits any warning-level diagnostics.
|
||||
error-on-warning = true
|
||||
"#
|
||||
)]
|
||||
pub error_on_warning: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1945,7 +1945,7 @@ reveal_type(C.a_complex) # revealed: int | float | complex
|
||||
reveal_type(C.a_tuple) # revealed: tuple[int]
|
||||
reveal_type(C.a_range) # revealed: range
|
||||
# TODO: revealed: slice[Any, Literal[1], Any]
|
||||
reveal_type(C.a_slice) # revealed: slice[Any, Any, Any]
|
||||
reveal_type(C.a_slice) # revealed: slice[Any, _StartT_co, _StartT_co | _StopT_co]
|
||||
reveal_type(C.a_type) # revealed: type
|
||||
reveal_type(C.a_none) # revealed: None
|
||||
```
|
||||
|
||||
@@ -11,28 +11,30 @@ reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14]
|
||||
reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11]
|
||||
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7]
|
||||
|
||||
# fmt: off
|
||||
|
||||
reveal_type(len( # revealed: Literal[7]
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
"""foo
|
||||
bar"""
|
||||
))
|
||||
|
||||
reveal_type(len( # revealed: Literal[9]
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
r"""foo\r
|
||||
bar"""
|
||||
))
|
||||
|
||||
reveal_type(len( # revealed: Literal[7]
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
b"""foo
|
||||
bar"""
|
||||
))
|
||||
reveal_type(len( # revealed: Literal[9]
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
rb"""foo\r
|
||||
bar"""
|
||||
))
|
||||
|
||||
# fmt: on
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Tuples
|
||||
@@ -48,17 +50,15 @@ reveal_type(len(tuple())) # revealed: int
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
|
||||
# fmt: off
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[1]
|
||||
reveal_type(len( # revealed: Literal[2]
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
reveal_type( # revealed: Literal[2]
|
||||
len(
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
# fmt: on
|
||||
)
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
@@ -19,16 +19,6 @@ reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other
|
||||
class:
|
||||
|
||||
```py
|
||||
class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base]
|
||||
|
||||
# TODO: should emit an error (fails at runtime)
|
||||
class AlsoBad(Generic[T], Generic[S]): ...
|
||||
```
|
||||
|
||||
You cannot use the same typevar more than once.
|
||||
|
||||
```py
|
||||
|
||||
@@ -86,26 +86,6 @@ S = TypeVar("S")
|
||||
reveal_type(S.__default__) # revealed: NoDefault
|
||||
```
|
||||
|
||||
### Using other typevars as a default
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar, Union
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U", default=T)
|
||||
V = TypeVar("V", default=Union[T, U])
|
||||
|
||||
class Valid(Generic[T, U, V]): ...
|
||||
|
||||
reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown]
|
||||
reveal_type(Valid[int]()) # revealed: Valid[int, int, int]
|
||||
reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str]
|
||||
reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None]
|
||||
|
||||
# TODO: error, default value for U isn't available in the generic context
|
||||
class Invalid(Generic[U]): ...
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
|
||||
@@ -40,25 +40,6 @@ def g[S]():
|
||||
reveal_type(S.__default__) # revealed: NoDefault
|
||||
```
|
||||
|
||||
### Using other typevars as a default
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
class Valid[T, U = T, V = T | U]: ...
|
||||
|
||||
reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown]
|
||||
reveal_type(Valid[int]()) # revealed: Valid[int, int, int]
|
||||
reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str]
|
||||
reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class Invalid[S = T]: ...
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
|
||||
@@ -251,53 +251,6 @@ from ty_extensions import dunder_all_names
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
### Augmenting list with a list or submodule `__all__` (2)
|
||||
|
||||
The same again, but the submodule is an attribute expression rather than a name expression:
|
||||
|
||||
`exporter/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`exporter/sub.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["foo"]
|
||||
|
||||
foo = 42
|
||||
```
|
||||
|
||||
`exporter/sub2.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["bar"]
|
||||
|
||||
bar = 56
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
import exporter.sub
|
||||
import exporter.sub2
|
||||
|
||||
__all__ = []
|
||||
|
||||
if True:
|
||||
__all__.extend(exporter.sub.__all__)
|
||||
__all__ += exporter.sub2.__all__
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import module
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
reveal_type(dunder_all_names(module)) # revealed: tuple[Literal["bar"], Literal["foo"]]
|
||||
```
|
||||
|
||||
### Extending with a list or submodule `__all__`
|
||||
|
||||
`subexporter.py`:
|
||||
|
||||
@@ -391,35 +391,28 @@ class E(
|
||||
|
||||
## `__bases__` lists with duplicate `Unknown` bases
|
||||
|
||||
We do not emit errors on classes where multiple bases are inferred as `Unknown`, `Todo` or `Any`.
|
||||
Usually having duplicate bases in a bases list like this would cause us to emit a diagnostic;
|
||||
however, for gradual types this would break the
|
||||
[gradual guarantee](https://typing.python.org/en/latest/spec/concepts.html#the-gradual-guarantee):
|
||||
the dynamic base can usually be materialised to a type that would lead to a resolvable MRO.
|
||||
|
||||
```py
|
||||
from unresolvable_module import UnknownBase1, UnknownBase2 # error: [unresolved-import]
|
||||
# error: [unresolved-import]
|
||||
from does_not_exist import unknown_object_1, unknown_object_2
|
||||
|
||||
reveal_type(UnknownBase1) # revealed: Unknown
|
||||
reveal_type(UnknownBase2) # revealed: Unknown
|
||||
reveal_type(unknown_object_1) # revealed: Unknown
|
||||
reveal_type(unknown_object_2) # revealed: Unknown
|
||||
|
||||
# no error here -- we respect the gradual guarantee:
|
||||
class Foo(UnknownBase1, UnknownBase2): ...
|
||||
# We *should* emit an error here to warn the user that we have no idea
|
||||
# what the MRO of this class should really be.
|
||||
# However, we don't complain about "duplicate base classes" here,
|
||||
# even though two classes are both inferred as being `Unknown`.
|
||||
#
|
||||
# (TODO: should we revisit this? Does it violate the gradual guarantee?
|
||||
# Should we just silently infer `[Foo, Unknown, object]` as the MRO here
|
||||
# without emitting any error at all? Not sure...)
|
||||
#
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
|
||||
class Foo(unknown_object_1, unknown_object_2): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
However, if there are duplicate class elements, we do emit an error, even if there are also multiple
|
||||
dynamic members. The following class definition will definitely fail, no matter what the dynamic
|
||||
bases materialize to:
|
||||
|
||||
```py
|
||||
# error: [duplicate-base] "Duplicate base class `Foo`"
|
||||
class Bar(UnknownBase1, Foo, UnknownBase2, Foo): ...
|
||||
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
|
||||
|
||||
```py
|
||||
@@ -492,29 +485,3 @@ reveal_type(BarCycle.__mro__) # revealed: tuple[<class 'BarCycle'>, Unknown, <c
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[<class 'Baz'>, Unknown, <class 'object'>]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[<class 'Spam'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Other classes with possible cycles
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```pyi
|
||||
class C(C.a): ...
|
||||
reveal_type(C.__class__) # revealed: <class 'type'>
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>]
|
||||
|
||||
class D(D.a):
|
||||
a: D
|
||||
#reveal_type(D.__class__) # revealed: <class 'type'>
|
||||
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, Unknown, <class 'object'>]
|
||||
|
||||
class E[T](E.a): ...
|
||||
#reveal_type(E.__class__) # revealed: <class 'type'>
|
||||
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, <class 'object'>]
|
||||
|
||||
class F[T](F(), F): ... # error: [cyclic-class-definition]
|
||||
#reveal_type(F.__class__) # revealed: <class 'type'>
|
||||
reveal_type(F.__mro__) # revealed: tuple[<class 'F[Unknown]'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ Just like for any other class base, it is an error for `Protocol` to appear mult
|
||||
class's bases:
|
||||
|
||||
```py
|
||||
class Foo(Protocol, Protocol): ... # error: [duplicate-base]
|
||||
class Foo(Protocol, Protocol): ... # error: [inconsistent-mro]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
```
|
||||
@@ -1569,11 +1569,11 @@ from typing import Protocol, Any
|
||||
from ty_extensions import is_fully_static, static_assert, is_assignable_to, is_subtype_of, is_equivalent_to
|
||||
|
||||
class RecursiveFullyStatic(Protocol):
|
||||
parent: RecursiveFullyStatic
|
||||
parent: RecursiveFullyStatic | None
|
||||
x: int
|
||||
|
||||
class RecursiveNonFullyStatic(Protocol):
|
||||
parent: RecursiveNonFullyStatic
|
||||
parent: RecursiveNonFullyStatic | None
|
||||
x: Any
|
||||
|
||||
static_assert(is_fully_static(RecursiveFullyStatic))
|
||||
@@ -1582,111 +1582,16 @@ static_assert(not is_fully_static(RecursiveNonFullyStatic))
|
||||
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
# TODO: currently leads to a stack overflow
|
||||
# static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
# static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
class AlsoRecursiveFullyStatic(Protocol):
|
||||
parent: AlsoRecursiveFullyStatic
|
||||
parent: AlsoRecursiveFullyStatic | None
|
||||
x: int
|
||||
|
||||
static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
class RecursiveOptionalParent(Protocol):
|
||||
parent: RecursiveOptionalParent | None
|
||||
|
||||
static_assert(is_fully_static(RecursiveOptionalParent))
|
||||
|
||||
static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent))
|
||||
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent))
|
||||
static_assert(not is_assignable_to(RecursiveOptionalParent, RecursiveNonFullyStatic))
|
||||
|
||||
class Other(Protocol):
|
||||
z: str
|
||||
|
||||
def _(rec: RecursiveFullyStatic, other: Other):
|
||||
reveal_type(rec.parent.parent.parent) # revealed: RecursiveFullyStatic
|
||||
|
||||
rec.parent.parent.parent = rec
|
||||
rec = rec.parent.parent.parent
|
||||
|
||||
rec.parent.parent.parent = other # error: [invalid-assignment]
|
||||
other = rec.parent.parent.parent # error: [invalid-assignment]
|
||||
|
||||
class Foo(Protocol):
|
||||
@property
|
||||
def x(self) -> "Foo": ...
|
||||
|
||||
class Bar(Protocol):
|
||||
@property
|
||||
def x(self) -> "Bar": ...
|
||||
|
||||
# TODO: this should pass
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(Foo, Bar))
|
||||
```
|
||||
|
||||
### Nested occurrences of self-reference
|
||||
|
||||
Make sure that we handle self-reference correctly, even if the self-reference appears deeply nested
|
||||
within the type of a protocol member:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, Callable
|
||||
from ty_extensions import Intersection, Not, is_fully_static, is_assignable_to, is_equivalent_to, static_assert
|
||||
|
||||
class C: ...
|
||||
|
||||
class GenericC[T](Protocol):
|
||||
pass
|
||||
|
||||
class Recursive(Protocol):
|
||||
direct: Recursive
|
||||
|
||||
union: None | Recursive
|
||||
|
||||
intersection1: Intersection[C, Recursive]
|
||||
intersection2: Intersection[C, Not[Recursive]]
|
||||
|
||||
t: tuple[int, tuple[str, Recursive]]
|
||||
|
||||
callable1: Callable[[int], Recursive]
|
||||
callable2: Callable[[Recursive], int]
|
||||
|
||||
subtype_of: type[Recursive]
|
||||
|
||||
generic: GenericC[Recursive]
|
||||
|
||||
def method(self, x: Recursive) -> Recursive: ...
|
||||
|
||||
nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive]
|
||||
|
||||
static_assert(is_fully_static(Recursive))
|
||||
static_assert(is_equivalent_to(Recursive, Recursive))
|
||||
static_assert(is_assignable_to(Recursive, Recursive))
|
||||
|
||||
def _(r: Recursive):
|
||||
reveal_type(r.direct) # revealed: Recursive
|
||||
reveal_type(r.union) # revealed: None | Recursive
|
||||
reveal_type(r.intersection1) # revealed: C & Recursive
|
||||
reveal_type(r.intersection2) # revealed: C & ~Recursive
|
||||
reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]]
|
||||
reveal_type(r.callable1) # revealed: (int, /) -> Recursive
|
||||
reveal_type(r.callable2) # revealed: (Recursive, /) -> int
|
||||
reveal_type(r.subtype_of) # revealed: type[Recursive]
|
||||
reveal_type(r.generic) # revealed: GenericC[Recursive]
|
||||
reveal_type(r.method(r)) # revealed: Recursive
|
||||
reveal_type(r.nested) # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> Recursive)
|
||||
|
||||
reveal_type(r.method(r).callable1(1).direct.t[1][1]) # revealed: Recursive
|
||||
# TODO: currently leads to a stack overflow
|
||||
# static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
|
||||
```
|
||||
|
||||
### Regression test: narrowing with self-referential protocols
|
||||
|
||||
@@ -44,12 +44,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:11:17
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
9 | # error: [not-iterable]
|
||||
10 | for x in Iterable():
|
||||
11 | reveal_type(x) # revealed: int
|
||||
| ^ `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -40,12 +40,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:8:17
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
6 | # error: [not-iterable]
|
||||
7 | for x in Bad():
|
||||
8 | reveal_type(x) # revealed: Unknown
|
||||
| ^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -63,12 +63,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:24:21
|
||||
--> src/mdtest_snippet.py:24:9
|
||||
|
|
||||
22 | for x in Iterable1():
|
||||
23 | # TODO... `int` might be ideal here?
|
||||
24 | reveal_type(x) # revealed: int | Unknown
|
||||
| ^ `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
25 |
|
||||
26 | # error: [not-iterable]
|
||||
|
|
||||
@@ -93,12 +93,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:29:21
|
||||
--> src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | for y in Iterable2():
|
||||
28 | # TODO... `int` might be ideal here?
|
||||
29 | reveal_type(y) # revealed: int | Unknown
|
||||
| ^ `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -60,12 +60,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:22:21
|
||||
--> src/mdtest_snippet.py:22:9
|
||||
|
|
||||
20 | for x in Iterable1():
|
||||
21 | # TODO: `str` might be better
|
||||
22 | reveal_type(x) # revealed: str | Unknown
|
||||
| ^ `str | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `str | Unknown`
|
||||
23 |
|
||||
24 | # error: [not-iterable]
|
||||
|
|
||||
@@ -89,12 +89,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:26:21
|
||||
--> src/mdtest_snippet.py:26:9
|
||||
|
|
||||
24 | # error: [not-iterable]
|
||||
25 | for y in Iterable2():
|
||||
26 | reveal_type(y) # revealed: str | int
|
||||
| ^ `str | int`
|
||||
| ^^^^^^^^^^^^^^ `str | int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -64,12 +64,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:18:21
|
||||
--> src/mdtest_snippet.py:18:9
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable1():
|
||||
18 | reveal_type(x) # revealed: int
|
||||
| ^ `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
19 |
|
||||
20 | class Iterable2:
|
||||
|
|
||||
@@ -93,12 +93,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:30:21
|
||||
--> src/mdtest_snippet.py:30:9
|
||||
|
|
||||
28 | for x in Iterable2():
|
||||
29 | # TODO: `int` would probably be better here:
|
||||
30 | reveal_type(x) # revealed: int | Unknown
|
||||
| ^ `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -67,12 +67,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:29:21
|
||||
--> src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable1():
|
||||
29 | reveal_type(x) # revealed: int | str
|
||||
| ^ `int | str`
|
||||
| ^^^^^^^^^^^^^^ `int | str`
|
||||
30 |
|
||||
31 | # error: [not-iterable]
|
||||
|
|
||||
@@ -96,12 +96,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:34:21
|
||||
--> src/mdtest_snippet.py:34:9
|
||||
|
|
||||
32 | for y in Iterable2():
|
||||
33 | # TODO: `int` would probably be better here:
|
||||
34 | reveal_type(y) # revealed: int | Unknown
|
||||
| ^ `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -52,12 +52,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:19:21
|
||||
--> src/mdtest_snippet.py:19:9
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Iterable():
|
||||
19 | reveal_type(x) # revealed: int | bytes
|
||||
| ^ `int | bytes`
|
||||
| ^^^^^^^^^^^^^^ `int | bytes`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -70,12 +70,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:33:21
|
||||
--> src/mdtest_snippet.py:33:9
|
||||
|
|
||||
31 | for x in Iterable1():
|
||||
32 | # TODO: `bytes | str` might be better
|
||||
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
||||
| ^ `bytes | str | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `bytes | str | Unknown`
|
||||
34 |
|
||||
35 | # error: [not-iterable]
|
||||
|
|
||||
@@ -99,12 +99,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:37:21
|
||||
--> src/mdtest_snippet.py:37:9
|
||||
|
|
||||
35 | # error: [not-iterable]
|
||||
36 | for y in Iterable2():
|
||||
37 | reveal_type(y) # revealed: bytes | str | int
|
||||
| ^ `bytes | str | int`
|
||||
| ^^^^^^^^^^^^^^ `bytes | str | int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -50,12 +50,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:18:21
|
||||
--> src/mdtest_snippet.py:18:9
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable():
|
||||
18 | reveal_type(x) # revealed: int | bytes
|
||||
| ^ `int | bytes`
|
||||
| ^^^^^^^^^^^^^^ `int | bytes`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -52,12 +52,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:19:21
|
||||
--> src/mdtest_snippet.py:19:9
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Test() if flag else Test2():
|
||||
19 | reveal_type(x) # revealed: int
|
||||
| ^ `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -47,12 +47,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:14:21
|
||||
--> src/mdtest_snippet.py:14:9
|
||||
|
|
||||
12 | # error: [not-iterable]
|
||||
13 | for x in Test() if flag else 42:
|
||||
14 | reveal_type(x) # revealed: int
|
||||
| ^ `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -46,6 +46,18 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:16:5
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> src/mdtest_snippet.py:16:17
|
||||
@@ -58,15 +70,3 @@ info: lint:possibly-unresolved-reference: Name `x` used when possibly not define
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:16:17
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| ^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -41,12 +41,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:9:17
|
||||
--> src/mdtest_snippet.py:9:5
|
||||
|
|
||||
7 | # error: [not-iterable]
|
||||
8 | for x in Bad():
|
||||
9 | reveal_type(x) # revealed: Unknown
|
||||
| ^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -46,12 +46,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:13:17
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
|
|
||||
11 | # error: [not-iterable]
|
||||
12 | for x in Iterable():
|
||||
13 | reveal_type(x) # revealed: int
|
||||
| ^ `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -57,12 +57,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:20:17
|
||||
--> src/mdtest_snippet.py:20:5
|
||||
|
|
||||
18 | # error: [not-iterable]
|
||||
19 | for x in Iterable1():
|
||||
20 | reveal_type(x) # revealed: int
|
||||
| ^ `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
21 |
|
||||
22 | # error: [not-iterable]
|
||||
|
|
||||
@@ -85,12 +85,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:24:17
|
||||
--> src/mdtest_snippet.py:24:5
|
||||
|
|
||||
22 | # error: [not-iterable]
|
||||
23 | for y in Iterable2():
|
||||
24 | reveal_type(y) # revealed: Unknown
|
||||
| ^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -30,12 +30,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/function
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:9:13
|
||||
--> src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | return x
|
||||
8 |
|
||||
9 | reveal_type(f(1)) # revealed: Literal[1]
|
||||
| ^^^^ `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^^^^ `Literal[1]`
|
||||
10 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
11 | # error: [invalid-argument-type]
|
||||
|
|
||||
@@ -44,11 +44,11 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:13
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
9 | reveal_type(f(1)) # revealed: Literal[1]
|
||||
10 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
| ^^^^^^^ `Literal[True]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^ `Literal[True]`
|
||||
11 | # error: [invalid-argument-type]
|
||||
12 | reveal_type(f("string")) # revealed: Unknown
|
||||
|
|
||||
@@ -57,12 +57,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:12:13
|
||||
--> src/mdtest_snippet.py:12:1
|
||||
|
|
||||
10 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
11 | # error: [invalid-argument-type]
|
||||
12 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -31,12 +31,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/function
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:9:13
|
||||
--> src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | return x
|
||||
8 |
|
||||
9 | reveal_type(f(1)) # revealed: int
|
||||
| ^^^^ `int`
|
||||
| ^^^^^^^^^^^^^^^^^ `int`
|
||||
10 | reveal_type(f(True)) # revealed: int
|
||||
11 | reveal_type(f(None)) # revealed: None
|
||||
|
|
||||
@@ -45,11 +45,11 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:13
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
9 | reveal_type(f(1)) # revealed: int
|
||||
10 | reveal_type(f(True)) # revealed: int
|
||||
| ^^^^^^^ `int`
|
||||
| ^^^^^^^^^^^^^^^^^^^^ `int`
|
||||
11 | reveal_type(f(None)) # revealed: None
|
||||
12 | # error: [invalid-argument-type]
|
||||
|
|
||||
@@ -58,12 +58,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:11:13
|
||||
--> src/mdtest_snippet.py:11:1
|
||||
|
|
||||
9 | reveal_type(f(1)) # revealed: int
|
||||
10 | reveal_type(f(True)) # revealed: int
|
||||
11 | reveal_type(f(None)) # revealed: None
|
||||
| ^^^^^^^ `None`
|
||||
| ^^^^^^^^^^^^^^^^^^^^ `None`
|
||||
12 | # error: [invalid-argument-type]
|
||||
13 | reveal_type(f("string")) # revealed: Unknown
|
||||
|
|
||||
@@ -72,12 +72,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:13:13
|
||||
--> src/mdtest_snippet.py:13:1
|
||||
|
|
||||
11 | reveal_type(f(None)) # revealed: None
|
||||
12 | # error: [invalid-argument-type]
|
||||
13 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -27,12 +27,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/function
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:6:13
|
||||
--> src/mdtest_snippet.py:6:1
|
||||
|
|
||||
4 | return x
|
||||
5 |
|
||||
6 | reveal_type(f(1)) # revealed: Literal[1]
|
||||
| ^^^^ `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^^^^ `Literal[1]`
|
||||
7 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
8 | # error: [invalid-argument-type]
|
||||
|
|
||||
@@ -41,11 +41,11 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:7:13
|
||||
--> src/mdtest_snippet.py:7:1
|
||||
|
|
||||
6 | reveal_type(f(1)) # revealed: Literal[1]
|
||||
7 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
| ^^^^^^^ `Literal[True]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^ `Literal[True]`
|
||||
8 | # error: [invalid-argument-type]
|
||||
9 | reveal_type(f("string")) # revealed: Unknown
|
||||
|
|
||||
@@ -54,12 +54,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:9:13
|
||||
--> src/mdtest_snippet.py:9:1
|
||||
|
|
||||
7 | reveal_type(f(True)) # revealed: Literal[True]
|
||||
8 | # error: [invalid-argument-type]
|
||||
9 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -28,12 +28,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/function
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:6:13
|
||||
--> src/mdtest_snippet.py:6:1
|
||||
|
|
||||
4 | return x
|
||||
5 |
|
||||
6 | reveal_type(f(1)) # revealed: int
|
||||
| ^^^^ `int`
|
||||
| ^^^^^^^^^^^^^^^^^ `int`
|
||||
7 | reveal_type(f(True)) # revealed: int
|
||||
8 | reveal_type(f(None)) # revealed: None
|
||||
|
|
||||
@@ -42,11 +42,11 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:7:13
|
||||
--> src/mdtest_snippet.py:7:1
|
||||
|
|
||||
6 | reveal_type(f(1)) # revealed: int
|
||||
7 | reveal_type(f(True)) # revealed: int
|
||||
| ^^^^^^^ `int`
|
||||
| ^^^^^^^^^^^^^^^^^^^^ `int`
|
||||
8 | reveal_type(f(None)) # revealed: None
|
||||
9 | # error: [invalid-argument-type]
|
||||
|
|
||||
@@ -55,12 +55,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:8:13
|
||||
--> src/mdtest_snippet.py:8:1
|
||||
|
|
||||
6 | reveal_type(f(1)) # revealed: int
|
||||
7 | reveal_type(f(True)) # revealed: int
|
||||
8 | reveal_type(f(None)) # revealed: None
|
||||
| ^^^^^^^ `None`
|
||||
| ^^^^^^^^^^^^^^^^^^^^ `None`
|
||||
9 | # error: [invalid-argument-type]
|
||||
10 | reveal_type(f("string")) # revealed: Unknown
|
||||
|
|
||||
@@ -69,12 +69,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:13
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
8 | reveal_type(f(None)) # revealed: None
|
||||
9 | # error: [invalid-argument-type]
|
||||
10 | reveal_type(f("string")) # revealed: Unknown
|
||||
| ^^^^^^^^^^^ `Unknown`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -128,12 +128,12 @@ info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:5:13
|
||||
--> src/mdtest_snippet.py:5:1
|
||||
|
|
||||
3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`"
|
||||
4 |
|
||||
5 | reveal_type(Foo.__mro__) # revealed: tuple[<class 'Foo'>, Unknown, <class 'object'>]
|
||||
| ^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `tuple[<class 'Foo'>, Unknown, <class 'object'>]`
|
||||
6 |
|
||||
7 | class Spam: ...
|
||||
|
|
||||
@@ -217,12 +217,12 @@ info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:27:13
|
||||
--> src/mdtest_snippet.py:27:1
|
||||
|
|
||||
25 | # fmt: on
|
||||
26 |
|
||||
27 | reveal_type(Ham.__mro__) # revealed: tuple[<class 'Ham'>, Unknown, <class 'object'>]
|
||||
| ^^^^^^^^^^^ `tuple[<class 'Ham'>, Unknown, <class 'object'>]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ `tuple[<class 'Ham'>, Unknown, <class 'object'>]`
|
||||
28 |
|
||||
29 | class Mushrooms: ...
|
||||
|
|
||||
@@ -256,12 +256,12 @@ info: `lint:duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:32:13
|
||||
--> src/mdtest_snippet.py:32:1
|
||||
|
|
||||
30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
31 |
|
||||
32 | reveal_type(Omelette.__mro__) # revealed: tuple[<class 'Omelette'>, Unknown, <class 'object'>]
|
||||
| ^^^^^^^^^^^^^^^^ `tuple[<class 'Omelette'>, Unknown, <class 'object'>]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `tuple[<class 'Omelette'>, Unknown, <class 'object'>]`
|
||||
33 |
|
||||
34 | # fmt: off
|
||||
|
|
||||
|
||||
@@ -41,6 +41,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:4:1
|
||||
|
|
||||
3 | # error: [call-non-callable]
|
||||
4 | reveal_type(Protocol()) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
||||
5 |
|
||||
6 | class MyProtocol(Protocol):
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:call-non-callable: Object of type `typing.Protocol` is not callable
|
||||
--> src/mdtest_snippet.py:4:13
|
||||
@@ -57,14 +70,14 @@ info: `lint:call-non-callable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:4:13
|
||||
|
|
||||
3 | # error: [call-non-callable]
|
||||
4 | reveal_type(Protocol()) # revealed: Unknown
|
||||
| ^^^^^^^^^^ `Unknown`
|
||||
5 |
|
||||
6 | class MyProtocol(Protocol):
|
||||
|
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
|
||||
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
|
||||
11 |
|
||||
12 | class GenericProtocol[T](Protocol):
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -93,13 +106,12 @@ info: `lint:call-non-callable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:13
|
||||
--> src/mdtest_snippet.py:16:1
|
||||
|
|
||||
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
|
||||
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
|
||||
| ^^^^^^^^^^^^ `MyProtocol`
|
||||
11 |
|
||||
12 | class GenericProtocol[T](Protocol):
|
||||
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
|
||||
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]`
|
||||
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
||||
|
|
||||
|
||||
```
|
||||
@@ -128,24 +140,12 @@ info: `lint:call-non-callable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:16:13
|
||||
|
|
||||
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
|
||||
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]`
|
||||
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:19:13
|
||||
--> src/mdtest_snippet.py:19:1
|
||||
|
|
||||
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
||||
18 |
|
||||
19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol`
|
||||
20 |
|
||||
21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
|
||||
|
|
||||
@@ -154,12 +154,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:23:13
|
||||
--> src/mdtest_snippet.py:23:1
|
||||
|
|
||||
21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
|
||||
22 |
|
||||
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]`
|
||||
24 | def f(x: type[MyProtocol]):
|
||||
25 | reveal_type(x()) # revealed: MyProtocol
|
||||
|
|
||||
@@ -168,12 +168,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:25:17
|
||||
--> src/mdtest_snippet.py:25:5
|
||||
|
|
||||
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
|
||||
24 | def f(x: type[MyProtocol]):
|
||||
25 | reveal_type(x()) # revealed: MyProtocol
|
||||
| ^^^ `MyProtocol`
|
||||
| ^^^^^^^^^^^^^^^^ `MyProtocol`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -83,12 +83,12 @@ info: `lint:invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:8:21
|
||||
--> src/mdtest_snippet.py:8:9
|
||||
|
|
||||
6 | def f(arg: object, arg2: type):
|
||||
7 | if isinstance(arg, HasX): # error: [invalid-argument-type]
|
||||
8 | reveal_type(arg) # revealed: HasX
|
||||
| ^^^ `HasX`
|
||||
| ^^^^^^^^^^^^^^^^ `HasX`
|
||||
9 | else:
|
||||
10 | reveal_type(arg) # revealed: ~HasX
|
||||
|
|
||||
@@ -97,12 +97,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:10:21
|
||||
--> src/mdtest_snippet.py:10:9
|
||||
|
|
||||
8 | reveal_type(arg) # revealed: HasX
|
||||
9 | else:
|
||||
10 | reveal_type(arg) # revealed: ~HasX
|
||||
| ^^^ `~HasX`
|
||||
| ^^^^^^^^^^^^^^^^ `~HasX`
|
||||
11 |
|
||||
12 | if issubclass(arg2, HasX): # error: [invalid-argument-type]
|
||||
|
|
||||
@@ -137,11 +137,11 @@ info: `lint:invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:13:21
|
||||
--> src/mdtest_snippet.py:13:9
|
||||
|
|
||||
12 | if issubclass(arg2, HasX): # error: [invalid-argument-type]
|
||||
13 | reveal_type(arg2) # revealed: type[HasX]
|
||||
| ^^^^ `type[HasX]`
|
||||
| ^^^^^^^^^^^^^^^^^ `type[HasX]`
|
||||
14 | else:
|
||||
15 | reveal_type(arg2) # revealed: type & ~type[HasX]
|
||||
|
|
||||
@@ -150,12 +150,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:15:21
|
||||
--> src/mdtest_snippet.py:15:9
|
||||
|
|
||||
13 | reveal_type(arg2) # revealed: type[HasX]
|
||||
14 | else:
|
||||
15 | reveal_type(arg2) # revealed: type & ~type[HasX]
|
||||
| ^^^^ `type & ~type[HasX]`
|
||||
| ^^^^^^^^^^^^^^^^^ `type & ~type[HasX]`
|
||||
16 | from typing import runtime_checkable
|
||||
|
|
||||
|
||||
@@ -163,12 +163,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:24:21
|
||||
--> src/mdtest_snippet.py:24:9
|
||||
|
|
||||
22 | def f(arg: object):
|
||||
23 | if isinstance(arg, RuntimeCheckableHasX): # no error!
|
||||
24 | reveal_type(arg) # revealed: RuntimeCheckableHasX
|
||||
| ^^^ `RuntimeCheckableHasX`
|
||||
| ^^^^^^^^^^^^^^^^ `RuntimeCheckableHasX`
|
||||
25 | else:
|
||||
26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX
|
||||
|
|
||||
@@ -177,12 +177,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:26:21
|
||||
--> src/mdtest_snippet.py:26:9
|
||||
|
|
||||
24 | reveal_type(arg) # revealed: RuntimeCheckableHasX
|
||||
25 | else:
|
||||
26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX
|
||||
| ^^^ `~RuntimeCheckableHasX`
|
||||
| ^^^^^^^^^^^^^^^^ `~RuntimeCheckableHasX`
|
||||
27 | @runtime_checkable
|
||||
28 | class OnlyMethodMembers(Protocol):
|
||||
|
|
||||
@@ -191,12 +191,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:33:21
|
||||
--> src/mdtest_snippet.py:33:9
|
||||
|
|
||||
31 | def f(arg1: type, arg2: type):
|
||||
32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members)
|
||||
33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
|
||||
| ^^^^ `type[RuntimeCheckableHasX]`
|
||||
| ^^^^^^^^^^^^^^^^^ `type[RuntimeCheckableHasX]`
|
||||
34 | else:
|
||||
35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
|
||||
|
|
||||
@@ -205,12 +205,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:35:21
|
||||
--> src/mdtest_snippet.py:35:9
|
||||
|
|
||||
33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
|
||||
34 | else:
|
||||
35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
|
||||
| ^^^^ `type & ~type[RuntimeCheckableHasX]`
|
||||
| ^^^^^^^^^^^^^^^^^ `type & ~type[RuntimeCheckableHasX]`
|
||||
36 |
|
||||
37 | if issubclass(arg2, OnlyMethodMembers): # no error!
|
||||
|
|
||||
@@ -219,11 +219,11 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:38:21
|
||||
--> src/mdtest_snippet.py:38:9
|
||||
|
|
||||
37 | if issubclass(arg2, OnlyMethodMembers): # no error!
|
||||
38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers]
|
||||
| ^^^^ `type[OnlyMethodMembers]`
|
||||
| ^^^^^^^^^^^^^^^^^ `type[OnlyMethodMembers]`
|
||||
39 | else:
|
||||
40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
|
||||
|
|
||||
@@ -232,12 +232,12 @@ info: revealed-type: Revealed type
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:40:21
|
||||
--> src/mdtest_snippet.py:40:9
|
||||
|
|
||||
38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers]
|
||||
39 | else:
|
||||
40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
|
||||
| ^^^^ `type & ~type[OnlyMethodMembers]`
|
||||
| ^^^^^^^^^^^^^^^^^ `type & ~type[OnlyMethodMembers]`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -125,17 +125,6 @@ static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
|
||||
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
|
||||
```
|
||||
|
||||
### Slice literals
|
||||
|
||||
The type of a slice literal is currently inferred as a specialization of `slice`.
|
||||
|
||||
```py
|
||||
from ty_extensions import TypeOf, is_assignable_to, static_assert
|
||||
|
||||
static_assert(is_assignable_to(TypeOf[1:2:3], slice))
|
||||
static_assert(is_assignable_to(TypeOf[1:2:3], slice[int]))
|
||||
```
|
||||
|
||||
## `type[…]` and class literals
|
||||
|
||||
In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`.
|
||||
|
||||
@@ -334,13 +334,18 @@ static_assert(is_subtype_of(TypeOf[typing], ModuleType))
|
||||
|
||||
### Slice literals
|
||||
|
||||
The type of a slice literal is currently inferred as a specialization of `slice`.
|
||||
The type of a slice literal is currently inferred as `slice`, which is a generic type whose default
|
||||
specialization includes `Any`. Slice literals therefore do not participate in the subtyping
|
||||
relationship.
|
||||
|
||||
TODO: Infer a specialized type for the slice literal
|
||||
|
||||
```py
|
||||
from ty_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
# slice's default specialization is slice[Any, Any, Any], which does not participate in subtyping.
|
||||
static_assert(not is_subtype_of(TypeOf[1:2:3], slice))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(TypeOf[1:2:3], slice[int]))
|
||||
```
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
Tanjun # hangs
|
||||
antidote # hangs / slow
|
||||
Tanjun # too many cycle iterations
|
||||
altair # cycle panics (try_metaclass_)
|
||||
artigraph # cycle panics (value_type_)
|
||||
colour # cycle panics (try_metaclass_)
|
||||
core # cycle panics (value_type_)
|
||||
cpython # access to field whilst being initialized, too many cycle iterations
|
||||
discord.py # some kind of hang, only when multi-threaded?
|
||||
freqtrade # hangs
|
||||
hydpy # too many iterations
|
||||
freqtrade # cycle panics (try_metaclass_)
|
||||
hydpy # cycle panics (try_metaclass_)
|
||||
ibis # cycle panics (try_metaclass_)
|
||||
pandas # slow
|
||||
pandas-stubs # hangs/slow, or else https://github.com/salsa-rs/salsa/issues/831
|
||||
pandera # stack overflow
|
||||
prefect # slow
|
||||
pandas-stubs # cycle panics (try_metaclass_)
|
||||
pandera # cycle panics (try_metaclass_)
|
||||
pylint # cycle panics (self-recursive type alias)
|
||||
pywin32 # bad use-def map (binding with definitely-visible unbound)
|
||||
schemathesis # https://github.com/salsa-rs/salsa/issues/831
|
||||
schemathesis # salsa cycle.rs:164 assertion fails, only in CI
|
||||
scikit-learn # success, but mypy-primer hangs processing the output
|
||||
spack # success, but mypy-primer hangs processing the output
|
||||
spark # too many iterations
|
||||
steam.py # hangs
|
||||
xarray # too many iterations
|
||||
spark # cycle panics (try_metaclass_)
|
||||
steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded
|
||||
xarray # cycle panics (try_metaclass_)
|
||||
|
||||
@@ -9,7 +9,7 @@ aioredis
|
||||
aiortc
|
||||
alectryon
|
||||
alerta
|
||||
altair
|
||||
antidote
|
||||
anyio
|
||||
apprise
|
||||
arviz
|
||||
@@ -25,13 +25,13 @@ boostedblob
|
||||
check-jsonschema
|
||||
cki-lib
|
||||
cloud-init
|
||||
colour
|
||||
com2ann
|
||||
comtypes
|
||||
cwltool
|
||||
dacite
|
||||
dd-trace-py
|
||||
dedupe
|
||||
discord.py
|
||||
django-stubs
|
||||
downforeveryone
|
||||
dragonchain
|
||||
@@ -42,7 +42,6 @@ git-revise
|
||||
graphql-core
|
||||
httpx-caching
|
||||
hydra-zen
|
||||
ibis
|
||||
ignite
|
||||
imagehash
|
||||
isort
|
||||
@@ -78,6 +77,7 @@ pip
|
||||
poetry
|
||||
porcupine
|
||||
ppb-vector
|
||||
prefect
|
||||
psycopg
|
||||
pwndbg
|
||||
pybind11
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::lint::{LintRegistry, RuleSelection};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
@@ -7,7 +9,7 @@ use ruff_db::{Db as SourceDb, Upcast};
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection;
|
||||
fn rule_selection(&self) -> Arc<RuleSelection>;
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry;
|
||||
}
|
||||
@@ -123,8 +125,8 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -6,10 +6,11 @@ use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
|
||||
use ruff_python_ast::{self as ast};
|
||||
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, SemanticIndex};
|
||||
use crate::types::{infer_expression_types, Truthiness, Type};
|
||||
use crate::symbol::{symbol_from_bindings, Boundness, Symbol};
|
||||
use crate::types::{infer_expression_types, Truthiness};
|
||||
use crate::{resolve_module, Db, ModuleName};
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
@@ -28,15 +29,20 @@ fn dunder_all_names_cycle_initial(_db: &dyn Db, _file: File) -> Option<FxHashSet
|
||||
|
||||
/// Returns a set of names in the `__all__` variable for `file`, [`None`] if it is not defined or
|
||||
/// if it contains invalid elements.
|
||||
#[salsa::tracked(returns(as_ref), cycle_fn=dunder_all_names_cycle_recover, cycle_initial=dunder_all_names_cycle_initial)]
|
||||
pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option<FxHashSet<Name>> {
|
||||
let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered();
|
||||
pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option<&FxHashSet<Name>> {
|
||||
#[allow(clippy::ref_option)]
|
||||
#[salsa::tracked(return_ref, cycle_fn=dunder_all_names_cycle_recover, cycle_initial=dunder_all_names_cycle_initial)]
|
||||
fn dunder_all_names_impl(db: &dyn Db, file: File) -> Option<FxHashSet<Name>> {
|
||||
let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered();
|
||||
|
||||
let module = parsed_module(db.upcast(), file);
|
||||
let index = semantic_index(db, file);
|
||||
let mut collector = DunderAllNamesCollector::new(db, file, index);
|
||||
collector.visit_body(module.suite());
|
||||
collector.into_names()
|
||||
let module = parsed_module(db.upcast(), file);
|
||||
let index = semantic_index(db, file);
|
||||
let mut collector = DunderAllNamesCollector::new(db, file, index);
|
||||
collector.visit_body(module.suite());
|
||||
collector.into_names()
|
||||
}
|
||||
|
||||
dunder_all_names_impl(db, file).as_ref()
|
||||
}
|
||||
|
||||
/// A visitor that collects the names in the `__all__` variable of a module.
|
||||
@@ -105,8 +111,18 @@ impl<'db> DunderAllNamesCollector<'db> {
|
||||
if attr != "__all__" {
|
||||
return false;
|
||||
}
|
||||
let Type::ModuleLiteral(module_literal) = self.standalone_expression_type(value)
|
||||
else {
|
||||
let Some(name_node) = value.as_name_expr() else {
|
||||
return false;
|
||||
};
|
||||
let Symbol::Type(ty, Boundness::Bound) = symbol_from_bindings(
|
||||
self.db,
|
||||
self.index
|
||||
.use_def_map(self.scope.file_scope_id(self.db))
|
||||
.bindings_at_use(name_node.scoped_use_id(self.db, self.scope)),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let Some(module_literal) = ty.into_module_literal() else {
|
||||
return false;
|
||||
};
|
||||
let Some(module_dunder_all_names) =
|
||||
@@ -182,21 +198,14 @@ impl<'db> DunderAllNamesCollector<'db> {
|
||||
dunder_all_names(self.db, module.file())
|
||||
}
|
||||
|
||||
/// Infer the type of a standalone expression.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if `expr` was not marked as a standalone expression during semantic indexing.
|
||||
fn standalone_expression_type(&self, expr: &ast::Expr) -> Type<'db> {
|
||||
infer_expression_types(self.db, self.index.expression(expr))
|
||||
.expression_type(expr.scoped_expression_id(self.db, self.scope))
|
||||
}
|
||||
|
||||
/// Evaluate the given expression and return its truthiness.
|
||||
///
|
||||
/// Returns [`None`] if the expression type doesn't implement `__bool__` correctly.
|
||||
fn evaluate_test_expr(&self, expr: &ast::Expr) -> Option<Truthiness> {
|
||||
self.standalone_expression_type(expr).try_bool(self.db).ok()
|
||||
infer_expression_types(self.db, self.index.expression(expr))
|
||||
.expression_type(expr.scoped_expression_id(self.db, self.scope))
|
||||
.try_bool(self.db)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Add valid names to the set.
|
||||
|
||||
@@ -349,7 +349,7 @@ impl SearchPaths {
|
||||
/// The editable-install search paths for the first `site-packages` directory
|
||||
/// should come between the two `site-packages` directories when it comes to
|
||||
/// module-resolution priority.
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
tracing::debug!("Resolving dynamic module resolution paths");
|
||||
|
||||
@@ -583,7 +583,7 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
/// This is needed because Salsa requires that all query arguments are salsa ingredients.
|
||||
#[salsa::interned(debug)]
|
||||
struct ModuleNameIngredient<'db> {
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub(super) name: ModuleName,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ use salsa::Setter;
|
||||
pub struct Program {
|
||||
pub python_version: PythonVersion,
|
||||
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub python_platform: PythonPlatform,
|
||||
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub(crate) search_paths: SearchPaths,
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
/// Returns the semantic index for `file`.
|
||||
///
|
||||
/// Prefer using [`symbol_table`] when working with symbols from a single scope.
|
||||
#[salsa::tracked(returns(ref), no_eq)]
|
||||
#[salsa::tracked(return_ref, no_eq)]
|
||||
pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
|
||||
let _span = tracing::trace_span!("semantic_index", ?file).entered();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
|
||||
/// Using [`symbol_table`] over [`semantic_index`] has the advantage that
|
||||
/// Salsa can avoid invalidating dependent queries if this scope's symbol table
|
||||
/// is unchanged.
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<SymbolTable> {
|
||||
let file = scope.file(db);
|
||||
let _span = tracing::trace_span!("symbol_table", scope=?scope.as_id(), ?file).entered();
|
||||
@@ -80,7 +80,7 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
|
||||
///
|
||||
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
|
||||
/// knowing the name of the current module, and whether it's a package.
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
|
||||
semantic_index(db, file).imported_modules.clone()
|
||||
}
|
||||
@@ -90,7 +90,7 @@ pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSe
|
||||
/// Using [`use_def_map`] over [`semantic_index`] has the advantage that
|
||||
/// Salsa can avoid invalidating dependent queries if this scope's use-def map
|
||||
/// is unchanged.
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseDefMap<'db>> {
|
||||
let file = scope.file(db);
|
||||
let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?file).entered();
|
||||
@@ -387,14 +387,6 @@ impl<'db> SemanticIndex<'db> {
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub(crate) fn is_standalone_expression(
|
||||
&self,
|
||||
expression_key: impl Into<ExpressionNodeKey>,
|
||||
) -> bool {
|
||||
self.expressions_by_node
|
||||
.contains_key(&expression_key.into())
|
||||
}
|
||||
|
||||
/// Returns the id of the scope that `node` creates.
|
||||
/// This is different from [`definition::Definition::scope`] which
|
||||
/// returns the scope in which that definition is defined in.
|
||||
@@ -599,7 +591,7 @@ mod tests {
|
||||
let TestCase { db, file } = test_case("");
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
let global_names = names(global_table);
|
||||
let global_names = names(&global_table);
|
||||
|
||||
assert_eq!(global_names, Vec::<&str>::new());
|
||||
}
|
||||
@@ -609,7 +601,7 @@ mod tests {
|
||||
let TestCase { db, file } = test_case("x");
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(global_table), vec!["x"]);
|
||||
assert_eq!(names(&global_table), vec!["x"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -617,7 +609,7 @@ mod tests {
|
||||
let TestCase { db, file } = test_case("x: int");
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(global_table), vec!["int", "x"]);
|
||||
assert_eq!(names(&global_table), vec!["int", "x"]);
|
||||
// TODO record definition
|
||||
}
|
||||
|
||||
@@ -627,7 +619,7 @@ mod tests {
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(names(global_table), vec!["foo"]);
|
||||
assert_eq!(names(&global_table), vec!["foo"]);
|
||||
let foo = global_table.symbol_id_by_name("foo").unwrap();
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
@@ -640,7 +632,7 @@ mod tests {
|
||||
let TestCase { db, file } = test_case("import foo.bar");
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(global_table), vec!["foo"]);
|
||||
assert_eq!(names(&global_table), vec!["foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -648,7 +640,7 @@ mod tests {
|
||||
let TestCase { db, file } = test_case("import foo.bar as baz");
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(global_table), vec!["baz"]);
|
||||
assert_eq!(names(&global_table), vec!["baz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -657,7 +649,7 @@ mod tests {
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(names(global_table), vec!["foo"]);
|
||||
assert_eq!(names(&global_table), vec!["foo"]);
|
||||
assert!(
|
||||
global_table
|
||||
.symbol_by_name("foo")
|
||||
@@ -682,7 +674,7 @@ mod tests {
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(names(global_table), vec!["foo", "x"]);
|
||||
assert_eq!(names(&global_table), vec!["foo", "x"]);
|
||||
assert!(
|
||||
global_table
|
||||
.symbol_by_name("foo")
|
||||
@@ -702,7 +694,7 @@ mod tests {
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(names(global_table), vec!["x"]);
|
||||
assert_eq!(names(&global_table), vec!["x"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let binding = use_def
|
||||
@@ -726,7 +718,7 @@ y = 2
|
||||
);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(global_table), vec!["C", "y"]);
|
||||
assert_eq!(names(&global_table), vec!["C", "y"]);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
|
||||
@@ -798,7 +790,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(global_table), vec!["str", "int", "f"]);
|
||||
assert_eq!(names(&global_table), vec!["str", "int", "f"]);
|
||||
|
||||
let [(function_scope_id, _function_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
@@ -855,7 +847,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert!(names(global_table).is_empty());
|
||||
assert!(names(&global_table).is_empty());
|
||||
|
||||
let [(lambda_scope_id, _lambda_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
@@ -1344,7 +1336,7 @@ match subject:
|
||||
|
||||
assert!(global_table.symbol_by_name("Foo").unwrap().is_used());
|
||||
assert_eq!(
|
||||
names(global_table),
|
||||
names(&global_table),
|
||||
vec!["subject", "a", "b", "c", "d", "e", "f", "g", "h", "Foo", "i", "j", "k", "l"]
|
||||
);
|
||||
|
||||
@@ -1389,7 +1381,7 @@ match 1:
|
||||
let global_scope_id = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope_id);
|
||||
|
||||
assert_eq!(names(global_table), vec!["first", "second"]);
|
||||
assert_eq!(names(&global_table), vec!["first", "second"]);
|
||||
|
||||
let use_def = use_def_map(&db, global_scope_id);
|
||||
for (name, expected_index) in [("first", 0), ("second", 0)] {
|
||||
@@ -1410,7 +1402,7 @@ match 1:
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(global_table), &["a", "x"]);
|
||||
assert_eq!(&names(&global_table), &["a", "x"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let binding = use_def
|
||||
@@ -1426,7 +1418,7 @@ match 1:
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(global_table), &["a", "x", "y"]);
|
||||
assert_eq!(&names(&global_table), &["a", "x", "y"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let x_binding = use_def
|
||||
@@ -1446,7 +1438,7 @@ match 1:
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]);
|
||||
assert_eq!(&names(&global_table), &["e", "a", "b", "c", "d"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let binding = use_def
|
||||
|
||||
@@ -1479,37 +1479,23 @@ where
|
||||
aug_assign @ ast::StmtAugAssign {
|
||||
range: _,
|
||||
target,
|
||||
op,
|
||||
op: _,
|
||||
value,
|
||||
},
|
||||
) => {
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
self.visit_expr(value);
|
||||
|
||||
match &**target {
|
||||
ast::Expr::Name(ast::ExprName { id, .. })
|
||||
if id == "__all__" && op.is_add() && self.in_module_scope() =>
|
||||
{
|
||||
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) =
|
||||
&**value
|
||||
{
|
||||
if attr == "__all__" {
|
||||
self.add_standalone_expression(value);
|
||||
}
|
||||
}
|
||||
|
||||
self.push_assignment(aug_assign.into());
|
||||
self.visit_expr(target);
|
||||
self.pop_assignment();
|
||||
}
|
||||
ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => {
|
||||
self.push_assignment(aug_assign.into());
|
||||
self.visit_expr(target);
|
||||
self.pop_assignment();
|
||||
}
|
||||
_ => {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
// See https://docs.python.org/3/library/ast.html#ast.AugAssign
|
||||
if matches!(
|
||||
**target,
|
||||
ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_)
|
||||
) {
|
||||
self.push_assignment(aug_assign.into());
|
||||
self.visit_expr(target);
|
||||
self.pop_assignment();
|
||||
} else {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
}
|
||||
ast::Stmt::If(node) => {
|
||||
@@ -1948,12 +1934,6 @@ where
|
||||
}
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
ast::Stmt::Expr(ast::StmtExpr { value, range: _ }) if self.in_module_scope() => {
|
||||
if let Some(expr) = dunder_all_extend_argument(value) {
|
||||
self.add_standalone_expression(expr);
|
||||
}
|
||||
self.visit_expr(value);
|
||||
}
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
@@ -2643,43 +2623,3 @@ impl<'a> Unpackable<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the single argument to `__all__.extend()`, if it is a call to `__all__.extend()`
|
||||
/// where it looks like the argument might be a `submodule.__all__` expression.
|
||||
/// Else, returns `None`.
|
||||
fn dunder_all_extend_argument(value: &ast::Expr) -> Option<&ast::Expr> {
|
||||
let ast::ExprCall {
|
||||
func,
|
||||
arguments:
|
||||
ast::Arguments {
|
||||
args,
|
||||
keywords,
|
||||
range: _,
|
||||
},
|
||||
..
|
||||
} = value.as_call_expr()?;
|
||||
|
||||
let ast::ExprAttribute { value, attr, .. } = func.as_attribute_expr()?;
|
||||
|
||||
let ast::ExprName { id, .. } = value.as_name_expr()?;
|
||||
|
||||
if id != "__all__" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if attr != "extend" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let [single_argument] = &**args else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let ast::ExprAttribute { value, attr, .. } = single_argument.as_attribute_expr()?;
|
||||
|
||||
(attr == "__all__").then_some(value)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct Definition<'db> {
|
||||
/// WARNING: Only access this field when doing type inference for the same
|
||||
/// file as where `Definition` is defined to avoid cross-file query dependencies.
|
||||
#[no_eq]
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
#[tracked]
|
||||
pub(crate) kind: DefinitionKind<'db>,
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) struct Expression<'db> {
|
||||
/// The expression node.
|
||||
#[no_eq]
|
||||
#[tracked]
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
pub(crate) node_ref: AstNodeRef<ast::Expr>,
|
||||
|
||||
/// An assignment statement, if this expression is immediately used as the rhs of that
|
||||
|
||||
@@ -83,7 +83,7 @@ pub(crate) struct PatternPredicate<'db> {
|
||||
|
||||
pub(crate) subject: Expression<'db>,
|
||||
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub(crate) kind: PatternPredicateKind<'db>,
|
||||
|
||||
pub(crate) guard: Option<Expression<'db>>,
|
||||
|
||||
@@ -43,7 +43,7 @@ fn exports_cycle_initial(_db: &dyn Db, _file: File) -> Box<[Name]> {
|
||||
Box::default()
|
||||
}
|
||||
|
||||
#[salsa::tracked(returns(deref), cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)]
|
||||
#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)]
|
||||
pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> {
|
||||
let module = parsed_module(db.upcast(), file);
|
||||
let mut finder = ExportFinder::new(db, file);
|
||||
|
||||
@@ -86,7 +86,7 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked(returns(ref))]
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
@@ -1014,7 +1014,7 @@ mod implicit_globals {
|
||||
/// Conceptually this function could be a `Set` rather than a list,
|
||||
/// but the number of symbols declared in this scope is likely to be very small,
|
||||
/// so the cost of hashing the names is likely to be more expensive than it's worth.
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> {
|
||||
let Some(module_type) = KnownClass::ModuleType
|
||||
.to_class_literal(db)
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding};
|
||||
pub(crate) use crate::types::class_base::ClassBase;
|
||||
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
|
||||
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
|
||||
use crate::types::generics::{GenericContext, Specialization, TypeMapping};
|
||||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
||||
@@ -81,7 +81,7 @@ mod definition;
|
||||
#[cfg(test)]
|
||||
mod property_tests;
|
||||
|
||||
#[salsa::tracked(returns(ref))]
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
|
||||
let _span = tracing::trace_span!("check_types", ?file).entered();
|
||||
|
||||
@@ -342,13 +342,13 @@ pub struct PropertyInstanceType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> PropertyInstanceType<'db> {
|
||||
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
|
||||
fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
|
||||
let getter = self
|
||||
.getter(db)
|
||||
.map(|ty| ty.apply_type_mapping(db, type_mapping));
|
||||
.map(|ty| ty.apply_specialization(db, specialization));
|
||||
let setter = self
|
||||
.setter(db)
|
||||
.map(|ty| ty.apply_type_mapping(db, type_mapping));
|
||||
.map(|ty| ty.apply_specialization(db, specialization));
|
||||
Self::new(db, getter, setter)
|
||||
}
|
||||
|
||||
@@ -520,6 +520,8 @@ pub enum Type<'db> {
|
||||
LiteralString,
|
||||
/// A bytes literal
|
||||
BytesLiteral(BytesLiteralType<'db>),
|
||||
/// A slice literal, e.g. `1:5`, `10:0:-1` or `:`
|
||||
SliceLiteral(SliceLiteralType<'db>),
|
||||
/// A heterogeneous tuple type, with elements of the given types in source order.
|
||||
// TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`.
|
||||
Tuple(TupleType<'db>),
|
||||
@@ -587,79 +589,6 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
|
||||
}
|
||||
|
||||
/// Replace references to the class `class` with a self-reference marker. This is currently
|
||||
/// used for recursive protocols, but could probably be extended to self-referential type-
|
||||
/// aliases and similar.
|
||||
#[must_use]
|
||||
pub fn replace_self_reference(&self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Type<'db> {
|
||||
match self {
|
||||
Self::ProtocolInstance(protocol) => {
|
||||
Self::ProtocolInstance(protocol.replace_self_reference(db, class))
|
||||
}
|
||||
|
||||
Self::Union(union) => UnionType::from_elements(
|
||||
db,
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.replace_self_reference(db, class)),
|
||||
),
|
||||
|
||||
Self::Intersection(intersection) => IntersectionBuilder::new(db)
|
||||
.positive_elements(
|
||||
intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.map(|ty| ty.replace_self_reference(db, class)),
|
||||
)
|
||||
.negative_elements(
|
||||
intersection
|
||||
.negative(db)
|
||||
.iter()
|
||||
.map(|ty| ty.replace_self_reference(db, class)),
|
||||
)
|
||||
.build(),
|
||||
|
||||
Self::Tuple(tuple) => TupleType::from_elements(
|
||||
db,
|
||||
tuple
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.replace_self_reference(db, class)),
|
||||
),
|
||||
|
||||
Self::Callable(callable) => Self::Callable(callable.replace_self_reference(db, class)),
|
||||
|
||||
Self::GenericAlias(_) | Self::TypeVar(_) => {
|
||||
// TODO: replace self-references in generic aliases and typevars
|
||||
*self
|
||||
}
|
||||
|
||||
Self::Dynamic(_)
|
||||
| Self::AlwaysFalsy
|
||||
| Self::AlwaysTruthy
|
||||
| Self::Never
|
||||
| Self::BooleanLiteral(_)
|
||||
| Self::BytesLiteral(_)
|
||||
| Self::StringLiteral(_)
|
||||
| Self::IntLiteral(_)
|
||||
| Self::LiteralString
|
||||
| Self::FunctionLiteral(_)
|
||||
| Self::ModuleLiteral(_)
|
||||
| Self::ClassLiteral(_)
|
||||
| Self::NominalInstance(_)
|
||||
| Self::KnownInstance(_)
|
||||
| Self::PropertyInstance(_)
|
||||
| Self::BoundMethod(_)
|
||||
| Self::WrapperDescriptor(_)
|
||||
| Self::MethodWrapper(_)
|
||||
| Self::DataclassDecorator(_)
|
||||
| Self::DataclassTransformer(_)
|
||||
| Self::SubclassOf(_)
|
||||
| Self::BoundSuper(_) => *self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_todo(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Self::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) => true,
|
||||
@@ -678,6 +607,7 @@ impl<'db> Type<'db> {
|
||||
| Self::StringLiteral(_)
|
||||
| Self::IntLiteral(_)
|
||||
| Self::LiteralString
|
||||
| Self::SliceLiteral(_)
|
||||
| Self::Dynamic(DynamicType::Unknown | DynamicType::Any)
|
||||
| Self::BoundMethod(_)
|
||||
| Self::WrapperDescriptor(_)
|
||||
@@ -975,6 +905,7 @@ impl<'db> Type<'db> {
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::Dynamic(_)
|
||||
@@ -1171,13 +1102,15 @@ impl<'db> Type<'db> {
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::ModuleLiteral(_),
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SliceLiteral(_),
|
||||
Type::StringLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::ModuleLiteral(_),
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SliceLiteral(_),
|
||||
) => false,
|
||||
|
||||
// All `StringLiteral` types are a subtype of `LiteralString`.
|
||||
@@ -1199,6 +1132,9 @@ impl<'db> Type<'db> {
|
||||
(Type::ModuleLiteral(_), _) => KnownClass::ModuleType
|
||||
.to_instance(db)
|
||||
.is_subtype_of(db, target),
|
||||
(Type::SliceLiteral(_), _) => {
|
||||
KnownClass::Slice.to_instance(db).is_subtype_of(db, target)
|
||||
}
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
@@ -1580,6 +1516,10 @@ impl<'db> Type<'db> {
|
||||
false
|
||||
}
|
||||
|
||||
(Type::SliceLiteral(_), _) => KnownClass::Slice
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, target),
|
||||
|
||||
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
|
||||
self_function_literal
|
||||
.into_callable_type(db)
|
||||
@@ -1771,6 +1711,7 @@ impl<'db> Type<'db> {
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::BoundMethod(..)
|
||||
| Type::MethodWrapper(..)
|
||||
@@ -1783,6 +1724,7 @@ impl<'db> Type<'db> {
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::BoundMethod(..)
|
||||
| Type::MethodWrapper(..)
|
||||
@@ -1809,6 +1751,7 @@ impl<'db> Type<'db> {
|
||||
| Type::DataclassDecorator(..)
|
||||
| Type::DataclassTransformer(..)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::LiteralString,
|
||||
)
|
||||
@@ -1825,6 +1768,7 @@ impl<'db> Type<'db> {
|
||||
| Type::DataclassDecorator(..)
|
||||
| Type::DataclassTransformer(..)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::LiteralString,
|
||||
Type::Tuple(..),
|
||||
@@ -1855,6 +1799,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(..)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::BoundMethod(..)
|
||||
| Type::MethodWrapper(..)
|
||||
@@ -1867,6 +1812,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(..)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::BoundMethod(..)
|
||||
| Type::MethodWrapper(..)
|
||||
@@ -1902,6 +1848,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::BooleanLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::ClassLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::ModuleLiteral(..)
|
||||
@@ -1915,6 +1862,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::BooleanLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::ClassLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::ModuleLiteral(..)
|
||||
@@ -2005,6 +1953,13 @@ impl<'db> Type<'db> {
|
||||
!KnownClass::Bytes.is_subclass_of(db, instance.class())
|
||||
}
|
||||
|
||||
(Type::SliceLiteral(..), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::SliceLiteral(..)) => {
|
||||
// A `Type::SliceLiteral` must be an instance of exactly `slice`
|
||||
// (it cannot be an instance of a `slice` subclass)
|
||||
!KnownClass::Slice.is_subclass_of(db, instance.class())
|
||||
}
|
||||
|
||||
// A class-literal type `X` is always disjoint from an instance type `Y`,
|
||||
// unless the type expressing "all instances of `Z`" is a subtype of of `Y`,
|
||||
// where `Z` is `X`'s metaclass.
|
||||
@@ -2050,8 +2005,14 @@ impl<'db> Type<'db> {
|
||||
false
|
||||
}
|
||||
|
||||
(Type::Callable(_), Type::StringLiteral(_) | Type::BytesLiteral(_))
|
||||
| (Type::StringLiteral(_) | Type::BytesLiteral(_), Type::Callable(_)) => {
|
||||
(
|
||||
Type::Callable(_),
|
||||
Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::SliceLiteral(_),
|
||||
)
|
||||
| (
|
||||
Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::SliceLiteral(_),
|
||||
Type::Callable(_),
|
||||
) => {
|
||||
// A callable type is disjoint from other literal types. For example,
|
||||
// `Type::StringLiteral` must be an instance of exactly `str`, not a subclass
|
||||
// of `str`, and `str` is not callable. The same applies to other literal types.
|
||||
@@ -2131,6 +2092,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
@@ -2195,6 +2157,7 @@ impl<'db> Type<'db> {
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::LiteralString => {
|
||||
// Note: The literal types included in this pattern are not true singletons.
|
||||
// There can be multiple Python objects (at different memory locations) that
|
||||
@@ -2321,6 +2284,7 @@ impl<'db> Type<'db> {
|
||||
| Type::BooleanLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::KnownInstance(..) => true,
|
||||
|
||||
Type::ProtocolInstance(..) => {
|
||||
@@ -2517,6 +2481,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::NominalInstance(_)
|
||||
@@ -2626,6 +2591,7 @@ impl<'db> Type<'db> {
|
||||
KnownClass::Str.to_instance(db).instance_member(db, name)
|
||||
}
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name),
|
||||
Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).instance_member(db, name),
|
||||
Type::Tuple(_) => KnownClass::Tuple.to_instance(db).instance_member(db, name),
|
||||
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name),
|
||||
@@ -3080,6 +3046,7 @@ impl<'db> Type<'db> {
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::LiteralString
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::Tuple(..)
|
||||
| Type::TypeVar(..)
|
||||
| Type::KnownInstance(..)
|
||||
@@ -3336,6 +3303,7 @@ impl<'db> Type<'db> {
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::AlwaysTruthy => Truthiness::AlwaysTrue,
|
||||
|
||||
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
|
||||
@@ -3449,10 +3417,14 @@ impl<'db> Type<'db> {
|
||||
/// [`CallErrorKind::NotCallable`].
|
||||
fn signatures(self, db: &'db dyn Db) -> Signatures<'db> {
|
||||
match self {
|
||||
Type::Callable(callable) => Signatures::single(match callable.signatures(db) {
|
||||
[signature] => CallableSignature::single(self, signature.clone()),
|
||||
signatures => CallableSignature::from_overloads(self, signatures.iter().cloned()),
|
||||
}),
|
||||
Type::Callable(callable) => {
|
||||
Signatures::single(match callable.signatures(db).as_ref() {
|
||||
[signature] => CallableSignature::single(self, signature.clone()),
|
||||
signatures => {
|
||||
CallableSignature::from_overloads(self, signatures.iter().cloned())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Type::BoundMethod(bound_method) => {
|
||||
let signature = bound_method.function(db).signature(db);
|
||||
@@ -4238,6 +4210,7 @@ impl<'db> Type<'db> {
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
@@ -4687,6 +4660,7 @@ impl<'db> Type<'db> {
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::LiteralString
|
||||
@@ -4739,6 +4713,7 @@ impl<'db> Type<'db> {
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::ModuleLiteral(_)
|
||||
@@ -4997,6 +4972,7 @@ impl<'db> Type<'db> {
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db),
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db),
|
||||
Type::SliceLiteral(_) => KnownClass::Slice.to_class_literal(db),
|
||||
Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db),
|
||||
Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db),
|
||||
Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db),
|
||||
@@ -5067,83 +5043,75 @@ impl<'db> Type<'db> {
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Specialization<'db>,
|
||||
) -> Type<'db> {
|
||||
self.apply_type_mapping(db, specialization.type_mapping())
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: TypeMapping<'a, 'db>,
|
||||
) -> Type<'db> {
|
||||
match self {
|
||||
Type::TypeVar(typevar) => type_mapping.get(db, typevar).unwrap_or(self),
|
||||
Type::TypeVar(typevar) => specialization.get(db, typevar).unwrap_or(self),
|
||||
|
||||
Type::FunctionLiteral(function) => {
|
||||
Type::FunctionLiteral(function.apply_type_mapping(db, type_mapping))
|
||||
Type::FunctionLiteral(function.apply_specialization(db, specialization))
|
||||
}
|
||||
|
||||
Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
method.function(db).apply_type_mapping(db, type_mapping),
|
||||
method.self_instance(db).apply_type_mapping(db, type_mapping),
|
||||
method.function(db).apply_specialization(db, specialization),
|
||||
method.self_instance(db).apply_specialization(db, specialization),
|
||||
)),
|
||||
|
||||
Type::NominalInstance(instance) => Type::NominalInstance(
|
||||
instance.apply_type_mapping(db, type_mapping),
|
||||
instance.apply_specialization(db, specialization),
|
||||
),
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(
|
||||
function.apply_type_mapping(db, type_mapping),
|
||||
function.apply_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => {
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(
|
||||
function.apply_type_mapping(db, type_mapping),
|
||||
function.apply_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => {
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(
|
||||
property.apply_type_mapping(db, type_mapping),
|
||||
property.apply_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => {
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(
|
||||
property.apply_type_mapping(db, type_mapping),
|
||||
property.apply_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
Type::Callable(callable) => {
|
||||
Type::Callable(callable.apply_type_mapping(db, type_mapping))
|
||||
Type::Callable(callable.apply_specialization(db, specialization))
|
||||
}
|
||||
|
||||
Type::GenericAlias(generic) => {
|
||||
let specialization = generic
|
||||
.specialization(db)
|
||||
.apply_type_mapping(db, type_mapping);
|
||||
.apply_specialization(db, specialization);
|
||||
Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization))
|
||||
}
|
||||
|
||||
Type::PropertyInstance(property) => {
|
||||
Type::PropertyInstance(property.apply_type_mapping(db, type_mapping))
|
||||
Type::PropertyInstance(property.apply_specialization(db, specialization))
|
||||
}
|
||||
|
||||
Type::Union(union) => union.map(db, |element| {
|
||||
element.apply_type_mapping(db, type_mapping)
|
||||
element.apply_specialization(db, specialization)
|
||||
}),
|
||||
Type::Intersection(intersection) => {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
for positive in intersection.positive(db) {
|
||||
builder =
|
||||
builder.add_positive(positive.apply_type_mapping(db, type_mapping));
|
||||
builder.add_positive(positive.apply_specialization(db, specialization));
|
||||
}
|
||||
for negative in intersection.negative(db) {
|
||||
builder =
|
||||
builder.add_negative(negative.apply_type_mapping(db, type_mapping));
|
||||
builder.add_negative(negative.apply_specialization(db, specialization));
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
@@ -5151,7 +5119,7 @@ impl<'db> Type<'db> {
|
||||
db,
|
||||
tuple
|
||||
.iter(db)
|
||||
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
|
||||
.map(|ty| ty.apply_specialization(db, specialization)),
|
||||
),
|
||||
|
||||
Type::Dynamic(_)
|
||||
@@ -5175,6 +5143,7 @@ impl<'db> Type<'db> {
|
||||
| Type::LiteralString
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::BoundSuper(_)
|
||||
// Same for `ProtocolInstance`
|
||||
| Type::ProtocolInstance(_)
|
||||
@@ -5265,6 +5234,7 @@ impl<'db> Type<'db> {
|
||||
| Type::LiteralString
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
@@ -5356,6 +5326,7 @@ impl<'db> Type<'db> {
|
||||
| Self::LiteralString
|
||||
| Self::IntLiteral(_)
|
||||
| Self::BytesLiteral(_)
|
||||
| Self::SliceLiteral(_)
|
||||
| Self::MethodWrapper(_)
|
||||
| Self::WrapperDescriptor(_)
|
||||
| Self::DataclassDecorator(_)
|
||||
@@ -5708,7 +5679,7 @@ pub enum TypeVarKind {
|
||||
#[salsa::interned(debug)]
|
||||
pub struct TypeVarInstance<'db> {
|
||||
/// The name of this TypeVar (e.g. `T`)
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
name: ast::name::Name,
|
||||
|
||||
/// The type var's definition
|
||||
@@ -6656,7 +6627,7 @@ impl<'db> OverloadedFunction<'db> {
|
||||
#[salsa::interned(debug)]
|
||||
pub struct FunctionType<'db> {
|
||||
/// Name of the function at definition.
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// Is this a function that we special-case somehow? If so, which one?
|
||||
@@ -6766,7 +6737,7 @@ impl<'db> FunctionType<'db> {
|
||||
///
|
||||
/// Were this not a salsa query, then the calling query
|
||||
/// would depend on the function's AST and rerun for every change in that file.
|
||||
#[salsa::tracked(returns(ref), cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial)]
|
||||
#[salsa::tracked(return_ref, cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial)]
|
||||
pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
|
||||
if let Some(overloaded) = self.to_overloaded(db) {
|
||||
FunctionSignature::Overloaded(
|
||||
@@ -6873,10 +6844,6 @@ impl<'db> FunctionType<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
|
||||
self.apply_specialization(db, type_mapping.into_specialization(db))
|
||||
}
|
||||
|
||||
fn find_legacy_typevars(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -6915,56 +6882,68 @@ impl<'db> FunctionType<'db> {
|
||||
/// 2. second `foo` definition, it would contain both overloads and still no implementation
|
||||
/// 3. third `foo` definition, it would contain both overloads and the implementation which is
|
||||
/// itself
|
||||
#[salsa::tracked(returns(as_ref))]
|
||||
fn to_overloaded(self, db: &'db dyn Db) -> Option<OverloadedFunction<'db>> {
|
||||
let mut current = self;
|
||||
let mut overloads = vec![];
|
||||
fn to_overloaded(self, db: &'db dyn Db) -> Option<&'db OverloadedFunction<'db>> {
|
||||
#[allow(clippy::ref_option)]
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn to_overloaded_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
function: FunctionType<'db>,
|
||||
) -> Option<OverloadedFunction<'db>> {
|
||||
let mut current = function;
|
||||
let mut overloads = vec![];
|
||||
|
||||
loop {
|
||||
// The semantic model records a use for each function on the name node. This is used
|
||||
// here to get the previous function definition with the same name.
|
||||
let scope = current.definition(db).scope(db);
|
||||
let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db));
|
||||
let use_id = current
|
||||
.body_scope(db)
|
||||
.node(db)
|
||||
.expect_function()
|
||||
.name
|
||||
.scoped_use_id(db, scope);
|
||||
loop {
|
||||
// The semantic model records a use for each function on the name node. This is used
|
||||
// here to get the previous function definition with the same name.
|
||||
let scope = current.definition(db).scope(db);
|
||||
let use_def =
|
||||
semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db));
|
||||
let use_id = current
|
||||
.body_scope(db)
|
||||
.node(db)
|
||||
.expect_function()
|
||||
.name
|
||||
.scoped_use_id(db, scope);
|
||||
|
||||
let Symbol::Type(Type::FunctionLiteral(previous), Boundness::Bound) =
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
let Symbol::Type(Type::FunctionLiteral(previous), Boundness::Bound) =
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
if previous.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
|
||||
overloads.push(previous);
|
||||
} else {
|
||||
break;
|
||||
if previous.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
|
||||
overloads.push(previous);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
current = previous;
|
||||
}
|
||||
|
||||
current = previous;
|
||||
// Overloads are inserted in reverse order, from bottom to top.
|
||||
overloads.reverse();
|
||||
|
||||
let implementation = if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
|
||||
overloads.push(function);
|
||||
None
|
||||
} else {
|
||||
Some(function)
|
||||
};
|
||||
|
||||
if overloads.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(OverloadedFunction {
|
||||
overloads,
|
||||
implementation,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Overloads are inserted in reverse order, from bottom to top.
|
||||
overloads.reverse();
|
||||
|
||||
let implementation = if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
|
||||
overloads.push(self);
|
||||
None
|
||||
} else {
|
||||
Some(self)
|
||||
};
|
||||
|
||||
if overloads.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(OverloadedFunction {
|
||||
overloads,
|
||||
implementation,
|
||||
})
|
||||
}
|
||||
// HACK: This is required because salsa doesn't support returning `Option<&T>` from tracked
|
||||
// functions yet. Refer to https://github.com/salsa-rs/salsa/pull/772. Remove the inner
|
||||
// function once it's supported.
|
||||
to_overloaded_impl(db, self).as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7157,7 +7136,7 @@ impl<'db> BoundMethodType<'db> {
|
||||
/// `CallableType`.
|
||||
#[salsa::interned(debug)]
|
||||
pub struct CallableType<'db> {
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
signatures: Box<[Signature<'db>]>,
|
||||
}
|
||||
|
||||
@@ -7213,12 +7192,15 @@ impl<'db> CallableType<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
|
||||
/// Apply a specialization to this callable type.
|
||||
///
|
||||
/// See [`Type::apply_specialization`] for more details.
|
||||
fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
|
||||
CallableType::from_overloads(
|
||||
db,
|
||||
self.signatures(db)
|
||||
.iter()
|
||||
.map(|signature| signature.apply_type_mapping(db, type_mapping)),
|
||||
.map(|signature| signature.apply_specialization(db, specialization)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7267,7 +7249,7 @@ impl<'db> CallableType<'db> {
|
||||
where
|
||||
F: Fn(&Signature<'db>, &Signature<'db>) -> bool,
|
||||
{
|
||||
match (self.signatures(db), other.signatures(db)) {
|
||||
match (&**self.signatures(db), &**other.signatures(db)) {
|
||||
([self_signature], [other_signature]) => {
|
||||
// Base case: both callable types contain a single signature.
|
||||
check_signature(self_signature, other_signature)
|
||||
@@ -7309,7 +7291,7 @@ impl<'db> CallableType<'db> {
|
||||
///
|
||||
/// See [`Type::is_equivalent_to`] for more details.
|
||||
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self.signatures(db), other.signatures(db)) {
|
||||
match (&**self.signatures(db), &**other.signatures(db)) {
|
||||
([self_signature], [other_signature]) => {
|
||||
// Common case: both callable types contain a single signature, use the custom
|
||||
// equivalence check instead of delegating it to the subtype check.
|
||||
@@ -7335,7 +7317,7 @@ impl<'db> CallableType<'db> {
|
||||
///
|
||||
/// See [`Type::is_gradual_equivalent_to`] for more details.
|
||||
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self.signatures(db), other.signatures(db)) {
|
||||
match (&**self.signatures(db), &**other.signatures(db)) {
|
||||
([self_signature], [other_signature]) => {
|
||||
self_signature.is_gradual_equivalent_to(db, other_signature)
|
||||
}
|
||||
@@ -7345,17 +7327,6 @@ impl<'db> CallableType<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`Type::replace_self_reference`].
|
||||
fn replace_self_reference(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
|
||||
CallableType::from_overloads(
|
||||
db,
|
||||
self.signatures(db)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|signature| signature.replace_self_reference(db, class)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a specific instance of `types.MethodWrapperType`
|
||||
@@ -7439,7 +7410,7 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
pub struct TypeAliasType<'db> {
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub name: ast::name::Name,
|
||||
|
||||
rhs_scope: ScopeId<'db>,
|
||||
@@ -7473,11 +7444,15 @@ pub(super) struct MetaclassCandidate<'db> {
|
||||
#[salsa::interned(debug)]
|
||||
pub struct UnionType<'db> {
|
||||
/// The union type includes values in any of these types.
|
||||
#[returns(deref)]
|
||||
pub elements: Box<[Type<'db>]>,
|
||||
#[return_ref]
|
||||
elements_boxed: Box<[Type<'db>]>,
|
||||
}
|
||||
|
||||
impl<'db> UnionType<'db> {
|
||||
fn elements(self, db: &'db dyn Db) -> &'db [Type<'db>] {
|
||||
self.elements_boxed(db)
|
||||
}
|
||||
|
||||
/// Create a union from a list of elements
|
||||
/// (which may be eagerly simplified into a different variant of [`Type`] altogether).
|
||||
pub fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
|
||||
@@ -7694,7 +7669,7 @@ impl<'db> UnionType<'db> {
|
||||
#[salsa::interned(debug)]
|
||||
pub struct IntersectionType<'db> {
|
||||
/// The intersection type includes only values in all of these types.
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
positive: FxOrderSet<Type<'db>>,
|
||||
|
||||
/// The intersection type does not include any value in any of these types.
|
||||
@@ -7702,7 +7677,7 @@ pub struct IntersectionType<'db> {
|
||||
/// Negation types aren't expressible in annotations, and are most likely to arise from type
|
||||
/// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them
|
||||
/// directly in intersections rather than as a separate type.
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
negative: FxOrderSet<Type<'db>>,
|
||||
}
|
||||
|
||||
@@ -7932,7 +7907,7 @@ impl<'db> IntersectionType<'db> {
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
pub struct StringLiteralType<'db> {
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
value: Box<str>,
|
||||
}
|
||||
|
||||
@@ -7953,7 +7928,7 @@ impl<'db> StringLiteralType<'db> {
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
pub struct BytesLiteralType<'db> {
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
value: Box<[u8]>,
|
||||
}
|
||||
|
||||
@@ -7963,9 +7938,21 @@ impl<'db> BytesLiteralType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned(debug)]
|
||||
pub struct SliceLiteralType<'db> {
|
||||
start: Option<i32>,
|
||||
stop: Option<i32>,
|
||||
step: Option<i32>,
|
||||
}
|
||||
|
||||
impl SliceLiteralType<'_> {
|
||||
fn as_tuple(self, db: &dyn Db) -> (Option<i32>, Option<i32>, Option<i32>) {
|
||||
(self.start(db), self.stop(db), self.step(db))
|
||||
}
|
||||
}
|
||||
#[salsa::interned(debug)]
|
||||
pub struct TupleType<'db> {
|
||||
#[returns(deref)]
|
||||
#[return_ref]
|
||||
elements: Box<[Type<'db>]>,
|
||||
}
|
||||
|
||||
@@ -8146,6 +8133,7 @@ impl<'db> SuperOwnerKind<'db> {
|
||||
/// Represent a bound super object like `super(PivotClass, owner)`
|
||||
#[salsa::interned(debug)]
|
||||
pub struct BoundSuperType<'db> {
|
||||
#[return_ref]
|
||||
pub pivot_class: ClassBase<'db>,
|
||||
#[return_ref]
|
||||
pub owner: SuperOwnerKind<'db>,
|
||||
@@ -8286,7 +8274,7 @@ impl<'db> BoundSuperType<'db> {
|
||||
.find_name_in_mro_with_policy(db, name, policy)
|
||||
.expect("Calling `find_name_in_mro` on dynamic type should return `Some`")
|
||||
}
|
||||
SuperOwnerKind::Class(class) => class,
|
||||
SuperOwnerKind::Class(class) => *class,
|
||||
SuperOwnerKind::Instance(instance) => instance.class(),
|
||||
};
|
||||
|
||||
|
||||
@@ -529,28 +529,6 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn positive_elements<I, T>(mut self, elements: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Type<'db>>,
|
||||
{
|
||||
for element in elements {
|
||||
self = self.add_positive(element.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn negative_elements<I, T>(mut self, elements: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Type<'db>>,
|
||||
{
|
||||
for element in elements {
|
||||
self = self.add_negative(element.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(mut self) -> Type<'db> {
|
||||
// Avoid allocating the UnionBuilder unnecessarily if we have just one intersection:
|
||||
if self.intersections.len() == 1 {
|
||||
|
||||
@@ -447,7 +447,7 @@ impl<'db> Bindings<'db> {
|
||||
overload.parameter_types()
|
||||
{
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
literal.value(db).starts_with(prefix.value(db)),
|
||||
literal.value(db).starts_with(&**prefix.value(db)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1368,6 +1368,21 @@ impl<'db> Binding<'db> {
|
||||
&self.parameter_tys
|
||||
}
|
||||
|
||||
/// Returns the bound types for each parameter, in parameter source order, with default values
|
||||
/// applied for arguments that weren't matched to a parameter. Returns `None` if there are any
|
||||
/// non-default arguments that weren't matched to a parameter.
|
||||
pub(crate) fn parameter_types_with_defaults(
|
||||
&self,
|
||||
signature: &Signature<'db>,
|
||||
) -> Option<Box<[Type<'db>]>> {
|
||||
signature
|
||||
.parameters()
|
||||
.iter()
|
||||
.zip(&self.parameter_tys)
|
||||
.map(|(parameter, parameter_ty)| parameter_ty.or(parameter.default_type()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn arguments_for_parameter<'a>(
|
||||
&'a self,
|
||||
argument_types: &'a CallArgumentTypes<'a, 'db>,
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::{
|
||||
};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::DeclarationWithConstraint;
|
||||
use crate::types::generics::{GenericContext, Specialization, TypeMapping};
|
||||
use crate::types::generics::{GenericContext, Specialization};
|
||||
use crate::types::signatures::{Parameter, Parameters};
|
||||
use crate::types::{
|
||||
CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature,
|
||||
@@ -60,6 +60,28 @@ fn explicit_bases_cycle_initial<'db>(
|
||||
Box::default()
|
||||
}
|
||||
|
||||
fn try_mro_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<Mro<'db>, MroError<'db>>,
|
||||
_count: u32,
|
||||
_self: ClassLiteral<'db>,
|
||||
_specialization: Option<Specialization<'db>>,
|
||||
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn try_mro_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_: ClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Ok(Mro::from_error(
|
||||
db,
|
||||
self_.apply_optional_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
#[expect(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
|
||||
fn inheritance_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
@@ -77,48 +99,6 @@ fn inheritance_cycle_initial<'db>(
|
||||
None
|
||||
}
|
||||
|
||||
fn try_mro_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<Mro<'db>, MroError<'db>>,
|
||||
_count: u32,
|
||||
_self: ClassLiteral<'db>,
|
||||
_specialization: Option<Specialization<'db>>,
|
||||
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn try_mro_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_: ClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Err(MroError::cycle(
|
||||
db,
|
||||
self_.apply_optional_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
fn try_metaclass_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>>,
|
||||
_count: u32,
|
||||
_self: ClassLiteral<'db>,
|
||||
) -> salsa::CycleRecoveryAction<
|
||||
Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>>,
|
||||
> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn try_metaclass_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_self_: ClassLiteral<'db>,
|
||||
) -> Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>> {
|
||||
Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::Cycle,
|
||||
})
|
||||
}
|
||||
|
||||
/// A category of classes with code generation capabilities (with synthesized methods).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum CodeGeneratorKind {
|
||||
@@ -167,11 +147,16 @@ impl<'db> GenericAlias<'db> {
|
||||
self.origin(db).definition(db)
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
|
||||
pub(super) fn apply_specialization(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Specialization<'db>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.origin(db),
|
||||
self.specialization(db).apply_type_mapping(db, type_mapping),
|
||||
self.specialization(db)
|
||||
.apply_specialization(db, specialization),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -251,14 +236,16 @@ impl<'db> ClassType<'db> {
|
||||
self.is_known(db, KnownClass::Object)
|
||||
}
|
||||
|
||||
pub(super) fn apply_type_mapping<'a>(
|
||||
pub(super) fn apply_specialization(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: TypeMapping<'a, 'db>,
|
||||
specialization: Specialization<'db>,
|
||||
) -> Self {
|
||||
match self {
|
||||
Self::NonGeneric(_) => self,
|
||||
Self::Generic(generic) => Self::Generic(generic.apply_type_mapping(db, type_mapping)),
|
||||
Self::Generic(generic) => {
|
||||
Self::Generic(generic.apply_specialization(db, specialization))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,7 +458,7 @@ impl<'db> From<ClassType<'db>> for Type<'db> {
|
||||
#[salsa::interned(debug)]
|
||||
pub struct ClassLiteral<'db> {
|
||||
/// Name of the class at definition
|
||||
#[returns(ref)]
|
||||
#[return_ref]
|
||||
pub(crate) name: ast::name::Name,
|
||||
|
||||
pub(crate) body_scope: ScopeId<'db>,
|
||||
@@ -482,23 +469,6 @@ pub struct ClassLiteral<'db> {
|
||||
pub(crate) dataclass_transformer_params: Option<DataclassTransformerParams>,
|
||||
}
|
||||
|
||||
#[expect(clippy::trivially_copy_pass_by_ref, clippy::ref_option)]
|
||||
fn pep695_generic_context_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Option<GenericContext<'db>>,
|
||||
_count: u32,
|
||||
_self: ClassLiteral<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Option<GenericContext<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn pep695_generic_context_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_self: ClassLiteral<'db>,
|
||||
) -> Option<GenericContext<'db>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> ClassLiteral<'db> {
|
||||
/// Return `true` if this class represents `known_class`
|
||||
@@ -524,7 +494,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
.or_else(|| self.inherited_legacy_generic_context(db))
|
||||
}
|
||||
|
||||
#[salsa::tracked(cycle_fn=pep695_generic_context_cycle_recover, cycle_initial=pep695_generic_context_cycle_initial)]
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
|
||||
let scope = self.body_scope(db);
|
||||
let class_def_node = scope.node(db).expect_class();
|
||||
@@ -634,8 +604,21 @@ impl<'db> ClassLiteral<'db> {
|
||||
///
|
||||
/// Were this not a salsa query, then the calling query
|
||||
/// would depend on the class's AST and rerun for every change in that file.
|
||||
#[salsa::tracked(returns(deref), cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)]
|
||||
pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
pub(super) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] {
|
||||
self.explicit_bases_query(db)
|
||||
}
|
||||
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class
|
||||
/// objects, and applying default specialization to any unspecialized generic class literals.
|
||||
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
|
||||
self.explicit_bases(db)
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|ty| ty.to_class_type(db))
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)]
|
||||
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
tracing::trace!("ClassLiteral::explicit_bases_query: {}", self.name(db));
|
||||
|
||||
let class_stmt = self.node(db);
|
||||
@@ -649,15 +632,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class
|
||||
/// objects, and applying default specialization to any unspecialized generic class literals.
|
||||
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
|
||||
self.explicit_bases(db)
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|ty| ty.to_class_type(db))
|
||||
}
|
||||
|
||||
/// Determine if this class is a protocol.
|
||||
///
|
||||
/// This method relies on the accuracy of the [`KnownClass::is_protocol`] method,
|
||||
@@ -696,7 +670,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
|
||||
/// Return the types of the decorators on this class
|
||||
#[salsa::tracked(returns(deref))]
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
tracing::trace!("ClassLiteral::decorators: {}", self.name(db));
|
||||
|
||||
@@ -742,7 +716,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
/// attribute on a class at runtime.
|
||||
///
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
#[salsa::tracked(returns(as_ref), cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial)]
|
||||
#[salsa::tracked(return_ref, cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial)]
|
||||
pub(super) fn try_mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -819,10 +793,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
|
||||
/// Return the metaclass of this class, or an error if the metaclass cannot be inferred.
|
||||
#[salsa::tracked(
|
||||
cycle_fn=try_metaclass_cycle_recover,
|
||||
cycle_initial=try_metaclass_cycle_initial,
|
||||
)]
|
||||
#[salsa::tracked]
|
||||
pub(super) fn try_metaclass(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -834,11 +805,8 @@ impl<'db> ClassLiteral<'db> {
|
||||
|
||||
if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() {
|
||||
// We emit diagnostics for cyclic class definitions elsewhere.
|
||||
// Avoid attempting to infer the metaclass if the class is cyclically defined.
|
||||
return Ok((SubclassOfType::subclass_of_unknown(), None));
|
||||
}
|
||||
|
||||
if self.try_mro(db, None).is_err_and(MroError::is_cycle) {
|
||||
// Avoid attempting to infer the metaclass if the class is cyclically defined:
|
||||
// it would be easy to enter an infinite loop.
|
||||
return Ok((SubclassOfType::subclass_of_unknown(), None));
|
||||
}
|
||||
|
||||
@@ -2701,47 +2669,6 @@ impl<'db> KnownClassLookupError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SliceLiteral {
|
||||
pub(crate) start: Option<i32>,
|
||||
pub(crate) stop: Option<i32>,
|
||||
pub(crate) step: Option<i32>,
|
||||
}
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
/// If this type represents a valid slice literal, returns a [`SliceLiteral`] describing it.
|
||||
/// Otherwise returns `None`.
|
||||
///
|
||||
/// The type must be a specialization of the `slice` builtin type, where the specialized
|
||||
/// typevars are statically known integers or `None`.
|
||||
pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option<SliceLiteral> {
|
||||
let ClassType::Generic(alias) = self.into_nominal_instance()?.class() else {
|
||||
return None;
|
||||
};
|
||||
if !alias.origin(db).is_known(db, KnownClass::Slice) {
|
||||
return None;
|
||||
}
|
||||
let [start, stop, step] = alias.specialization(db).types(db) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let to_u32 = |ty: &Type<'db>| match ty {
|
||||
Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(),
|
||||
Type::BooleanLiteral(b) => Some(Some(i32::from(*b))),
|
||||
Type::NominalInstance(instance)
|
||||
if instance.class().is_known(db, KnownClass::NoneType) =>
|
||||
{
|
||||
Some(None)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Some(SliceLiteral {
|
||||
start: to_u32(start)?,
|
||||
stop: to_u32(stop)?,
|
||||
step: to_u32(step)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct MetaclassError<'db> {
|
||||
kind: MetaclassErrorKind<'db>,
|
||||
@@ -2776,8 +2703,6 @@ pub(super) enum MetaclassErrorKind<'db> {
|
||||
NotCallable(Type<'db>),
|
||||
/// The metaclass is of a union type whose some members are not callable
|
||||
PartlyNotCallable(Type<'db>),
|
||||
/// A cycle was encountered attempting to determine the metaclass
|
||||
Cycle,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user