Compare commits
27 Commits
micha/sals
...
0.11.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2370297cde | ||
|
|
a137cb18d4 | ||
|
|
03a4d56624 | ||
|
|
642eac452d | ||
|
|
c1b875799b | ||
|
|
6cd8a49638 | ||
|
|
12ce445ff7 | ||
|
|
f46ed8d410 | ||
|
|
6c177e2bbe | ||
|
|
3d2485eb1b | ||
|
|
f78367979e | ||
|
|
b705664d49 | ||
|
|
f51f1f7153 | ||
|
|
9b694ada82 | ||
|
|
4d81a41107 | ||
|
|
981bd70d39 | ||
|
|
0763331f7f | ||
|
|
da8540862d | ||
|
|
6a5533c44c | ||
|
|
d608eae126 | ||
|
|
067a8ac574 | ||
|
|
5eb215e8e5 | ||
|
|
91aa853b9c | ||
|
|
57bf7dfbd9 | ||
|
|
67cd94ed64 | ||
|
|
aac862822f | ||
|
|
3755ac9fac |
7
.github/mypy-primer-ty.toml
vendored
Normal file
7
.github/mypy-primer-ty.toml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
#: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"
|
||||
4
.github/workflows/mypy_primer.yaml
vendored
4
.github/workflows/mypy_primer.yaml
vendored
@@ -50,6 +50,10 @@ 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"
|
||||
|
||||
@@ -5,6 +5,7 @@ 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/.*|
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# 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
48
Cargo.lock
generated
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.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.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1771,6 +1771,15 @@ 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"
|
||||
@@ -2549,7 +2558,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2583,6 +2592,7 @@ dependencies = [
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
@@ -2701,6 +2711,7 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"markdown",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
"regex",
|
||||
@@ -2709,6 +2720,7 @@ dependencies = [
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_notebook",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
@@ -2725,7 +2737,9 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
"ty",
|
||||
"ty_project",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2785,7 +2799,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2813,6 +2827,7 @@ dependencies = [
|
||||
"regex",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
@@ -2874,6 +2889,13 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_options_metadata"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
@@ -3112,7 +3134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3160,6 +3182,7 @@ dependencies = [
|
||||
"ruff_graph",
|
||||
"ruff_linter",
|
||||
"ruff_macros",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_semantic",
|
||||
@@ -3237,7 +3260,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=2c869364a9592d06fdf45c422e1e4a7265a8fe8a#2c869364a9592d06fdf45c422e1e4a7265a8fe8a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3260,12 +3283,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=2c869364a9592d06fdf45c422e1e4a7265a8fe8a#2c869364a9592d06fdf45c422e1e4a7265a8fe8a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=2c869364a9592d06fdf45c422e1e4a7265a8fe8a#2c869364a9592d06fdf45c422e1e4a7265a8fe8a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=7edce6e248f35c8114b4b021cdb474a3fb2813b3#7edce6e248f35c8114b4b021cdb474a3fb2813b3"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -4013,6 +4036,7 @@ dependencies = [
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_macros",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_text_size",
|
||||
@@ -4219,6 +4243,12 @@ 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"
|
||||
@@ -4595,7 +4625,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -23,6 +23,7 @@ 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" }
|
||||
@@ -37,6 +38,7 @@ 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" }
|
||||
@@ -124,7 +126,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 = "2c869364a9592d06fdf45c422e1e4a7265a8fe8a" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "7edce6e248f35c8114b4b021cdb474a3fb2813b3" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -182,7 +184,7 @@ wild = { version = "2" }
|
||||
zip = { version = "0.6.6", default-features = false }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["getrandom"]
|
||||
ignored = ["getrandom", "ruff_options_metadata"]
|
||||
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.11.8/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.8/install.ps1 | iex"
|
||||
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"
|
||||
```
|
||||
|
||||
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.8
|
||||
rev: v0.11.9
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -20,6 +20,7 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] }
|
||||
ruff_linter = { workspace = true, features = ["clap"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true, features = ["serde"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
@@ -22,12 +22,12 @@ use ruff_linter::settings::types::{
|
||||
PythonVersion, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -2,10 +2,8 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
|
||||
use itertools::Itertools;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_workspace::{
|
||||
options::Options,
|
||||
options_base::{OptionField, OptionSet, OptionsMetadata, Visit},
|
||||
};
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_workspace::options::Options;
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
|
||||
@@ -2,8 +2,8 @@ use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::args::HelpFormat;
|
||||
|
||||
use ruff_options_metadata::OptionsMetadata;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
#[expect(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {
|
||||
|
||||
@@ -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, SyntaxErrorMessage};
|
||||
use ruff_linter::message::Message;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
@@ -102,11 +102,7 @@ impl Diagnostics {
|
||||
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
|
||||
let dummy = SourceFileBuilder::new(name, "").finish();
|
||||
Self::new(
|
||||
vec![Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: err.to_string(),
|
||||
range: TextRange::default(),
|
||||
file: dummy,
|
||||
})],
|
||||
vec![Message::syntax_error(err, TextRange::default(), dummy)],
|
||||
FxHashMap::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,13 +59,7 @@ type KeyDiagnosticFields = (
|
||||
Severity,
|
||||
);
|
||||
|
||||
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,
|
||||
)];
|
||||
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[];
|
||||
|
||||
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
|
||||
SystemPathBuf::from("src").join(file.name())
|
||||
@@ -203,7 +197,7 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDi
|
||||
diagnostic.id(),
|
||||
diagnostic
|
||||
.primary_span()
|
||||
.map(|span| span.file())
|
||||
.map(|span| span.expect_ty_file())
|
||||
.map(|file| file.path(db).as_str()),
|
||||
diagnostic
|
||||
.primary_span()
|
||||
|
||||
@@ -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;
|
||||
use crate::Db;
|
||||
use crate::{files::File, Db};
|
||||
|
||||
use self::render::FileResolver;
|
||||
mod render;
|
||||
mod stylesheet;
|
||||
|
||||
@@ -115,10 +115,9 @@ impl Diagnostic {
|
||||
/// callers should prefer using this with `write!` instead of `writeln!`.
|
||||
pub fn display<'a>(
|
||||
&'a self,
|
||||
db: &'a dyn Db,
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
) -> DisplayDiagnostic<'a> {
|
||||
let resolver = FileResolver::new(db);
|
||||
DisplayDiagnostic::new(resolver, config, self)
|
||||
}
|
||||
|
||||
@@ -233,6 +232,16 @@ 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 {
|
||||
@@ -267,11 +276,7 @@ impl Ord for RenderingSortKey<'_> {
|
||||
self.diagnostic.primary_span(),
|
||||
other.diagnostic.primary_span(),
|
||||
) {
|
||||
let order = span1
|
||||
.file()
|
||||
.path(self.db)
|
||||
.as_str()
|
||||
.cmp(span2.file().path(self.db).as_str());
|
||||
let order = span1.file().path(&self.db).cmp(span2.file().path(&self.db));
|
||||
if order.is_ne() {
|
||||
return order;
|
||||
}
|
||||
@@ -643,6 +648,10 @@ impl DiagnosticId {
|
||||
DiagnosticId::UnknownRule => "unknown-rule",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_invalid_syntax(&self) -> bool {
|
||||
matches!(self, Self::InvalidSyntax)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
|
||||
@@ -668,6 +677,62 @@ 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
|
||||
@@ -675,14 +740,14 @@ impl std::fmt::Display for DiagnosticId {
|
||||
/// the entire file. For example, when the file should be executable but isn't.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
file: File,
|
||||
file: UnifiedFile,
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Returns the `File` attached to this `Span`.
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
/// Returns the `UnifiedFile` attached to this `Span`.
|
||||
pub fn file(&self) -> &UnifiedFile {
|
||||
&self.file
|
||||
}
|
||||
|
||||
/// Returns the range, if available, attached to this `Span`.
|
||||
@@ -703,10 +768,38 @@ 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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, Severity, SubDiagnostic,
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
|
||||
SubDiagnostic,
|
||||
};
|
||||
|
||||
/// A type that implements `std::fmt::Display` for diagnostic rendering.
|
||||
@@ -30,17 +31,16 @@ 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: FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
annotate_renderer: AnnotateRenderer,
|
||||
diag: &'a Diagnostic,
|
||||
}
|
||||
|
||||
impl<'a> DisplayDiagnostic<'a> {
|
||||
pub(crate) fn new(
|
||||
resolver: FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
diag: &'a Diagnostic,
|
||||
) -> DisplayDiagnostic<'a> {
|
||||
@@ -86,11 +86,13 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(self.resolver.path(span.file()), stylesheet.emphasis)
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let input = self.resolver.input(span.file());
|
||||
let start = input.as_source_code().line_column(range.start());
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
write!(
|
||||
f,
|
||||
@@ -115,7 +117,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()))?;
|
||||
@@ -144,7 +146,7 @@ struct Resolved<'a> {
|
||||
|
||||
impl<'a> Resolved<'a> {
|
||||
/// Creates a new resolved set of diagnostics.
|
||||
fn new(resolver: &FileResolver<'a>, diag: &'a Diagnostic) -> Resolved<'a> {
|
||||
fn new(resolver: &'a dyn FileResolver, diag: &'a Diagnostic) -> Resolved<'a> {
|
||||
let mut diagnostics = vec![];
|
||||
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, diag));
|
||||
for sub in &diag.inner.subs {
|
||||
@@ -182,7 +184,7 @@ struct ResolvedDiagnostic<'a> {
|
||||
impl<'a> ResolvedDiagnostic<'a> {
|
||||
/// Resolve a single diagnostic.
|
||||
fn from_diagnostic(
|
||||
resolver: &FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
diag: &'a Diagnostic,
|
||||
) -> ResolvedDiagnostic<'a> {
|
||||
let annotations: Vec<_> = diag
|
||||
@@ -190,9 +192,9 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = resolver.path(ann.span.file);
|
||||
let input = resolver.input(ann.span.file);
|
||||
ResolvedAnnotation::new(path, &input, ann)
|
||||
let path = ann.span.file.path(resolver);
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann)
|
||||
})
|
||||
.collect();
|
||||
let message = if diag.inner.message.as_str().is_empty() {
|
||||
@@ -216,7 +218,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
|
||||
/// Resolve a single sub-diagnostic.
|
||||
fn from_sub_diagnostic(
|
||||
resolver: &FileResolver<'a>,
|
||||
resolver: &'a dyn FileResolver,
|
||||
diag: &'a SubDiagnostic,
|
||||
) -> ResolvedDiagnostic<'a> {
|
||||
let annotations: Vec<_> = diag
|
||||
@@ -224,9 +226,9 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = resolver.path(ann.span.file);
|
||||
let input = resolver.input(ann.span.file);
|
||||
ResolvedAnnotation::new(path, &input, ann)
|
||||
let path = ann.span.file.path(resolver);
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann)
|
||||
})
|
||||
.collect();
|
||||
ResolvedDiagnostic {
|
||||
@@ -259,10 +261,18 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
continue;
|
||||
};
|
||||
|
||||
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();
|
||||
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();
|
||||
// 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
|
||||
@@ -304,7 +314,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
#[derive(Debug)]
|
||||
struct ResolvedAnnotation<'a> {
|
||||
path: &'a str,
|
||||
input: Input,
|
||||
diagnostic_source: DiagnosticSource,
|
||||
range: TextRange,
|
||||
line_start: OneIndexed,
|
||||
line_end: OneIndexed,
|
||||
@@ -318,8 +328,12 @@ 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, input: &Input, ann: &'a Annotation) -> Option<ResolvedAnnotation<'a>> {
|
||||
let source = input.as_source_code();
|
||||
fn new(
|
||||
path: &'a str,
|
||||
diagnostic_source: &DiagnosticSource,
|
||||
ann: &'a Annotation,
|
||||
) -> Option<ResolvedAnnotation<'a>> {
|
||||
let source = diagnostic_source.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.
|
||||
@@ -345,7 +359,7 @@ impl<'a> ResolvedAnnotation<'a> {
|
||||
};
|
||||
Some(ResolvedAnnotation {
|
||||
path,
|
||||
input: input.clone(),
|
||||
diagnostic_source: diagnostic_source.clone(),
|
||||
range,
|
||||
line_start,
|
||||
line_end,
|
||||
@@ -510,8 +524,8 @@ impl<'r> RenderableSnippet<'r> {
|
||||
!anns.is_empty(),
|
||||
"creating a renderable snippet requires a non-zero number of annotations",
|
||||
);
|
||||
let input = &anns[0].input;
|
||||
let source = input.as_source_code();
|
||||
let diagnostic_source = &anns[0].diagnostic_source;
|
||||
let source = diagnostic_source.as_source_code();
|
||||
let has_primary = anns.iter().any(|ann| ann.is_primary);
|
||||
|
||||
let line_start = context_before(
|
||||
@@ -527,7 +541,7 @@ impl<'r> RenderableSnippet<'r> {
|
||||
|
||||
let snippet_start = source.line_start(line_start);
|
||||
let snippet_end = source.line_end(line_end);
|
||||
let snippet = input
|
||||
let snippet = diagnostic_source
|
||||
.as_source_code()
|
||||
.slice(TextRange::new(snippet_start, snippet_end));
|
||||
|
||||
@@ -613,7 +627,7 @@ impl<'r> RenderableAnnotation<'r> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that facilitates the retrieval of source code from a `Span`.
|
||||
/// A trait 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
|
||||
@@ -628,36 +642,24 @@ 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(crate) struct FileResolver<'a> {
|
||||
db: &'a dyn Db,
|
||||
}
|
||||
|
||||
impl<'a> FileResolver<'a> {
|
||||
/// Creates a new resolver from a Salsa database.
|
||||
pub(crate) fn new(db: &'a dyn Db) -> FileResolver<'a> {
|
||||
FileResolver { db }
|
||||
}
|
||||
|
||||
pub trait FileResolver {
|
||||
/// 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(),
|
||||
)
|
||||
}
|
||||
fn path(&self, file: File) -> &str;
|
||||
|
||||
/// Returns the input contents associated with the file given.
|
||||
fn input(&self, file: File) -> Input {
|
||||
Input {
|
||||
text: source_text(self.db, file),
|
||||
line_index: line_index(self.db, file),
|
||||
}
|
||||
}
|
||||
fn input(&self, file: File) -> Input;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FileResolver<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "<salsa based file resolver>")
|
||||
impl FileResolver for &dyn Db {
|
||||
fn path(&self, file: File) -> &str {
|
||||
relativize_path(self.system().current_directory(), file.path(*self).as_str())
|
||||
}
|
||||
|
||||
fn input(&self, file: File) -> Input {
|
||||
Input {
|
||||
text: source_text(*self, file),
|
||||
line_index: line_index(*self, file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,16 +669,9 @@ impl std::fmt::Debug for FileResolver<'_> {
|
||||
/// This contains the actual content of that input as well as a
|
||||
/// line index for efficiently querying its contents.
|
||||
#[derive(Clone, Debug)]
|
||||
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)
|
||||
}
|
||||
pub struct Input {
|
||||
pub(crate) text: SourceText,
|
||||
pub(crate) line_index: LineIndex,
|
||||
}
|
||||
|
||||
/// Returns the line number accounting for the given `len`
|
||||
@@ -730,6 +725,7 @@ mod tests {
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::system::{DbWithWritableSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
use crate::Upcast;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -2174,8 +2170,9 @@ watermelon
|
||||
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
|
||||
let span = self.path(path);
|
||||
|
||||
let text = source_text(&self.db, span.file());
|
||||
let line_index = line_index(&self.db, span.file());
|
||||
let file = span.expect_ty_file();
|
||||
let text = source_text(&self.db, file);
|
||||
let line_index = line_index(&self.db, file);
|
||||
let source = SourceCode::new(text.as_str(), &line_index);
|
||||
|
||||
let (line_start, offset_start) = parse_line_offset(line_offset_start);
|
||||
@@ -2237,7 +2234,7 @@ watermelon
|
||||
///
|
||||
/// (This will set the "printed" flag on `Diagnostic`.)
|
||||
fn render(&self, diag: &Diagnostic) -> String {
|
||||
diag.display(&self.db, &self.config).to_string()
|
||||
diag.display(&self.db.upcast(), &self.config).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ impl std::panic::RefUnwindSafe for Files {}
|
||||
#[salsa::input]
|
||||
pub struct File {
|
||||
/// The path of the file (immutable).
|
||||
#[return_ref]
|
||||
#[returns(ref)]
|
||||
pub path: FilePath,
|
||||
|
||||
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::Db;
|
||||
#[salsa::input(debug)]
|
||||
pub struct FileRoot {
|
||||
/// The path of a root is guaranteed to never change.
|
||||
#[return_ref]
|
||||
path_buf: SystemPathBuf,
|
||||
#[returns(deref)]
|
||||
pub path: SystemPathBuf,
|
||||
|
||||
/// The kind of the root at the time of its creation.
|
||||
kind_at_time_of_creation: FileRootKind,
|
||||
@@ -32,10 +32,6 @@ 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()
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
use crate::system::TestSystem;
|
||||
use crate::system::{DbWithTestSystem, System};
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::Db;
|
||||
use crate::{Db, Upcast};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
@@ -136,7 +136,16 @@ mod tests {
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
ruff_python_ast::PythonVersion::latest()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(return_ref, no_eq)]
|
||||
#[salsa::tracked(returns(ref), no_eq)]
|
||||
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
let _span = tracing::trace_span!("parsed_module", ?file).entered();
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ 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 }
|
||||
@@ -31,6 +33,7 @@ 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 }
|
||||
@@ -44,6 +47,7 @@ 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 }
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_ty_schema};
|
||||
use crate::{
|
||||
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
|
||||
generate_ty_options, generate_ty_rules, generate_ty_schema,
|
||||
};
|
||||
|
||||
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
|
||||
|
||||
@@ -38,5 +41,8 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
generate_docs::main(&generate_docs::Args {
|
||||
dry_run: args.mode.is_dry_run(),
|
||||
})?;
|
||||
generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?;
|
||||
generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?;
|
||||
generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Generate CLI help.
|
||||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, str};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Generate Markdown documentation for applicable rules.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write as _;
|
||||
@@ -13,8 +12,8 @@ use strum::IntoEnumIterator;
|
||||
|
||||
use ruff_diagnostics::FixAvailability;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_options_metadata::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
use itertools::Itertools;
|
||||
use std::fmt::Write;
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_python_trivia::textwrap;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
|
||||
pub(crate) fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -11,8 +11,8 @@ use strum::IntoEnumIterator;
|
||||
use ruff_diagnostics::FixAvailability;
|
||||
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
|
||||
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
|
||||
use ruff_options_metadata::OptionsMetadata;
|
||||
use ruff_workspace::options::Options;
|
||||
use ruff_workspace::options_base::OptionsMetadata;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
|
||||
334
crates/ruff_dev/src/generate_ty_cli_reference.rs
Normal file
334
crates/ruff_dev/src/generate_ty_cli_reference.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! Generate a Markdown-compatible reference for the ty command-line interface.
|
||||
use std::cmp::max;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Command, CommandFactory};
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::StrComparison;
|
||||
|
||||
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
use ty::Cli;
|
||||
|
||||
const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"];
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let reference_string = generate();
|
||||
let filename = "crates/ty/docs/cli.md";
|
||||
let reference_path = PathBuf::from(ROOT_DIR).join(filename);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{reference_string}");
|
||||
}
|
||||
Mode::Check => {
|
||||
match std::fs::read_to_string(reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &reference_string);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
bail!("{filename} not found, please run `{REGENERATE_ALL_COMMAND}`");
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Write => match std::fs::read_to_string(&reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
println!("Updating: {filename}");
|
||||
std::fs::write(reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
println!("Updating: {filename}");
|
||||
std::fs::write(reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut ty = Cli::command();
|
||||
|
||||
// It is very important to build the command before beginning inspection or subcommands
|
||||
// will be missing all of the propagated options.
|
||||
ty.build();
|
||||
|
||||
let mut parents = Vec::new();
|
||||
|
||||
output.push_str("# CLI Reference\n\n");
|
||||
generate_command(&mut output, &ty, &mut parents);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[allow(clippy::format_push_string)]
|
||||
fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) {
|
||||
if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the command header.
|
||||
let name = if parents.is_empty() {
|
||||
command.get_name().to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{} {}",
|
||||
parents.iter().map(|cmd| cmd.get_name()).join(" "),
|
||||
command.get_name()
|
||||
)
|
||||
};
|
||||
|
||||
// Display the top-level `ty` command at the same level as its children
|
||||
let level = max(2, parents.len() + 1);
|
||||
output.push_str(&format!("{} {name}\n\n", "#".repeat(level)));
|
||||
|
||||
// Display the command description.
|
||||
if let Some(about) = command.get_long_about().or_else(|| command.get_about()) {
|
||||
output.push_str(&about.to_string());
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
// Display the usage
|
||||
{
|
||||
// This appears to be the simplest way to get rendered usage from Clap,
|
||||
// it is complicated to render it manually. It's annoying that it
|
||||
// requires a mutable reference but it doesn't really matter.
|
||||
let mut command = command.clone();
|
||||
output.push_str("<h3 class=\"cli-reference\">Usage</h3>\n\n");
|
||||
output.push_str(&format!(
|
||||
"```\n{}\n```",
|
||||
command
|
||||
.render_usage()
|
||||
.to_string()
|
||||
.trim_start_matches("Usage: "),
|
||||
));
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
if command.get_name() == "help" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a list of child commands
|
||||
let mut subcommands = command.get_subcommands().peekable();
|
||||
let has_subcommands = subcommands.peek().is_some();
|
||||
if has_subcommands {
|
||||
output.push_str("<h3 class=\"cli-reference\">Commands</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for subcommand in subcommands {
|
||||
if subcommand.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
let subcommand_name = format!("{name} {}", subcommand.get_name());
|
||||
output.push_str(&format!(
|
||||
"<dt><a href=\"#{}\"><code>{subcommand_name}</code></a></dt>",
|
||||
subcommand_name.replace(' ', "-")
|
||||
));
|
||||
if let Some(about) = subcommand.get_about() {
|
||||
output.push_str(&format!(
|
||||
"<dd>{}</dd>\n",
|
||||
markdown::to_html(&about.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Do not display options for commands with children
|
||||
if !has_subcommands {
|
||||
let name_key = name.replace(' ', "-");
|
||||
|
||||
// Display positional arguments
|
||||
let mut arguments = command
|
||||
.get_positionals()
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.peekable();
|
||||
|
||||
if arguments.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Arguments</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
|
||||
for arg in arguments {
|
||||
let id = format!("{name_key}--{}", arg.get_id());
|
||||
output.push_str(&format!("<dt id=\"{id}\">"));
|
||||
output.push_str(&format!(
|
||||
"<a href=\"#{id}\"<code>{}</code></a>",
|
||||
arg.get_id().to_string().to_uppercase(),
|
||||
));
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>\n\n");
|
||||
}
|
||||
|
||||
// Display options and flags
|
||||
let mut options = command
|
||||
.get_arguments()
|
||||
.filter(|arg| !arg.is_positional())
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.sorted_by_key(|arg| arg.get_id())
|
||||
.peekable();
|
||||
|
||||
if options.peek().is_some() {
|
||||
output.push_str("<h3 class=\"cli-reference\">Options</h3>\n\n");
|
||||
output.push_str("<dl class=\"cli-reference\">");
|
||||
for opt in options {
|
||||
let Some(long) = opt.get_long() else { continue };
|
||||
let id = format!("{name_key}--{long}");
|
||||
|
||||
output.push_str(&format!("<dt id=\"{id}\">"));
|
||||
output.push_str(&format!("<a href=\"#{id}\"><code>--{long}</code></a>"));
|
||||
for long_alias in opt.get_all_aliases().into_iter().flatten() {
|
||||
output.push_str(&format!(", <code>--{long_alias}</code>"));
|
||||
}
|
||||
if let Some(short) = opt.get_short() {
|
||||
output.push_str(&format!(", <code>-{short}</code>"));
|
||||
}
|
||||
for short_alias in opt.get_all_short_aliases().into_iter().flatten() {
|
||||
output.push_str(&format!(", <code>-{short_alias}</code>"));
|
||||
}
|
||||
|
||||
// Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts`
|
||||
if opt
|
||||
.get_num_args()
|
||||
.unwrap_or_else(|| 1.into())
|
||||
.takes_values()
|
||||
{
|
||||
if let Some(values) = opt.get_value_names() {
|
||||
for value in values {
|
||||
output.push_str(&format!(
|
||||
" <i>{}</i>",
|
||||
value.to_lowercase().replace('_', "-")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push_str("</dt>");
|
||||
if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) {
|
||||
output.push_str("<dd>");
|
||||
output.push_str(&format!("{}\n", markdown::to_html(&help.to_string())));
|
||||
emit_env_option(opt, output);
|
||||
emit_default_option(opt, output);
|
||||
emit_possible_options(opt, output);
|
||||
output.push_str("</dd>");
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("</dl>");
|
||||
}
|
||||
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
|
||||
parents.push(command);
|
||||
|
||||
// Recurse to all of the subcommands.
|
||||
for subcommand in command.get_subcommands() {
|
||||
generate_command(output, subcommand, parents);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
fn emit_env_option(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_env_set() {
|
||||
return;
|
||||
}
|
||||
if let Some(env) = opt.get_env() {
|
||||
output.push_str(&markdown::to_html(&format!(
|
||||
"May also be set with the `{}` environment variable.",
|
||||
env.to_string_lossy()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_default_option(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_default_value_set() || !opt.get_num_args().expect("built").takes_values() {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = opt.get_default_values();
|
||||
if !values.is_empty() {
|
||||
let value = format!(
|
||||
"\n[default: {}]",
|
||||
opt.get_default_values()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.join(",")
|
||||
);
|
||||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_possible_options(opt: &clap::Arg, output: &mut String) {
|
||||
if opt.is_hide_possible_values_set() {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = opt.get_possible_values();
|
||||
if !values.is_empty() {
|
||||
let value = format!(
|
||||
"\nPossible values:\n{}",
|
||||
values
|
||||
.into_iter()
|
||||
.filter(|value| !value.is_hide_set())
|
||||
.map(|value| {
|
||||
let name = value.get_name();
|
||||
value.get_help().map_or_else(
|
||||
|| format!(" - `{name}`"),
|
||||
|help| format!(" - `{name}`: {help}"),
|
||||
)
|
||||
})
|
||||
.collect_vec()
|
||||
.join("\n"),
|
||||
);
|
||||
output.push_str(&markdown::to_html(&value));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_cli_reference_is_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })
|
||||
}
|
||||
}
|
||||
257
crates/ruff_dev/src/generate_ty_options.rs
Normal file
257
crates/ruff_dev/src/generate_ty_options.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
|
||||
|
||||
use anyhow::bail;
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::StrComparison;
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
|
||||
use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit};
|
||||
use ty_project::metadata::Options;
|
||||
|
||||
use crate::{
|
||||
generate_all::{Mode, REGENERATE_ALL_COMMAND},
|
||||
ROOT_DIR,
|
||||
};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
/// Write the generated table to stdout (rather than to `crates/ty/docs/configuration.md`).
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
|
||||
let mut output = String::new();
|
||||
let file_name = "crates/ty/docs/configuration.md";
|
||||
let markdown_path = PathBuf::from(ROOT_DIR).join(file_name);
|
||||
|
||||
generate_set(
|
||||
&mut output,
|
||||
Set::Toplevel(Options::metadata()),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{output}");
|
||||
}
|
||||
Mode::Check => {
|
||||
let current = std::fs::read_to_string(&markdown_path)?;
|
||||
if output == current {
|
||||
println!("Up-to-date: {file_name}",);
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &output);
|
||||
bail!("{file_name} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}",);
|
||||
}
|
||||
}
|
||||
Mode::Write => {
|
||||
let current = std::fs::read_to_string(&markdown_path)?;
|
||||
if current == output {
|
||||
println!("Up-to-date: {file_name}",);
|
||||
} else {
|
||||
println!("Updating: {file_name}",);
|
||||
std::fs::write(markdown_path, output.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
match &set {
|
||||
Set::Toplevel(_) => {
|
||||
output.push_str("# Configuration\n");
|
||||
}
|
||||
Set::Named { name, .. } => {
|
||||
let title = parents
|
||||
.iter()
|
||||
.filter_map(|set| set.name())
|
||||
.chain(std::iter::once(name.as_str()))
|
||||
.join(".");
|
||||
writeln!(output, "## `{title}`\n",).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(documentation) = set.metadata().documentation() {
|
||||
output.push_str(documentation);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
let mut visitor = CollectOptionsVisitor::default();
|
||||
set.metadata().record(&mut visitor);
|
||||
|
||||
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
|
||||
|
||||
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
|
||||
|
||||
parents.push(set);
|
||||
|
||||
// Generate the fields.
|
||||
for (name, field) in &fields {
|
||||
emit_field(output, name, field, parents.as_slice());
|
||||
output.push_str("---\n\n");
|
||||
}
|
||||
|
||||
// Generate all the sub-sets.
|
||||
for (set_name, sub_set) in &sets {
|
||||
generate_set(
|
||||
output,
|
||||
Set::Named {
|
||||
name: set_name.to_string(),
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
);
|
||||
}
|
||||
|
||||
parents.pop();
|
||||
}
|
||||
|
||||
enum Set {
|
||||
Toplevel(OptionSet),
|
||||
Named { name: String, set: OptionSet },
|
||||
}
|
||||
|
||||
impl Set {
|
||||
fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Set::Toplevel(_) => None,
|
||||
Set::Named { name, .. } => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &OptionSet {
|
||||
match self {
|
||||
Set::Toplevel(set) => set,
|
||||
Set::Named { set, .. } => set,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) {
|
||||
let header_level = if parents.is_empty() { "###" } else { "####" };
|
||||
|
||||
let _ = writeln!(output, "{header_level} [`{name}`]");
|
||||
|
||||
output.push('\n');
|
||||
|
||||
if let Some(deprecated) = &field.deprecated {
|
||||
output.push_str("!!! warning \"Deprecated\"\n");
|
||||
output.push_str(" This option has been deprecated");
|
||||
|
||||
if let Some(since) = deprecated.since {
|
||||
write!(output, " in {since}").unwrap();
|
||||
}
|
||||
|
||||
output.push('.');
|
||||
|
||||
if let Some(message) = deprecated.message {
|
||||
writeln!(output, " {message}").unwrap();
|
||||
}
|
||||
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output.push_str(field.doc);
|
||||
output.push_str("\n\n");
|
||||
let _ = writeln!(output, "**Default value**: `{}`", field.default);
|
||||
output.push('\n');
|
||||
let _ = writeln!(output, "**Type**: `{}`", field.value_type);
|
||||
output.push('\n');
|
||||
output.push_str("**Example usage** (`pyproject.toml`):\n\n");
|
||||
output.push_str(&format_example(
|
||||
&format_header(
|
||||
field.scope,
|
||||
field.example,
|
||||
parents,
|
||||
ConfigurationFile::PyprojectToml,
|
||||
),
|
||||
field.example,
|
||||
));
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
fn format_example(header: &str, content: &str) -> String {
|
||||
if header.is_empty() {
|
||||
format!("```toml\n{content}\n```\n",)
|
||||
} else {
|
||||
format!("```toml\n{header}\n{content}\n```\n",)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the TOML header for the example usage for a given option.
|
||||
///
|
||||
/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`.
|
||||
fn format_header(
|
||||
scope: Option<&str>,
|
||||
example: &str,
|
||||
parents: &[Set],
|
||||
configuration: ConfigurationFile,
|
||||
) -> String {
|
||||
let tool_parent = match configuration {
|
||||
ConfigurationFile::PyprojectToml => Some("tool.ty"),
|
||||
ConfigurationFile::TyToml => None,
|
||||
};
|
||||
|
||||
let header = tool_parent
|
||||
.into_iter()
|
||||
.chain(parents.iter().filter_map(|parent| parent.name()))
|
||||
.chain(scope)
|
||||
.join(".");
|
||||
|
||||
// Ex) `[[tool.ty.xx]]`
|
||||
if example.starts_with(&format!("[[{header}")) {
|
||||
return String::new();
|
||||
}
|
||||
// Ex) `[tool.ty.rules]`
|
||||
if example.starts_with(&format!("[{header}")) {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
if header.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("[{header}]")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectOptionsVisitor {
|
||||
groups: Vec<(String, OptionSet)>,
|
||||
fields: Vec<(String, OptionField)>,
|
||||
}
|
||||
|
||||
impl Visit for CollectOptionsVisitor {
|
||||
fn record_set(&mut self, name: &str, group: OptionSet) {
|
||||
self.groups.push((name.to_owned(), group));
|
||||
}
|
||||
|
||||
fn record_field(&mut self, name: &str, field: OptionField) {
|
||||
self.fields.push((name.to_owned(), field));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum ConfigurationFile {
|
||||
PyprojectToml,
|
||||
#[expect(dead_code)]
|
||||
TyToml,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
use super::{main, Args};
|
||||
|
||||
#[test]
|
||||
fn ty_configuration_markdown_up_to_date() -> Result<()> {
|
||||
main(&Args { mode: Mode::Check })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
143
crates/ruff_dev/src/generate_ty_rules.rs
Normal file
143
crates/ruff_dev/src/generate_ty_rules.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! 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(¤t, &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 })
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! 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};
|
||||
@@ -15,6 +17,9 @@ 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;
|
||||
@@ -44,8 +49,10 @@ 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.
|
||||
@@ -88,7 +95,9 @@ 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)?,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Print the AST for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Print the `LibCST` CST for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Print the token stream for a given Python file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Run round-trip source code generation on a given Python or Jupyter notebook file.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -88,8 +88,8 @@ impl Db for ModuleDb {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -15,6 +15,7 @@ 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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: &'a SourceFile,
|
||||
source_code: SourceFile,
|
||||
}
|
||||
|
||||
impl<'a> Diff<'a> {
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -49,8 +49,9 @@ impl Serialize for ExpandedMessages<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value {
|
||||
let source_code = message.source_file().to_source_code();
|
||||
let notebook_index = context.notebook_index(message.filename());
|
||||
let source_file = message.source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
let notebook_index = context.notebook_index(&message.filename());
|
||||
|
||||
let fix = message.fix().map(|fix| {
|
||||
json!({
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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;
|
||||
|
||||
@@ -45,7 +47,7 @@ mod text;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Message {
|
||||
Diagnostic(DiagnosticMessage),
|
||||
SyntaxError(SyntaxErrorMessage),
|
||||
SyntaxError(db::Diagnostic),
|
||||
}
|
||||
|
||||
/// A diagnostic message corresponding to a rule violation.
|
||||
@@ -59,14 +61,6 @@ 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),
|
||||
@@ -83,6 +77,17 @@ 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,
|
||||
@@ -114,14 +119,14 @@ impl Message {
|
||||
.next()
|
||||
.map_or(TextSize::new(0), TextLen::text_len);
|
||||
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!(
|
||||
Message::syntax_error(
|
||||
format_args!(
|
||||
"SyntaxError: {}",
|
||||
DisplayParseErrorType::new(&parse_error.error)
|
||||
),
|
||||
range: TextRange::at(parse_error.location.start(), len),
|
||||
TextRange::at(parse_error.location.start(), len),
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a [`Message`] from the given [`UnsupportedSyntaxError`].
|
||||
@@ -129,11 +134,11 @@ impl Message {
|
||||
unsupported_syntax_error: &UnsupportedSyntaxError,
|
||||
file: SourceFile,
|
||||
) -> Message {
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!("SyntaxError: {unsupported_syntax_error}"),
|
||||
range: unsupported_syntax_error.range,
|
||||
Message::syntax_error(
|
||||
format_args!("SyntaxError: {unsupported_syntax_error}"),
|
||||
unsupported_syntax_error.range,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a [`Message`] from the given [`SemanticSyntaxError`].
|
||||
@@ -141,11 +146,11 @@ impl Message {
|
||||
semantic_syntax_error: &SemanticSyntaxError,
|
||||
file: SourceFile,
|
||||
) -> Message {
|
||||
Message::SyntaxError(SyntaxErrorMessage {
|
||||
message: format!("SyntaxError: {semantic_syntax_error}"),
|
||||
range: semantic_syntax_error.range,
|
||||
Message::syntax_error(
|
||||
format_args!("SyntaxError: {semantic_syntax_error}"),
|
||||
semantic_syntax_error.range,
|
||||
file,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> {
|
||||
@@ -168,8 +173,11 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is a syntax error message.
|
||||
pub const fn is_syntax_error(&self) -> bool {
|
||||
matches!(self, Message::SyntaxError(_))
|
||||
pub fn is_syntax_error(&self) -> bool {
|
||||
match self {
|
||||
Message::Diagnostic(_) => false,
|
||||
Message::SyntaxError(diag) => diag.id().is_invalid_syntax(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a message kind.
|
||||
@@ -192,7 +200,11 @@ impl Message {
|
||||
pub fn body(&self) -> &str {
|
||||
match self {
|
||||
Message::Diagnostic(m) => &m.kind.body,
|
||||
Message::SyntaxError(m) => &m.message,
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,27 +246,47 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Returns the filename for the message.
|
||||
pub fn filename(&self) -> &str {
|
||||
self.source_file().name()
|
||||
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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the start source location for the message.
|
||||
pub fn compute_start_location(&self) -> LineColumn {
|
||||
self.source_file()
|
||||
.to_source_code()
|
||||
.line_column(self.start())
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the end source location for the message.
|
||||
pub fn compute_end_location(&self) -> LineColumn {
|
||||
self.source_file().to_source_code().line_column(self.end())
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
Message::SyntaxError(m) => &m.file,
|
||||
Message::Diagnostic(m) => m.file.clone(),
|
||||
Message::SyntaxError(m) => m.expect_primary_span().expect_ruff_file().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +307,10 @@ impl Ranged for Message {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Message::Diagnostic(m) => m.range,
|
||||
Message::SyntaxError(m) => m.range,
|
||||
Message::SyntaxError(m) => m
|
||||
.expect_primary_span()
|
||||
.range()
|
||||
.expect("Expected range for ruff span"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,11 +328,11 @@ impl Deref for MessageWithLocation<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<MessageWithLocation>> {
|
||||
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<String, Vec<MessageWithLocation>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in messages {
|
||||
grouped_messages
|
||||
.entry(message.filename())
|
||||
.entry(message.filename().to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(MessageWithLocation {
|
||||
message,
|
||||
|
||||
@@ -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()),
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ impl Serialize for ExpandedMessages<'_> {
|
||||
}
|
||||
|
||||
fn message_to_rdjson_value(message: &Message) -> Value {
|
||||
let source_code = message.source_file().to_source_code();
|
||||
let source_file = message.source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
|
||||
let start_location = source_code.line_column(message.start());
|
||||
let end_location = source_code.line_column(message.end());
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,7 +191,8 @@ impl Display for MessageCodeFrame<'_> {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let source_code = self.message.source_file().to_source_code();
|
||||
let source_file = self.message.source_file();
|
||||
let source_code = 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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -86,8 +86,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl crate::options_base::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn crate::options_base::Visit) {
|
||||
impl ruff_options_metadata::OptionsMetadata for #ident {
|
||||
fn record(visit: &mut dyn ruff_options_metadata::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, crate::options_base::OptionSet::of::<#path>()))
|
||||
ident.span() => (visit.record_set(#kebab_name, ruff_options_metadata::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(crate::options_base::Deprecated { since: #since, message: #note }))
|
||||
quote!(Some(ruff_options_metadata::Deprecated { since: #since, message: #note }))
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
|
||||
Ok(quote_spanned!(
|
||||
ident.span() => {
|
||||
visit.record_field(#kebab_name, crate::options_base::OptionField{
|
||||
visit.record_field(#kebab_name, ruff_options_metadata::OptionField{
|
||||
doc: &#doc,
|
||||
default: &#default,
|
||||
value_type: &#value_type,
|
||||
|
||||
19
crates/ruff_options_metadata/Cargo.toml
Normal file
19
crates/ruff_options_metadata/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[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
|
||||
@@ -1,6 +1,3 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
/// Visits [`OptionsMetadata`].
|
||||
@@ -42,8 +39,8 @@ where
|
||||
}
|
||||
|
||||
/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(::serde::Serialize), serde(untagged))]
|
||||
pub enum OptionEntry {
|
||||
/// A single option.
|
||||
Field(OptionField),
|
||||
@@ -102,7 +99,7 @@ impl OptionSet {
|
||||
/// ### Test for the existence of a child option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// struct WithOptions;
|
||||
///
|
||||
@@ -125,7 +122,7 @@ impl OptionSet {
|
||||
/// ### Test for the existence of a nested option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// struct Root;
|
||||
///
|
||||
@@ -176,7 +173,7 @@ impl OptionSet {
|
||||
/// ### Find a child option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// struct WithOptions;
|
||||
///
|
||||
@@ -201,7 +198,7 @@ impl OptionSet {
|
||||
/// ### Find a nested option
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
/// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
|
||||
///
|
||||
/// static HARD_TABS: OptionField = OptionField {
|
||||
/// doc: "Use hard tabs for indentation and spaces for alignment.",
|
||||
@@ -345,51 +342,14 @@ 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, Serialize)]
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
|
||||
pub struct OptionField {
|
||||
pub doc: &'static str,
|
||||
/// Ex) `"false"`
|
||||
@@ -402,7 +362,8 @@ pub struct OptionField {
|
||||
pub deprecated: Option<Deprecated>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
|
||||
pub struct Deprecated {
|
||||
pub since: Option<&'static str>,
|
||||
pub message: Option<&'static str>,
|
||||
@@ -432,3 +393,48 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,11 @@ 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)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use ruff_linter::{
|
||||
directives::{extract_directives, Flags},
|
||||
generate_noqa_edits,
|
||||
linter::check_path,
|
||||
message::{DiagnosticMessage, Message, SyntaxErrorMessage},
|
||||
message::{DiagnosticMessage, Message},
|
||||
package::PackageRoot,
|
||||
packaging::detect_package_root,
|
||||
registry::AsRule,
|
||||
@@ -173,10 +173,10 @@ pub(crate) fn check(
|
||||
locator.to_index(),
|
||||
encoding,
|
||||
)),
|
||||
Message::SyntaxError(syntax_error_message) => {
|
||||
Message::SyntaxError(_) => {
|
||||
if show_syntax_errors {
|
||||
Some(syntax_error_to_lsp_diagnostic(
|
||||
syntax_error_message,
|
||||
&message,
|
||||
&source_kind,
|
||||
locator.to_index(),
|
||||
encoding,
|
||||
@@ -322,7 +322,7 @@ fn to_lsp_diagnostic(
|
||||
}
|
||||
|
||||
fn syntax_error_to_lsp_diagnostic(
|
||||
syntax_error: SyntaxErrorMessage,
|
||||
syntax_error: &Message,
|
||||
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.message,
|
||||
message: syntax_error.body().to_string(),
|
||||
related_information: None,
|
||||
data: None,
|
||||
},
|
||||
|
||||
@@ -195,7 +195,7 @@ impl SourceFile {
|
||||
}
|
||||
}
|
||||
|
||||
fn index(&self) -> &LineIndex {
|
||||
pub fn index(&self) -> &LineIndex {
|
||||
self.inner
|
||||
.line_index
|
||||
.get_or_init(|| LineIndex::from_source_text(self.source_text()))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use js_sys::Error;
|
||||
use ruff_linter::message::{DiagnosticMessage, Message, SyntaxErrorMessage};
|
||||
use ruff_linter::message::{DiagnosticMessage, Message};
|
||||
use ruff_linter::settings::types::PythonVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -230,15 +230,13 @@ impl Workspace {
|
||||
.collect(),
|
||||
}),
|
||||
},
|
||||
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,
|
||||
}
|
||||
}
|
||||
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,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ ruff_formatter = { workspace = true }
|
||||
ruff_graph = { workspace = true, features = ["serde", "schemars"] }
|
||||
ruff_linter = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true, features = ["serde"] }
|
||||
ruff_python_semantic = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -3,7 +3,6 @@ pub mod options;
|
||||
pub mod pyproject;
|
||||
pub mod resolver;
|
||||
|
||||
pub mod options_base;
|
||||
mod settings;
|
||||
|
||||
pub use settings::{FileResolverSettings, FormatterSettings, Settings};
|
||||
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -32,6 +31,7 @@ use ruff_linter::settings::types::{
|
||||
};
|
||||
use ruff_linter::{warn_user_once, RuleSelector};
|
||||
use ruff_macros::{CombineOptions, OptionsMetadata};
|
||||
use ruff_options_metadata::{OptionsMetadata, Visit};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle};
|
||||
use ruff_python_semantic::NameImports;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "ty"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
# 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
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
|
||||
140
crates/ty/docs/cli.md
Normal file
140
crates/ty/docs/cli.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# CLI Reference
|
||||
|
||||
## ty
|
||||
|
||||
An extremely fast Python type checker.
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty <COMMAND>
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Commands</h3>
|
||||
|
||||
<dl class="cli-reference"><dt><a href="#ty-check"><code>ty check</code></a></dt><dd><p>Check a project for type errors</p></dd>
|
||||
<dt><a href="#ty-server"><code>ty server</code></a></dt><dd><p>Start the language server</p></dd>
|
||||
<dt><a href="#ty-version"><code>ty version</code></a></dt><dd><p>Display ty's version</p></dd>
|
||||
<dt><a href="#ty-help"><code>ty help</code></a></dt><dd><p>Print this message or the help of the given subcommand(s)</p></dd>
|
||||
</dl>
|
||||
|
||||
## ty check
|
||||
|
||||
Check a project for type errors
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty check [OPTIONS] [PATH]...
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Arguments</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-check--paths"><a href="#ty-check--paths"<code>PATHS</code></a></dt><dd><p>List of files or directories to check [default: the project root]</p>
|
||||
</dd></dl>
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-check--color"><a href="#ty-check--color"><code>--color</code></a> <i>when</i></dt><dd><p>Control when colored output is used</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>auto</code>: Display colors if the output goes to an interactive terminal</li>
|
||||
<li><code>always</code>: Always display colors</li>
|
||||
<li><code>never</code>: Never display colors</li>
|
||||
</ul></dd><dt id="ty-check--config"><a href="#ty-check--config"><code>--config</code></a>, <code>-c</code> <i>config-option</i></dt><dd><p>A TOML <code><KEY> = <VALUE></code> pair</p>
|
||||
</dd><dt id="ty-check--error"><a href="#ty-check--error"><code>--error</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'error'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--error-on-warning"><a href="#ty-check--error-on-warning"><code>--error-on-warning</code></a></dt><dd><p>Use exit code 1 if there are any warning-level diagnostics</p>
|
||||
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times)</p>
|
||||
</dd><dt id="ty-check--help"><a href="#ty-check--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help (see a summary with '-h')</p>
|
||||
</dd><dt id="ty-check--ignore"><a href="#ty-check--ignore"><code>--ignore</code></a> <i>rule</i></dt><dd><p>Disables the rule. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints</li>
|
||||
<li><code>concise</code>: Print diagnostics concisely, one per line</li>
|
||||
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
|
||||
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
|
||||
</dd><dt id="ty-check--python"><a href="#ty-check--python"><code>--python</code></a> <i>path</i></dt><dd><p>Path to the Python installation from which ty resolves type information and third-party dependencies.</p>
|
||||
<p>If not specified, ty will look at the <code>VIRTUAL_ENV</code> environment variable.</p>
|
||||
<p>ty will search in the path's <code>site-packages</code> directories for type information and third-party imports.</p>
|
||||
<p>This option is commonly used to specify the path to a virtual environment.</p>
|
||||
</dd><dt id="ty-check--python-platform"><a href="#ty-check--python-platform"><code>--python-platform</code></a>, <code>--platform</code> <i>platform</i></dt><dd><p>Target platform to assume when resolving types.</p>
|
||||
<p>This is used to specialize the type of <code>sys.platform</code> and will affect the visibility of platform-specific functions and attributes. If the value is set to <code>all</code>, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.</p>
|
||||
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>3.7</code></li>
|
||||
<li><code>3.8</code></li>
|
||||
<li><code>3.9</code></li>
|
||||
<li><code>3.10</code></li>
|
||||
<li><code>3.11</code></li>
|
||||
<li><code>3.12</code></li>
|
||||
<li><code>3.13</code></li>
|
||||
</ul></dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
|
||||
</dd><dt id="ty-check--typeshed"><a href="#ty-check--typeshed"><code>--typeshed</code></a>, <code>--custom-typeshed-dir</code> <i>path</i></dt><dd><p>Custom directory to use for stdlib typeshed stubs</p>
|
||||
</dd><dt id="ty-check--verbose"><a href="#ty-check--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output (or <code>-vv</code> and <code>-vvv</code> for more verbose output)</p>
|
||||
</dd><dt id="ty-check--warn"><a href="#ty-check--warn"><code>--warn</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'warn'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--watch"><a href="#ty-check--watch"><code>--watch</code></a>, <code>-W</code></dt><dd><p>Watch files for changes and recheck files related to the changed files</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty server
|
||||
|
||||
Start the language server
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty server
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-server--help"><a href="#ty-server--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty version
|
||||
|
||||
Display ty's version
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty version
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-version--help"><a href="#ty-version--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty generate-shell-completion
|
||||
|
||||
Generate shell completion
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty generate-shell-completion <SHELL>
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Arguments</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-generate-shell-completion--shell"><a href="#ty-generate-shell-completion--shell"<code>SHELL</code></a></dt></dl>
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-generate-shell-completion--help"><a href="#ty-generate-shell-completion--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help</p>
|
||||
</dd></dl>
|
||||
|
||||
## ty help
|
||||
|
||||
Print this message or the help of the given subcommand(s)
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
ty help [COMMAND]
|
||||
```
|
||||
|
||||
220
crates/ty/docs/configuration.md
Normal file
220
crates/ty/docs/configuration.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
1554
crates/ty/docs/rules.md
Normal file
1554
crates/ty/docs/rules.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,22 @@
|
||||
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};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf, ValueSource};
|
||||
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(crate) struct Args {
|
||||
pub struct Cli {
|
||||
#[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.
|
||||
@@ -86,6 +89,9 @@ 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>,
|
||||
@@ -140,7 +146,7 @@ impl CheckCommand {
|
||||
.no_respect_ignore_files
|
||||
.then_some(false)
|
||||
.or(self.respect_ignore_files);
|
||||
Options {
|
||||
let options = Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: self
|
||||
.python_version
|
||||
@@ -166,7 +172,9 @@ impl CheckCommand {
|
||||
rules,
|
||||
respect_ignore_files,
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
// Merge with options passed in via --config
|
||||
options.combine(self.config.into_options().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,3 +307,55 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
397
crates/ty/src/lib.rs
Normal file
397
crates/ty/src/lib.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
mod args;
|
||||
mod logging;
|
||||
mod python_version;
|
||||
mod version;
|
||||
|
||||
pub use args::Cli;
|
||||
|
||||
use std::io::{self, stdout, BufWriter, Write};
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::Upcast;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use ty_project::metadata::options::Options;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{watch, Db};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_server::run_server;
|
||||
|
||||
pub fn run() -> anyhow::Result<ExitStatus> {
|
||||
setup_rayon();
|
||||
|
||||
let args = wild::args_os();
|
||||
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
|
||||
.context("Failed to read CLI arguments from file")?;
|
||||
let args = Cli::parse_from(args);
|
||||
|
||||
match args.command {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
Command::Version => version().map(|()| ExitStatus::Success),
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Cli::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn version() -> Result<()> {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
let version_info = crate::version::version();
|
||||
writeln!(stdout, "ty {}", &version_info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
set_colored_override(args.color);
|
||||
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
tracing::debug!("Version: {}", version::version());
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cwd = {
|
||||
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
|
||||
SystemPathBuf::from_path_buf(cwd)
|
||||
.map_err(|path| {
|
||||
anyhow!(
|
||||
"The current working directory `{}` contains non-Unicode characters. ty only supports Unicode paths.",
|
||||
path.display()
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let project_path = args
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| {
|
||||
if project.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(project, &cwd))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided project path `{project}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
|
||||
let check_paths: Vec<_> = args
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
|
||||
if !check_paths.is_empty() {
|
||||
db.project().set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
ctrlc::set_handler(move || {
|
||||
let mut lock = main_loop_cancellation_token.lock().unwrap();
|
||||
|
||||
if let Some(token) = lock.take() {
|
||||
token.stop();
|
||||
}
|
||||
})?;
|
||||
|
||||
let exit_status = if watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db)?
|
||||
};
|
||||
|
||||
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
|
||||
|
||||
std::mem::forget(db);
|
||||
|
||||
if exit_zero {
|
||||
Ok(ExitStatus::Success)
|
||||
} else {
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ExitStatus {
|
||||
/// Checking was successful and there were no errors.
|
||||
Success = 0,
|
||||
|
||||
/// Checking was successful but there were errors.
|
||||
Failure = 1,
|
||||
|
||||
/// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...)
|
||||
Error = 2,
|
||||
|
||||
/// Internal ty error (panic, or any other error that isn't due to the user using the
|
||||
/// program incorrectly or transient environment errors).
|
||||
InternalError = 101,
|
||||
}
|
||||
|
||||
impl Termination for ExitStatus {
|
||||
fn report(self) -> ExitCode {
|
||||
ExitCode::from(self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
struct MainLoop {
|
||||
/// Sender that can be used to send messages to the main loop.
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
|
||||
/// Receiver for the messages sent **to** the main loop.
|
||||
receiver: crossbeam_channel::Receiver<MainLoopMessage>,
|
||||
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<ProjectWatcher>,
|
||||
|
||||
cli_options: Options,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
Self {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
}
|
||||
|
||||
fn watch(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
tracing::debug!("Starting watch mode");
|
||||
let sender = self.sender.clone();
|
||||
let watcher = watch::directory_watcher(move |event| {
|
||||
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
|
||||
})?;
|
||||
|
||||
self.watcher = Some(ProjectWatcher::new(watcher, db));
|
||||
|
||||
self.run(db)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn run(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
|
||||
let result = self.main_loop(db);
|
||||
|
||||
tracing::debug!("Exiting main loop");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
// Schedule the first check.
|
||||
tracing::debug!("Starting main loop");
|
||||
|
||||
let mut revision = 0u64;
|
||||
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the project. This needs to be done in a separate thread
|
||||
// to prevent blocking the main loop here.
|
||||
rayon::spawn(move || {
|
||||
match db.check() {
|
||||
Ok(result) => {
|
||||
// Send the result back to the main loop for printing.
|
||||
sender
|
||||
.send(MainLoopMessage::CheckCompleted { result, revision })
|
||||
.unwrap();
|
||||
}
|
||||
Err(cancelled) => {
|
||||
tracing::debug!("Check has been cancelled: {cancelled:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MainLoopMessage::CheckCompleted {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format)
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize());
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
||||
let mut stdout = stdout().lock();
|
||||
|
||||
if result.is_empty() {
|
||||
writeln!(stdout, "{}", "All checks passed!".green().bold())?;
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
} else {
|
||||
let mut max_severity = Severity::Info;
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
diagnostic.display(&db.upcast(), &display_config)
|
||||
)?;
|
||||
|
||||
max_severity = max_severity.max(diagnostic.severity());
|
||||
}
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
|
||||
if max_severity.is_fatal() {
|
||||
tracing::warn!("A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.");
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(match max_severity {
|
||||
Severity::Info => ExitStatus::Success,
|
||||
Severity::Warning => {
|
||||
if terminal_settings.error_on_warning {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
}
|
||||
}
|
||||
Severity::Error => ExitStatus::Failure,
|
||||
Severity::Fatal => ExitStatus::InternalError,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::trace!("Counts after last check:\n{}", countme::get_all());
|
||||
}
|
||||
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes, Some(&self.cli_options));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
}
|
||||
MainLoopMessage::Exit => {
|
||||
// Cancel any pending queries and wait for them to complete.
|
||||
// TODO: Don't use Salsa internal APIs
|
||||
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
|
||||
let _ = db.zalsa_mut();
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Waiting for next main loop message.");
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MainLoopCancellationToken {
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
}
|
||||
|
||||
impl MainLoopCancellationToken {
|
||||
fn stop(self) {
|
||||
self.sender.send(MainLoopMessage::Exit).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Message sent from the orchestrator to the main loop.
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Diagnostic>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn set_colored_override(color: Option<TerminalColor>) {
|
||||
let Some(color) = color else {
|
||||
return;
|
||||
};
|
||||
|
||||
match color {
|
||||
TerminalColor::Auto => {
|
||||
colored::control::unset_override();
|
||||
}
|
||||
TerminalColor::Always => {
|
||||
colored::control::set_override(true);
|
||||
}
|
||||
TerminalColor::Never => {
|
||||
colored::control::set_override(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the global rayon thread pool to never use more than `TY_MAX_PARALLELISM` threads.
|
||||
fn setup_rayon() {
|
||||
ThreadPoolBuilder::default()
|
||||
.num_threads(max_parallelism().get())
|
||||
// Use a reasonably large stack size to avoid running into stack overflows too easily. The
|
||||
// size was chosen in such a way as to still be able to handle large expressions involving
|
||||
// binary operators (x + x + … + x) both during the AST walk in semantic index building as
|
||||
// well as during type checking. Using this stack size, we can handle handle expressions
|
||||
// that are several times larger than the corresponding limits in existing type checkers.
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.build_global()
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,39 +1,13 @@
|
||||
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 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;
|
||||
use std::io;
|
||||
use ty::{run, ExitStatus};
|
||||
|
||||
pub fn main() -> ExitStatus {
|
||||
setup_rayon();
|
||||
|
||||
run().unwrap_or_else(|error| {
|
||||
use std::io::Write;
|
||||
use io::Write;
|
||||
|
||||
// Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken.
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
let mut stderr = 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)
|
||||
@@ -57,364 +31,3 @@ 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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -196,12 +197,12 @@ fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:5:1
|
||||
--> test.py:5:13
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal["linux"]`
|
||||
| ^^^^^^^^^^^^ `Literal["linux"]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -214,12 +215,12 @@ fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:5:1
|
||||
--> test.py:5:13
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `LiteralString`
|
||||
| ^^^^^^^^^^^^ `LiteralString`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -368,12 +369,12 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
// Assert that there's an `unresolved-reference` diagnostic (error)
|
||||
// and a `division-by-zero` diagnostic (error).
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -388,15 +389,15 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> test.py:7:7
|
||||
error: lint:unresolved-reference: Name `prin` used when not defined
|
||||
--> test.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| ^
|
||||
7 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -408,7 +409,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
r#"
|
||||
[tool.ty.rules]
|
||||
division-by-zero = "warn" # demote to warn
|
||||
possibly-unresolved-reference = "ignore"
|
||||
unresolved-reference = "ignore"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
@@ -447,12 +448,12 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
// Assert that there's an `unresolved-reference` diagnostic (error),
|
||||
// a `division-by-zero` (error) and a unresolved-import (error) diagnostic by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -479,15 +480,15 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> test.py:9:7
|
||||
error: lint:unresolved-reference: Name `prin` used when not defined
|
||||
--> test.py:9:1
|
||||
|
|
||||
7 | x = a
|
||||
8 |
|
||||
9 | print(x) # possibly-unresolved-reference
|
||||
| ^
|
||||
9 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
@@ -498,7 +499,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
case
|
||||
.command()
|
||||
.arg("--ignore")
|
||||
.arg("possibly-unresolved-reference")
|
||||
.arg("unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
.arg("--warn")
|
||||
@@ -550,12 +551,12 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
prin(x) # unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
// Assert that there's a `unresolved-reference` diagnostic (error)
|
||||
// and a `division-by-zero` (error) by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -570,15 +571,15 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
|
|
||||
info: `lint:division-by-zero` is enabled by default
|
||||
|
||||
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
|
||||
--> test.py:7:7
|
||||
error: lint:unresolved-reference: Name `prin` used when not defined
|
||||
--> test.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| ^
|
||||
7 | prin(x) # unresolved-reference
|
||||
| ^^^^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -588,13 +589,13 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
.command()
|
||||
.arg("--error")
|
||||
.arg("possibly-unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("unresolved-reference")
|
||||
.arg("--warn")
|
||||
.arg("division-by-zero")
|
||||
// Override the error severity with warning
|
||||
.arg("--ignore")
|
||||
.arg("possibly-unresolved-reference"),
|
||||
.arg("unresolved-reference"),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -675,7 +676,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(), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -685,7 +686,7 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -710,11 +711,11 @@ fn exit_code_only_info() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:3:1
|
||||
--> test.py:3:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
| ^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -740,11 +741,11 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:3:1
|
||||
--> test.py:3:13
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
| ^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -759,7 +760,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"), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -769,7 +770,7 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -792,7 +793,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -802,7 +803,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
@@ -822,7 +823,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -833,7 +834,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
@@ -862,7 +863,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
|
||||
"###,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -873,7 +874,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` is enabled by default
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
@@ -902,7 +903,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -913,7 +914,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
| ^
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
info: `lint:unresolved-reference` is enabled by default
|
||||
info: `lint:unresolved-reference` was selected on the command line
|
||||
|
||||
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
--> test.py:3:7
|
||||
@@ -950,7 +951,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x)
|
||||
prin(x)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
@@ -962,50 +963,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: true
|
||||
exit_code: 0
|
||||
----- 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
|
||||
|
||||
warning: 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` 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"
|
||||
@@ -1022,15 +979,59 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
|
|
||||
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
|
||||
error: lint:unresolved-reference: Name `prin` used when not defined
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x)
|
||||
| ^
|
||||
7 | prin(x)
|
||||
| ^^^^
|
||||
|
|
||||
info: `lint:possibly-unresolved-reference` was selected in the configuration file
|
||||
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"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- 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
|
||||
|
||||
warning: lint:unresolved-reference: Name `prin` used when not defined
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | prin(x)
|
||||
| ^^^^
|
||||
|
|
||||
info: `lint:unresolved-reference` was selected in the configuration file
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
@@ -1180,7 +1181,7 @@ fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r"
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -1218,7 +1219,7 @@ fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type] test.py:5:1: Revealed type: `Literal["hello"]`
|
||||
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
@@ -1247,12 +1248,12 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> test.py:4:1
|
||||
--> test.py:4:13
|
||||
|
|
||||
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
|
||||
@@ -1263,6 +1264,202 @@ 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,
|
||||
|
||||
@@ -120,8 +120,8 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -136,6 +136,7 @@ mod tests {
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
||||
Severity, Span,
|
||||
};
|
||||
use ruff_db::Upcast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
#[test]
|
||||
@@ -773,7 +774,7 @@ mod tests {
|
||||
.message("Cursor offset"),
|
||||
);
|
||||
|
||||
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
|
||||
write!(buf, "{}", diagnostic.display(&self.db.upcast(), &config)).unwrap();
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ mod tests {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::latest(),
|
||||
python_version: PythonVersion::latest_ty(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
|
||||
@@ -204,6 +204,7 @@ 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::{
|
||||
@@ -227,7 +228,7 @@ mod tests {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::latest(),
|
||||
python_version: PythonVersion::latest_ty(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
@@ -285,7 +286,7 @@ mod tests {
|
||||
.format(DiagnosticFormat::Full);
|
||||
for diagnostic in diagnostics {
|
||||
let diag = diagnostic.into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
write!(buf, "{}", diag.display(&self.db.upcast(), &config)).unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
|
||||
@@ -15,6 +15,7 @@ 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 }
|
||||
@@ -44,11 +45,7 @@ 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"]
|
||||
|
||||
|
||||
@@ -126,6 +126,16 @@ 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 {}
|
||||
|
||||
@@ -139,7 +149,7 @@ impl SemanticDb for ProjectDatabase {
|
||||
project.is_file_open(self, file)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
@@ -317,7 +327,7 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,21 +60,21 @@ pub struct Project {
|
||||
///
|
||||
/// Setting the open files to a non-`None` value changes `check` to only check the
|
||||
/// open files rather than all files in the project.
|
||||
#[return_ref]
|
||||
#[returns(as_deref)]
|
||||
#[default]
|
||||
open_fileset: Option<Arc<FxHashSet<File>>>,
|
||||
|
||||
/// The first-party files of this project.
|
||||
#[default]
|
||||
#[return_ref]
|
||||
#[returns(ref)]
|
||||
file_set: IndexedFiles,
|
||||
|
||||
/// The metadata describing the project, including the unresolved options.
|
||||
#[return_ref]
|
||||
#[returns(ref)]
|
||||
pub metadata: ProjectMetadata,
|
||||
|
||||
/// The resolved project settings.
|
||||
#[return_ref]
|
||||
#[returns(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]
|
||||
#[return_ref]
|
||||
#[returns(deref)]
|
||||
included_paths_list: Vec<SystemPathBuf>,
|
||||
|
||||
/// Diagnostics that were generated when resolving the project settings.
|
||||
#[return_ref]
|
||||
#[returns(deref)]
|
||||
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]
|
||||
#[salsa::tracked(returns(deref))]
|
||||
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).as_deref()
|
||||
self.open_fileset(db)
|
||||
}
|
||||
|
||||
/// Sets the open files in the project.
|
||||
|
||||
@@ -8,7 +8,7 @@ use ty_python_semantic::ProgramSettings;
|
||||
use crate::combine::Combine;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
|
||||
use crate::metadata::value::ValueSource;
|
||||
use options::Options;
|
||||
pub use options::Options;
|
||||
use options::TyTomlError;
|
||||
|
||||
mod configuration_file;
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::Db;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_macros::{Combine, OptionsMetadata};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -14,31 +14,63 @@ use ty_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPath
|
||||
|
||||
use super::settings::{Settings, TerminalSettings};
|
||||
|
||||
/// The options for the project.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[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 lints and their severity.
|
||||
/// 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.
|
||||
#[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(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
|
||||
pub 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)
|
||||
@@ -61,7 +93,7 @@ impl Options {
|
||||
.environment
|
||||
.as_ref()
|
||||
.and_then(|env| env.python_version.as_deref().copied())
|
||||
.unwrap_or_default();
|
||||
.unwrap_or(PythonVersion::latest_ty());
|
||||
let python_platform = self
|
||||
.environment
|
||||
.as_ref()
|
||||
@@ -226,22 +258,33 @@ impl Options {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[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 tailor its use of type stub files, which conditionalizes type definitions based on the 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.
|
||||
#[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 tailor its use of type stub files,
|
||||
/// which conditionalize type definitions based on the platform.
|
||||
/// 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
|
||||
@@ -250,18 +293,40 @@ 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.
|
||||
@@ -271,15 +336,31 @@ 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)]
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct SrcOptions {
|
||||
/// The root of the project, used for finding first-party modules.
|
||||
/// The root(s) 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>,
|
||||
}
|
||||
|
||||
@@ -301,7 +382,9 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TerminalOptions {
|
||||
@@ -309,10 +392,25 @@ 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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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::TooLargeMajor(minor))?;
|
||||
u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMinor(minor))?;
|
||||
|
||||
Ok(Some(
|
||||
requires_python
|
||||
|
||||
@@ -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, _StartT_co, _StartT_co | _StopT_co]
|
||||
reveal_type(C.a_slice) # revealed: slice[Any, Any, Any]
|
||||
reveal_type(C.a_type) # revealed: type
|
||||
reveal_type(C.a_none) # revealed: None
|
||||
```
|
||||
|
||||
@@ -45,6 +45,18 @@ 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
|
||||
|
||||
@@ -9,6 +9,9 @@ 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
|
||||
|
||||
@@ -252,12 +252,8 @@ 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
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
```
|
||||
|
||||
@@ -11,30 +11,28 @@ 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]
|
||||
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
# fmt: off
|
||||
|
||||
reveal_type(len( # revealed: Literal[7]
|
||||
"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
))
|
||||
|
||||
reveal_type(len( # revealed: Literal[9]
|
||||
r"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
))
|
||||
|
||||
reveal_type(len( # revealed: Literal[7]
|
||||
b"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
))
|
||||
reveal_type(len( # revealed: Literal[9]
|
||||
rb"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
### Tuples
|
||||
@@ -50,15 +48,17 @@ 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( # revealed: Literal[2]
|
||||
len(
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
reveal_type(len( # revealed: Literal[2]
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
# fmt: on
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
@@ -19,6 +19,16 @@ reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other
|
||||
class:
|
||||
|
||||
```py
|
||||
class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base]
|
||||
|
||||
# TODO: should emit an error (fails at runtime)
|
||||
class AlsoBad(Generic[T], Generic[S]): ...
|
||||
```
|
||||
|
||||
You cannot use the same typevar more than once.
|
||||
|
||||
```py
|
||||
|
||||
@@ -86,6 +86,26 @@ S = TypeVar("S")
|
||||
reveal_type(S.__default__) # revealed: NoDefault
|
||||
```
|
||||
|
||||
### Using other typevars as a default
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar, Union
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U", default=T)
|
||||
V = TypeVar("V", default=Union[T, U])
|
||||
|
||||
class Valid(Generic[T, U, V]): ...
|
||||
|
||||
reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown]
|
||||
reveal_type(Valid[int]()) # revealed: Valid[int, int, int]
|
||||
reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str]
|
||||
reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None]
|
||||
|
||||
# TODO: error, default value for U isn't available in the generic context
|
||||
class Invalid(Generic[U]): ...
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
|
||||
@@ -40,6 +40,25 @@ def g[S]():
|
||||
reveal_type(S.__default__) # revealed: NoDefault
|
||||
```
|
||||
|
||||
### Using other typevars as a default
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
class Valid[T, U = T, V = T | U]: ...
|
||||
|
||||
reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown]
|
||||
reveal_type(Valid[int]()) # revealed: Valid[int, int, int]
|
||||
reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str]
|
||||
reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class Invalid[S = T]: ...
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
|
||||
@@ -251,6 +251,53 @@ 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`:
|
||||
@@ -269,6 +316,8 @@ import subexporter
|
||||
|
||||
__all__ = []
|
||||
__all__.extend(["C", "D"])
|
||||
__all__.extend(("E", "F"))
|
||||
__all__.extend({"G", "H"})
|
||||
__all__.extend(subexporter.__all__)
|
||||
|
||||
class C: ...
|
||||
@@ -281,7 +330,7 @@ class D: ...
|
||||
import exporter
|
||||
from ty_extensions import dunder_all_names
|
||||
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]]
|
||||
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"], Literal["E"], Literal["F"], Literal["G"], Literal["H"]]
|
||||
reveal_type(dunder_all_names(exporter))
|
||||
```
|
||||
|
||||
|
||||
@@ -391,28 +391,35 @@ 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
|
||||
# error: [unresolved-import]
|
||||
from does_not_exist import unknown_object_1, unknown_object_2
|
||||
from unresolvable_module import UnknownBase1, UnknownBase2 # error: [unresolved-import]
|
||||
|
||||
reveal_type(unknown_object_1) # revealed: Unknown
|
||||
reveal_type(unknown_object_2) # revealed: Unknown
|
||||
reveal_type(UnknownBase1) # revealed: Unknown
|
||||
reveal_type(UnknownBase2) # revealed: Unknown
|
||||
|
||||
# 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): ...
|
||||
# no error here -- we respect the gradual guarantee:
|
||||
class Foo(UnknownBase1, UnknownBase2): ...
|
||||
|
||||
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
|
||||
@@ -485,3 +492,29 @@ reveal_type(BarCycle.__mro__) # revealed: tuple[<class 'BarCycle'>, Unknown, <c
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[<class 'Baz'>, Unknown, <class 'object'>]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[<class 'Spam'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
## Other classes with possible cycles
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```pyi
|
||||
class C(C.a): ...
|
||||
reveal_type(C.__class__) # revealed: <class 'type'>
|
||||
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>]
|
||||
|
||||
class D(D.a):
|
||||
a: D
|
||||
#reveal_type(D.__class__) # revealed: <class 'type'>
|
||||
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, Unknown, <class 'object'>]
|
||||
|
||||
class E[T](E.a): ...
|
||||
#reveal_type(E.__class__) # revealed: <class 'type'>
|
||||
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, <class 'object'>]
|
||||
|
||||
class F[T](F(), F): ... # error: [cyclic-class-definition]
|
||||
#reveal_type(F.__class__) # revealed: <class 'type'>
|
||||
reveal_type(F.__mro__) # revealed: tuple[<class 'F[Unknown]'>, Unknown, <class 'object'>]
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ Just like for any other class base, it is an error for `Protocol` to appear mult
|
||||
class's bases:
|
||||
|
||||
```py
|
||||
class Foo(Protocol, Protocol): ... # error: [inconsistent-mro]
|
||||
class Foo(Protocol, Protocol): ... # error: [duplicate-base]
|
||||
|
||||
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 | None
|
||||
parent: RecursiveFullyStatic
|
||||
x: int
|
||||
|
||||
class RecursiveNonFullyStatic(Protocol):
|
||||
parent: RecursiveNonFullyStatic | None
|
||||
parent: RecursiveNonFullyStatic
|
||||
x: Any
|
||||
|
||||
static_assert(is_fully_static(RecursiveFullyStatic))
|
||||
@@ -1582,16 +1582,111 @@ static_assert(not is_fully_static(RecursiveNonFullyStatic))
|
||||
static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
# TODO: currently leads to a stack overflow
|
||||
# static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
# static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic))
|
||||
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic))
|
||||
|
||||
class AlsoRecursiveFullyStatic(Protocol):
|
||||
parent: AlsoRecursiveFullyStatic | None
|
||||
parent: AlsoRecursiveFullyStatic
|
||||
x: int
|
||||
|
||||
# TODO: currently leads to a stack overflow
|
||||
# static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic))
|
||||
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
|
||||
```
|
||||
|
||||
### Regression test: narrowing with self-referential protocols
|
||||
|
||||
@@ -32,8 +32,14 @@ def f():
|
||||
y = ""
|
||||
|
||||
global x
|
||||
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
|
||||
# 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
|
||||
@@ -48,8 +54,7 @@ def outer():
|
||||
|
||||
def inner():
|
||||
global x
|
||||
# TODO: revealed: int
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
@@ -87,8 +92,7 @@ def f():
|
||||
```py
|
||||
def f():
|
||||
global x
|
||||
# TODO this should also not be an error
|
||||
y = x # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
y = x
|
||||
x = 1 # No error.
|
||||
|
||||
x = 2
|
||||
@@ -99,79 +103,111 @@ 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) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
print(x)
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
print(x)
|
||||
|
||||
def f():
|
||||
global x
|
||||
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
print(x)
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
print(x)
|
||||
|
||||
def f():
|
||||
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x, y
|
||||
print(x)
|
||||
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
print(x)
|
||||
|
||||
def f():
|
||||
global x, y
|
||||
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x, y
|
||||
print(x)
|
||||
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
print(x)
|
||||
|
||||
def f():
|
||||
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
x = 1
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
global x
|
||||
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
x = 1
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x, y
|
||||
del x
|
||||
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
del x
|
||||
|
||||
def f():
|
||||
global x, y
|
||||
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x, y
|
||||
del x
|
||||
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
del x
|
||||
|
||||
def f():
|
||||
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
del x
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
del x
|
||||
|
||||
def f():
|
||||
global x
|
||||
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
del x
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
del x
|
||||
|
||||
def f():
|
||||
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x, y
|
||||
del x
|
||||
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
del x
|
||||
|
||||
def f():
|
||||
global x, y
|
||||
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x, y
|
||||
del x
|
||||
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
del x
|
||||
|
||||
def f():
|
||||
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
print(f"{x=}")
|
||||
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
|
||||
|
||||
# still an error in module scope
|
||||
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
|
||||
global x
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -44,12 +44,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
--> src/mdtest_snippet.py:11:17
|
||||
|
|
||||
9 | # error: [not-iterable]
|
||||
10 | for x in Iterable():
|
||||
11 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
| ^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -40,12 +40,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
--> src/mdtest_snippet.py:8:17
|
||||
|
|
||||
6 | # error: [not-iterable]
|
||||
7 | for x in Bad():
|
||||
8 | reveal_type(x) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
| ^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -63,12 +63,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:24:9
|
||||
--> src/mdtest_snippet.py:24:21
|
||||
|
|
||||
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:9
|
||||
--> src/mdtest_snippet.py:29:21
|
||||
|
|
||||
27 | for y in Iterable2():
|
||||
28 | # TODO... `int` might be ideal here?
|
||||
29 | reveal_type(y) # revealed: int | Unknown
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
| ^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -60,12 +60,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:22:9
|
||||
--> src/mdtest_snippet.py:22:21
|
||||
|
|
||||
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:9
|
||||
--> src/mdtest_snippet.py:26:21
|
||||
|
|
||||
24 | # error: [not-iterable]
|
||||
25 | for y in Iterable2():
|
||||
26 | reveal_type(y) # revealed: str | int
|
||||
| ^^^^^^^^^^^^^^ `str | int`
|
||||
| ^ `str | int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -64,12 +64,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:18:9
|
||||
--> src/mdtest_snippet.py:18:21
|
||||
|
|
||||
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:9
|
||||
--> src/mdtest_snippet.py:30:21
|
||||
|
|
||||
28 | for x in Iterable2():
|
||||
29 | # TODO: `int` would probably be better here:
|
||||
30 | reveal_type(x) # revealed: int | Unknown
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
| ^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -67,12 +67,12 @@ info: `lint:not-iterable` is enabled by default
|
||||
|
||||
```
|
||||
info: revealed-type: Revealed type
|
||||
--> src/mdtest_snippet.py:29:9
|
||||
--> src/mdtest_snippet.py:29:21
|
||||
|
|
||||
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:9
|
||||
--> src/mdtest_snippet.py:34:21
|
||||
|
|
||||
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
Reference in New Issue
Block a user