Compare commits

..

3 Commits

Author SHA1 Message Date
Micha Reiser
05854142e7 Test Salsa #851 2025-05-08 10:53:51 +02:00
David Peter
33eb008fb6 Disabled event handler if tracing is not enabled 2025-05-08 10:28:59 +02:00
David Peter
04d4febfc4 [ty] Update salsa 2025-05-08 10:17:32 +02:00
164 changed files with 1791 additions and 5758 deletions

View File

@@ -1,7 +0,0 @@
#:schema ../ty.schema.json
# Configuration overrides for the mypy primer run
# Enable off-by-default rules.
[rules]
possibly-unresolved-reference = "warn"
unused-ignore-comment = "warn"

View File

@@ -50,10 +50,6 @@ jobs:
run: |
cd ruff
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
mkdir -p ~/.config/ty
cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml
PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)"
echo "new commit"

View File

@@ -5,7 +5,6 @@ exclude: |
.github/workflows/release.yml|
crates/ty_vendored/vendor/.*|
crates/ty_project/resources/.*|
crates/ty/docs/(configuration|rules|cli).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

48
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,9 +2725,7 @@ dependencies = [
"tracing",
"tracing-indicatif",
"tracing-subscriber",
"ty",
"ty_project",
"url",
]
[[package]]
@@ -2799,7 +2785,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.9"
version = "0.11.8"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2827,7 +2813,6 @@ dependencies = [
"regex",
"ruff_annotate_snippets",
"ruff_cache",
"ruff_db",
"ruff_diagnostics",
"ruff_macros",
"ruff_notebook",
@@ -2889,13 +2874,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 +3112,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.9"
version = "0.11.8"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3182,7 +3160,6 @@ dependencies = [
"ruff_graph",
"ruff_linter",
"ruff_macros",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_semantic",
@@ -3260,7 +3237,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=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
dependencies = [
"boxcar",
"compact_str",
@@ -3283,12 +3260,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=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
[[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=ef93d3830ffb8dc621fef1ba7b234ccef9dc3639#ef93d3830ffb8dc621fef1ba7b234ccef9dc3639"
dependencies = [
"heck",
"proc-macro2",
@@ -4036,7 +4013,6 @@ dependencies = [
"ruff_cache",
"ruff_db",
"ruff_macros",
"ruff_options_metadata",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_text_size",
@@ -4243,12 +4219,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 +4595,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 = "ef93d3830ffb8dc621fef1ba7b234ccef9dc3639" }
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

@@ -439,7 +439,7 @@ impl LintCacheData {
.map(|msg| {
// Make sure that all message use the same source file.
assert_eq!(
msg.file,
&msg.file,
messages.first().unwrap().source_file(),
"message uses a different source file"
);

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

@@ -15,7 +15,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::message::Message;
use ruff_linter::message::{Message, SyntaxErrorMessage};
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
@@ -102,7 +102,11 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::syntax_error(err, TextRange::default(), dummy)],
vec![Message::SyntaxError(SyntaxErrorMessage {
message: err.to_string(),
range: TextRange::default(),
file: dummy,
})],
FxHashMap::default(),
)
}

View File

@@ -59,7 +59,13 @@ type KeyDiagnosticFields = (
Severity,
);
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[];
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[(
DiagnosticId::lint("unused-ignore-comment"),
Some("/src/tomllib/_parser.py"),
Some(22299..22333),
"Unused blanket `type: ignore` directive",
Severity::Warning,
)];
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
SystemPathBuf::from("src").join(file.name())
@@ -197,7 +203,7 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDi
diagnostic.id(),
diagnostic
.primary_span()
.map(|span| span.expect_ty_file())
.map(|span| span.file())
.map(|file| file.path(db).as_str()),
diagnostic
.primary_span()

View File

@@ -1,15 +1,15 @@
use std::{fmt::Formatter, sync::Arc};
use render::{FileResolver, Input};
use ruff_source_file::{SourceCode, SourceFile};
use thiserror::Error;
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange};
pub use self::render::DisplayDiagnostic;
use crate::{files::File, Db};
use crate::files::File;
use crate::Db;
use self::render::FileResolver;
mod render;
mod stylesheet;
@@ -115,9 +115,10 @@ impl Diagnostic {
/// callers should prefer using this with `write!` instead of `writeln!`.
pub fn display<'a>(
&'a self,
resolver: &'a dyn FileResolver,
db: &'a dyn Db,
config: &'a DisplayDiagnosticConfig,
) -> DisplayDiagnostic<'a> {
let resolver = FileResolver::new(db);
DisplayDiagnostic::new(resolver, config, self)
}
@@ -232,16 +233,6 @@ impl Diagnostic {
self.primary_annotation().map(|ann| ann.tags.as_slice())
}
/// Returns the "primary" span of this diagnostic, panicking if it does not exist.
///
/// This should typically only be used when working with diagnostics in Ruff, where diagnostics
/// are currently required to have a primary span.
///
/// See [`Diagnostic::primary_span`] for more details.
pub fn expect_primary_span(&self) -> Span {
self.primary_span().expect("Expected a primary span")
}
/// Returns a key that can be used to sort two diagnostics into the canonical order
/// in which they should appear when rendered.
pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a {
@@ -276,7 +267,11 @@ impl Ord for RenderingSortKey<'_> {
self.diagnostic.primary_span(),
other.diagnostic.primary_span(),
) {
let order = span1.file().path(&self.db).cmp(span2.file().path(&self.db));
let order = span1
.file()
.path(self.db)
.as_str()
.cmp(span2.file().path(self.db).as_str());
if order.is_ne() {
return order;
}
@@ -648,10 +643,6 @@ impl DiagnosticId {
DiagnosticId::UnknownRule => "unknown-rule",
})
}
pub fn is_invalid_syntax(&self) -> bool {
matches!(self, Self::InvalidSyntax)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
@@ -677,62 +668,6 @@ impl std::fmt::Display for DiagnosticId {
}
}
/// A unified file representation for both ruff and ty.
///
/// Such a representation is needed for rendering [`Diagnostic`]s that can optionally contain
/// [`Annotation`]s with [`Span`]s that need to refer to the text of a file. However, ty and ruff
/// use very different file types: a `Copy`-able salsa-interned [`File`], and a heavier-weight
/// [`SourceFile`], respectively.
///
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
/// emitting diagnostics from both ty and ruff.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnifiedFile {
Ty(File),
Ruff(SourceFile),
}
impl UnifiedFile {
pub fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str {
match self {
UnifiedFile::Ty(file) => resolver.path(*file),
UnifiedFile::Ruff(file) => file.name(),
}
}
fn diagnostic_source(&self, resolver: &dyn FileResolver) -> DiagnosticSource {
match self {
UnifiedFile::Ty(file) => DiagnosticSource::Ty(resolver.input(*file)),
UnifiedFile::Ruff(file) => DiagnosticSource::Ruff(file.clone()),
}
}
}
/// A unified wrapper for types that can be converted to a [`SourceCode`].
///
/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code.
/// [`DiagnosticSource`] wraps both of these and provides the single
/// [`DiagnosticSource::as_source_code`] method to produce a [`SourceCode`] with the appropriate
/// lifetimes.
///
/// See [`UnifiedFile::diagnostic_source`] for a way to obtain a [`DiagnosticSource`] from a file
/// and [`FileResolver`].
#[derive(Clone, Debug)]
enum DiagnosticSource {
Ty(Input),
Ruff(SourceFile),
}
impl DiagnosticSource {
/// Returns this input as a `SourceCode` for convenient querying.
fn as_source_code(&self) -> SourceCode {
match self {
DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index),
DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()),
}
}
}
/// A span represents the source of a diagnostic.
///
/// It consists of a `File` and an optional range into that file. When the
@@ -740,14 +675,14 @@ impl DiagnosticSource {
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
file: UnifiedFile,
file: File,
range: Option<TextRange>,
}
impl Span {
/// Returns the `UnifiedFile` attached to this `Span`.
pub fn file(&self) -> &UnifiedFile {
&self.file
/// Returns the `File` attached to this `Span`.
pub fn file(&self) -> File {
self.file
}
/// Returns the range, if available, attached to this `Span`.
@@ -768,38 +703,10 @@ impl Span {
pub fn with_optional_range(self, range: Option<TextRange>) -> Span {
Span { range, ..self }
}
/// Returns the [`File`] attached to this [`Span`].
///
/// Panics if the file is a [`UnifiedFile::Ruff`] instead of a [`UnifiedFile::Ty`].
pub fn expect_ty_file(&self) -> File {
match self.file {
UnifiedFile::Ty(file) => file,
UnifiedFile::Ruff(_) => panic!("Expected a ty `File`, found a ruff `SourceFile`"),
}
}
/// Returns the [`SourceFile`] attached to this [`Span`].
///
/// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`].
pub fn expect_ruff_file(&self) -> &SourceFile {
match &self.file {
UnifiedFile::Ty(_) => panic!("Expected a ruff `SourceFile`, found a ty `File`"),
UnifiedFile::Ruff(file) => file,
}
}
}
impl From<File> for Span {
fn from(file: File) -> Span {
let file = UnifiedFile::Ty(file);
Span { file, range: None }
}
}
impl From<SourceFile> for Span {
fn from(file: SourceFile) -> Self {
let file = UnifiedFile::Ruff(file);
Span { file, range: None }
}
}

View File

@@ -16,8 +16,7 @@ use crate::{
};
use super::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
SubDiagnostic,
Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, Severity, SubDiagnostic,
};
/// A type that implements `std::fmt::Display` for diagnostic rendering.
@@ -31,16 +30,17 @@ use super::{
/// values. When using Salsa, this most commonly corresponds to the lifetime
/// of a Salsa `Db`.
/// * The lifetime of the diagnostic being rendered.
#[derive(Debug)]
pub struct DisplayDiagnostic<'a> {
config: &'a DisplayDiagnosticConfig,
resolver: &'a dyn FileResolver,
resolver: FileResolver<'a>,
annotate_renderer: AnnotateRenderer,
diag: &'a Diagnostic,
}
impl<'a> DisplayDiagnostic<'a> {
pub(crate) fn new(
resolver: &'a dyn FileResolver,
resolver: FileResolver<'a>,
config: &'a DisplayDiagnosticConfig,
diag: &'a Diagnostic,
) -> DisplayDiagnostic<'a> {
@@ -86,13 +86,11 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
write!(
f,
" {path}",
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
path = fmt_styled(self.resolver.path(span.file()), stylesheet.emphasis)
)?;
if let Some(range) = span.range() {
let diagnostic_source = span.file().diagnostic_source(self.resolver);
let start = diagnostic_source
.as_source_code()
.line_column(range.start());
let input = self.resolver.input(span.file());
let start = input.as_source_code().line_column(range.start());
write!(
f,
@@ -117,7 +115,7 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
.emphasis(stylesheet.emphasis)
.none(stylesheet.none);
let resolved = Resolved::new(self.resolver, self.diag);
let resolved = Resolved::new(&self.resolver, self.diag);
let renderable = resolved.to_renderable(self.config.context);
for diag in renderable.diagnostics.iter() {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
@@ -146,7 +144,7 @@ struct Resolved<'a> {
impl<'a> Resolved<'a> {
/// Creates a new resolved set of diagnostics.
fn new(resolver: &'a dyn FileResolver, diag: &'a Diagnostic) -> Resolved<'a> {
fn new(resolver: &FileResolver<'a>, diag: &'a Diagnostic) -> Resolved<'a> {
let mut diagnostics = vec![];
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, diag));
for sub in &diag.inner.subs {
@@ -184,7 +182,7 @@ struct ResolvedDiagnostic<'a> {
impl<'a> ResolvedDiagnostic<'a> {
/// Resolve a single diagnostic.
fn from_diagnostic(
resolver: &'a dyn FileResolver,
resolver: &FileResolver<'a>,
diag: &'a Diagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
@@ -192,9 +190,9 @@ impl<'a> ResolvedDiagnostic<'a> {
.annotations
.iter()
.filter_map(|ann| {
let path = ann.span.file.path(resolver);
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann)
let path = resolver.path(ann.span.file);
let input = resolver.input(ann.span.file);
ResolvedAnnotation::new(path, &input, ann)
})
.collect();
let message = if diag.inner.message.as_str().is_empty() {
@@ -218,7 +216,7 @@ impl<'a> ResolvedDiagnostic<'a> {
/// Resolve a single sub-diagnostic.
fn from_sub_diagnostic(
resolver: &'a dyn FileResolver,
resolver: &FileResolver<'a>,
diag: &'a SubDiagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
@@ -226,9 +224,9 @@ impl<'a> ResolvedDiagnostic<'a> {
.annotations
.iter()
.filter_map(|ann| {
let path = ann.span.file.path(resolver);
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann)
let path = resolver.path(ann.span.file);
let input = resolver.input(ann.span.file);
ResolvedAnnotation::new(path, &input, ann)
})
.collect();
ResolvedDiagnostic {
@@ -261,18 +259,10 @@ impl<'a> ResolvedDiagnostic<'a> {
continue;
};
let prev_context_ends = context_after(
&prev.diagnostic_source.as_source_code(),
context,
prev.line_end,
)
.get();
let this_context_begins = context_before(
&ann.diagnostic_source.as_source_code(),
context,
ann.line_start,
)
.get();
let prev_context_ends =
context_after(&prev.input.as_source_code(), context, prev.line_end).get();
let this_context_begins =
context_before(&ann.input.as_source_code(), context, ann.line_start).get();
// The boundary case here is when `prev_context_ends`
// is exactly one less than `this_context_begins`. In
// that case, the context windows are adajcent and we
@@ -314,7 +304,7 @@ impl<'a> ResolvedDiagnostic<'a> {
#[derive(Debug)]
struct ResolvedAnnotation<'a> {
path: &'a str,
diagnostic_source: DiagnosticSource,
input: Input,
range: TextRange,
line_start: OneIndexed,
line_end: OneIndexed,
@@ -328,12 +318,8 @@ impl<'a> ResolvedAnnotation<'a> {
/// `path` is the path of the file that this annotation points to.
///
/// `input` is the contents of the file that this annotation points to.
fn new(
path: &'a str,
diagnostic_source: &DiagnosticSource,
ann: &'a Annotation,
) -> Option<ResolvedAnnotation<'a>> {
let source = diagnostic_source.as_source_code();
fn new(path: &'a str, input: &Input, ann: &'a Annotation) -> Option<ResolvedAnnotation<'a>> {
let source = input.as_source_code();
let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) {
// An annotation with no range AND no message is probably(?)
// meaningless, but we should try to render it anyway.
@@ -359,7 +345,7 @@ impl<'a> ResolvedAnnotation<'a> {
};
Some(ResolvedAnnotation {
path,
diagnostic_source: diagnostic_source.clone(),
input: input.clone(),
range,
line_start,
line_end,
@@ -524,8 +510,8 @@ impl<'r> RenderableSnippet<'r> {
!anns.is_empty(),
"creating a renderable snippet requires a non-zero number of annotations",
);
let diagnostic_source = &anns[0].diagnostic_source;
let source = diagnostic_source.as_source_code();
let input = &anns[0].input;
let source = input.as_source_code();
let has_primary = anns.iter().any(|ann| ann.is_primary);
let line_start = context_before(
@@ -541,7 +527,7 @@ impl<'r> RenderableSnippet<'r> {
let snippet_start = source.line_start(line_start);
let snippet_end = source.line_end(line_end);
let snippet = diagnostic_source
let snippet = input
.as_source_code()
.slice(TextRange::new(snippet_start, snippet_end));
@@ -627,7 +613,7 @@ impl<'r> RenderableAnnotation<'r> {
}
}
/// A trait that facilitates the retrieval of source code from a `Span`.
/// A type that facilitates the retrieval of source code from a `Span`.
///
/// At present, this is tightly coupled with a Salsa database. In the future,
/// it is intended for this resolver to become an abstraction providing a
@@ -642,36 +628,55 @@ impl<'r> RenderableAnnotation<'r> {
/// callers will need to pass in a different "resolver" for turning `Span`s
/// into actual file paths/contents. The infrastructure for this isn't fully in
/// place, but this type serves to demarcate the intended abstraction boundary.
pub trait FileResolver {
/// Returns the path associated with the file given.
fn path(&self, file: File) -> &str;
/// Returns the input contents associated with the file given.
fn input(&self, file: File) -> Input;
pub(crate) struct FileResolver<'a> {
db: &'a dyn Db,
}
impl FileResolver for &dyn Db {
fn path(&self, file: File) -> &str {
relativize_path(self.system().current_directory(), file.path(*self).as_str())
impl<'a> FileResolver<'a> {
/// Creates a new resolver from a Salsa database.
pub(crate) fn new(db: &'a dyn Db) -> FileResolver<'a> {
FileResolver { db }
}
/// Returns the path associated with the file given.
fn path(&self, file: File) -> &'a str {
relativize_path(
self.db.system().current_directory(),
file.path(self.db).as_str(),
)
}
/// Returns the input contents associated with the file given.
fn input(&self, file: File) -> Input {
Input {
text: source_text(*self, file),
line_index: line_index(*self, file),
text: source_text(self.db, file),
line_index: line_index(self.db, file),
}
}
}
impl std::fmt::Debug for FileResolver<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "<salsa based file resolver>")
}
}
/// An abstraction over a unit of user input.
///
/// A single unit of user input usually corresponds to a `File`.
/// This contains the actual content of that input as well as a
/// line index for efficiently querying its contents.
#[derive(Clone, Debug)]
pub struct Input {
pub(crate) text: SourceText,
pub(crate) line_index: LineIndex,
struct Input {
text: SourceText,
line_index: LineIndex,
}
impl Input {
/// Returns this input as a `SourceCode` for convenient querying.
fn as_source_code(&self) -> SourceCode<'_, '_> {
SourceCode::new(self.text.as_str(), &self.line_index)
}
}
/// Returns the line number accounting for the given `len`
@@ -725,7 +730,6 @@ mod tests {
use crate::files::system_path_to_file;
use crate::system::{DbWithWritableSystem, SystemPath};
use crate::tests::TestDb;
use crate::Upcast;
use super::*;
@@ -2170,9 +2174,8 @@ watermelon
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
let span = self.path(path);
let file = span.expect_ty_file();
let text = source_text(&self.db, file);
let line_index = line_index(&self.db, file);
let text = source_text(&self.db, span.file());
let line_index = line_index(&self.db, span.file());
let source = SourceCode::new(text.as_str(), &line_index);
let (line_start, offset_start) = parse_line_offset(line_offset_start);
@@ -2234,7 +2237,7 @@ watermelon
///
/// (This will set the "printed" flag on `Diagnostic`.)
fn render(&self, diag: &Diagnostic) -> String {
diag.display(&self.db.upcast(), &self.config).to_string()
diag.display(&self.db, &self.config).to_string()
}
}

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

@@ -67,7 +67,7 @@ mod tests {
use crate::system::TestSystem;
use crate::system::{DbWithTestSystem, System};
use crate::vendored::VendoredFileSystem;
use crate::{Db, Upcast};
use crate::Db;
type Events = Arc<Mutex<Vec<salsa::Event>>>;
@@ -136,16 +136,7 @@ mod tests {
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
ruff_python_ast::PythonVersion::latest_ty()
}
}
impl Upcast<dyn Db> for TestDb {
fn upcast(&self) -> &(dyn Db + 'static) {
self
}
fn upcast_mut(&mut self) -> &mut (dyn Db + 'static) {
self
ruff_python_ast::PythonVersion::latest()
}
}

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 }
@@ -47,7 +44,6 @@ toml = { workspace = true, features = ["parse"] }
tracing = { workspace = true }
tracing-indicatif = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url = { workspace = true }
[dev-dependencies]
indoc = { workspace = true }

View File

@@ -2,10 +2,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,
};
use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_ty_schema};
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
@@ -41,8 +38,5 @@ 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,143 +0,0 @@
//! Generates the rules table for ty
use std::borrow::Cow;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Result};
use itertools::Itertools as _;
use pretty_assertions::StrComparison;
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
use crate::ROOT_DIR;
#[derive(clap::Args)]
pub(crate) struct Args {
/// Write the generated table to stdout (rather than to `ty.schema.json`).
#[arg(long, default_value_t, value_enum)]
pub(crate) mode: Mode,
}
pub(crate) fn main(args: &Args) -> Result<()> {
let markdown = generate_markdown();
let filename = "crates/ty/docs/rules.md";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
match args.mode {
Mode::DryRun => {
println!("{markdown}");
}
Mode::Check => {
let current = fs::read_to_string(schema_path)?;
if current == markdown {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &markdown);
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
}
}
Mode::Write => {
let current = fs::read_to_string(&schema_path)?;
if current == markdown {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
fs::write(schema_path, markdown.as_bytes())?;
}
}
}
Ok(())
}
fn generate_markdown() -> String {
let registry = &*ty_project::DEFAULT_LINT_REGISTRY;
let mut output = String::new();
let _ = writeln!(&mut output, "# Rules\n");
let mut lints: Vec<_> = registry.lints().iter().collect();
lints.sort_by(|a, b| {
a.default_level()
.cmp(&b.default_level())
.reverse()
.then_with(|| a.name().cmp(&b.name()))
});
for lint in lints {
let _ = writeln!(&mut output, "## `{rule_name}`\n", rule_name = lint.name());
// Increase the header-level by one
let documentation = lint
.documentation_lines()
.map(|line| {
if line.starts_with('#') {
Cow::Owned(format!("#{line}"))
} else {
Cow::Borrowed(line)
}
})
.join("\n");
let _ = writeln!(
&mut output,
r#"**Default level**: {level}
<details>
<summary>{summary}</summary>
{documentation}
### Links
* [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name})
* [View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
</details>
"#,
level = lint.default_level(),
// GitHub doesn't support markdown in `summary` headers
summary = replace_inline_code(lint.summary()),
encoded_name = url::form_urlencoded::byte_serialize(lint.name().as_str().as_bytes())
.collect::<String>(),
file = url::form_urlencoded::byte_serialize(lint.file().replace('\\', "/").as_bytes())
.collect::<String>(),
line = lint.line(),
);
}
output
}
/// Replaces inline code blocks (`code`) with `<code>code</code>`
fn replace_inline_code(input: &str) -> String {
let mut output = String::new();
let mut parts = input.split('`');
while let Some(before) = parts.next() {
if let Some(between) = parts.next() {
output.push_str(before);
output.push_str("<code>");
output.push_str(between);
output.push_str("</code>");
} else {
output.push_str(before);
}
}
output
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::generate_all::Mode;
use super::{main, Args};
#[test]
fn ty_rules_up_to_date() -> Result<()> {
main(&Args { mode: Mode::Check })
}
}

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,9 +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;
mod print_cst;
@@ -49,10 +44,8 @@ enum Command {
GenerateTySchema(generate_ty_schema::Args),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable,
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.
@@ -95,9 +88,7 @@ fn main() -> Result<ExitCode> {
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Command::GenerateTySchema(args) => generate_ty_schema::main(&args)?,
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 }
@@ -15,7 +15,6 @@ license = { workspace = true }
[dependencies]
ruff_annotate_snippets = { workspace = true }
ruff_cache = { workspace = true }
ruff_db = { workspace = true }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }

View File

@@ -91,14 +91,14 @@ from airflow.operators.sql import (
BaseSQLOperator,
BranchSQLOperator,
SQLColumnCheckOperator,
SQLTableCheckOperator,
SQLTablecheckOperator,
_convert_to_float_if_possible,
parse_boolean,
)
BaseSQLOperator()
BranchSQLOperator()
SQLTableCheckOperator()
SQLTablecheckOperator()
SQLColumnCheckOperator()
_convert_to_float_if_possible()
parse_boolean()

View File

@@ -17,7 +17,7 @@ impl Emitter for AzureEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let location = if context.is_notebook(&message.filename()) {
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()

View File

@@ -22,7 +22,7 @@ use crate::text_helpers::ShowNonprinting;
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
pub(super) struct Diff<'a> {
fix: &'a Fix,
source_code: SourceFile,
source_code: &'a SourceFile,
}
impl<'a> Diff<'a> {

View File

@@ -19,7 +19,7 @@ impl Emitter for GithubEmitter {
) -> anyhow::Result<()> {
for message in messages {
let source_location = message.compute_start_location();
let location = if context.is_notebook(&message.filename()) {
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
@@ -43,7 +43,7 @@ impl Emitter for GithubEmitter {
write!(
writer,
"{path}:{row}:{column}:",
path = relativize_path(&*message.filename()),
path = relativize_path(message.filename()),
row = location.line,
column = location.column,
)?;

View File

@@ -62,7 +62,7 @@ impl Serialize for SerializedMessages<'_> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let lines = if self.context.is_notebook(&message.filename()) {
let lines = if self.context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
json!({
@@ -77,8 +77,8 @@ impl Serialize for SerializedMessages<'_> {
};
let path = self.project_dir.as_ref().map_or_else(
|| relativize_path(&*message.filename()),
|project_dir| relativize_path_to(&*message.filename(), project_dir),
|| relativize_path(message.filename()),
|project_dir| relativize_path_to(message.filename(), project_dir),
);
let mut message_fingerprint = fingerprint(message, &path, 0);

View File

@@ -65,7 +65,7 @@ impl Emitter for GroupedEmitter {
let column_length = calculate_print_width(max_column_length);
// Print the filename.
writeln!(writer, "{}:", relativize_path(&*filename).underline())?;
writeln!(writer, "{}:", relativize_path(filename).underline())?;
// Print each message.
for message in messages {
@@ -73,7 +73,7 @@ impl Emitter for GroupedEmitter {
writer,
"{}",
DisplayGroupedMessage {
notebook_index: context.notebook_index(&message.filename()),
notebook_index: context.notebook_index(message.filename()),
message,
show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes,

View File

@@ -49,9 +49,8 @@ impl Serialize for ExpandedMessages<'_> {
}
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
let source_file = message.source_file();
let source_code = source_file.to_source_code();
let notebook_index = context.notebook_index(&message.filename());
let source_code = message.source_file().to_source_code();
let notebook_index = context.notebook_index(message.filename());
let fix = message.fix().map(|fix| {
json!({

View File

@@ -32,7 +32,7 @@ impl Emitter for JunitEmitter {
report.add_test_suite(test_suite);
} else {
for (filename, messages) in group_messages_by_filename(messages) {
let mut test_suite = TestSuite::new(&filename);
let mut test_suite = TestSuite::new(filename);
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
@@ -44,7 +44,7 @@ impl Emitter for JunitEmitter {
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.body());
let location = if context.is_notebook(&message.filename()) {
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
@@ -66,7 +66,7 @@ impl Emitter for JunitEmitter {
},
status,
);
let file_path = Path::new(&*filename);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());

View File

@@ -1,10 +1,8 @@
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
use ruff_db::diagnostic::{self as db, Annotation, DiagnosticId, Severity, Span};
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::FxHashMap;
@@ -47,7 +45,7 @@ mod text;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message {
Diagnostic(DiagnosticMessage),
SyntaxError(db::Diagnostic),
SyntaxError(SyntaxErrorMessage),
}
/// A diagnostic message corresponding to a rule violation.
@@ -61,6 +59,14 @@ pub struct DiagnosticMessage {
pub noqa_offset: TextSize,
}
/// A syntax error message raised by the parser.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyntaxErrorMessage {
pub message: String,
pub range: TextRange,
pub file: SourceFile,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessageKind {
Diagnostic(Rule),
@@ -77,17 +83,6 @@ impl MessageKind {
}
impl Message {
pub fn syntax_error(
message: impl std::fmt::Display,
range: TextRange,
file: SourceFile,
) -> Message {
let mut diag = db::Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = Span::from(file).with_range(range);
diag.annotate(Annotation::primary(span).message(message));
Self::SyntaxError(diag)
}
/// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation.
pub fn from_diagnostic(
diagnostic: Diagnostic,
@@ -119,14 +114,14 @@ impl Message {
.next()
.map_or(TextSize::new(0), TextLen::text_len);
Message::syntax_error(
format_args!(
Message::SyntaxError(SyntaxErrorMessage {
message: format!(
"SyntaxError: {}",
DisplayParseErrorType::new(&parse_error.error)
),
TextRange::at(parse_error.location.start(), len),
range: TextRange::at(parse_error.location.start(), len),
file,
)
})
}
/// Create a [`Message`] from the given [`UnsupportedSyntaxError`].
@@ -134,11 +129,11 @@ impl Message {
unsupported_syntax_error: &UnsupportedSyntaxError,
file: SourceFile,
) -> Message {
Message::syntax_error(
format_args!("SyntaxError: {unsupported_syntax_error}"),
unsupported_syntax_error.range,
Message::SyntaxError(SyntaxErrorMessage {
message: format!("SyntaxError: {unsupported_syntax_error}"),
range: unsupported_syntax_error.range,
file,
)
})
}
/// Create a [`Message`] from the given [`SemanticSyntaxError`].
@@ -146,11 +141,11 @@ impl Message {
semantic_syntax_error: &SemanticSyntaxError,
file: SourceFile,
) -> Message {
Message::syntax_error(
format_args!("SyntaxError: {semantic_syntax_error}"),
semantic_syntax_error.range,
Message::SyntaxError(SyntaxErrorMessage {
message: format!("SyntaxError: {semantic_syntax_error}"),
range: semantic_syntax_error.range,
file,
)
})
}
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
@@ -173,11 +168,8 @@ impl Message {
}
/// Returns `true` if `self` is a syntax error message.
pub fn is_syntax_error(&self) -> bool {
match self {
Message::Diagnostic(_) => false,
Message::SyntaxError(diag) => diag.id().is_invalid_syntax(),
}
pub const fn is_syntax_error(&self) -> bool {
matches!(self, Message::SyntaxError(_))
}
/// Returns a message kind.
@@ -200,11 +192,7 @@ impl Message {
pub fn body(&self) -> &str {
match self {
Message::Diagnostic(m) => &m.kind.body,
Message::SyntaxError(m) => m
.primary_annotation()
.expect("Expected a primary annotation for a ruff diagnostic")
.get_message()
.expect("Expected a message for a ruff diagnostic"),
Message::SyntaxError(m) => &m.message,
}
}
@@ -246,47 +234,27 @@ impl Message {
}
/// Returns the filename for the message.
pub fn filename(&self) -> Cow<'_, str> {
match self {
Message::Diagnostic(m) => Cow::Borrowed(m.file.name()),
Message::SyntaxError(diag) => Cow::Owned(
diag.expect_primary_span()
.expect_ruff_file()
.name()
.to_string(),
),
}
pub fn filename(&self) -> &str {
self.source_file().name()
}
/// Computes the start source location for the message.
pub fn compute_start_location(&self) -> LineColumn {
match self {
Message::Diagnostic(m) => m.file.to_source_code().line_column(m.range.start()),
Message::SyntaxError(diag) => diag
.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.start()),
}
self.source_file()
.to_source_code()
.line_column(self.start())
}
/// Computes the end source location for the message.
pub fn compute_end_location(&self) -> LineColumn {
match self {
Message::Diagnostic(m) => m.file.to_source_code().line_column(m.range.end()),
Message::SyntaxError(diag) => diag
.expect_primary_span()
.expect_ruff_file()
.to_source_code()
.line_column(self.end()),
}
self.source_file().to_source_code().line_column(self.end())
}
/// Returns the [`SourceFile`] which the message belongs to.
pub fn source_file(&self) -> SourceFile {
pub fn source_file(&self) -> &SourceFile {
match self {
Message::Diagnostic(m) => m.file.clone(),
Message::SyntaxError(m) => m.expect_primary_span().expect_ruff_file().clone(),
Message::Diagnostic(m) => &m.file,
Message::SyntaxError(m) => &m.file,
}
}
}
@@ -307,10 +275,7 @@ impl Ranged for Message {
fn range(&self) -> TextRange {
match self {
Message::Diagnostic(m) => m.range,
Message::SyntaxError(m) => m
.expect_primary_span()
.range()
.expect("Expected range for ruff span"),
Message::SyntaxError(m) => m.range,
}
}
}
@@ -328,11 +293,11 @@ impl Deref for MessageWithLocation<'_> {
}
}
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<String, Vec<MessageWithLocation>> {
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<MessageWithLocation>> {
let mut grouped_messages = BTreeMap::default();
for message in messages {
grouped_messages
.entry(message.filename().to_string())
.entry(message.filename())
.or_insert_with(Vec::new)
.push(MessageWithLocation {
message,

View File

@@ -18,7 +18,7 @@ impl Emitter for PylintEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let row = if context.is_notebook(&message.filename()) {
let row = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
OneIndexed::from_zero_indexed(0)
@@ -39,7 +39,7 @@ impl Emitter for PylintEmitter {
writeln!(
writer,
"{path}:{row}: {body}",
path = relativize_path(&*message.filename()),
path = relativize_path(message.filename()),
)?;
}

View File

@@ -57,8 +57,7 @@ impl Serialize for ExpandedMessages<'_> {
}
fn message_to_rdjson_value(message: &Message) -> Value {
let source_file = message.source_file();
let source_code = source_file.to_source_code();
let source_code = message.source_file().to_source_code();
let start_location = source_code.line_column(message.start());
let end_location = source_code.line_column(message.end());

View File

@@ -121,7 +121,7 @@ impl SarifResult {
fn from_message(message: &Message) -> Result<Self> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let path = normalize_path(&*message.filename());
let path = normalize_path(message.filename());
Ok(Self {
rule: message.rule(),
level: "error".to_string(),
@@ -141,7 +141,7 @@ impl SarifResult {
fn from_message(message: &Message) -> Result<Self> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let path = normalize_path(&*message.filename());
let path = normalize_path(message.filename());
Ok(Self {
rule: message.rule(),
level: "error".to_string(),

View File

@@ -73,12 +73,12 @@ impl Emitter for TextEmitter {
write!(
writer,
"{path}{sep}",
path = relativize_path(&*message.filename()).bold(),
path = relativize_path(message.filename()).bold(),
sep = ":".cyan(),
)?;
let start_location = message.compute_start_location();
let notebook_index = context.notebook_index(&message.filename());
let notebook_index = context.notebook_index(message.filename());
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(notebook_index) = notebook_index {
@@ -191,8 +191,7 @@ impl Display for MessageCodeFrame<'_> {
Vec::new()
};
let source_file = self.message.source_file();
let source_code = source_file.to_source_code();
let source_code = self.message.source_file().to_source_code();
let content_start_index = source_code.line_index(self.message.start());
let mut start_index = content_start_index.saturating_sub(2);

View File

@@ -288,7 +288,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
}
}
["airflow", "operators", "sql", rest] => match *rest {
"BaseSQLOperator" | "BranchSQLOperator" | "SQLTableCheckOperator" => {
"BaseSQLOperator" | "BranchSQLOperator" | "SQLTablecheckOperator" => {
ProviderReplacement::SourceModuleMovedToProvider {
name: (*rest).to_string(),
module: "airflow.providers.common.sql.operators.sql",

View File

@@ -216,7 +216,7 @@ AIR302_common_sql.py:99:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is mov
99 | BaseSQLOperator()
| ^^^^^^^^^^^^^^^ AIR302
100 | BranchSQLOperator()
101 | SQLTableCheckOperator()
101 | SQLTablecheckOperator()
|
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead.
@@ -225,26 +225,26 @@ AIR302_common_sql.py:100:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is
99 | BaseSQLOperator()
100 | BranchSQLOperator()
| ^^^^^^^^^^^^^^^^^ AIR302
101 | SQLTableCheckOperator()
101 | SQLTablecheckOperator()
102 | SQLColumnCheckOperator()
|
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead.
AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTableCheckOperator` is moved into `common-sql` provider in Airflow 3.0;
AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTablecheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
99 | BaseSQLOperator()
100 | BranchSQLOperator()
101 | SQLTableCheckOperator()
101 | SQLTablecheckOperator()
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
102 | SQLColumnCheckOperator()
103 | _convert_to_float_if_possible()
|
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTableCheckOperator` instead.
= help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTablecheckOperator` instead.
AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0;
|
100 | BranchSQLOperator()
101 | SQLTableCheckOperator()
101 | SQLTablecheckOperator()
102 | SQLColumnCheckOperator()
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
103 | _convert_to_float_if_possible()
@@ -254,7 +254,7 @@ AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator
AIR302_common_sql.py:103:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0;
|
101 | SQLTableCheckOperator()
101 | SQLTablecheckOperator()
102 | SQLColumnCheckOperator()
103 | _convert_to_float_if_possible()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302

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

@@ -59,11 +59,6 @@ impl PythonVersion {
Self::PY313
}
pub const fn latest_ty() -> Self {
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
Self::PY313
}
pub const fn as_tuple(self) -> (u8, u8) {
(self.major, self.minor)
}

View File

@@ -14,7 +14,7 @@ use ruff_linter::{
directives::{extract_directives, Flags},
generate_noqa_edits,
linter::check_path,
message::{DiagnosticMessage, Message},
message::{DiagnosticMessage, Message, SyntaxErrorMessage},
package::PackageRoot,
packaging::detect_package_root,
registry::AsRule,
@@ -173,10 +173,10 @@ pub(crate) fn check(
locator.to_index(),
encoding,
)),
Message::SyntaxError(_) => {
Message::SyntaxError(syntax_error_message) => {
if show_syntax_errors {
Some(syntax_error_to_lsp_diagnostic(
&message,
syntax_error_message,
&source_kind,
locator.to_index(),
encoding,
@@ -322,7 +322,7 @@ fn to_lsp_diagnostic(
}
fn syntax_error_to_lsp_diagnostic(
syntax_error: &Message,
syntax_error: SyntaxErrorMessage,
source_kind: &SourceKind,
index: &LineIndex,
encoding: PositionEncoding,
@@ -331,7 +331,7 @@ fn syntax_error_to_lsp_diagnostic(
let cell: usize;
if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) {
NotebookRange { cell, range } = syntax_error.range().to_notebook_range(
NotebookRange { cell, range } = syntax_error.range.to_notebook_range(
source_kind.source_code(),
index,
notebook_index,
@@ -340,7 +340,7 @@ fn syntax_error_to_lsp_diagnostic(
} else {
cell = usize::default();
range = syntax_error
.range()
.range
.to_range(source_kind.source_code(), index, encoding);
}
@@ -353,7 +353,7 @@ fn syntax_error_to_lsp_diagnostic(
code: None,
code_description: None,
source: Some(DIAGNOSTIC_NAME.into()),
message: syntax_error.body().to_string(),
message: syntax_error.message,
related_information: None,
data: None,
},

View File

@@ -195,7 +195,7 @@ impl SourceFile {
}
}
pub fn index(&self) -> &LineIndex {
fn index(&self) -> &LineIndex {
self.inner
.line_index
.get_or_init(|| LineIndex::from_source_text(self.source_text()))

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

@@ -1,7 +1,7 @@
use std::path::Path;
use js_sys::Error;
use ruff_linter::message::{DiagnosticMessage, Message};
use ruff_linter::message::{DiagnosticMessage, Message, SyntaxErrorMessage};
use ruff_linter::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
@@ -230,13 +230,15 @@ impl Workspace {
.collect(),
}),
},
Message::SyntaxError(_) => ExpandedMessage {
code: None,
message: message.body().to_string(),
start_location: source_code.line_column(message.range().start()).into(),
end_location: source_code.line_column(message.range().end()).into(),
fix: None,
},
Message::SyntaxError(SyntaxErrorMessage { message, range, .. }) => {
ExpandedMessage {
code: None,
message,
start_location: source_code.line_column(range.start()).into(),
end_location: source_code.line_column(range.end()).into(),
fix: None,
}
}
})
.collect();

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,13 +1,12 @@
[package]
name = "ty"
version = "0.0.0"
# required for correct pypi metadata
homepage = "https://github.com/astral-sh/ty/"
documentation = "https://github.com/astral-sh/ty/"
# Releases occur in this other repository!
repository = "https://github.com/astral-sh/ty/"
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
documentation.workspace = true
# Releases occur in this other repository!
repository = "https://github.com/astral-sh/ty/"
authors.workspace = true
license.workspace = true

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

File diff suppressed because it is too large Load Diff

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,39 @@
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 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 +57,364 @@ 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, &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,7 +1,6 @@
use anyhow::Context;
use insta::internals::SettingsBindDropGuard;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use ruff_python_ast::PythonVersion;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -197,12 +196,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 +214,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
@@ -369,12 +368,12 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
for a in range(0, int(y)):
x = a
prin(x) # unresolved-reference
print(x) # possibly-unresolved-reference
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error)
// and a `division-by-zero` diagnostic (error).
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
@@ -389,15 +388,15 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
info: `lint:division-by-zero` is enabled by default
error: lint:unresolved-reference: Name `prin` used when not defined
--> test.py:7:1
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:7:7
|
5 | x = a
6 |
7 | prin(x) # unresolved-reference
| ^^^^
7 | print(x) # possibly-unresolved-reference
| ^
|
info: `lint:unresolved-reference` is enabled by default
info: `lint:possibly-unresolved-reference` is enabled by default
Found 2 diagnostics
@@ -409,7 +408,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
r#"
[tool.ty.rules]
division-by-zero = "warn" # demote to warn
unresolved-reference = "ignore"
possibly-unresolved-reference = "ignore"
"#,
)?;
@@ -448,12 +447,12 @@ fn cli_rule_severity() -> anyhow::Result<()> {
for a in range(0, int(y)):
x = a
prin(x) # unresolved-reference
print(x) # possibly-unresolved-reference
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error),
// a `division-by-zero` (error) and a unresolved-import (error) diagnostic by default.
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
@@ -480,15 +479,15 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
info: `lint:division-by-zero` is enabled by default
error: lint:unresolved-reference: Name `prin` used when not defined
--> test.py:9:1
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:9:7
|
7 | x = a
8 |
9 | prin(x) # unresolved-reference
| ^^^^
9 | print(x) # possibly-unresolved-reference
| ^
|
info: `lint:unresolved-reference` is enabled by default
info: `lint:possibly-unresolved-reference` is enabled by default
Found 3 diagnostics
@@ -499,7 +498,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
case
.command()
.arg("--ignore")
.arg("unresolved-reference")
.arg("possibly-unresolved-reference")
.arg("--warn")
.arg("division-by-zero")
.arg("--warn")
@@ -551,12 +550,12 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
for a in range(0, int(y)):
x = a
prin(x) # unresolved-reference
print(x) # possibly-unresolved-reference
"#,
)?;
// Assert that there's a `unresolved-reference` diagnostic (error)
// and a `division-by-zero` (error) by default.
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
@@ -571,15 +570,15 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
info: `lint:division-by-zero` is enabled by default
error: lint:unresolved-reference: Name `prin` used when not defined
--> test.py:7:1
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:7:7
|
5 | x = a
6 |
7 | prin(x) # unresolved-reference
| ^^^^
7 | print(x) # possibly-unresolved-reference
| ^
|
info: `lint:unresolved-reference` is enabled by default
info: `lint:possibly-unresolved-reference` is enabled by default
Found 2 diagnostics
@@ -589,13 +588,13 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case
.command()
.arg("--warn")
.arg("unresolved-reference")
.arg("--error")
.arg("possibly-unresolved-reference")
.arg("--warn")
.arg("division-by-zero")
// Override the error severity with warning
.arg("--ignore")
.arg("unresolved-reference"),
.arg("possibly-unresolved-reference"),
@r"
success: true
exit_code: 0
@@ -676,7 +675,7 @@ fn cli_unknown_rules() -> anyhow::Result<()> {
fn exit_code_only_warnings() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
@@ -686,7 +685,7 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` was selected on the command line
info: `lint:unresolved-reference` is enabled by default
Found 1 diagnostic
@@ -711,11 +710,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 +740,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
@@ -760,7 +759,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
success: false
exit_code: 1
----- stdout -----
@@ -770,7 +769,7 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` was selected on the command line
info: `lint:unresolved-reference` is enabled by default
Found 1 diagnostic
@@ -793,7 +792,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
),
])?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@@ -803,7 +802,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` was selected on the command line
info: `lint:unresolved-reference` is enabled by default
Found 1 diagnostic
@@ -823,7 +822,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@@ -834,7 +833,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
| ^
3 | print(4[1]) # [non-subscriptable]
|
info: `lint:unresolved-reference` was selected on the command line
info: `lint:unresolved-reference` is enabled by default
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
@@ -863,7 +862,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
"###,
)?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
success: false
exit_code: 1
----- stdout -----
@@ -874,7 +873,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
| ^
3 | print(4[1]) # [non-subscriptable]
|
info: `lint:unresolved-reference` was selected on the command line
info: `lint:unresolved-reference` is enabled by default
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
@@ -903,7 +902,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r"
success: true
exit_code: 0
----- stdout -----
@@ -914,7 +913,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
| ^
3 | print(4[1]) # [non-subscriptable]
|
info: `lint:unresolved-reference` was selected on the command line
info: `lint:unresolved-reference` is enabled by default
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
@@ -951,7 +950,7 @@ fn user_configuration() -> anyhow::Result<()> {
for a in range(0, int(y)):
x = a
prin(x)
print(x)
"#,
),
])?;
@@ -963,50 +962,6 @@ fn user_configuration() -> anyhow::Result<()> {
"XDG_CONFIG_HOME"
};
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: `lint:division-by-zero` was selected in the configuration file
error: lint:unresolved-reference: Name `prin` used when not defined
--> main.py:7:1
|
5 | x = a
6 |
7 | prin(x)
| ^^^^
|
info: `lint:unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
"
);
// The user-level configuration sets the severity for `unresolved-reference` to warn.
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
// has higher precedence.
case.write_file(
config_directory.join("ty/ty.toml"),
r#"
[rules]
division-by-zero = "error"
unresolved-reference = "warn"
"#,
)?;
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
@@ -1023,15 +978,59 @@ fn user_configuration() -> anyhow::Result<()> {
|
info: `lint:division-by-zero` was selected in the configuration file
warning: lint:unresolved-reference: Name `prin` used when not defined
--> main.py:7:1
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> main.py:7:7
|
5 | x = a
6 |
7 | prin(x)
| ^^^^
7 | print(x)
| ^
|
info: `lint:unresolved-reference` was selected in the configuration file
info: `lint:possibly-unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
"
);
// The user-level configuration promotes `possibly-unresolved-reference` to an error.
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
// has higher precedence.
case.write_file(
config_directory.join("ty/ty.toml"),
r#"
[rules]
division-by-zero = "error"
possibly-unresolved-reference = "error"
"#,
)?;
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: `lint:division-by-zero` was selected in the configuration file
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^
|
info: `lint:possibly-unresolved-reference` was selected in the configuration file
Found 2 diagnostics
@@ -1181,7 +1180,7 @@ fn concise_diagnostics() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r"
success: false
exit_code: 1
----- stdout -----
@@ -1219,7 +1218,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 +1247,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
@@ -1264,202 +1263,6 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"ty.toml",
&*format!(
r#"
[environment]
python-version = "{}"
python-platform = "linux"
"#,
PythonVersion::default()
),
),
(
"main.py",
r#"
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-attribute: Type `<module 'os'>` has no attribute `grantpt`
--> main.py:4:1
|
2 | import os
3 |
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
| ^^^^^^^^^^
|
info: `lint:unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
");
// Use default (which should be latest supported)
let case = TestCase::with_files([
(
"ty.toml",
r#"
[environment]
python-platform = "linux"
"#,
),
(
"main.py",
r#"
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
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

@@ -136,7 +136,6 @@ mod tests {
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
Severity, Span,
};
use ruff_db::Upcast;
use ruff_text_size::{Ranged, TextRange};
#[test]
@@ -774,7 +773,7 @@ mod tests {
.message("Cursor offset"),
);
write!(buf, "{}", diagnostic.display(&self.db.upcast(), &config)).unwrap();
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
buf
}

View File

@@ -191,7 +191,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest_ty(),
python_version: PythonVersion::latest(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],

View File

@@ -204,7 +204,6 @@ mod tests {
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_db::Upcast;
use ruff_python_ast::PythonVersion;
use ruff_text_size::TextSize;
use ty_python_semantic::{
@@ -228,7 +227,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest_ty(),
python_version: PythonVersion::latest(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
@@ -286,7 +285,7 @@ mod tests {
.format(DiagnosticFormat::Full);
for diagnostic in diagnostics {
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db.upcast(), &config)).unwrap();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
buf

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

@@ -126,16 +126,6 @@ impl Upcast<dyn IdeDb> for ProjectDatabase {
}
}
impl Upcast<dyn Db> for ProjectDatabase {
fn upcast(&self) -> &(dyn Db + 'static) {
self
}
fn upcast_mut(&mut self) -> &mut (dyn Db + 'static) {
self
}
}
#[salsa::db]
impl IdeDb for ProjectDatabase {}
@@ -149,7 +139,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 +317,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)
@@ -93,7 +61,7 @@ impl Options {
.environment
.as_ref()
.and_then(|env| env.python_version.as_deref().copied())
.unwrap_or(PythonVersion::latest_ty());
.unwrap_or_default();
let python_platform = self
.environment
.as_ref()
@@ -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

@@ -103,7 +103,7 @@ impl Project {
let major =
u8::try_from(major).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(major))?;
let minor =
u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMinor(minor))?;
u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(minor))?;
Ok(Some(
requires_python

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

@@ -45,18 +45,6 @@ reveal_type(a | a) # revealed: Literal[True]
reveal_type(a | b) # revealed: Literal[True]
reveal_type(b | a) # revealed: Literal[True]
reveal_type(b | b) # revealed: Literal[False]
# bitwise AND
reveal_type(a & a) # revealed: Literal[True]
reveal_type(a & b) # revealed: Literal[False]
reveal_type(b & a) # revealed: Literal[False]
reveal_type(b & b) # revealed: Literal[False]
# bitwise XOR
reveal_type(a ^ a) # revealed: Literal[False]
reveal_type(a ^ b) # revealed: Literal[True]
reveal_type(b ^ a) # revealed: Literal[True]
reveal_type(b ^ b) # revealed: Literal[False]
```
## Arithmetic with a variable

View File

@@ -9,9 +9,6 @@ reveal_type(3 * -1) # revealed: Literal[-3]
reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
reveal_type(3 | 4) # revealed: Literal[7]
reveal_type(5 & 6) # revealed: Literal[4]
reveal_type(7 ^ 2) # revealed: Literal[5]
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`"
reveal_type(2 + "f") # revealed: Unknown

View File

@@ -252,8 +252,12 @@ def _():
## Load before `global` declaration
This should be an error, but it's not yet.
TODO implement `SemanticSyntaxContext::global`
```py
def f():
x = 1
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
global x
```

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`:
@@ -316,8 +269,6 @@ import subexporter
__all__ = []
__all__.extend(["C", "D"])
__all__.extend(("E", "F"))
__all__.extend({"G", "H"})
__all__.extend(subexporter.__all__)
class C: ...
@@ -330,7 +281,7 @@ class D: ...
import exporter
from ty_extensions import dunder_all_names
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"], Literal["E"], Literal["F"], Literal["G"], Literal["H"]]
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]]
reveal_type(dunder_all_names(exporter))
```

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

@@ -32,14 +32,8 @@ def f():
y = ""
global x
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
x = ""
global z
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
z = ""
z: int
```
## Nested intervening scope
@@ -54,7 +48,8 @@ def outer():
def inner():
global x
reveal_type(x) # revealed: int
# TODO: revealed: int
reveal_type(x) # revealed: str
```
## Narrowing
@@ -92,7 +87,8 @@ def f():
```py
def f():
global x
y = x
# TODO this should also not be an error
y = x # error: [unresolved-reference] "Name `x` used when not defined"
x = 1 # No error.
x = 2
@@ -103,111 +99,79 @@ x = 2
Using a name prior to its `global` declaration in the same scope is a syntax error.
```py
x = 1
def f():
print(x)
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
global x
print(x)
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
print(x)
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
global x, y
print(x)
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
x = 1
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
global x
x = 1
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
del x
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
global x
del x
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
print(f"{x=}")
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
# still an error in module scope
x = None
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
```
## Local bindings override preceding `global` bindings
```py
x = 42
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[42]
x = "56"
reveal_type(x) # revealed: Literal["56"]
```
## Local assignment prevents falling back to the outer scope
```py
x = 42
def f():
# error: [unresolved-reference] "Name `x` used when not defined"
reveal_type(x) # revealed: Unknown
x = "56"
reveal_type(x) # revealed: Literal["56"]
```
## Annotating a `global` binding is a syntax error
```py
x: int = 1
def f():
global x
x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global"
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
```

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

Some files were not shown because too many files have changed in this diff Show More