Compare commits

..

2 Commits

Author SHA1 Message Date
Micha Reiser
f78c03c5dc Move more projects to good.txt 2025-05-08 19:34:56 +02:00
Micha Reiser
1fa5e1fbbc Update salsa 2025-05-08 19:28:35 +02:00
121 changed files with 1416 additions and 3256 deletions

View File

@@ -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/.*|

View File

@@ -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
View File

@@ -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]]

View File

@@ -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]

View File

@@ -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

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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<()> {

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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();

View File

@@ -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 }

View File

@@ -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(())
}

View File

@@ -1,4 +1,5 @@
//! Generate CLI help.
#![allow(clippy::print_stdout)]
use std::path::PathBuf;
use std::{fs, str};

View File

@@ -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;

View File

@@ -1,3 +1,5 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -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();

View File

@@ -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 = "🧪";

View File

@@ -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(&current, &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 })
}
}

View File

@@ -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(&current, &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(())
}
}

View File

@@ -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 _;

View File

@@ -1,3 +1,5 @@
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::fs;
use std::path::PathBuf;

View File

@@ -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)?,

View File

@@ -1,4 +1,5 @@
//! Print the AST for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::path::PathBuf;

View File

@@ -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;

View File

@@ -1,4 +1,5 @@
//! Print the token stream for a given Python file.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::path::PathBuf;

View File

@@ -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;

View File

@@ -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 {

View File

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

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}

View File

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

View File

@@ -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"] }

View File

@@ -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};

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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>&lt;KEY&gt; = &lt;VALUE&gt;</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]
```

View File

@@ -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"
```
---

View File

@@ -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
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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"]

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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;

View 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>,
}

View File

@@ -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
```

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`:

View File

@@ -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'>]
```

View File

@@ -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

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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`
|
```

View File

@@ -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
|

View File

@@ -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`
|
```

View File

@@ -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]`
|
```

View File

@@ -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`.

View File

@@ -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]))
```

View File

@@ -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_)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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>,

View File

@@ -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

View File

@@ -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>>,

View File

@@ -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);

View 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);

View 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)

View File

@@ -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(),
};

View File

@@ -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 {

View File

@@ -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>,

View File

@@ -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