Compare commits
33 Commits
david/data
...
jack/not_l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84b07e0f23 | ||
|
|
eb67413a3e | ||
|
|
b31102a9ee | ||
|
|
d473f0e1dc | ||
|
|
db61d3c69c | ||
|
|
7f3cd6352e | ||
|
|
71cd7bb170 | ||
|
|
062342c03b | ||
|
|
a18f76158d | ||
|
|
8f400bb37a | ||
|
|
1eff0300d3 | ||
|
|
fea84e8777 | ||
|
|
79fe538458 | ||
|
|
f7234cb474 | ||
|
|
35a33f045e | ||
|
|
f32f7a3b48 | ||
|
|
68106dd631 | ||
|
|
ab3af924ef | ||
|
|
05139a323b | ||
|
|
5eb5ec987d | ||
|
|
1a099886ab | ||
|
|
a8f2c26143 | ||
|
|
fda188953f | ||
|
|
546f1b7b39 | ||
|
|
7533a0bfdb | ||
|
|
3ee3434187 | ||
|
|
149350bf39 | ||
|
|
6a42d28867 | ||
|
|
ce2bdb9357 | ||
|
|
d78d10dd94 | ||
|
|
36276143be | ||
|
|
2643dc5b7a | ||
|
|
738692baff |
85
.github/workflows/ty-ecosystem-analyzer_comment.yaml
vendored
Normal file
85
.github/workflows/ty-ecosystem-analyzer_comment.yaml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: PR comment (ty ecosystem-analyzer)
|
||||
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
workflow_run:
|
||||
workflows: [ty ecosystem-analyzer]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
workflow_run_id:
|
||||
description: The ty ecosystem-analyzer workflow that triggers the workflow run
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download PR number
|
||||
with:
|
||||
name: pr-number
|
||||
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Parse pull request number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [[ -f pr-number ]]
|
||||
then
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: "Download comment.md"
|
||||
id: download-comment
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: comment.md
|
||||
workflow: ty-ecosystem-analyzer.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/comment
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
if: ${{ steps.download-comment.outputs.found_artifact == 'true' }}
|
||||
run: |
|
||||
# Guard against malicious ty ecosystem-analyzer results that symlink to a secret
|
||||
# file on this runner
|
||||
if [[ -L pr/comment/comment.md ]]
|
||||
then
|
||||
echo "Error: comment.md cannot be a symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note: this identifier is used to find the comment to update on subsequent runs
|
||||
echo '<!-- generated-comment ty ecosystem-analyzer -->' > comment.md
|
||||
echo >> comment.md
|
||||
cat pr/comment/comment.md >> comment.md
|
||||
|
||||
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
|
||||
cat comment.md >> "$GITHUB_OUTPUT"
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
if: steps.generate-comment.outcome == 'success'
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "<!-- generated-comment ty ecosystem-analyzer -->"
|
||||
|
||||
- name: Create or update comment
|
||||
if: steps.find-comment.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
body-path: comment.md
|
||||
edit-mode: replace
|
||||
@@ -6,7 +6,7 @@ exclude: |
|
||||
crates/ty_vendored/vendor/.*|
|
||||
crates/ty_project/resources/.*|
|
||||
crates/ty_python_semantic/resources/corpus/.*|
|
||||
crates/ty/docs/(configuration|rules|cli).md|
|
||||
crates/ty/docs/(configuration|rules|cli|environment).md|
|
||||
crates/ruff_benchmark/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
|
||||
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -2874,6 +2874,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ty_static",
|
||||
"web-time",
|
||||
"zip",
|
||||
]
|
||||
@@ -2917,6 +2918,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"ty",
|
||||
"ty_project",
|
||||
"ty_static",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -4165,6 +4167,7 @@ dependencies = [
|
||||
"ty_project",
|
||||
"ty_python_semantic",
|
||||
"ty_server",
|
||||
"ty_static",
|
||||
"wild",
|
||||
]
|
||||
|
||||
@@ -4268,6 +4271,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
"ty_static",
|
||||
"ty_test",
|
||||
"ty_vendored",
|
||||
]
|
||||
@@ -4299,6 +4303,13 @@ dependencies = [
|
||||
"ty_vendored",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty_static"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"ruff_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty_test"
|
||||
version = "0.0.0"
|
||||
@@ -4327,6 +4338,7 @@ dependencies = [
|
||||
"toml",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
"ty_static",
|
||||
"ty_vendored",
|
||||
]
|
||||
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -44,6 +44,7 @@ ty_ide = { path = "crates/ty_ide" }
|
||||
ty_project = { path = "crates/ty_project", default-features = false }
|
||||
ty_python_semantic = { path = "crates/ty_python_semantic" }
|
||||
ty_server = { path = "crates/ty_server" }
|
||||
ty_static = { path = "crates/ty_static" }
|
||||
ty_test = { path = "crates/ty_test" }
|
||||
ty_vendored = { path = "crates/ty_vendored" }
|
||||
|
||||
@@ -83,7 +84,7 @@ get-size2 = { version = "0.5.0", features = [
|
||||
"derive",
|
||||
"smallvec",
|
||||
"hashbrown",
|
||||
"compact-str"
|
||||
"compact-str",
|
||||
] }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
@@ -173,7 +174,7 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features =
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"ansi",
|
||||
"smallvec"
|
||||
"smallvec",
|
||||
] }
|
||||
tryfn = { version = "0.2.1" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
@@ -183,11 +184,7 @@ unicode-width = { version = "0.2.0" }
|
||||
unicode_names2 = { version = "1.2.2" }
|
||||
unicode-normalization = { version = "0.1.23" }
|
||||
url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
] }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.92" }
|
||||
wasm-bindgen-test = { version = "0.3.42" }
|
||||
@@ -222,8 +219,8 @@ must_use_candidate = "allow"
|
||||
similar_names = "allow"
|
||||
single_match_else = "allow"
|
||||
too_many_lines = "allow"
|
||||
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
|
||||
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
|
||||
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
|
||||
unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often.
|
||||
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
|
||||
needless_raw_string_hashes = "allow"
|
||||
# Disallowed restriction lints
|
||||
|
||||
@@ -681,7 +681,7 @@ mod tests {
|
||||
UnsafeFixes::Enabled,
|
||||
)
|
||||
.unwrap();
|
||||
if diagnostics.inner.iter().any(Diagnostic::is_syntax_error) {
|
||||
if diagnostics.inner.iter().any(Diagnostic::is_invalid_syntax) {
|
||||
parse_errors.push(path.clone());
|
||||
}
|
||||
paths.push(path);
|
||||
|
||||
@@ -9,15 +9,15 @@ use ignore::Error;
|
||||
use log::{debug, error, warn};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use rayon::prelude::*;
|
||||
use ruff_linter::message::diagnostic_from_violation;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::panic::catch_unwind;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::registry::Rule;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{LinterSettings, flags};
|
||||
use ruff_linter::{IOError, fs, warn_user_once};
|
||||
use ruff_linter::{IOError, Violation, fs, warn_user_once};
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::resolver::{
|
||||
@@ -129,11 +129,7 @@ pub(crate) fn check(
|
||||
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
|
||||
|
||||
Diagnostics::new(
|
||||
vec![diagnostic_from_violation(
|
||||
IOError { message },
|
||||
TextRange::default(),
|
||||
&dummy,
|
||||
)],
|
||||
vec![IOError { message }.into_diagnostic(TextRange::default(), &dummy)],
|
||||
FxHashMap::default(),
|
||||
)
|
||||
} else {
|
||||
@@ -166,7 +162,9 @@ pub(crate) fn check(
|
||||
|a, b| (a.0 + b.0, a.1 + b.1),
|
||||
);
|
||||
|
||||
all_diagnostics.inner.sort();
|
||||
all_diagnostics
|
||||
.inner
|
||||
.sort_by(Diagnostic::ruff_start_ordering);
|
||||
|
||||
// Store the caches.
|
||||
caches.persist()?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::packaging;
|
||||
use ruff_linter::settings::flags;
|
||||
@@ -52,6 +53,8 @@ pub(crate) fn check_stdin(
|
||||
noqa,
|
||||
fix_mode,
|
||||
)?;
|
||||
diagnostics.inner.sort_unstable();
|
||||
diagnostics
|
||||
.inner
|
||||
.sort_unstable_by(Diagnostic::ruff_start_ordering);
|
||||
Ok(diagnostics)
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ use log::{debug, warn};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_linter::codes::Rule;
|
||||
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
|
||||
use ruff_linter::message::{create_syntax_error_diagnostic, diagnostic_from_violation};
|
||||
use ruff_linter::message::create_syntax_error_diagnostic;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{LinterSettings, flags};
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::{IOError, fs};
|
||||
use ruff_linter::{IOError, Violation, fs};
|
||||
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
|
||||
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
@@ -62,13 +62,12 @@ impl Diagnostics {
|
||||
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
|
||||
let source_file = SourceFileBuilder::new(name, "").finish();
|
||||
Self::new(
|
||||
vec![diagnostic_from_violation(
|
||||
vec![
|
||||
IOError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
&source_file,
|
||||
)],
|
||||
}
|
||||
.into_diagnostic(TextRange::default(), &source_file),
|
||||
],
|
||||
FxHashMap::default(),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,7 @@ ruff_python_parser = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true, features = ["get-size"] }
|
||||
ruff_text_size = { workspace = true }
|
||||
ty_static = { workspace = true }
|
||||
|
||||
anstyle = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
|
||||
@@ -83,7 +83,7 @@ impl Diagnostic {
|
||||
///
|
||||
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
|
||||
/// message.
|
||||
pub fn syntax_error(
|
||||
pub fn invalid_syntax(
|
||||
span: impl Into<Span>,
|
||||
message: impl IntoDiagnosticMessage,
|
||||
range: impl Ranged,
|
||||
@@ -365,7 +365,7 @@ impl Diagnostic {
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is a syntax error message.
|
||||
pub fn is_syntax_error(&self) -> bool {
|
||||
pub fn is_invalid_syntax(&self) -> bool {
|
||||
self.id().is_invalid_syntax()
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ impl Diagnostic {
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
pub fn to_url(&self) -> Option<String> {
|
||||
if self.is_syntax_error() {
|
||||
if self.is_invalid_syntax() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(
|
||||
@@ -447,20 +447,16 @@ impl Diagnostic {
|
||||
pub fn expect_range(&self) -> TextRange {
|
||||
self.range().expect("Expected a range for the primary span")
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Diagnostic {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Diagnostic {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(
|
||||
(self.ruff_source_file()?, self.range()?.start())
|
||||
.cmp(&(other.ruff_source_file()?, other.range()?.start())),
|
||||
)
|
||||
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
|
||||
///
|
||||
/// Panics if either diagnostic has no primary span, if the span has no range, or if its file is
|
||||
/// not a `SourceFile`.
|
||||
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
|
||||
(self.expect_ruff_source_file(), self.expect_range().start()).cmp(&(
|
||||
other.expect_ruff_source_file(),
|
||||
other.expect_range().start(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHasher;
|
||||
use std::hash::BuildHasherDefault;
|
||||
use std::num::NonZeroUsize;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
pub mod diagnostic;
|
||||
pub mod display;
|
||||
@@ -50,8 +51,8 @@ pub trait Db: salsa::Database {
|
||||
/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or
|
||||
/// watching the files for changes.
|
||||
pub fn max_parallelism() -> NonZeroUsize {
|
||||
std::env::var("TY_MAX_PARALLELISM")
|
||||
.or_else(|_| std::env::var("RAYON_NUM_THREADS"))
|
||||
std::env::var(EnvVars::TY_MAX_PARALLELISM)
|
||||
.or_else(|_| std::env::var(EnvVars::RAYON_NUM_THREADS))
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_else(|| {
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ty = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["schemars"] }
|
||||
ty_static = { workspace = true }
|
||||
ruff = { workspace = true }
|
||||
ruff_formatter = { workspace = true }
|
||||
ruff_linter = { workspace = true, features = ["schemars"] }
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference,
|
||||
generate_ty_options, generate_ty_rules, generate_ty_schema,
|
||||
generate_ty_env_vars_reference, generate_ty_options, generate_ty_rules, generate_ty_schema,
|
||||
};
|
||||
|
||||
pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
|
||||
@@ -44,5 +44,8 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
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 })?;
|
||||
generate_ty_env_vars_reference::main(&generate_ty_env_vars_reference::Args {
|
||||
mode: args.mode,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
119
crates/ruff_dev/src/generate_ty_env_vars_reference.rs
Normal file
119
crates/ruff_dev/src/generate_ty_env_vars_reference.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! Generate the environment variables reference from `ty_static::EnvVars`.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::bail;
|
||||
use pretty_assertions::StrComparison;
|
||||
|
||||
use ty_static::EnvVars;
|
||||
|
||||
use crate::generate_all::Mode;
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub(crate) fn main(args: &Args) -> anyhow::Result<()> {
|
||||
let reference_string = generate();
|
||||
let filename = "environment.md";
|
||||
let reference_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("crates")
|
||||
.join("ty")
|
||||
.join("docs")
|
||||
.join(filename);
|
||||
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{reference_string}");
|
||||
}
|
||||
Mode::Check => match 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 `cargo dev generate-ty-env-vars-reference`:\n{comparison}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
bail!(
|
||||
"{filename} not found, please run `cargo dev generate-ty-env-vars-reference`"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!(
|
||||
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Mode::Write => {
|
||||
// Ensure the docs directory exists
|
||||
if let Some(parent) = reference_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
match fs::read_to_string(&reference_path) {
|
||||
Ok(current) => {
|
||||
if current == reference_string {
|
||||
println!("Up-to-date: {filename}");
|
||||
} else {
|
||||
println!("Updating: {filename}");
|
||||
fs::write(&reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
println!("Updating: {filename}");
|
||||
fs::write(&reference_path, reference_string.as_bytes())?;
|
||||
}
|
||||
Err(err) => {
|
||||
bail!(
|
||||
"{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str("# Environment variables\n\n");
|
||||
|
||||
// Partition and sort environment variables into TY_ and external variables.
|
||||
let (ty_vars, external_vars): (BTreeSet<_>, BTreeSet<_>) = EnvVars::metadata()
|
||||
.iter()
|
||||
.partition(|(var, _)| var.starts_with("TY_"));
|
||||
|
||||
output.push_str("ty defines and respects the following environment variables:\n\n");
|
||||
|
||||
for (var, doc) in ty_vars {
|
||||
output.push_str(&render(var, doc));
|
||||
}
|
||||
|
||||
output.push_str("## Externally-defined variables\n\n");
|
||||
output.push_str("ty also reads the following externally defined environment variables:\n\n");
|
||||
|
||||
for (var, doc) in external_vars {
|
||||
output.push_str(&render(var, doc));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Render an environment variable and its documentation.
|
||||
fn render(var: &str, doc: &str) -> String {
|
||||
format!("### `{var}`\n\n{doc}\n\n")
|
||||
}
|
||||
@@ -18,6 +18,7 @@ mod generate_json_schema;
|
||||
mod generate_options;
|
||||
mod generate_rules_table;
|
||||
mod generate_ty_cli_reference;
|
||||
mod generate_ty_env_vars_reference;
|
||||
mod generate_ty_options;
|
||||
mod generate_ty_rules;
|
||||
mod generate_ty_schema;
|
||||
@@ -53,6 +54,8 @@ enum Command {
|
||||
/// Generate a Markdown-compatible listing of configuration options.
|
||||
GenerateOptions,
|
||||
GenerateTyOptions(generate_ty_options::Args),
|
||||
/// Generate environment variables reference for ty.
|
||||
GenerateTyEnvVarsReference(generate_ty_env_vars_reference::Args),
|
||||
/// Generate CLI help.
|
||||
GenerateCliHelp(generate_cli_help::Args),
|
||||
/// Generate Markdown docs.
|
||||
@@ -98,6 +101,7 @@ fn main() -> Result<ExitCode> {
|
||||
Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?,
|
||||
Command::GenerateOptions => println!("{}", generate_options::generate()),
|
||||
Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?,
|
||||
Command::GenerateTyEnvVarsReference(args) => generate_ty_env_vars_reference::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,10 +1,10 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 23 and 41
|
||||
B017 - on lines 24, 28, 46, 49, 52, and 58
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
import pytest
|
||||
import pytest, contextlib
|
||||
|
||||
CONSTANT = True
|
||||
|
||||
28
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_1.py
vendored
Normal file
28
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_1.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 20, 21, 25, and 26
|
||||
"""
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
|
||||
def something_else() -> None:
|
||||
for i in (1, 2, 3):
|
||||
print(i)
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class Foobar(unittest.TestCase):
|
||||
def call_form_raises(self) -> None:
|
||||
self.assertRaises(Exception, something_else)
|
||||
self.assertRaises(BaseException, something_else)
|
||||
|
||||
|
||||
def test_pytest_call_form() -> None:
|
||||
pytest.raises(Exception, something_else)
|
||||
pytest.raises(BaseException, something_else)
|
||||
|
||||
pytest.raises(Exception, something_else, match="hello")
|
||||
@@ -7,7 +7,9 @@ use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_optional_as_none_in_union_enabled;
|
||||
use crate::preview::{
|
||||
is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled,
|
||||
};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
|
||||
@@ -1236,6 +1238,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
|
||||
ruff::rules::non_octal_permissions(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::AssertRaisesException)
|
||||
&& is_assert_raises_exception_call_enabled(checker.settings())
|
||||
{
|
||||
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_rule_enabled(&[
|
||||
|
||||
@@ -64,7 +64,6 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use crate::checkers::ast::annotation::AnnotationContext;
|
||||
use crate::docstrings::extraction::ExtractionTarget;
|
||||
use crate::importer::{ImportRequest, Importer, ResolutionError};
|
||||
use crate::message::diagnostic_from_violation;
|
||||
use crate::noqa::NoqaMapping;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::is_undefined_export_in_dunder_init_enabled;
|
||||
@@ -671,7 +670,11 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
| SemanticSyntaxErrorKind::InvalidStarExpression
|
||||
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
||||
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
|
||||
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
|
||||
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
@@ -3158,7 +3161,7 @@ impl<'a> LintContext<'a> {
|
||||
) -> DiagnosticGuard<'chk, 'a> {
|
||||
DiagnosticGuard {
|
||||
context: self,
|
||||
diagnostic: Some(diagnostic_from_violation(kind, range, &self.source_file)),
|
||||
diagnostic: Some(kind.into_diagnostic(range, &self.source_file)),
|
||||
rule: T::rule(),
|
||||
}
|
||||
}
|
||||
@@ -3177,7 +3180,7 @@ impl<'a> LintContext<'a> {
|
||||
if self.is_rule_enabled(rule) {
|
||||
Some(DiagnosticGuard {
|
||||
context: self,
|
||||
diagnostic: Some(diagnostic_from_violation(kind, range, &self.source_file)),
|
||||
diagnostic: Some(kind.into_diagnostic(range, &self.source_file)),
|
||||
rule,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -618,8 +618,7 @@ mod tests {
|
||||
use crate::fix::edits::{
|
||||
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
|
||||
};
|
||||
use crate::message::diagnostic_from_violation;
|
||||
use crate::{Edit, Fix, Locator};
|
||||
use crate::{Edit, Fix, Locator, Violation};
|
||||
|
||||
/// Parse the given source using [`Mode::Module`] and return the first statement.
|
||||
fn parse_first_stmt(source: &str) -> Result<Stmt> {
|
||||
@@ -750,8 +749,8 @@ x = 1 \
|
||||
let diag = {
|
||||
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
|
||||
let mut iter = edits.into_iter();
|
||||
let mut diagnostic = diagnostic_from_violation(
|
||||
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
|
||||
// The choice of rule here is arbitrary.
|
||||
let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic(
|
||||
TextRange::default(),
|
||||
&SourceFileBuilder::new("<filename>", "<code>").finish(),
|
||||
);
|
||||
|
||||
@@ -172,11 +172,10 @@ mod tests {
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::fix::{FixResult, apply_fixes};
|
||||
use crate::message::diagnostic_from_violation;
|
||||
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
|
||||
use crate::{Edit, Fix};
|
||||
use crate::{Locator, Violation};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
|
||||
fn create_diagnostics(
|
||||
@@ -187,8 +186,7 @@ mod tests {
|
||||
edit.into_iter()
|
||||
.map(|edit| {
|
||||
// The choice of rule here is arbitrary.
|
||||
let mut diagnostic = diagnostic_from_violation(
|
||||
MissingNewlineAtEndOfFile,
|
||||
let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic(
|
||||
edit.range(),
|
||||
&SourceFileBuilder::new(filename, source).finish(),
|
||||
);
|
||||
|
||||
@@ -514,7 +514,7 @@ pub fn lint_only(
|
||||
|
||||
LinterResult {
|
||||
has_valid_syntax: parsed.has_valid_syntax(),
|
||||
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_syntax_error),
|
||||
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_invalid_syntax),
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
@@ -629,7 +629,7 @@ pub fn lint_fix<'a>(
|
||||
|
||||
if iterations == 0 {
|
||||
has_valid_syntax = parsed.has_valid_syntax();
|
||||
has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_syntax_error);
|
||||
has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_invalid_syntax);
|
||||
} else {
|
||||
// If the source code had no syntax errors on the first pass, but
|
||||
// does on a subsequent pass, then we've introduced a
|
||||
|
||||
@@ -24,7 +24,6 @@ pub use sarif::SarifEmitter;
|
||||
pub use text::TextEmitter;
|
||||
|
||||
use crate::Fix;
|
||||
use crate::Violation;
|
||||
use crate::registry::Rule;
|
||||
|
||||
mod azure;
|
||||
@@ -108,28 +107,6 @@ where
|
||||
diagnostic
|
||||
}
|
||||
|
||||
// TODO(brent) We temporarily allow this to avoid updating all of the call sites to add
|
||||
// references. I expect this method to go away or change significantly with the rest of the
|
||||
// diagnostic refactor, but if it still exists in this form at the end of the refactor, we
|
||||
// should just update the call sites.
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
pub fn diagnostic_from_violation<T: Violation>(
|
||||
kind: T,
|
||||
range: TextRange,
|
||||
file: &SourceFile,
|
||||
) -> Diagnostic {
|
||||
create_lint_diagnostic(
|
||||
Violation::message(&kind),
|
||||
Violation::fix_title(&kind),
|
||||
range,
|
||||
None,
|
||||
None,
|
||||
file.clone(),
|
||||
None,
|
||||
T::rule(),
|
||||
)
|
||||
}
|
||||
|
||||
struct MessageWithLocation<'a> {
|
||||
message: &'a Diagnostic,
|
||||
start_location: LineColumn,
|
||||
|
||||
@@ -1225,8 +1225,6 @@ mod tests {
|
||||
use ruff_source_file::{LineEnding, SourceFileBuilder};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::Edit;
|
||||
use crate::message::diagnostic_from_violation;
|
||||
use crate::noqa::{
|
||||
Directive, LexicalError, NoqaLexerOutput, NoqaMapping, add_noqa_inner, lex_codes,
|
||||
lex_file_exemption, lex_inline_noqa,
|
||||
@@ -1234,6 +1232,7 @@ mod tests {
|
||||
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
|
||||
use crate::rules::pyflakes::rules::UnusedVariable;
|
||||
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
|
||||
use crate::{Edit, Violation};
|
||||
use crate::{Locator, generate_noqa_edits};
|
||||
|
||||
fn assert_lexed_ranges_match_slices(
|
||||
@@ -2832,10 +2831,10 @@ mod tests {
|
||||
assert_eq!(output, format!("{contents}"));
|
||||
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
|
||||
let messages = [diagnostic_from_violation(
|
||||
UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
},
|
||||
let messages = [UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
}
|
||||
.into_diagnostic(
|
||||
TextRange::new(TextSize::from(0), TextSize::from(0)),
|
||||
&source_file,
|
||||
)];
|
||||
@@ -2856,15 +2855,14 @@ mod tests {
|
||||
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
|
||||
let messages = [
|
||||
diagnostic_from_violation(
|
||||
AmbiguousVariableName("x".to_string()),
|
||||
AmbiguousVariableName("x".to_string()).into_diagnostic(
|
||||
TextRange::new(TextSize::from(0), TextSize::from(0)),
|
||||
&source_file,
|
||||
),
|
||||
diagnostic_from_violation(
|
||||
UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
},
|
||||
UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
}
|
||||
.into_diagnostic(
|
||||
TextRange::new(TextSize::from(0), TextSize::from(0)),
|
||||
&source_file,
|
||||
),
|
||||
@@ -2887,15 +2885,14 @@ mod tests {
|
||||
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
|
||||
let messages = [
|
||||
diagnostic_from_violation(
|
||||
AmbiguousVariableName("x".to_string()),
|
||||
AmbiguousVariableName("x".to_string()).into_diagnostic(
|
||||
TextRange::new(TextSize::from(0), TextSize::from(0)),
|
||||
&source_file,
|
||||
),
|
||||
diagnostic_from_violation(
|
||||
UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
},
|
||||
UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
}
|
||||
.into_diagnostic(
|
||||
TextRange::new(TextSize::from(0), TextSize::from(0)),
|
||||
&source_file,
|
||||
),
|
||||
@@ -2931,11 +2928,8 @@ print(
|
||||
"#;
|
||||
let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect();
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish();
|
||||
let messages = [diagnostic_from_violation(
|
||||
PrintfStringFormatting,
|
||||
TextRange::new(12.into(), 79.into()),
|
||||
&source_file,
|
||||
)];
|
||||
let messages = [PrintfStringFormatting
|
||||
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
|
||||
let comment_ranges = CommentRanges::default();
|
||||
let edits = generate_noqa_edits(
|
||||
path,
|
||||
@@ -2964,11 +2958,8 @@ foo;
|
||||
bar =
|
||||
";
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish();
|
||||
let messages = [diagnostic_from_violation(
|
||||
UselessSemicolon,
|
||||
TextRange::new(4.into(), 5.into()),
|
||||
&source_file,
|
||||
)];
|
||||
let messages =
|
||||
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
|
||||
let noqa_line_for = NoqaMapping::default();
|
||||
let comment_ranges = CommentRanges::default();
|
||||
let edits = generate_noqa_edits(
|
||||
|
||||
@@ -125,3 +125,8 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19063
|
||||
pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::SourceFile;
|
||||
|
||||
use crate::IOError;
|
||||
use crate::message::diagnostic_from_violation;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::ruff::rules::InvalidPyprojectToml;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{IOError, Violation};
|
||||
|
||||
/// RUF200
|
||||
pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) -> Vec<Diagnostic> {
|
||||
@@ -30,11 +29,8 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings)
|
||||
source_file.name(),
|
||||
);
|
||||
if settings.rules.enabled(Rule::IOError) {
|
||||
let diagnostic = diagnostic_from_violation(
|
||||
IOError { message },
|
||||
TextRange::default(),
|
||||
source_file,
|
||||
);
|
||||
let diagnostic =
|
||||
IOError { message }.into_diagnostic(TextRange::default(), source_file);
|
||||
messages.push(diagnostic);
|
||||
} else {
|
||||
warn!(
|
||||
@@ -56,11 +52,8 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings)
|
||||
|
||||
if settings.rules.enabled(Rule::InvalidPyprojectToml) {
|
||||
let toml_err = err.message().to_string();
|
||||
let diagnostic = diagnostic_from_violation(
|
||||
InvalidPyprojectToml { message: toml_err },
|
||||
range,
|
||||
source_file,
|
||||
);
|
||||
let diagnostic =
|
||||
InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file);
|
||||
messages.push(diagnostic);
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ impl Violation for SuspiciousXmlrpcImport {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import wsgiref.handlers.CGIHandler
|
||||
/// from wsgiref.handlers import CGIHandler
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -16,11 +16,14 @@ mod tests {
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
|
||||
#[test_case(Rule::AssertFalse, Path::new("B011.py"))]
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017.py"))]
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
|
||||
#[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))]
|
||||
#[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))]
|
||||
#[test_case(Rule::ClassAsDataStructure, Path::new("class_as_data_structure.py"))]
|
||||
@@ -174,4 +177,23 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
|
||||
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Expr, WithItem};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, WithItem};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
@@ -56,6 +56,48 @@ impl fmt::Display for ExceptionKind {
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_blind_exception(
|
||||
semantic: &ruff_python_semantic::SemanticModel<'_>,
|
||||
func: &Expr,
|
||||
arguments: &Arguments,
|
||||
) -> Option<ExceptionKind> {
|
||||
let is_assert_raises = matches!(
|
||||
func,
|
||||
&Expr::Attribute(ast::ExprAttribute { ref attr, .. }) if attr.as_str() == "assertRaises"
|
||||
);
|
||||
|
||||
let is_pytest_raises = semantic
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"]));
|
||||
|
||||
if !(is_assert_raises || is_pytest_raises) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_pytest_raises {
|
||||
if arguments.find_keyword("match").is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if arguments
|
||||
.find_positional(1)
|
||||
.is_some_and(|arg| matches!(arg, Expr::StringLiteral(_) | Expr::BytesLiteral(_)))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let first_arg = arguments.args.first()?;
|
||||
|
||||
let builtin_symbol = semantic.resolve_builtin_symbol(first_arg)?;
|
||||
|
||||
match builtin_symbol {
|
||||
"Exception" => Some(ExceptionKind::Exception),
|
||||
"BaseException" => Some(ExceptionKind::BaseException),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// B017
|
||||
pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) {
|
||||
for item in items {
|
||||
@@ -73,33 +115,31 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let [arg] = &*arguments.args else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let semantic = checker.semantic();
|
||||
|
||||
let Some(builtin_symbol) = semantic.resolve_builtin_symbol(arg) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let exception = match builtin_symbol {
|
||||
"Exception" => ExceptionKind::Exception,
|
||||
"BaseException" => ExceptionKind::BaseException,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if !(matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
|
||||
|| semantic
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["pytest", "raises"])
|
||||
})
|
||||
&& arguments.find_keyword("match").is_none())
|
||||
if let Some(exception) =
|
||||
detect_blind_exception(checker.semantic(), func.as_ref(), arguments)
|
||||
{
|
||||
continue;
|
||||
checker.report_diagnostic(AssertRaisesException { exception }, item.range());
|
||||
}
|
||||
|
||||
checker.report_diagnostic(AssertRaisesException { exception }, item.range());
|
||||
}
|
||||
}
|
||||
|
||||
/// B017 (call form)
|
||||
pub(crate) fn assert_raises_exception_call(
|
||||
checker: &Checker,
|
||||
ast::ExprCall {
|
||||
func,
|
||||
arguments,
|
||||
range,
|
||||
node_index: _,
|
||||
}: &ast::ExprCall,
|
||||
) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if arguments.args.len() < 2 && arguments.find_argument("func", 1).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(exception) = detect_blind_exception(semantic, func.as_ref(), arguments) {
|
||||
checker.report_diagnostic(AssertRaisesException { exception }, *range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B017.py:23:14: B017 Do not assert blind exception: `Exception`
|
||||
B017_0.py:23:14: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
21 | class Foobar(unittest.TestCase):
|
||||
22 | def evil_raises(self) -> None:
|
||||
@@ -10,7 +10,7 @@ B017.py:23:14: B017 Do not assert blind exception: `Exception`
|
||||
24 | raise Exception("Evil I say!")
|
||||
|
|
||||
|
||||
B017.py:27:14: B017 Do not assert blind exception: `BaseException`
|
||||
B017_0.py:27:14: B017 Do not assert blind exception: `BaseException`
|
||||
|
|
||||
26 | def also_evil_raises(self) -> None:
|
||||
27 | with self.assertRaises(BaseException):
|
||||
@@ -18,7 +18,7 @@ B017.py:27:14: B017 Do not assert blind exception: `BaseException`
|
||||
28 | raise Exception("Evil I say!")
|
||||
|
|
||||
|
||||
B017.py:45:10: B017 Do not assert blind exception: `Exception`
|
||||
B017_0.py:45:10: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
44 | def test_pytest_raises():
|
||||
45 | with pytest.raises(Exception):
|
||||
@@ -26,7 +26,7 @@ B017.py:45:10: B017 Do not assert blind exception: `Exception`
|
||||
46 | raise ValueError("Hello")
|
||||
|
|
||||
|
||||
B017.py:48:10: B017 Do not assert blind exception: `Exception`
|
||||
B017_0.py:48:10: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
46 | raise ValueError("Hello")
|
||||
47 |
|
||||
@@ -35,7 +35,7 @@ B017.py:48:10: B017 Do not assert blind exception: `Exception`
|
||||
49 | raise ValueError("Hello")
|
||||
|
|
||||
|
||||
B017.py:57:36: B017 Do not assert blind exception: `Exception`
|
||||
B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
55 | raise ValueError("This is also fine")
|
||||
56 |
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B017_0.py:23:14: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
21 | class Foobar(unittest.TestCase):
|
||||
22 | def evil_raises(self) -> None:
|
||||
23 | with self.assertRaises(Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
24 | raise Exception("Evil I say!")
|
||||
|
|
||||
|
||||
B017_0.py:27:14: B017 Do not assert blind exception: `BaseException`
|
||||
|
|
||||
26 | def also_evil_raises(self) -> None:
|
||||
27 | with self.assertRaises(BaseException):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
28 | raise Exception("Evil I say!")
|
||||
|
|
||||
|
||||
B017_0.py:45:10: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
44 | def test_pytest_raises():
|
||||
45 | with pytest.raises(Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
46 | raise ValueError("Hello")
|
||||
|
|
||||
|
||||
B017_0.py:48:10: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
46 | raise ValueError("Hello")
|
||||
47 |
|
||||
48 | with pytest.raises(Exception), pytest.raises(ValueError):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
49 | raise ValueError("Hello")
|
||||
|
|
||||
|
||||
B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
55 | raise ValueError("This is also fine")
|
||||
56 |
|
||||
57 | with contextlib.nullcontext(), pytest.raises(Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
58 | raise ValueError("Multiple context managers")
|
||||
|
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B017_1.py:20:9: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
18 | class Foobar(unittest.TestCase):
|
||||
19 | def call_form_raises(self) -> None:
|
||||
20 | self.assertRaises(Exception, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
21 | self.assertRaises(BaseException, something_else)
|
||||
|
|
||||
|
||||
B017_1.py:21:9: B017 Do not assert blind exception: `BaseException`
|
||||
|
|
||||
19 | def call_form_raises(self) -> None:
|
||||
20 | self.assertRaises(Exception, something_else)
|
||||
21 | self.assertRaises(BaseException, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
|
|
||||
|
||||
B017_1.py:25:5: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
24 | def test_pytest_call_form() -> None:
|
||||
25 | pytest.raises(Exception, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
26 | pytest.raises(BaseException, something_else)
|
||||
|
|
||||
|
||||
B017_1.py:26:5: B017 Do not assert blind exception: `BaseException`
|
||||
|
|
||||
24 | def test_pytest_call_form() -> None:
|
||||
25 | pytest.raises(Exception, something_else)
|
||||
26 | pytest.raises(BaseException, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
27 |
|
||||
28 | pytest.raises(Exception, something_else, match="hello")
|
||||
|
|
||||
@@ -60,12 +60,14 @@ impl Violation for IndentationWithInvalidMultiple {
|
||||
/// ```python
|
||||
/// if True:
|
||||
/// # a = 1
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// if True:
|
||||
/// # a = 1
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
|
||||
@@ -43,12 +43,12 @@ impl AlwaysFixableViolation for MultipleSpacesAfterKeyword {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// True and False
|
||||
/// x and y
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// True and False
|
||||
/// x and y
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MultipleSpacesBeforeKeyword;
|
||||
|
||||
@@ -238,6 +238,9 @@ impl Violation for DocstringExtraneousYields {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class FasterThanLightError(ArithmeticError): ...
|
||||
///
|
||||
///
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
/// """Calculate speed as distance divided by time.
|
||||
///
|
||||
@@ -256,6 +259,9 @@ impl Violation for DocstringExtraneousYields {
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class FasterThanLightError(ArithmeticError): ...
|
||||
///
|
||||
///
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
/// """Calculate speed as distance divided by time.
|
||||
///
|
||||
|
||||
@@ -774,7 +774,7 @@ mod tests {
|
||||
messages.sort_by_key(|diagnostic| diagnostic.expect_range().start());
|
||||
let actual = messages
|
||||
.iter()
|
||||
.filter(|msg| !msg.is_syntax_error())
|
||||
.filter(|msg| !msg.is_invalid_syntax())
|
||||
.map(Diagnostic::name)
|
||||
.collect::<Vec<_>>();
|
||||
let expected: Vec<_> = expected.iter().map(|rule| rule.name().as_str()).collect();
|
||||
|
||||
@@ -10,11 +10,11 @@ use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for access to the first or last element of `str.split()` without
|
||||
/// Checks for access to the first or last element of `str.split()` or `str.rsplit()` without
|
||||
/// `maxsplit=1`
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling `str.split()` without `maxsplit` set splits on every delimiter in the
|
||||
/// Calling `str.split()` or `str.rsplit()` without passing `maxsplit=1` splits on every delimiter in the
|
||||
/// string. When accessing only the first or last element of the result, it
|
||||
/// would be more efficient to only split once.
|
||||
///
|
||||
@@ -29,14 +29,44 @@ use crate::checkers::ast::Checker;
|
||||
/// url = "www.example.com"
|
||||
/// prefix = url.split(".", maxsplit=1)[0]
|
||||
/// ```
|
||||
///
|
||||
/// To access the last element, use `str.rsplit()` instead of `str.split()`:
|
||||
/// ```python
|
||||
/// url = "www.example.com"
|
||||
/// suffix = url.rsplit(".", maxsplit=1)[-1]
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MissingMaxsplitArg;
|
||||
pub(crate) struct MissingMaxsplitArg {
|
||||
index: SliceBoundary,
|
||||
actual_split_type: String,
|
||||
}
|
||||
|
||||
/// Represents the index of the slice used for this rule (which can only be 0 or -1)
|
||||
enum SliceBoundary {
|
||||
First,
|
||||
Last,
|
||||
}
|
||||
|
||||
impl Violation for MissingMaxsplitArg {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Accessing only the first or last element of `str.split()` without setting `maxsplit=1`"
|
||||
.to_string()
|
||||
let MissingMaxsplitArg {
|
||||
index,
|
||||
actual_split_type,
|
||||
} = self;
|
||||
|
||||
let suggested_split_type = match index {
|
||||
SliceBoundary::First => "split",
|
||||
SliceBoundary::Last => "rsplit",
|
||||
};
|
||||
|
||||
if actual_split_type == suggested_split_type {
|
||||
format!("Pass `maxsplit=1` into `str.{actual_split_type}()`")
|
||||
} else {
|
||||
format!(
|
||||
"Instead of `str.{actual_split_type}()`, call `str.{suggested_split_type}()` and pass `maxsplit=1`",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +112,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if !matches!(index, Some(0 | -1)) {
|
||||
return;
|
||||
}
|
||||
let slice_boundary = match index {
|
||||
Some(0) => SliceBoundary::First,
|
||||
Some(-1) => SliceBoundary::Last,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else {
|
||||
return;
|
||||
@@ -129,5 +161,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
|
||||
}
|
||||
}
|
||||
|
||||
checker.report_diagnostic(MissingMaxsplitArg, expr.range());
|
||||
checker.report_diagnostic(
|
||||
MissingMaxsplitArg {
|
||||
index: slice_boundary,
|
||||
actual_split_type: attr.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
12 | # Errors
|
||||
13 | ## Test split called directly on string literal
|
||||
@@ -11,7 +11,7 @@ missing_maxsplit_arg.py:14:1: PLC0207 Accessing only the first or last element o
|
||||
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
13 | ## Test split called directly on string literal
|
||||
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -21,7 +21,7 @@ missing_maxsplit_arg.py:15:1: PLC0207 Accessing only the first or last element o
|
||||
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
|
||||
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
|
||||
@@ -30,7 +30,7 @@ missing_maxsplit_arg.py:16:1: PLC0207 Accessing only the first or last element o
|
||||
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
||||
|
|
||||
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
|
||||
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -40,7 +40,7 @@ missing_maxsplit_arg.py:17:1: PLC0207 Accessing only the first or last element o
|
||||
19 | ## Test split called on string variable
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
19 | ## Test split called on string variable
|
||||
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -49,7 +49,7 @@ missing_maxsplit_arg.py:20:1: PLC0207 Accessing only the first or last element o
|
||||
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
19 | ## Test split called on string variable
|
||||
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -59,7 +59,7 @@ missing_maxsplit_arg.py:21:1: PLC0207 Accessing only the first or last element o
|
||||
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
|
||||
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
|
||||
@@ -68,7 +68,7 @@ missing_maxsplit_arg.py:22:1: PLC0207 Accessing only the first or last element o
|
||||
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
||||
|
|
||||
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
|
||||
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -78,7 +78,7 @@ missing_maxsplit_arg.py:23:1: PLC0207 Accessing only the first or last element o
|
||||
25 | ## Test split called on class attribute
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
25 | ## Test split called on class attribute
|
||||
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -87,7 +87,7 @@ missing_maxsplit_arg.py:26:1: PLC0207 Accessing only the first or last element o
|
||||
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
25 | ## Test split called on class attribute
|
||||
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -97,7 +97,7 @@ missing_maxsplit_arg.py:27:1: PLC0207 Accessing only the first or last element o
|
||||
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
|
||||
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
|
||||
@@ -106,7 +106,7 @@ missing_maxsplit_arg.py:28:1: PLC0207 Accessing only the first or last element o
|
||||
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
||||
|
|
||||
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
|
||||
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -116,7 +116,7 @@ missing_maxsplit_arg.py:29:1: PLC0207 Accessing only the first or last element o
|
||||
31 | ## Test split called on sliced string
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
31 | ## Test split called on sliced string
|
||||
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -125,7 +125,7 @@ missing_maxsplit_arg.py:32:1: PLC0207 Accessing only the first or last element o
|
||||
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
31 | ## Test split called on sliced string
|
||||
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -135,7 +135,7 @@ missing_maxsplit_arg.py:33:1: PLC0207 Accessing only the first or last element o
|
||||
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
|
||||
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -145,7 +145,7 @@ missing_maxsplit_arg.py:34:1: PLC0207 Accessing only the first or last element o
|
||||
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
|
||||
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -155,7 +155,7 @@ missing_maxsplit_arg.py:35:1: PLC0207 Accessing only the first or last element o
|
||||
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
||||
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
||||
@@ -165,7 +165,7 @@ missing_maxsplit_arg.py:36:1: PLC0207 Accessing only the first or last element o
|
||||
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
||||
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -174,7 +174,7 @@ missing_maxsplit_arg.py:37:1: PLC0207 Accessing only the first or last element o
|
||||
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
||||
|
|
||||
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -184,7 +184,7 @@ missing_maxsplit_arg.py:38:1: PLC0207 Accessing only the first or last element o
|
||||
40 | ## Test sep given as named argument
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
40 | ## Test sep given as named argument
|
||||
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
|
||||
@@ -193,7 +193,7 @@ missing_maxsplit_arg.py:41:1: PLC0207 Accessing only the first or last element o
|
||||
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
40 | ## Test sep given as named argument
|
||||
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
|
||||
@@ -203,7 +203,7 @@ missing_maxsplit_arg.py:42:1: PLC0207 Accessing only the first or last element o
|
||||
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
|
||||
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
|
||||
@@ -212,7 +212,7 @@ missing_maxsplit_arg.py:43:1: PLC0207 Accessing only the first or last element o
|
||||
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
||||
|
|
||||
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
|
||||
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
|
||||
@@ -222,7 +222,7 @@ missing_maxsplit_arg.py:44:1: PLC0207 Accessing only the first or last element o
|
||||
46 | ## Special cases
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
46 | ## Special cases
|
||||
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
|
||||
@@ -231,7 +231,7 @@ missing_maxsplit_arg.py:47:1: PLC0207 Accessing only the first or last element o
|
||||
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
46 | ## Special cases
|
||||
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
|
||||
@@ -240,7 +240,7 @@ missing_maxsplit_arg.py:48:1: PLC0207 Accessing only the first or last element o
|
||||
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
|
||||
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
|
||||
@@ -250,7 +250,7 @@ missing_maxsplit_arg.py:49:1: PLC0207 Accessing only the first or last element o
|
||||
51 | ## Test class attribute named split
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
51 | ## Test class attribute named split
|
||||
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -259,7 +259,7 @@ missing_maxsplit_arg.py:52:1: PLC0207 Accessing only the first or last element o
|
||||
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
||||
|
|
||||
51 | ## Test class attribute named split
|
||||
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -269,7 +269,7 @@ missing_maxsplit_arg.py:53:1: PLC0207 Accessing only the first or last element o
|
||||
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
||||
|
|
||||
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
|
||||
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
|
||||
@@ -278,7 +278,7 @@ missing_maxsplit_arg.py:54:1: PLC0207 Accessing only the first or last element o
|
||||
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
||||
|
|
||||
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
|
||||
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
|
||||
@@ -288,14 +288,14 @@ missing_maxsplit_arg.py:55:1: PLC0207 Accessing only the first or last element o
|
||||
57 | ## Test unpacked dict literal kwargs
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:58:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:58:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
57 | ## Test unpacked dict literal kwargs
|
||||
58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
177 | # Errors
|
||||
178 | kwargs_without_maxsplit = {"seq": ","}
|
||||
@@ -305,7 +305,7 @@ missing_maxsplit_arg.py:179:1: PLC0207 Accessing only the first or last element
|
||||
181 | kwargs_with_maxsplit = {"maxsplit": 1}
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
180 | # OK
|
||||
181 | kwargs_with_maxsplit = {"maxsplit": 1}
|
||||
@@ -315,7 +315,7 @@ missing_maxsplit_arg.py:182:1: PLC0207 Accessing only the first or last element
|
||||
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
|
||||
|
|
||||
|
||||
missing_maxsplit_arg.py:184:1: PLC0207 Accessing only the first or last element of `str.split()` without setting `maxsplit=1`
|
||||
missing_maxsplit_arg.py:184:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
||||
|
|
||||
182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
|
||||
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
|
||||
|
||||
@@ -292,7 +292,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
|
||||
.chain(parsed.errors().iter().map(|parse_error| {
|
||||
create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error)
|
||||
}))
|
||||
.sorted()
|
||||
.sorted_by(Diagnostic::ruff_start_ordering)
|
||||
.collect();
|
||||
(messages, transformed)
|
||||
}
|
||||
@@ -317,7 +317,7 @@ fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind)
|
||||
|
||||
/// Print the lint diagnostics in `diagnostics`.
|
||||
fn print_diagnostics(mut diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceKind) -> String {
|
||||
diagnostics.retain(|msg| !msg.is_syntax_error());
|
||||
diagnostics.retain(|msg| !msg.is_invalid_syntax());
|
||||
|
||||
if let Some(notebook) = source.as_ipy_notebook() {
|
||||
print_jupyter_messages(&diagnostics, path, notebook)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use crate::codes::Rule;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::SourceFile;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::{codes::Rule, message::create_lint_diagnostic};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum FixAvailability {
|
||||
@@ -28,7 +32,7 @@ pub trait ViolationMetadata {
|
||||
fn explain() -> Option<&'static str>;
|
||||
}
|
||||
|
||||
pub trait Violation: ViolationMetadata {
|
||||
pub trait Violation: ViolationMetadata + Sized {
|
||||
/// `None` in the case a fix is never available or otherwise Some
|
||||
/// [`FixAvailability`] describing the available fix.
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
@@ -48,6 +52,20 @@ pub trait Violation: ViolationMetadata {
|
||||
|
||||
/// Returns the format strings used by [`message`](Violation::message).
|
||||
fn message_formats() -> &'static [&'static str];
|
||||
|
||||
/// Convert the violation into a [`Diagnostic`].
|
||||
fn into_diagnostic(self, range: TextRange, file: &SourceFile) -> Diagnostic {
|
||||
create_lint_diagnostic(
|
||||
self.message(),
|
||||
self.fix_title(),
|
||||
range,
|
||||
None,
|
||||
None,
|
||||
file.clone(),
|
||||
None,
|
||||
Self::rule(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait exists just to make implementing the [`Violation`] trait more
|
||||
|
||||
95
crates/ruff_macros/src/env_vars.rs
Normal file
95
crates/ruff_macros/src/env_vars.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ImplItem, ItemImpl};
|
||||
|
||||
pub(crate) fn attribute_env_vars_metadata(mut input: ItemImpl) -> TokenStream {
|
||||
// Verify that this is an impl for EnvVars
|
||||
let impl_type = &input.self_ty;
|
||||
|
||||
let mut env_var_entries = Vec::new();
|
||||
let mut hidden_vars = Vec::new();
|
||||
|
||||
// Process each item in the impl block
|
||||
for item in &mut input.items {
|
||||
if let ImplItem::Const(const_item) = item {
|
||||
// Extract the const name and value
|
||||
let const_name = &const_item.ident;
|
||||
let const_expr = &const_item.expr;
|
||||
|
||||
// Check if the const has the #[attr_hidden] attribute
|
||||
let is_hidden = const_item
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("attr_hidden"));
|
||||
|
||||
// Remove our custom attributes
|
||||
const_item.attrs.retain(|attr| {
|
||||
!attr.path().is_ident("attr_hidden")
|
||||
&& !attr.path().is_ident("attr_env_var_pattern")
|
||||
});
|
||||
|
||||
if is_hidden {
|
||||
hidden_vars.push(const_name.clone());
|
||||
} else {
|
||||
// Extract documentation from doc comments
|
||||
let doc_attrs: Vec<_> = const_item
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("doc"))
|
||||
.collect();
|
||||
|
||||
if !doc_attrs.is_empty() {
|
||||
// Convert doc attributes to a single string
|
||||
let doc_string = extract_doc_string(&doc_attrs);
|
||||
env_var_entries.push((const_name.clone(), const_expr.clone(), doc_string));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the metadata method.
|
||||
let metadata_entries: Vec<_> = env_var_entries
|
||||
.iter()
|
||||
.map(|(_name, expr, doc)| {
|
||||
quote! {
|
||||
(#expr, #doc)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let metadata_impl = quote! {
|
||||
impl #impl_type {
|
||||
/// Returns metadata for all non-hidden environment variables.
|
||||
pub fn metadata() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
#(#metadata_entries),*
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#input
|
||||
#metadata_impl
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract documentation from doc attributes into a single string
|
||||
fn extract_doc_string(attrs: &[&syn::Attribute]) -> String {
|
||||
attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
if let syn::Meta::NameValue(meta) = &attr.meta {
|
||||
if let syn::Expr::Lit(syn::ExprLit {
|
||||
lit: syn::Lit::Str(lit_str),
|
||||
..
|
||||
}) = &meta.value
|
||||
{
|
||||
return Some(lit_str.value().trim().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! This crate implements internal macros for the `ruff` library.
|
||||
//! This crate implements internal macros for the `ruff` and `ty` libraries.
|
||||
|
||||
use crate::cache_key::derive_cache_key;
|
||||
use crate::newtype_index::generate_newtype_index;
|
||||
@@ -11,6 +11,7 @@ mod combine;
|
||||
mod combine_options;
|
||||
mod config;
|
||||
mod derive_message_formats;
|
||||
mod env_vars;
|
||||
mod kebab_case;
|
||||
mod map_codes;
|
||||
mod newtype_index;
|
||||
@@ -144,3 +145,15 @@ pub fn newtype_index(_metadata: TokenStream, input: TokenStream) -> TokenStream
|
||||
|
||||
TokenStream::from(output)
|
||||
}
|
||||
|
||||
/// Generates metadata for environment variables declared in the impl block.
|
||||
///
|
||||
/// This attribute macro should be applied to an `impl EnvVars` block.
|
||||
/// It will generate a `metadata()` method that returns all non-hidden
|
||||
/// environment variables with their documentation.
|
||||
#[proc_macro_attribute]
|
||||
pub fn attribute_env_vars_metadata(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as syn::ItemImpl);
|
||||
|
||||
env_vars::attribute_env_vars_metadata(input).into()
|
||||
}
|
||||
|
||||
@@ -952,6 +952,9 @@ impl Display for SemanticSyntaxError {
|
||||
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
|
||||
write!(f, "name `{name}` is used prior to global declaration")
|
||||
}
|
||||
SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { name, start: _ } => {
|
||||
write!(f, "name `{name}` is used prior to nonlocal declaration")
|
||||
}
|
||||
SemanticSyntaxErrorKind::InvalidStarExpression => {
|
||||
f.write_str("Starred expression cannot be used here")
|
||||
}
|
||||
@@ -977,6 +980,15 @@ impl Display for SemanticSyntaxError {
|
||||
SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
write!(f, "nonlocal declaration not allowed at module level")
|
||||
}
|
||||
SemanticSyntaxErrorKind::NonlocalAndGlobal(name) => {
|
||||
write!(f, "name `{name}` is nonlocal and global")
|
||||
}
|
||||
SemanticSyntaxErrorKind::AnnotatedGlobal(name) => {
|
||||
write!(f, "annotated name `{name}` can't be global")
|
||||
}
|
||||
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
|
||||
write!(f, "annotated name `{name}` can't be nonlocal")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1207,6 +1219,24 @@ pub enum SemanticSyntaxErrorKind {
|
||||
/// [#111123]: https://github.com/python/cpython/issues/111123
|
||||
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
|
||||
|
||||
/// Represents the use of a `nonlocal` variable before its `nonlocal` declaration.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// counter = 0
|
||||
/// def increment():
|
||||
/// print(f"Adding 1 to {counter}")
|
||||
/// nonlocal counter # SyntaxError: name 'counter' is used prior to nonlocal declaration
|
||||
/// counter += 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Known Issues
|
||||
///
|
||||
/// See [`LoadBeforeGlobalDeclaration`][Self::LoadBeforeGlobalDeclaration].
|
||||
LoadBeforeNonlocalDeclaration { name: String, start: TextSize },
|
||||
|
||||
/// Represents the use of a starred expression in an invalid location, such as a `return` or
|
||||
/// `yield` statement.
|
||||
///
|
||||
@@ -1307,6 +1337,15 @@ pub enum SemanticSyntaxErrorKind {
|
||||
|
||||
/// Represents a nonlocal declaration at module level
|
||||
NonlocalDeclarationAtModuleLevel,
|
||||
|
||||
/// Represents the same variable declared as both nonlocal and global
|
||||
NonlocalAndGlobal(String),
|
||||
|
||||
/// Represents a type annotation on a variable that's been declared global
|
||||
AnnotatedGlobal(String),
|
||||
|
||||
/// Represents a type annotation on a variable that's been declared nonlocal
|
||||
AnnotatedNonlocal(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
|
||||
@@ -163,7 +163,7 @@ pub(crate) fn check(
|
||||
.into_iter()
|
||||
.zip(noqa_edits)
|
||||
.filter_map(|(message, noqa_edit)| {
|
||||
if message.is_syntax_error() && !show_syntax_errors {
|
||||
if message.is_invalid_syntax() && !show_syntax_errors {
|
||||
None
|
||||
} else {
|
||||
Some(to_lsp_diagnostic(
|
||||
|
||||
@@ -19,6 +19,7 @@ ruff_python_ast = { workspace = true }
|
||||
ty_python_semantic = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["zstd"] }
|
||||
ty_server = { workspace = true }
|
||||
ty_static = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
|
||||
55
crates/ty/docs/environment.md
Normal file
55
crates/ty/docs/environment.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Environment variables
|
||||
|
||||
ty defines and respects the following environment variables:
|
||||
|
||||
### `TY_LOG`
|
||||
|
||||
If set, ty will use this value as the log level for its `--verbose` output.
|
||||
Accepts any filter compatible with the `tracing_subscriber` crate.
|
||||
|
||||
For example:
|
||||
|
||||
- `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line
|
||||
- `TY_LOG=trace` will enable all trace-level logging.
|
||||
|
||||
See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
|
||||
for more.
|
||||
|
||||
### `TY_LOG_PROFILE`
|
||||
|
||||
If set to `"1"` or `"true"`, ty will enable flamegraph profiling.
|
||||
This creates a `tracing.folded` file that can be used to generate flame graphs
|
||||
for performance analysis.
|
||||
|
||||
### `TY_MAX_PARALLELISM`
|
||||
|
||||
Specifies an upper limit for the number of tasks ty is allowed to run in parallel.
|
||||
|
||||
For example, how many files should be checked in parallel.
|
||||
This isn't the same as a thread limit. ty may spawn additional threads
|
||||
when necessary, e.g. to watch for file system changes or a dedicated UI thread.
|
||||
|
||||
## Externally-defined variables
|
||||
|
||||
ty also reads the following externally defined environment variables:
|
||||
|
||||
### `CONDA_PREFIX`
|
||||
|
||||
Used to detect an activated Conda environment location.
|
||||
If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
|
||||
|
||||
### `RAYON_NUM_THREADS`
|
||||
|
||||
Specifies an upper limit for the number of threads ty uses when performing work in parallel.
|
||||
Equivalent to `TY_MAX_PARALLELISM`.
|
||||
|
||||
This is a standard Rayon environment variable.
|
||||
|
||||
### `VIRTUAL_ENV`
|
||||
|
||||
Used to detect an activated virtual environment.
|
||||
|
||||
### `XDG_CONFIG_HOME`
|
||||
|
||||
Path to user-level configuration directory on Unix systems.
|
||||
|
||||
137
crates/ty/docs/rules.md
generated
137
crates/ty/docs/rules.md
generated
@@ -36,7 +36,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L98)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L142)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -88,7 +88,7 @@ f(int) # error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L168)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -117,7 +117,7 @@ a = 1
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L193)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -147,7 +147,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L219)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -177,7 +177,7 @@ class B(A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L263)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -202,7 +202,7 @@ class B(A, A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L284)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L426)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -334,7 +334,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L451)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -445,7 +445,7 @@ an atypical memory layout.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L470)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L510)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -496,7 +496,7 @@ a: int = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1514)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L583)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L584)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -575,7 +575,7 @@ with 1:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L604)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -602,7 +602,7 @@ a: str
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -644,7 +644,7 @@ except ZeroDivisionError:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L664)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -675,7 +675,7 @@ class C[U](Generic[T]): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L689)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L738)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -735,12 +735,35 @@ class B(metaclass=f): ...
|
||||
|
||||
- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
|
||||
|
||||
## `invalid-nonlocal`
|
||||
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-nonlocal) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
Detects `nonlocal` statements that don't match a binding in any enclosing scope.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Unmatched `nonlocal` statements will raise a `SyntaxError` at runtime.
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
def f():
|
||||
nonlocal x # error: no binding for nonlocal 'x' found
|
||||
```
|
||||
|
||||
## `invalid-overload`
|
||||
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -788,7 +811,7 @@ def foo(x: int) -> int: ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -812,7 +835,7 @@ def f(a: int = ''): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L398)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -844,7 +867,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L828)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L829)
|
||||
</small>
|
||||
|
||||
Checks for `raise` statements that raise non-exceptions or use invalid
|
||||
@@ -891,7 +914,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L491)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -914,7 +937,7 @@ def func() -> int:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L872)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -968,7 +991,7 @@ TODO #14889
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L717)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L718)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -993,7 +1016,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L910)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1021,7 +1044,7 @@ TYPE_CHECKING = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L934)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L935)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1049,7 +1072,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L986)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L987)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1081,7 +1104,7 @@ f(10) # Error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L958)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1113,7 +1136,7 @@ class C:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1015)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1146,7 +1169,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1169,7 +1192,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1196,7 +1219,7 @@ func("string") # error: [no-matching-overload]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1218,7 +1241,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1103)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1242,7 +1265,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1154)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1155)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1296,7 +1319,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1490)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1491)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1324,7 +1347,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1351,7 +1374,7 @@ class B(A): ... # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1376,7 +1399,7 @@ f("foo") # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1269)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1402,7 +1425,7 @@ def _(x: int):
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1311)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1446,7 +1469,7 @@ class A:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1368)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1369)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1471,7 +1494,7 @@ f(x=1, y=2) # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1389)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1497,7 +1520,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1520,7 +1543,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1431)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1543,7 +1566,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1123)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1124)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1578,7 +1601,7 @@ b1 < b2 < b1 # exception raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1449)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1450)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1604,7 +1627,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1655,7 +1678,7 @@ a = 20 / 0 # type: ignore
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1175)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1681,7 +1704,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L116)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1711,7 +1734,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1741,7 +1764,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1543)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1766,7 +1789,7 @@ cast(int, f()) # Redundant
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1817,7 +1840,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L550)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L551)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1854,7 +1877,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L245)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1876,7 +1899,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1223)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
@@ -4,6 +4,7 @@ mod python_version;
|
||||
mod version;
|
||||
|
||||
pub use args::Cli;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
use std::io::{self, BufWriter, Write, stdout};
|
||||
use std::process::{ExitCode, Termination};
|
||||
@@ -144,7 +145,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
};
|
||||
|
||||
let mut stdout = stdout().lock();
|
||||
match std::env::var("TY_MEMORY_REPORT").as_deref() {
|
||||
match std::env::var(EnvVars::TY_MEMORY_REPORT).as_deref() {
|
||||
Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?,
|
||||
Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?,
|
||||
Ok("full") => write!(stdout, "{}", db.salsa_memory_dump().display_full())?,
|
||||
|
||||
@@ -12,6 +12,7 @@ use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::fmt::format::Writer;
|
||||
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
/// Logging flags to `#[command(flatten)]` into your CLI
|
||||
#[derive(clap::Args, Debug, Clone, Default)]
|
||||
@@ -84,7 +85,7 @@ pub(crate) fn setup_tracing(
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
// The `TY_LOG` environment variable overrides the default log level.
|
||||
let filter = if let Ok(log_env_variable) = std::env::var("TY_LOG") {
|
||||
let filter = if let Ok(log_env_variable) = std::env::var(EnvVars::TY_LOG) {
|
||||
EnvFilter::builder()
|
||||
.parse(log_env_variable)
|
||||
.context("Failed to parse directives specified in TY_LOG environment variable.")?
|
||||
@@ -165,7 +166,7 @@ fn setup_profile<S>() -> (
|
||||
where
|
||||
S: Subscriber + for<'span> LookupSpan<'span>,
|
||||
{
|
||||
if let Ok("1" | "true") = std::env::var("TY_LOG_PROFILE").as_deref() {
|
||||
if let Ok("1" | "true") = std::env::var(EnvVars::TY_LOG_PROFILE).as_deref() {
|
||||
let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded")
|
||||
.expect("Flame layer to be created");
|
||||
(Some(layer), Some(guard))
|
||||
|
||||
@@ -10,7 +10,7 @@ use ty_python_semantic::{Completion, NameKind, SemanticModel};
|
||||
use crate::Db;
|
||||
use crate::find_node::covering_node;
|
||||
|
||||
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion> {
|
||||
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion<'_>> {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
|
||||
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
|
||||
@@ -1223,33 +1223,33 @@ quux.<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
bar
|
||||
baz
|
||||
foo
|
||||
__annotations__
|
||||
__class__
|
||||
__delattr__
|
||||
__dict__
|
||||
__dir__
|
||||
__doc__
|
||||
__eq__
|
||||
__format__
|
||||
__getattribute__
|
||||
__getstate__
|
||||
__hash__
|
||||
__init__
|
||||
__init_subclass__
|
||||
__module__
|
||||
__ne__
|
||||
__new__
|
||||
__reduce__
|
||||
__reduce_ex__
|
||||
__repr__
|
||||
__setattr__
|
||||
__sizeof__
|
||||
__str__
|
||||
__subclasshook__
|
||||
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
|
||||
bar :: Unknown | Literal[2]
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type
|
||||
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dir__ :: bound method object.__dir__() -> Iterable[str]
|
||||
__doc__ :: str | None
|
||||
__eq__ :: bound method object.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method object.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method object.__getstate__() -> object
|
||||
__hash__ :: bound method object.__hash__() -> int
|
||||
__init__ :: bound method Quux.__init__() -> Unknown
|
||||
__init_subclass__ :: bound method object.__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method object.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method object.__new__() -> Self
|
||||
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method object.__repr__() -> str
|
||||
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method object.__sizeof__() -> int
|
||||
__str__ :: bound method object.__str__() -> str
|
||||
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
|
||||
");
|
||||
}
|
||||
|
||||
@@ -1268,33 +1268,33 @@ quux.b<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @r"
|
||||
bar
|
||||
baz
|
||||
foo
|
||||
__annotations__
|
||||
__class__
|
||||
__delattr__
|
||||
__dict__
|
||||
__dir__
|
||||
__doc__
|
||||
__eq__
|
||||
__format__
|
||||
__getattribute__
|
||||
__getstate__
|
||||
__hash__
|
||||
__init__
|
||||
__init_subclass__
|
||||
__module__
|
||||
__ne__
|
||||
__new__
|
||||
__reduce__
|
||||
__reduce_ex__
|
||||
__repr__
|
||||
__setattr__
|
||||
__sizeof__
|
||||
__str__
|
||||
__subclasshook__
|
||||
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
|
||||
bar :: Unknown | Literal[2]
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type
|
||||
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dir__ :: bound method object.__dir__() -> Iterable[str]
|
||||
__doc__ :: str | None
|
||||
__eq__ :: bound method object.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method object.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method object.__getstate__() -> object
|
||||
__hash__ :: bound method object.__hash__() -> int
|
||||
__init__ :: bound method Quux.__init__() -> Unknown
|
||||
__init_subclass__ :: bound method object.__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method object.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method object.__new__() -> Self
|
||||
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method object.__repr__() -> str
|
||||
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method object.__sizeof__() -> int
|
||||
__str__ :: bound method object.__str__() -> str
|
||||
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
|
||||
");
|
||||
}
|
||||
|
||||
@@ -1321,6 +1321,89 @@ class Quux:
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_attributes1() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
class Quux:
|
||||
some_attribute: int = 1
|
||||
|
||||
def __init__(self):
|
||||
self.foo = 1
|
||||
self.bar = 2
|
||||
self.baz = 3
|
||||
|
||||
def some_method(self) -> int:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def some_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def some_class_method(self) -> int:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def some_static_method(self) -> int:
|
||||
return 1
|
||||
|
||||
Quux.<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
|
||||
mro :: def mro(self) -> list[type]
|
||||
some_attribute :: int
|
||||
some_class_method :: bound method <class 'Quux'>.some_class_method() -> int
|
||||
some_method :: def some_method(self) -> int
|
||||
some_property :: property
|
||||
some_static_method :: def some_static_method(self) -> int
|
||||
__annotations__ :: dict[str, Any]
|
||||
__base__ :: type | None
|
||||
__bases__ :: tuple[type, ...]
|
||||
__basicsize__ :: int
|
||||
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any
|
||||
__class__ :: <class 'type'>
|
||||
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
||||
__dict__ :: MappingProxyType[str, Any]
|
||||
__dictoffset__ :: int
|
||||
__dir__ :: def __dir__(self) -> Iterable[str]
|
||||
__doc__ :: str | None
|
||||
__eq__ :: def __eq__(self, value: object, /) -> bool
|
||||
__flags__ :: int
|
||||
__format__ :: def __format__(self, format_spec: str, /) -> str
|
||||
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
||||
__getstate__ :: def __getstate__(self) -> object
|
||||
__hash__ :: def __hash__(self) -> int
|
||||
__init__ :: def __init__(self) -> Unknown
|
||||
__init_subclass__ :: def __init_subclass__(cls) -> None
|
||||
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
|
||||
__itemsize__ :: int
|
||||
__module__ :: str
|
||||
__mro__ :: tuple[<class 'type'>, <class 'object'>]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self
|
||||
__or__ :: def __or__(self, value: Any, /) -> UnionType
|
||||
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: def __ror__(self, value: Any, /) -> UnionType
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
|
||||
__subclasses__ :: def __subclasses__(self: Self) -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'object'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
__weakrefoffset__ :: int
|
||||
");
|
||||
}
|
||||
|
||||
// We don't yet take function parameters into account.
|
||||
#[test]
|
||||
fn call_prefix1() {
|
||||
@@ -2366,7 +2449,22 @@ importlib.<CURSOR>
|
||||
self.completions_if(|c| !c.builtin)
|
||||
}
|
||||
|
||||
fn completions_without_builtins_with_types(&self) -> String {
|
||||
self.completions_if_snapshot(
|
||||
|c| !c.builtin,
|
||||
|c| format!("{} :: {}", c.name, c.ty.display(&self.db)),
|
||||
)
|
||||
}
|
||||
|
||||
fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String {
|
||||
self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string())
|
||||
}
|
||||
|
||||
fn completions_if_snapshot(
|
||||
&self,
|
||||
predicate: impl Fn(&Completion) -> bool,
|
||||
snapshot: impl Fn(&Completion) -> String,
|
||||
) -> String {
|
||||
let completions = completion(&self.db, self.cursor.file, self.cursor.offset);
|
||||
if completions.is_empty() {
|
||||
return "<No completions found>".to_string();
|
||||
@@ -2374,7 +2472,7 @@ importlib.<CURSOR>
|
||||
let included = completions
|
||||
.iter()
|
||||
.filter(|label| predicate(label))
|
||||
.map(|completion| completion.name.as_str().to_string())
|
||||
.map(snapshot)
|
||||
.collect::<Vec<String>>();
|
||||
if included.is_empty() {
|
||||
// It'd be nice to include the actual number of
|
||||
|
||||
@@ -285,16 +285,7 @@ impl Project {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut file_diagnostics: Vec<_> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic)
|
||||
.collect();
|
||||
|
||||
let check_diagnostics = self.check_file_impl(db, file);
|
||||
file_diagnostics.extend(check_diagnostics);
|
||||
|
||||
file_diagnostics
|
||||
self.check_file_impl(db, file)
|
||||
}
|
||||
|
||||
/// Opens a file in the project.
|
||||
@@ -500,11 +491,11 @@ impl Project {
|
||||
parsed_ref
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| Diagnostic::syntax_error(file, &error.error, error)),
|
||||
.map(|error| Diagnostic::invalid_syntax(file, &error.error, error)),
|
||||
);
|
||||
|
||||
diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| {
|
||||
let mut error = Diagnostic::syntax_error(file, error, error);
|
||||
let mut error = Diagnostic::invalid_syntax(file, error, error);
|
||||
add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax");
|
||||
error
|
||||
}));
|
||||
|
||||
@@ -22,6 +22,7 @@ ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_python_literal = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ty_static = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
@@ -52,6 +53,7 @@ strum_macros = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["testing", "os"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ty_python_semantic = { workspace = true, features = ["testing"] }
|
||||
ty_static = { workspace = true }
|
||||
ty_test = { workspace = true }
|
||||
ty_vendored = { workspace = true }
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[.
|
||||
@@ -1791,6 +1791,80 @@ date.year = 2025
|
||||
date.tz = "UTC"
|
||||
```
|
||||
|
||||
### Return type of `__setattr__`
|
||||
|
||||
If the return type of the `__setattr__` method is `Never`, we do not allow any attribute assignments
|
||||
on instances of that class:
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
|
||||
class Frozen:
|
||||
existing: int = 1
|
||||
|
||||
def __setattr__(self, name, value) -> Never:
|
||||
raise AttributeError("Attributes can not be modified")
|
||||
|
||||
instance = Frozen()
|
||||
instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `non_existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||
instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||
```
|
||||
|
||||
### `__setattr__` on `object`
|
||||
|
||||
`object` has a custom `__setattr__` implementation, but we still emit an error if a non-existing
|
||||
attribute is assigned on an `object` instance.
|
||||
|
||||
```py
|
||||
obj = object()
|
||||
obj.non_existing = 1 # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
### Setting attributes on `Never` / `Any`
|
||||
|
||||
Setting attributes on `Never` itself should be allowed (even though it has a `__setattr__` attribute
|
||||
of type `Never`):
|
||||
|
||||
```py
|
||||
from typing_extensions import Never, Any
|
||||
|
||||
def _(n: Never):
|
||||
reveal_type(n.__setattr__) # revealed: Never
|
||||
|
||||
# No error:
|
||||
n.non_existing = 1
|
||||
```
|
||||
|
||||
And similarly for `Any`:
|
||||
|
||||
```py
|
||||
def _(a: Any):
|
||||
reveal_type(a.__setattr__) # revealed: Any
|
||||
|
||||
# No error:
|
||||
a.non_existing = 1
|
||||
```
|
||||
|
||||
### Possibly unbound `__setattr__` method
|
||||
|
||||
If a `__setattr__` method is only partially bound, the behavior is still the same:
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class Frozen:
|
||||
if flag():
|
||||
def __setattr__(self, name, value) -> Never:
|
||||
raise AttributeError("Attributes can not be modified")
|
||||
|
||||
instance = Frozen()
|
||||
instance.non_existing = 2 # error: [invalid-assignment]
|
||||
instance.existing = 2 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### `argparse.Namespace`
|
||||
|
||||
A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`:
|
||||
|
||||
@@ -558,6 +558,50 @@ class C(Base):
|
||||
reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
|
||||
```
|
||||
|
||||
## Conditionally defined fields
|
||||
|
||||
### Statically known conditions
|
||||
|
||||
Fields that are defined in always-reachable branches are always present in the synthesized
|
||||
`__init__` method. Fields that are defined in never-reachable branches are not present:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
normal: int
|
||||
|
||||
if 1 + 2 == 3:
|
||||
always_present: str
|
||||
|
||||
if 1 + 2 == 4:
|
||||
never_present: bool
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, normal: int, always_present: str) -> None
|
||||
```
|
||||
|
||||
### Dynamic conditions
|
||||
|
||||
If a field is conditionally defined, we currently assume that it is always present. A more complex
|
||||
alternative here would be to synthesized a union of all possible `__init__` signatures:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
normal: int
|
||||
|
||||
if flag():
|
||||
conditionally_present: str
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_present: str) -> None
|
||||
```
|
||||
|
||||
## Generic dataclasses
|
||||
|
||||
```toml
|
||||
@@ -789,6 +833,23 @@ class Fails: # error: [duplicate-kw-only]
|
||||
reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
|
||||
```
|
||||
|
||||
This also works if `KW_ONLY` is used in a conditional branch:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
@dataclass
|
||||
class D: # error: [duplicate-kw-only]
|
||||
x: int
|
||||
_1: KW_ONLY
|
||||
|
||||
if flag():
|
||||
y: str
|
||||
_2: KW_ONLY
|
||||
z: float
|
||||
```
|
||||
|
||||
## Other special cases
|
||||
|
||||
### `dataclasses.dataclass`
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Dataclass fields
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class Member:
|
||||
name: str
|
||||
role: str = field(default="user")
|
||||
tag: str | None = field(default=None, init=False)
|
||||
|
||||
# TODO: this should not include the `tag` parameter, since it has `init=False` set
|
||||
# revealed: (self: Member, name: str, role: str = Unknown, tag: str | None = Unknown) -> None
|
||||
reveal_type(Member.__init__)
|
||||
|
||||
alice = Member(name="Alice", role="admin")
|
||||
reveal_type(alice.role) # revealed: str
|
||||
alice.role = "moderator"
|
||||
|
||||
# TODO: this should be an error, `tag` has `init=False`
|
||||
bob = Member(name="Bob", tag="VIP")
|
||||
```
|
||||
|
||||
## The `field` function
|
||||
|
||||
```py
|
||||
from dataclasses import field
|
||||
|
||||
# TODO: this should be `Literal[1]`. This is currently blocked on enum support, because
|
||||
# the `dataclasses.field` overloads make use of a `_MISSING_TYPE` enum, for which we
|
||||
# infer a @Todo type, and therefore pick the wrong overload.
|
||||
reveal_type(field(default=1)) # revealed: Unknown
|
||||
```
|
||||
@@ -1304,7 +1304,7 @@ scope of the name that was declared `global`, can add a symbol to the global nam
|
||||
def f():
|
||||
global g, h
|
||||
|
||||
g: bool = True
|
||||
g = True
|
||||
|
||||
f()
|
||||
```
|
||||
|
||||
@@ -83,7 +83,7 @@ def f():
|
||||
x = 1
|
||||
def g() -> None:
|
||||
nonlocal x
|
||||
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
|
||||
global x # error: [invalid-syntax] "name `x` is nonlocal and global"
|
||||
x = None
|
||||
```
|
||||
|
||||
@@ -209,5 +209,18 @@ x: int = 1
|
||||
|
||||
def f():
|
||||
global x
|
||||
x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global"
|
||||
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be global"
|
||||
```
|
||||
|
||||
## Global declarations affect the inferred type of the binding
|
||||
|
||||
Even if the `global` declaration isn't used in an assignment, we conservatively assume it could be:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
global x
|
||||
|
||||
# TODO: reveal_type(x) # revealed: Unknown | Literal["1"]
|
||||
```
|
||||
|
||||
@@ -43,3 +43,295 @@ def f():
|
||||
def h():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## The `nonlocal` keyword
|
||||
|
||||
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
|
||||
enclosing scopes. This example isn't a type error, because the inner `x` shadows the outer one:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
def g():
|
||||
x = "hello" # allowed
|
||||
```
|
||||
|
||||
With `nonlocal` it is a type error, because `x` refers to the same place in both scopes:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## Local variable bindings "look ahead" to any assignment in the current scope
|
||||
|
||||
The binding `x = 2` in `g` causes the earlier read of `x` to refer to `g`'s not-yet-initialized
|
||||
binding, rather than to `x = 1` in `f`'s scope:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
if x == 1: # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
x = 2
|
||||
```
|
||||
|
||||
The `nonlocal` keyword makes this example legal (and makes the assignment `x = 2` affect the outer
|
||||
scope):
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
if x == 1:
|
||||
x = 2
|
||||
```
|
||||
|
||||
For the same reason, using the `+=` operator in an inner scope is an error without `nonlocal`
|
||||
(unless you shadow the outer variable first):
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x += 1 # error: [unresolved-reference] "Name `x` used when not defined"
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x = 1
|
||||
x += 1 # allowed, but doesn't affect the outer scope
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x += 1 # allowed, and affects the outer scope
|
||||
```
|
||||
|
||||
## `nonlocal` declarations must match an outer binding
|
||||
|
||||
`nonlocal x` isn't allowed when there's no binding for `x` in an enclosing scope:
|
||||
|
||||
```py
|
||||
def f():
|
||||
def g():
|
||||
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x, y # error: [invalid-nonlocal] "no binding for nonlocal `y` found"
|
||||
```
|
||||
|
||||
A global `x` doesn't work. The target must be in a function-like scope:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
def g():
|
||||
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
|
||||
|
||||
def f():
|
||||
global x
|
||||
def g():
|
||||
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
|
||||
```
|
||||
|
||||
A class-scoped `x` also doesn't work:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x = 1
|
||||
@staticmethod
|
||||
def f():
|
||||
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
|
||||
```
|
||||
|
||||
## `nonlocal` uses the closest binding
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x = 2
|
||||
def h():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||
```
|
||||
|
||||
## `nonlocal` "chaining"
|
||||
|
||||
Multiple `nonlocal` statements can "chain" through nested scopes:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
def h():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
And the `nonlocal` chain can skip over a scope that doesn't bind the variable:
|
||||
|
||||
```py
|
||||
def f1():
|
||||
x = 1
|
||||
def f2():
|
||||
nonlocal x
|
||||
def f3():
|
||||
# No binding; this scope gets skipped.
|
||||
def f4():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
But a `global` statement breaks the chain:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
global x
|
||||
def h():
|
||||
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
|
||||
```
|
||||
|
||||
## `nonlocal` bindings respect declared types from the defining scope, even without a binding
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int
|
||||
def g():
|
||||
nonlocal x
|
||||
x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## A complicated mixture of `nonlocal` chaining, empty scopes, and the `global` keyword
|
||||
|
||||
```py
|
||||
def f1():
|
||||
# The original bindings of `x`, `y`, and `z` with type declarations.
|
||||
x: int = 1
|
||||
y: int = 1
|
||||
z: int = 1
|
||||
|
||||
def f2():
|
||||
# This scope doesn't touch `x`, `y`, or `z` at all.
|
||||
|
||||
def f3():
|
||||
# This scope treats declares `x` nonlocal and `y` as global, and it shadows `z` without
|
||||
# giving it a new type declaration.
|
||||
nonlocal x
|
||||
x = 2
|
||||
global y
|
||||
y = 2
|
||||
z = 2
|
||||
|
||||
def f4():
|
||||
# This scope sees `x` from `f1` and `z` from `f3`, but it doesn't see `y` at all,
|
||||
# because of the `global` keyword above.
|
||||
nonlocal x, y, z # error: [invalid-nonlocal] "no binding for nonlocal `y` found"
|
||||
x = "string" # error: [invalid-assignment]
|
||||
z = "string" # not an error
|
||||
```
|
||||
|
||||
## TODO: `nonlocal` affects the inferred type in the outer scope
|
||||
|
||||
Without `nonlocal`, `g` can't write to `x`, and the inferred type of `x` in `f`'s scope isn't
|
||||
affected by `g`:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
But with `nonlocal`, `g` could write to `x`, and that affects its inferred type in `f`. That's true
|
||||
regardless of whether `g` actually writes to `x`. With a write:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
x += 1
|
||||
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||
# TODO: should be `Unknown | Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
Without a write:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
# TODO: should be `Unknown | Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Annotating a `nonlocal` binding is a syntax error
|
||||
|
||||
```py
|
||||
def f():
|
||||
x: int = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be nonlocal"
|
||||
```
|
||||
|
||||
## Use before `nonlocal`
|
||||
|
||||
Using a name prior to its `nonlocal` declaration in the same scope is a syntax error:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
x = 2
|
||||
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
|
||||
```
|
||||
|
||||
This is true even if there are multiple `nonlocal` declarations of the same variable, as long as any
|
||||
of them come after the usage:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
x = 2
|
||||
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
nonlocal x
|
||||
nonlocal x
|
||||
x = 2 # allowed
|
||||
```
|
||||
|
||||
## `nonlocal` before outer initialization
|
||||
|
||||
`nonlocal x` works even if `x` isn't bound in the enclosing scope until afterwards:
|
||||
|
||||
```py
|
||||
def f():
|
||||
def g():
|
||||
# This is allowed, because of the subsequent definition of `x`.
|
||||
nonlocal x
|
||||
x = 1
|
||||
```
|
||||
|
||||
@@ -37,6 +37,18 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
|
||||
23 | e: bytes
|
||||
24 |
|
||||
25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
|
||||
26 | def flag() -> bool:
|
||||
27 | return True
|
||||
28 |
|
||||
29 | @dataclass
|
||||
30 | class D: # error: [duplicate-kw-only]
|
||||
31 | x: int
|
||||
32 | _1: KW_ONLY
|
||||
33 |
|
||||
34 | if flag():
|
||||
35 | y: str
|
||||
36 | _2: KW_ONLY
|
||||
37 | z: float
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -109,6 +121,23 @@ info[revealed-type]: Revealed type
|
||||
24 |
|
||||
25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
|
||||
| ^^^^^^^^^^^^^^ `(self: Fails, a: int, *, c: str, e: bytes) -> None`
|
||||
26 | def flag() -> bool:
|
||||
27 | return True
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[duplicate-kw-only]: Dataclass has more than one field annotated with `KW_ONLY`
|
||||
--> src/mdtest_snippet.py:30:7
|
||||
|
|
||||
29 | @dataclass
|
||||
30 | class D: # error: [duplicate-kw-only]
|
||||
| ^
|
||||
31 | x: int
|
||||
32 | _1: KW_ONLY
|
||||
|
|
||||
info: `KW_ONLY` fields: `_1`, `_2`
|
||||
info: rule `duplicate-kw-only` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: final.md - `typing.Final` - Full diagnostics
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import Final
|
||||
2 |
|
||||
3 | MY_CONSTANT: Final[int] = 1
|
||||
4 |
|
||||
5 | # more code
|
||||
6 |
|
||||
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
|
||||
8 | from _stat import ST_INO
|
||||
9 |
|
||||
10 | ST_INO = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not allowed
|
||||
--> src/mdtest_snippet.py:3:14
|
||||
|
|
||||
1 | from typing import Final
|
||||
2 |
|
||||
3 | MY_CONSTANT: Final[int] = 1
|
||||
| ---------- Symbol declared as `Final` here
|
||||
4 |
|
||||
5 | # more code
|
||||
6 |
|
||||
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
|
||||
| ^^^^^^^^^^^^^^^ Symbol later reassigned here
|
||||
8 | from _stat import ST_INO
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Reassignment of `Final` symbol `ST_INO` is not allowed
|
||||
--> src/mdtest_snippet.py:10:1
|
||||
|
|
||||
8 | from _stat import ST_INO
|
||||
9 |
|
||||
10 | ST_INO = 1 # error: [invalid-assignment]
|
||||
| ^^^^^^^^^^ Reassignment of `Final` symbol
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -82,7 +82,7 @@ info: Type variable defined here
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 |
|
||||
4 | T = TypeVar("T", bound=int)
|
||||
| ^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
5 |
|
||||
6 | def f(x: T) -> T:
|
||||
|
|
||||
|
||||
@@ -97,7 +97,7 @@ info: Type variable defined here
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 |
|
||||
4 | T = TypeVar("T", int, None)
|
||||
| ^
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
5 |
|
||||
6 | def f(x: T) -> T:
|
||||
|
|
||||
|
||||
@@ -1774,6 +1774,25 @@ static_assert(is_subtype_of(type[B], Callable[[str], B]))
|
||||
static_assert(not is_subtype_of(type[B], Callable[[int], B]))
|
||||
```
|
||||
|
||||
### Dataclasses
|
||||
|
||||
Dataclasses synthesize a `__init__` method.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from ty_extensions import TypeOf, static_assert, is_subtype_of
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
x: "A" | None
|
||||
|
||||
static_assert(is_subtype_of(type[A], Callable[[A], A]))
|
||||
static_assert(is_subtype_of(type[A], Callable[[None], A]))
|
||||
static_assert(is_subtype_of(type[A], Callable[[A | None], A]))
|
||||
static_assert(not is_subtype_of(type[A], Callable[[int], A]))
|
||||
```
|
||||
|
||||
### Bound methods
|
||||
|
||||
```py
|
||||
|
||||
@@ -100,9 +100,13 @@ reveal_type(C().FINAL_D) # revealed: Unknown
|
||||
|
||||
## Not modifiable
|
||||
|
||||
### Names
|
||||
|
||||
Symbols qualified with `Final` cannot be reassigned, and attempting to do so will result in an
|
||||
error:
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Final, Annotated
|
||||
|
||||
@@ -114,13 +118,96 @@ FINAL_E: Final[int]
|
||||
FINAL_E = 1
|
||||
FINAL_F: Final = 1
|
||||
|
||||
# TODO: all of these should be errors
|
||||
FINAL_A = 2
|
||||
FINAL_B = 2
|
||||
FINAL_C = 2
|
||||
FINAL_D = 2
|
||||
FINAL_E = 2
|
||||
FINAL_F = 2
|
||||
FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed"
|
||||
FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed"
|
||||
FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed"
|
||||
FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed"
|
||||
FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed"
|
||||
FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed"
|
||||
|
||||
def global_use():
|
||||
global FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F
|
||||
FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed"
|
||||
FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed"
|
||||
FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed"
|
||||
FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed"
|
||||
FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed"
|
||||
FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed"
|
||||
|
||||
def local_use():
|
||||
# These are not errors, because they refer to local variables
|
||||
FINAL_A = 2
|
||||
FINAL_B = 2
|
||||
FINAL_C = 2
|
||||
FINAL_D = 2
|
||||
FINAL_E = 2
|
||||
FINAL_F = 2
|
||||
|
||||
def nonlocal_use():
|
||||
X: Final[int] = 1
|
||||
def inner():
|
||||
nonlocal X
|
||||
X = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `X` is not allowed: Reassignment of `Final` symbol"
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F
|
||||
|
||||
FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed"
|
||||
FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed"
|
||||
FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed"
|
||||
FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed"
|
||||
FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed"
|
||||
FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed"
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Assignments to attributes qualified with `Final` are also not allowed:
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
|
||||
class C:
|
||||
FINAL_A: Final[int] = 1
|
||||
FINAL_B: Final = 1
|
||||
|
||||
def __init__(self):
|
||||
self.FINAL_C: Final[int] = 1
|
||||
self.FINAL_D: Final = 1
|
||||
|
||||
# TODO: these should be errors (that mention `Final`)
|
||||
C.FINAL_A = 2
|
||||
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
|
||||
C.FINAL_B = 2
|
||||
|
||||
# TODO: these should be errors (that mention `Final`)
|
||||
c = C()
|
||||
c.FINAL_A = 2
|
||||
# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`"
|
||||
c.FINAL_B = 2
|
||||
c.FINAL_C = 2
|
||||
c.FINAL_D = 2
|
||||
```
|
||||
|
||||
## Mutability
|
||||
|
||||
Objects qualified with `Final` *can be modified*. `Final` represents a constant reference to an
|
||||
object, but that object itself may still be mutable:
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
|
||||
class C:
|
||||
x: int = 1
|
||||
|
||||
FINAL_C_INSTANCE: Final[C] = C()
|
||||
FINAL_C_INSTANCE.x = 2
|
||||
|
||||
FINAL_LIST: Final[list[int]] = [1, 2, 3]
|
||||
FINAL_LIST[0] = 4
|
||||
```
|
||||
|
||||
## Too many arguments
|
||||
@@ -168,4 +255,28 @@ class C:
|
||||
NO_RHS: Final
|
||||
```
|
||||
|
||||
## Full diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Annotated assignment:
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
|
||||
MY_CONSTANT: Final[int] = 1
|
||||
|
||||
# more code
|
||||
|
||||
MY_CONSTANT = 2 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
Imported `Final` symbol:
|
||||
|
||||
```py
|
||||
from _stat import ST_INO
|
||||
|
||||
ST_INO = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final
|
||||
|
||||
@@ -20,3 +20,4 @@ spack # slow, success, but mypy-primer hangs processing the output
|
||||
spark # too many iterations
|
||||
steam.py # hangs (single threaded)
|
||||
xarray # too many iterations
|
||||
zope.interface # https://github.com/astral-sh/ty/issues/764
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
AutoSplit
|
||||
DateType
|
||||
Expression
|
||||
PyGithub
|
||||
PyWinCtl
|
||||
|
||||
@@ -15,7 +15,7 @@ pub use program::{
|
||||
PythonVersionWithSource, SearchPathSettings,
|
||||
};
|
||||
pub use python_platform::PythonPlatform;
|
||||
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
|
||||
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
|
||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
|
||||
|
||||
|
||||
@@ -1421,7 +1421,7 @@ impl RequiresExplicitReExport {
|
||||
/// ```py
|
||||
/// def _():
|
||||
/// x = 1
|
||||
///
|
||||
///
|
||||
/// x = 2
|
||||
///
|
||||
/// if flag():
|
||||
|
||||
@@ -193,6 +193,12 @@ pub(crate) enum EagerSnapshotResult<'map, 'db> {
|
||||
NoLongerInEagerContext,
|
||||
}
|
||||
|
||||
#[derive(Debug, Update, get_size2::GetSize)]
|
||||
pub(crate) enum NotLocalVariableKind {
|
||||
Nonlocal,
|
||||
Global,
|
||||
}
|
||||
|
||||
/// The place tables and use-def maps for all scopes in a file.
|
||||
#[derive(Debug, Update, get_size2::GetSize)]
|
||||
pub(crate) struct SemanticIndex<'db> {
|
||||
@@ -217,8 +223,8 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`].
|
||||
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
||||
|
||||
/// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains.
|
||||
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
|
||||
/// Map from the file-local [`FileScopeId`] to the set of explicit-nonlocal symbols it contains.
|
||||
not_locals_by_scope: FxHashMap<FileScopeId, FxHashMap<ScopedPlaceId, NotLocalVariableKind>>,
|
||||
|
||||
/// Use-def map for each scope in this file.
|
||||
use_def_maps: IndexVec<FileScopeId, ArcUseDefMap<'db>>,
|
||||
@@ -308,9 +314,21 @@ impl<'db> SemanticIndex<'db> {
|
||||
symbol: ScopedPlaceId,
|
||||
scope: FileScopeId,
|
||||
) -> bool {
|
||||
self.globals_by_scope
|
||||
.get(&scope)
|
||||
.is_some_and(|globals| globals.contains(&symbol))
|
||||
let Some(scope) = self.not_locals_by_scope.get(&scope) else {
|
||||
return false;
|
||||
};
|
||||
matches!(scope.get(&symbol), Some(NotLocalVariableKind::Global))
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_is_nonlocal_in_scope(
|
||||
&self,
|
||||
symbol: ScopedPlaceId,
|
||||
scope: FileScopeId,
|
||||
) -> bool {
|
||||
let Some(scope) = self.not_locals_by_scope.get(&scope) else {
|
||||
return false;
|
||||
};
|
||||
matches!(scope.get(&symbol), Some(NotLocalVariableKind::Nonlocal))
|
||||
}
|
||||
|
||||
/// Returns the id of the parent scope.
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::semantic_index::reachability_constraints::{
|
||||
use crate::semantic_index::use_def::{
|
||||
EagerSnapshotKey, FlowSnapshot, ScopedEagerSnapshotId, UseDefMapBuilder,
|
||||
};
|
||||
use crate::semantic_index::{ArcUseDefMap, SemanticIndex};
|
||||
use crate::semantic_index::{ArcUseDefMap, NotLocalVariableKind, SemanticIndex};
|
||||
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue};
|
||||
use crate::{Db, Program};
|
||||
|
||||
@@ -103,7 +103,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
||||
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
|
||||
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
|
||||
not_locals_by_scope: FxHashMap<FileScopeId, FxHashMap<ScopedPlaceId, NotLocalVariableKind>>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
@@ -141,7 +141,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
definitions_by_node: FxHashMap::default(),
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
globals_by_scope: FxHashMap::default(),
|
||||
not_locals_by_scope: FxHashMap::default(),
|
||||
|
||||
imported_modules: FxHashSet::default(),
|
||||
generator_functions: FxHashSet::default(),
|
||||
@@ -1046,7 +1046,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.scopes_by_node.shrink_to_fit();
|
||||
self.generator_functions.shrink_to_fit();
|
||||
self.eager_snapshots.shrink_to_fit();
|
||||
self.globals_by_scope.shrink_to_fit();
|
||||
self.not_locals_by_scope.shrink_to_fit();
|
||||
|
||||
SemanticIndex {
|
||||
place_tables,
|
||||
@@ -1054,7 +1054,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
definitions_by_node: self.definitions_by_node,
|
||||
expressions_by_node: self.expressions_by_node,
|
||||
scope_ids_by_scope: self.scope_ids_by_scope,
|
||||
globals_by_scope: self.globals_by_scope,
|
||||
not_locals_by_scope: self.not_locals_by_scope,
|
||||
ast_ids,
|
||||
scopes_by_expression: self.scopes_by_expression,
|
||||
scopes_by_node: self.scopes_by_node,
|
||||
@@ -1422,6 +1422,32 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
|
||||
if let ast::Expr::Name(name) = &*node.target {
|
||||
let symbol_id = self.add_symbol(name.id.clone());
|
||||
let scope_id = self.current_scope();
|
||||
// Check whether the variable has been declared global or nonlocal.
|
||||
if let Some(not_locals) = self.not_locals_by_scope.get(&scope_id) {
|
||||
if let Some(not_local_kind) = not_locals.get(&symbol_id) {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: match not_local_kind {
|
||||
NotLocalVariableKind::Global => {
|
||||
SemanticSyntaxErrorKind::AnnotatedGlobal(
|
||||
name.id.as_str().into(),
|
||||
)
|
||||
}
|
||||
NotLocalVariableKind::Nonlocal => {
|
||||
SemanticSyntaxErrorKind::AnnotatedNonlocal(
|
||||
name.id.as_str().into(),
|
||||
)
|
||||
}
|
||||
},
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
|
||||
if matches!(
|
||||
*node.target,
|
||||
@@ -1864,6 +1890,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
let symbol_id = self.add_symbol(name.id.clone());
|
||||
let symbol_table = self.current_place_table();
|
||||
let symbol = symbol_table.place_expr(symbol_id);
|
||||
// Check whether the variable has already been accessed in this scope.
|
||||
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration {
|
||||
@@ -1875,10 +1902,61 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
});
|
||||
}
|
||||
let scope_id = self.current_scope();
|
||||
self.globals_by_scope
|
||||
// Check whether the variable has also been declared nonlocal.
|
||||
if let Some(not_locals) = self.not_locals_by_scope.get(&scope_id) {
|
||||
if let Some(NotLocalVariableKind::Nonlocal) = not_locals.get(&symbol_id) {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.not_locals_by_scope
|
||||
.entry(scope_id)
|
||||
.or_default()
|
||||
.insert(symbol_id);
|
||||
.insert(symbol_id, NotLocalVariableKind::Global);
|
||||
}
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
ast::Stmt::Nonlocal(ast::StmtNonlocal {
|
||||
range: _,
|
||||
node_index: _,
|
||||
names,
|
||||
}) => {
|
||||
for name in names {
|
||||
let symbol_id = self.add_symbol(name.id.clone());
|
||||
let symbol_table = self.current_place_table();
|
||||
let symbol = symbol_table.place_expr(symbol_id);
|
||||
// Make sure the variable exists in an enclosing scope. But note that its
|
||||
// definition might come below the inner scope.
|
||||
|
||||
// Check whether the variable has already been accessed in this scope.
|
||||
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration {
|
||||
name: name.to_string(),
|
||||
start: name.range.start(),
|
||||
},
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
let scope_id = self.current_scope();
|
||||
// Check whether the variable has also been declared global.
|
||||
if let Some(not_locals) = self.not_locals_by_scope.get(&scope_id) {
|
||||
if let Some(NotLocalVariableKind::Global) = not_locals.get(&symbol_id) {
|
||||
self.report_semantic_error(SemanticSyntaxError {
|
||||
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
|
||||
range: name.range,
|
||||
python_version: self.python_version,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.not_locals_by_scope
|
||||
.entry(scope_id)
|
||||
.or_default()
|
||||
.insert(symbol_id, NotLocalVariableKind::Nonlocal);
|
||||
}
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
@@ -618,6 +618,15 @@ impl DefinitionKind<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_import(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_)
|
||||
| DefinitionKind::StarImport(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] of the definition target.
|
||||
///
|
||||
/// A definition target would mainly be the node representing the place being defined i.e.,
|
||||
@@ -668,8 +677,20 @@ impl DefinitionKind<'_> {
|
||||
DefinitionKind::Class(class) => class.node(module).range(),
|
||||
DefinitionKind::TypeAlias(type_alias) => type_alias.node(module).range(),
|
||||
DefinitionKind::NamedExpression(named) => named.node(module).range(),
|
||||
DefinitionKind::Assignment(assignment) => assignment.target.node(module).range(),
|
||||
DefinitionKind::AnnotatedAssignment(assign) => assign.target.node(module).range(),
|
||||
DefinitionKind::Assignment(assign) => {
|
||||
let target_range = assign.target.node(module).range();
|
||||
let value_range = assign.value.node(module).range();
|
||||
target_range.cover(value_range)
|
||||
}
|
||||
DefinitionKind::AnnotatedAssignment(assign) => {
|
||||
let target_range = assign.target.node(module).range();
|
||||
if let Some(ref value) = assign.value {
|
||||
let value_range = value.node(module).range();
|
||||
target_range.cover(value_range)
|
||||
} else {
|
||||
target_range
|
||||
}
|
||||
}
|
||||
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.node(module).range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.target.node(module).range(),
|
||||
DefinitionKind::Comprehension(comp) => comp.target(module).range(),
|
||||
|
||||
@@ -665,7 +665,7 @@ impl PlaceTable {
|
||||
}
|
||||
|
||||
/// Returns the place named `name`.
|
||||
#[allow(unused)] // used in tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExprWithFlags> {
|
||||
let id = self.place_id_by_name(name)?;
|
||||
Some(self.place_expr(id))
|
||||
|
||||
@@ -306,7 +306,10 @@ pub(crate) struct UseDefMap<'db> {
|
||||
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
|
||||
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
|
||||
/// valid assignment to our own annotation.
|
||||
bindings_by_declaration: FxHashMap<Definition<'db>, Bindings>,
|
||||
///
|
||||
/// If we see a binding to a `Final`-qualified symbol, we also need this map to find previous
|
||||
/// bindings to that symbol. If there are any, the assignment is invalid.
|
||||
bindings_by_definition: FxHashMap<Definition<'db>, Bindings>,
|
||||
|
||||
/// [`PlaceState`] visible at end of scope for each place.
|
||||
end_of_scope_places: IndexVec<ScopedPlaceId, PlaceState>,
|
||||
@@ -448,12 +451,12 @@ impl<'db> UseDefMap<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
pub(crate) fn bindings_at_definition(
|
||||
&self,
|
||||
declaration: Definition<'db>,
|
||||
definition: Definition<'db>,
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
self.bindings_iterator(
|
||||
&self.bindings_by_declaration[&declaration],
|
||||
&self.bindings_by_definition[&definition],
|
||||
BoundnessAnalysis::BasedOnUnboundVisibility,
|
||||
)
|
||||
}
|
||||
@@ -744,8 +747,8 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Live declarations for each so-far-recorded binding.
|
||||
declarations_by_binding: FxHashMap<Definition<'db>, Declarations>,
|
||||
|
||||
/// Live bindings for each so-far-recorded declaration.
|
||||
bindings_by_declaration: FxHashMap<Definition<'db>, Bindings>,
|
||||
/// Live bindings for each so-far-recorded definition.
|
||||
bindings_by_definition: FxHashMap<Definition<'db>, Bindings>,
|
||||
|
||||
/// Currently live bindings and declarations for each place.
|
||||
place_states: IndexVec<ScopedPlaceId, PlaceState>,
|
||||
@@ -772,7 +775,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
reachability: ScopedReachabilityConstraintId::ALWAYS_TRUE,
|
||||
node_reachability: FxHashMap::default(),
|
||||
declarations_by_binding: FxHashMap::default(),
|
||||
bindings_by_declaration: FxHashMap::default(),
|
||||
bindings_by_definition: FxHashMap::default(),
|
||||
place_states: IndexVec::new(),
|
||||
reachable_definitions: IndexVec::new(),
|
||||
eager_snapshots: EagerSnapshots::default(),
|
||||
@@ -808,6 +811,9 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
binding: Definition<'db>,
|
||||
is_place_name: bool,
|
||||
) {
|
||||
self.bindings_by_definition
|
||||
.insert(binding, self.place_states[place].bindings().clone());
|
||||
|
||||
let def_id = self.all_definitions.push(DefinitionState::Defined(binding));
|
||||
let place_state = &mut self.place_states[place];
|
||||
self.declarations_by_binding
|
||||
@@ -942,7 +948,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
.all_definitions
|
||||
.push(DefinitionState::Defined(declaration));
|
||||
let place_state = &mut self.place_states[place];
|
||||
self.bindings_by_declaration
|
||||
self.bindings_by_definition
|
||||
.insert(declaration, place_state.bindings().clone());
|
||||
place_state.record_declaration(def_id, self.reachability);
|
||||
|
||||
@@ -1119,7 +1125,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
self.bindings_by_use.shrink_to_fit();
|
||||
self.node_reachability.shrink_to_fit();
|
||||
self.declarations_by_binding.shrink_to_fit();
|
||||
self.bindings_by_declaration.shrink_to_fit();
|
||||
self.bindings_by_definition.shrink_to_fit();
|
||||
self.eager_snapshots.shrink_to_fit();
|
||||
|
||||
UseDefMap {
|
||||
@@ -1132,7 +1138,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
end_of_scope_places: self.place_states,
|
||||
reachable_definitions: self.reachable_definitions,
|
||||
declarations_by_binding: self.declarations_by_binding,
|
||||
bindings_by_declaration: self.bindings_by_declaration,
|
||||
bindings_by_definition: self.bindings_by_definition,
|
||||
eager_snapshots: self.eager_snapshots,
|
||||
end_of_scope_reachability: self.reachability,
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ impl<'db> SemanticModel<'db> {
|
||||
&self,
|
||||
import: &ast::StmtImportFrom,
|
||||
_name: Option<usize>,
|
||||
) -> Vec<Completion> {
|
||||
) -> Vec<Completion<'db>> {
|
||||
let module_name = match ModuleName::from_import_statement(self.db, self.file, import) {
|
||||
Ok(module_name) => module_name,
|
||||
Err(err) => {
|
||||
@@ -62,7 +62,7 @@ impl<'db> SemanticModel<'db> {
|
||||
|
||||
/// Returns completions for symbols available in the given module as if
|
||||
/// it were imported by this model's `File`.
|
||||
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion> {
|
||||
fn module_completions(&self, module_name: &ModuleName) -> Vec<Completion<'db>> {
|
||||
let Some(module) = resolve_module(self.db, module_name) else {
|
||||
tracing::debug!("Could not resolve module from `{module_name:?}`");
|
||||
return vec![];
|
||||
@@ -71,17 +71,22 @@ impl<'db> SemanticModel<'db> {
|
||||
let builtin = module.is_known(KnownModule::Builtins);
|
||||
crate::types::all_members(self.db, ty)
|
||||
.into_iter()
|
||||
.map(|name| Completion { name, builtin })
|
||||
.map(|member| Completion {
|
||||
name: member.name,
|
||||
ty: member.ty,
|
||||
builtin,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns completions for symbols available in a `object.<CURSOR>` context.
|
||||
pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec<Completion> {
|
||||
pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec<Completion<'db>> {
|
||||
let ty = node.value.inferred_type(self);
|
||||
crate::types::all_members(self.db, ty)
|
||||
.into_iter()
|
||||
.map(|name| Completion {
|
||||
name,
|
||||
.map(|member| Completion {
|
||||
name: member.name,
|
||||
ty: member.ty,
|
||||
builtin: false,
|
||||
})
|
||||
.collect()
|
||||
@@ -92,7 +97,7 @@ impl<'db> SemanticModel<'db> {
|
||||
///
|
||||
/// If a scope could not be determined, then completions for the global
|
||||
/// scope of this model's `File` are returned.
|
||||
pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec<Completion> {
|
||||
pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec<Completion<'db>> {
|
||||
let index = semantic_index(self.db, self.file);
|
||||
|
||||
// TODO: We currently use `try_expression_scope_id` here as a hotfix for [1].
|
||||
@@ -115,8 +120,9 @@ impl<'db> SemanticModel<'db> {
|
||||
for (file_scope, _) in index.ancestor_scopes(file_scope) {
|
||||
completions.extend(
|
||||
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
|
||||
.map(|name| Completion {
|
||||
name,
|
||||
.map(|member| Completion {
|
||||
name: member.name,
|
||||
ty: member.ty,
|
||||
builtin: false,
|
||||
}),
|
||||
);
|
||||
@@ -163,9 +169,11 @@ impl NameKind {
|
||||
|
||||
/// A suggestion for code completion.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Completion {
|
||||
pub struct Completion<'db> {
|
||||
/// The label shown to the user for this suggestion.
|
||||
pub name: Name,
|
||||
/// The type of this completion.
|
||||
pub ty: Type<'db>,
|
||||
/// Whether this suggestion came from builtins or not.
|
||||
///
|
||||
/// At time of writing (2025-06-26), this information
|
||||
@@ -175,6 +183,94 @@ pub struct Completion {
|
||||
pub builtin: bool,
|
||||
}
|
||||
|
||||
impl<'db> Completion<'db> {
|
||||
/// Returns the "kind" of this completion.
|
||||
///
|
||||
/// This is meant to be a very general classification of this completion.
|
||||
/// Typically, this is communicated from the LSP server to a client, and
|
||||
/// the client uses this information to help improve the UX (perhaps by
|
||||
/// assigning an icon of some kind to the completion).
|
||||
pub fn kind(&self, db: &'db dyn Db) -> Option<CompletionKind> {
|
||||
fn imp<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<CompletionKind> {
|
||||
Some(match ty {
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::Callable(_) => CompletionKind::Function,
|
||||
Type::BoundMethod(_) | Type::MethodWrapper(_) => CompletionKind::Method,
|
||||
Type::ModuleLiteral(_) => CompletionKind::Module,
|
||||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
|
||||
CompletionKind::Class
|
||||
}
|
||||
// This is a little weird for "struct." I'm mostly interpreting
|
||||
// "struct" here as a more general "object." ---AG
|
||||
Type::NominalInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_) => CompletionKind::Struct,
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_) => CompletionKind::Value,
|
||||
Type::ProtocolInstance(_) => CompletionKind::Interface,
|
||||
Type::TypeVar(_) => CompletionKind::TypeParameter,
|
||||
Type::Union(union) => union.elements(db).iter().find_map(|&ty| imp(db, ty))?,
|
||||
Type::Intersection(intersection) => {
|
||||
intersection.iter_positive(db).find_map(|ty| imp(db, ty))?
|
||||
}
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy => return None,
|
||||
})
|
||||
}
|
||||
imp(db, self.ty)
|
||||
}
|
||||
}
|
||||
|
||||
/// The "kind" of a completion.
|
||||
///
|
||||
/// This is taken directly from the LSP completion specification:
|
||||
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind>
|
||||
///
|
||||
/// The idea here is that `Completion::kind` defines the mapping to this from
|
||||
/// `Type` (and possibly other information), which might be interesting and
|
||||
/// contentious. Then the outer edges map this to the LSP types, which is
|
||||
/// expected to be mundane and boring.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CompletionKind {
|
||||
Text,
|
||||
Method,
|
||||
Function,
|
||||
Constructor,
|
||||
Field,
|
||||
Variable,
|
||||
Class,
|
||||
Interface,
|
||||
Module,
|
||||
Property,
|
||||
Unit,
|
||||
Value,
|
||||
Enum,
|
||||
Keyword,
|
||||
Snippet,
|
||||
Color,
|
||||
File,
|
||||
Reference,
|
||||
Folder,
|
||||
EnumMember,
|
||||
Constant,
|
||||
Struct,
|
||||
Event,
|
||||
Operator,
|
||||
TypeParameter,
|
||||
}
|
||||
|
||||
pub trait HasType {
|
||||
/// Returns the inferred type of `self`.
|
||||
///
|
||||
|
||||
@@ -23,6 +23,7 @@ use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_trivia::Cursor;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextLen, TextRange};
|
||||
use ty_static::EnvVars;
|
||||
|
||||
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
|
||||
|
||||
@@ -149,7 +150,7 @@ impl PythonEnvironment {
|
||||
PythonEnvironment::new(path, origin, system)
|
||||
}
|
||||
|
||||
if let Ok(virtual_env) = system.env_var("VIRTUAL_ENV") {
|
||||
if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) {
|
||||
return resolve_environment(
|
||||
system,
|
||||
SystemPath::new(&virtual_env),
|
||||
@@ -158,7 +159,7 @@ impl PythonEnvironment {
|
||||
.map(Some);
|
||||
}
|
||||
|
||||
if let Ok(conda_env) = system.env_var("CONDA_PREFIX") {
|
||||
if let Ok(conda_env) = system.env_var(EnvVars::CONDA_PREFIX) {
|
||||
return resolve_environment(
|
||||
system,
|
||||
SystemPath::new(&conda_env),
|
||||
|
||||
@@ -106,7 +106,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
|
||||
index
|
||||
.semantic_syntax_errors()
|
||||
.iter()
|
||||
.map(|error| Diagnostic::syntax_error(file, error, error)),
|
||||
.map(|error| Diagnostic::invalid_syntax(file, error, error)),
|
||||
);
|
||||
|
||||
check_suppressions(db, file, &mut diagnostics);
|
||||
@@ -1104,7 +1104,9 @@ impl<'db> Type<'db> {
|
||||
Type::FunctionLiteral(function_literal) => {
|
||||
Some(Type::Callable(function_literal.into_callable_type(db)))
|
||||
}
|
||||
Type::BoundMethod(bound_method) => Some(bound_method.into_callable_type(db)),
|
||||
Type::BoundMethod(bound_method) => {
|
||||
Some(Type::Callable(bound_method.into_callable_type(db)))
|
||||
}
|
||||
|
||||
Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
|
||||
let call_symbol = self
|
||||
@@ -2613,7 +2615,7 @@ impl<'db> Type<'db> {
|
||||
/// See also: [`Type::member`]
|
||||
fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> {
|
||||
if let Type::ModuleLiteral(module) = self {
|
||||
module.static_member(db, name)
|
||||
module.static_member(db, name).place
|
||||
} else if let place @ Place::Type(_, _) = self.class_member(db, name.into()).place {
|
||||
place
|
||||
} else if let Some(place @ Place::Type(_, _)) =
|
||||
@@ -3067,7 +3069,7 @@ impl<'db> Type<'db> {
|
||||
Place::bound(Type::IntLiteral(i64::from(bool_value))).into()
|
||||
}
|
||||
|
||||
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
|
||||
Type::ModuleLiteral(module) => module.static_member(db, name_str),
|
||||
|
||||
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
|
||||
db,
|
||||
@@ -7166,8 +7168,8 @@ fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
}
|
||||
|
||||
impl<'db> BoundMethodType<'db> {
|
||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
Type::Callable(CallableType::new(
|
||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
||||
CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(
|
||||
self.function(db)
|
||||
@@ -7177,7 +7179,7 @@ impl<'db> BoundMethodType<'db> {
|
||||
.map(signatures::Signature::bind_self),
|
||||
),
|
||||
false,
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self {
|
||||
@@ -7511,15 +7513,14 @@ pub struct ModuleLiteralType<'db> {
|
||||
impl get_size2::GetSize for ModuleLiteralType<'_> {}
|
||||
|
||||
impl<'db> ModuleLiteralType<'db> {
|
||||
fn static_member(self, db: &'db dyn Db, name: &str) -> Place<'db> {
|
||||
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
// `__dict__` is a very special member that is never overridden by module globals;
|
||||
// we should always look it up directly as an attribute on `types.ModuleType`,
|
||||
// never in the global scope of the module.
|
||||
if name == "__dict__" {
|
||||
return KnownClass::ModuleType
|
||||
.to_instance(db)
|
||||
.member(db, "__dict__")
|
||||
.place;
|
||||
.member(db, "__dict__");
|
||||
}
|
||||
|
||||
// If the file that originally imported the module has also imported a submodule
|
||||
@@ -7538,7 +7539,8 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
full_submodule_name.extend(&submodule_name);
|
||||
if imported_submodules.contains(&full_submodule_name) {
|
||||
if let Some(submodule) = resolve_module(db, &full_submodule_name) {
|
||||
return Place::bound(Type::module_literal(db, importing_file, &submodule));
|
||||
return Place::bound(Type::module_literal(db, importing_file, &submodule))
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7547,7 +7549,6 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
.file()
|
||||
.map(|file| imported_symbol(db, file, name, None))
|
||||
.unwrap_or_default()
|
||||
.place
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -668,7 +668,7 @@ impl<'db> Bindings<'db> {
|
||||
ide_support::all_members(db, *ty)
|
||||
.into_iter()
|
||||
.sorted()
|
||||
.map(|member| Type::string_literal(db, &member)),
|
||||
.map(|member| Type::string_literal(db, &member.name)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +602,7 @@ impl<'db> ClassType<'db> {
|
||||
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
|
||||
// by always respecting the signature of the metaclass `__call__`, rather than
|
||||
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
|
||||
return metaclass_dunder_call_function.into_callable_type(db);
|
||||
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
|
||||
}
|
||||
|
||||
let dunder_new_function_symbol = self_ty
|
||||
@@ -661,27 +661,33 @@ impl<'db> ClassType<'db> {
|
||||
// same parameters as the `__init__` method after it is bound, and with the return type of
|
||||
// the concrete type of `Self`.
|
||||
let synthesized_dunder_init_callable =
|
||||
if let Place::Type(Type::FunctionLiteral(dunder_init_function), _) =
|
||||
dunder_init_function_symbol
|
||||
{
|
||||
let synthesized_signature = |signature: Signature<'db>| {
|
||||
Signature::new(signature.parameters().clone(), Some(correct_return_type))
|
||||
.bind_self()
|
||||
if let Place::Type(ty, _) = dunder_init_function_symbol {
|
||||
let signature = match ty {
|
||||
Type::FunctionLiteral(dunder_init_function) => {
|
||||
Some(dunder_init_function.signature(db))
|
||||
}
|
||||
Type::Callable(callable) => Some(callable.signatures(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let synthesized_dunder_init_signature = CallableSignature::from_overloads(
|
||||
dunder_init_function
|
||||
.signature(db)
|
||||
.overloads
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(synthesized_signature),
|
||||
);
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
synthesized_dunder_init_signature,
|
||||
true,
|
||||
)))
|
||||
if let Some(signature) = signature {
|
||||
let synthesized_signature = |signature: &Signature<'db>| {
|
||||
Signature::new(signature.parameters().clone(), Some(correct_return_type))
|
||||
.bind_self()
|
||||
};
|
||||
|
||||
let synthesized_dunder_init_signature = CallableSignature::from_overloads(
|
||||
signature.overloads.iter().map(synthesized_signature),
|
||||
);
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
synthesized_dunder_init_signature,
|
||||
true,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1650,7 +1656,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
if !declarations
|
||||
.clone()
|
||||
.all(|DeclarationWithConstraint { declaration, .. }| {
|
||||
declaration.is_defined_and(|declaration| {
|
||||
declaration.is_undefined_or(|declaration| {
|
||||
matches!(
|
||||
declaration.kind(db),
|
||||
DefinitionKind::AnnotatedAssignment(..)
|
||||
@@ -2042,7 +2048,11 @@ impl<'db> ClassLiteral<'db> {
|
||||
|
||||
/// A helper function for `instance_member` that looks up the `name` attribute only on
|
||||
/// this class, not on its superclasses.
|
||||
fn own_instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
pub(crate) fn own_instance_member(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
// TODO: There are many things that are not yet implemented here:
|
||||
// - `typing.Final`
|
||||
// - Proper diagnostics
|
||||
|
||||
@@ -85,6 +85,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&STATIC_ASSERT_ERROR);
|
||||
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
|
||||
registry.register_lint(&REDUNDANT_CAST);
|
||||
registry.register_lint(&INVALID_NONLOCAL);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -1560,6 +1561,25 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Detects `nonlocal` statements that don't match a binding in any enclosing scope.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Unmatched `nonlocal` statements will raise a `SyntaxError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// nonlocal x # error: no binding for nonlocal 'x' found
|
||||
/// ```
|
||||
pub(crate) static INVALID_NONLOCAL = {
|
||||
summary: "detects unmatched `nonlocal` statements",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of type check diagnostics.
|
||||
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
|
||||
pub struct TypeCheckDiagnostics {
|
||||
|
||||
@@ -766,7 +766,7 @@ impl<'db> FunctionType<'db> {
|
||||
self.literal(db).signature(db, self.type_mappings(db))
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`Type::Callable`].
|
||||
/// Convert the `FunctionType` into a [`CallableType`].
|
||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
||||
CallableType::new(db, self.signature(db), false)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
|
||||
use crate::semantic_index::definition::DefinitionKind;
|
||||
use crate::semantic_index::place::ScopeId;
|
||||
@@ -14,7 +17,7 @@ use rustc_hash::FxHashSet;
|
||||
pub(crate) fn all_declarations_and_bindings<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope_id: ScopeId<'db>,
|
||||
) -> impl Iterator<Item = Name> + 'db {
|
||||
) -> impl Iterator<Item = Member<'db>> + 'db {
|
||||
let use_def_map = use_def_map(db, scope_id);
|
||||
let table = place_table(db, scope_id);
|
||||
|
||||
@@ -24,10 +27,13 @@ pub(crate) fn all_declarations_and_bindings<'db>(
|
||||
place_from_declarations(db, declarations)
|
||||
.ok()
|
||||
.and_then(|result| {
|
||||
result
|
||||
.place
|
||||
.ignore_possibly_unbound()
|
||||
.and_then(|_| table.place_expr(symbol_id).as_name().cloned())
|
||||
result.place.ignore_possibly_unbound().and_then(|ty| {
|
||||
table
|
||||
.place_expr(symbol_id)
|
||||
.as_name()
|
||||
.cloned()
|
||||
.map(|name| Member { name, ty })
|
||||
})
|
||||
})
|
||||
})
|
||||
.chain(
|
||||
@@ -36,17 +42,23 @@ pub(crate) fn all_declarations_and_bindings<'db>(
|
||||
.filter_map(move |(symbol_id, bindings)| {
|
||||
place_from_bindings(db, bindings)
|
||||
.ignore_possibly_unbound()
|
||||
.and_then(|_| table.place_expr(symbol_id).as_name().cloned())
|
||||
.and_then(|ty| {
|
||||
table
|
||||
.place_expr(symbol_id)
|
||||
.as_name()
|
||||
.cloned()
|
||||
.map(|name| Member { name, ty })
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
struct AllMembers {
|
||||
members: FxHashSet<Name>,
|
||||
struct AllMembers<'db> {
|
||||
members: FxHashSet<Member<'db>>,
|
||||
}
|
||||
|
||||
impl AllMembers {
|
||||
fn of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Self {
|
||||
impl<'db> AllMembers<'db> {
|
||||
fn of(db: &'db dyn Db, ty: Type<'db>) -> Self {
|
||||
let mut all_members = Self {
|
||||
members: FxHashSet::default(),
|
||||
};
|
||||
@@ -54,7 +66,7 @@ impl AllMembers {
|
||||
all_members
|
||||
}
|
||||
|
||||
fn extend_with_type<'db>(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
match ty {
|
||||
Type::Union(union) => self.members.extend(
|
||||
union
|
||||
@@ -76,7 +88,6 @@ impl AllMembers {
|
||||
|
||||
Type::NominalInstance(instance) => {
|
||||
let (class_literal, _specialization) = instance.class.class_literal(db);
|
||||
self.extend_with_class_members(db, class_literal);
|
||||
self.extend_with_instance_members(db, class_literal);
|
||||
}
|
||||
|
||||
@@ -180,68 +191,143 @@ impl AllMembers {
|
||||
}
|
||||
}
|
||||
|
||||
self.members
|
||||
.insert(place_table.place_expr(symbol_id).expect_name().clone());
|
||||
self.members.insert(Member {
|
||||
name: place_table.place_expr(symbol_id).expect_name().clone(),
|
||||
ty,
|
||||
});
|
||||
}
|
||||
|
||||
let module_name = module.name();
|
||||
self.members.extend(
|
||||
imported_modules(db, literal.importing_file(db))
|
||||
.iter()
|
||||
.filter_map(|submodule_name| submodule_name.relative_to(module_name))
|
||||
.filter_map(|relative_submodule_name| {
|
||||
Some(Name::from(relative_submodule_name.components().next()?))
|
||||
.filter_map(|submodule_name| {
|
||||
let module = resolve_module(db, submodule_name)?;
|
||||
let ty = Type::module_literal(db, file, &module);
|
||||
Some((submodule_name, ty))
|
||||
})
|
||||
.filter_map(|(submodule_name, ty)| {
|
||||
let relative = submodule_name.relative_to(module_name)?;
|
||||
Some((relative, ty))
|
||||
})
|
||||
.filter_map(|(relative_submodule_name, ty)| {
|
||||
let name = Name::from(relative_submodule_name.components().next()?);
|
||||
Some(Member { name, ty })
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_with_declarations_and_bindings(&mut self, db: &dyn Db, scope_id: ScopeId) {
|
||||
self.members
|
||||
.extend(all_declarations_and_bindings(db, scope_id));
|
||||
}
|
||||
|
||||
fn extend_with_class_members<'db>(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
class_literal: ClassLiteral<'db>,
|
||||
) {
|
||||
fn extend_with_class_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
|
||||
for parent in class_literal
|
||||
.iter_mro(db, None)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.map(|class| class.class_literal(db).0)
|
||||
{
|
||||
let parent_ty = Type::ClassLiteral(parent);
|
||||
let parent_scope = parent.body_scope(db);
|
||||
self.extend_with_declarations_and_bindings(db, parent_scope);
|
||||
for Member { name, .. } in all_declarations_and_bindings(db, parent_scope) {
|
||||
let result = parent_ty.member(db, name.as_str());
|
||||
let Some(ty) = result.place.ignore_possibly_unbound() else {
|
||||
continue;
|
||||
};
|
||||
self.members.insert(Member { name, ty });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_with_instance_members<'db>(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
class_literal: ClassLiteral<'db>,
|
||||
) {
|
||||
fn extend_with_instance_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
|
||||
for parent in class_literal
|
||||
.iter_mro(db, None)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.map(|class| class.class_literal(db).0)
|
||||
{
|
||||
let parent_instance = Type::instance(db, parent.default_specialization(db));
|
||||
let class_body_scope = parent.body_scope(db);
|
||||
let file = class_body_scope.file(db);
|
||||
let index = semantic_index(db, file);
|
||||
for function_scope_id in attribute_scopes(db, class_body_scope) {
|
||||
let place_table = index.place_table(function_scope_id);
|
||||
self.members
|
||||
.extend(place_table.instance_attributes().cloned());
|
||||
for place_expr in place_table.places() {
|
||||
let Some(name) = place_expr.as_instance_attribute() else {
|
||||
continue;
|
||||
};
|
||||
let result = parent_instance.member(db, name.as_str());
|
||||
let Some(ty) = result.place.ignore_possibly_unbound() else {
|
||||
continue;
|
||||
};
|
||||
self.members.insert(Member {
|
||||
name: name.clone(),
|
||||
ty,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This is very similar to `extend_with_class_members`,
|
||||
// but uses the type of the class instance to query the
|
||||
// class member. This gets us the right type for each
|
||||
// member, e.g., `SomeClass.__delattr__` is not a bound
|
||||
// method, but `instance_of_SomeClass.__delattr__` is.
|
||||
for Member { name, .. } in all_declarations_and_bindings(db, class_body_scope) {
|
||||
let result = parent_instance.member(db, name.as_str());
|
||||
let Some(ty) = result.place.ignore_possibly_unbound() else {
|
||||
continue;
|
||||
};
|
||||
self.members.insert(Member { name, ty });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A member of a type.
|
||||
///
|
||||
/// This represents a single item in (ideally) the list returned by
|
||||
/// `dir(object)`.
|
||||
///
|
||||
/// The equality, comparison and hashing traits implemented for
|
||||
/// this type are done so by taking only the name into account. At
|
||||
/// present, this is because we assume the name is enough to uniquely
|
||||
/// identify each attribute on an object. This is perhaps complicated
|
||||
/// by overloads, but they only get represented by one member for
|
||||
/// now. Moreover, it is convenient to be able to sort collections of
|
||||
/// members, and a `Type` currently (as of 2025-07-09) has no way to do
|
||||
/// ordered comparisons.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Member<'db> {
|
||||
pub name: Name,
|
||||
pub ty: Type<'db>,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Member<'_> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Member<'_> {}
|
||||
|
||||
impl<'db> PartialEq for Member<'db> {
|
||||
fn eq(&self, rhs: &Member<'db>) -> bool {
|
||||
self.name == rhs.name
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Ord for Member<'db> {
|
||||
fn cmp(&self, rhs: &Member<'db>) -> Ordering {
|
||||
self.name.cmp(&rhs.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> PartialOrd for Member<'db> {
|
||||
fn partial_cmp(&self, rhs: &Member<'db>) -> Option<Ordering> {
|
||||
Some(self.cmp(rhs))
|
||||
}
|
||||
}
|
||||
|
||||
/// List all members of a given type: anything that would be valid when accessed
|
||||
/// as an attribute on an object of the given type.
|
||||
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Name> {
|
||||
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet<Member<'db>> {
|
||||
AllMembers::of(db, ty).members
|
||||
}
|
||||
|
||||
|
||||
@@ -92,15 +92,16 @@ use crate::types::diagnostic::{
|
||||
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
|
||||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
|
||||
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
|
||||
POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
|
||||
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
|
||||
UNSUPPORTED_OPERATOR, report_implicit_return_type, report_instance_layout_conflict,
|
||||
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
|
||||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
|
||||
report_invalid_return_type, report_possibly_unbound_attribute,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_NONLOCAL, INVALID_PARAMETER_DEFAULT,
|
||||
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
|
||||
IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
|
||||
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
|
||||
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
|
||||
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
|
||||
report_invalid_assignment, report_invalid_attribute_assignment,
|
||||
report_invalid_generator_function_return_type, report_invalid_return_type,
|
||||
report_possibly_unbound_attribute,
|
||||
};
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
@@ -1564,24 +1565,74 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let mut bound_ty = ty;
|
||||
|
||||
let global_use_def_map = self.index.use_def_map(FileScopeId::global());
|
||||
let nonlocal_use_def_map;
|
||||
let place_id = binding.place(self.db());
|
||||
let place = place_table.place_expr(place_id);
|
||||
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id);
|
||||
let declarations = if skip_non_global_scopes {
|
||||
let (declarations, is_local) = if skip_non_global_scopes {
|
||||
match self
|
||||
.index
|
||||
.place_table(FileScopeId::global())
|
||||
.place_id_by_expr(&place.expr)
|
||||
{
|
||||
Some(id) => global_use_def_map.end_of_scope_declarations(id),
|
||||
// This case is a syntax error (load before global declaration) but ignore that here
|
||||
None => use_def.declarations_at_binding(binding),
|
||||
Some(id) => (global_use_def_map.end_of_scope_declarations(id), false),
|
||||
// This variable shows up in `global` declarations but doesn't have an explicit
|
||||
// binding in the global scope.
|
||||
None => (use_def.declarations_at_binding(binding), true),
|
||||
}
|
||||
} else if self
|
||||
.index
|
||||
.symbol_is_nonlocal_in_scope(place_id, file_scope_id)
|
||||
{
|
||||
// If we run out of ancestor scopes without finding a definition, we'll fall back to
|
||||
// the local scope. This will also be a syntax error in `infer_nonlocal_statement` (no
|
||||
// binding for `nonlocal` found), but ignore that here.
|
||||
let mut declarations = use_def.declarations_at_binding(binding);
|
||||
let mut is_local = true;
|
||||
// Walk up parent scopes looking for the enclosing scope that has definition of this
|
||||
// name. `ancestor_scopes` includes the current scope, so skip that one.
|
||||
for (enclosing_scope_file_id, enclosing_scope) in
|
||||
self.index.ancestor_scopes(file_scope_id).skip(1)
|
||||
{
|
||||
// Ignore class scopes and the global scope.
|
||||
if !enclosing_scope.kind().is_function_like() {
|
||||
continue;
|
||||
}
|
||||
let enclosing_place_table = self.index.place_table(enclosing_scope_file_id);
|
||||
let Some(enclosing_place_id) = enclosing_place_table.place_id_by_expr(&place.expr)
|
||||
else {
|
||||
// This ancestor scope doesn't have a binding. Keep going.
|
||||
continue;
|
||||
};
|
||||
if self
|
||||
.index
|
||||
.symbol_is_nonlocal_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||
{
|
||||
// The variable is `nonlocal` in this ancestor scope. Keep going.
|
||||
continue;
|
||||
}
|
||||
if self
|
||||
.index
|
||||
.symbol_is_global_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||
{
|
||||
// The variable is `global` in this ancestor scope. This breaks the `nonlocal`
|
||||
// chain, and it's a syntax error in `infer_nonlocal_statement`. Ignore that
|
||||
// here and just bail out of this loop.
|
||||
break;
|
||||
}
|
||||
// We found the closest definition. Note that (unlike in `infer_place_load`) this
|
||||
// does *not* need to be a binding. It could be just `x: int`.
|
||||
nonlocal_use_def_map = self.index.use_def_map(enclosing_scope_file_id);
|
||||
declarations = nonlocal_use_def_map.end_of_scope_declarations(enclosing_place_id);
|
||||
is_local = false;
|
||||
break;
|
||||
}
|
||||
(declarations, is_local)
|
||||
} else {
|
||||
use_def.declarations_at_binding(binding)
|
||||
(use_def.declarations_at_binding(binding), true)
|
||||
};
|
||||
|
||||
let declared_ty = place_from_declarations(self.db(), declarations)
|
||||
let (declared_ty, is_modifiable) = place_from_declarations(self.db(), declarations)
|
||||
.and_then(|place_and_quals| {
|
||||
Ok(
|
||||
if matches!(place_and_quals.place, Place::Type(_, Boundness::Bound)) {
|
||||
@@ -1600,8 +1651,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.map(
|
||||
|PlaceAndQualifiers {
|
||||
place: resolved_place,
|
||||
..
|
||||
qualifiers,
|
||||
}| {
|
||||
let is_modifiable = !qualifiers.contains(TypeQualifiers::FINAL);
|
||||
|
||||
if resolved_place.is_unbound() && !place_table.place_expr(place_id).is_name() {
|
||||
if let AnyNodeRef::ExprAttribute(ast::ExprAttribute {
|
||||
value, attr, ..
|
||||
@@ -1611,7 +1664,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
if let Place::Type(ty, Boundness::Bound) =
|
||||
value_type.member(db, attr).place
|
||||
{
|
||||
return ty;
|
||||
// TODO: also consider qualifiers on the attribute
|
||||
return (ty, is_modifiable);
|
||||
}
|
||||
} else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript {
|
||||
value,
|
||||
@@ -1623,12 +1677,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let slice_ty = self.infer_expression(slice);
|
||||
let result_ty =
|
||||
self.infer_subscript_expression_types(value, value_ty, slice_ty);
|
||||
return result_ty;
|
||||
return (result_ty, is_modifiable);
|
||||
}
|
||||
}
|
||||
resolved_place
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::unknown())
|
||||
(
|
||||
resolved_place
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::unknown()),
|
||||
is_modifiable,
|
||||
)
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|(ty, conflicting)| {
|
||||
@@ -1640,8 +1697,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
format_enumeration(conflicting.iter().map(|ty| ty.display(db)))
|
||||
));
|
||||
}
|
||||
ty.inner_type()
|
||||
(
|
||||
ty.inner_type(),
|
||||
!ty.qualifiers.contains(TypeQualifiers::FINAL),
|
||||
)
|
||||
});
|
||||
|
||||
if !is_modifiable {
|
||||
let mut previous_bindings = use_def.bindings_at_definition(binding);
|
||||
|
||||
// An assignment to a local `Final`-qualified symbol is only an error if there are prior bindings
|
||||
|
||||
let previous_definition = previous_bindings
|
||||
.next()
|
||||
.and_then(|r| r.binding.definition());
|
||||
|
||||
if !is_local || previous_definition.is_some() {
|
||||
let place = place_table.place_expr(binding.place(db));
|
||||
if let Some(builder) = self.context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
binding.full_range(self.db(), self.module()),
|
||||
) {
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Reassignment of `Final` symbol `{place}` is not allowed"
|
||||
));
|
||||
|
||||
diagnostic.set_primary_message("Reassignment of `Final` symbol");
|
||||
|
||||
if let Some(previous_definition) = previous_definition {
|
||||
// It is not very helpful to show the previous definition if it results from
|
||||
// an import. Ideally, we would show the original definition in the external
|
||||
// module, but that information is currently not threaded through attribute
|
||||
// lookup.
|
||||
if !previous_definition.kind(db).is_import() {
|
||||
if let DefinitionKind::AnnotatedAssignment(assignment) =
|
||||
previous_definition.kind(db)
|
||||
{
|
||||
let range = assignment.annotation(self.module()).range();
|
||||
diagnostic.annotate(
|
||||
self.context
|
||||
.secondary(range)
|
||||
.message("Symbol declared as `Final` here"),
|
||||
);
|
||||
} else {
|
||||
let range =
|
||||
previous_definition.full_range(self.db(), self.module());
|
||||
diagnostic.annotate(
|
||||
self.context
|
||||
.secondary(range)
|
||||
.message("Symbol declared as `Final` here"),
|
||||
);
|
||||
}
|
||||
diagnostic.set_primary_message("Symbol later reassigned here");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bound_ty.is_assignable_to(db, declared_ty) {
|
||||
report_invalid_assignment(&self.context, node, declared_ty, bound_ty);
|
||||
// allow declarations to override inference in case of invalid assignment
|
||||
@@ -1721,7 +1834,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.is_declaration()
|
||||
);
|
||||
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
|
||||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||
let prior_bindings = use_def.bindings_at_definition(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
let inferred_ty = place_from_bindings(self.db(), prior_bindings)
|
||||
.with_qualifiers(TypeQualifiers::empty())
|
||||
@@ -2144,12 +2257,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
ast::Stmt::Raise(raise) => self.infer_raise_statement(raise),
|
||||
ast::Stmt::Return(ret) => self.infer_return_statement(ret),
|
||||
ast::Stmt::Delete(delete) => self.infer_delete_statement(delete),
|
||||
ast::Stmt::Nonlocal(nonlocal) => self.infer_nonlocal_statement(nonlocal),
|
||||
ast::Stmt::Break(_)
|
||||
| ast::Stmt::Continue(_)
|
||||
| ast::Stmt::Pass(_)
|
||||
| ast::Stmt::IpyEscapeCommand(_)
|
||||
| ast::Stmt::Global(_)
|
||||
| ast::Stmt::Nonlocal(_) => {
|
||||
| ast::Stmt::Global(_) => {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
@@ -3345,167 +3458,193 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
dataclass_params.is_some_and(|params| params.contains(DataclassParams::FROZEN))
|
||||
};
|
||||
|
||||
match object_ty.class_member(db, attribute.into()) {
|
||||
meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot assign to ClassVar `{attribute}` \
|
||||
from an instance of type `{ty}`",
|
||||
ty = object_ty.display(self.db()),
|
||||
));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
PlaceAndQualifiers {
|
||||
place: Place::Type(meta_attr_ty, meta_attr_boundness),
|
||||
qualifiers: _,
|
||||
} => {
|
||||
if is_read_only() {
|
||||
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
|
||||
// assigning the attributed by the normal mechanism.
|
||||
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
|
||||
db,
|
||||
"__setattr__",
|
||||
&mut CallArgumentTypes::positional([
|
||||
Type::StringLiteral(StringLiteralType::new(db, Box::from(attribute))),
|
||||
value_ty,
|
||||
]),
|
||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||
);
|
||||
|
||||
let check_setattr_return_type = |result: Bindings<'db>| -> bool {
|
||||
match result.return_type(db) {
|
||||
Type::Never => {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Property `{attribute}` defined in `{ty}` is read-only",
|
||||
ty = object_ty.display(self.db()),
|
||||
));
|
||||
"Cannot assign to attribute `{attribute}` on type `{}` \
|
||||
whose `__setattr__` method returns `Never`/`NoReturn`",
|
||||
object_ty.display(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) =
|
||||
meta_attr_ty.class_member(db, "__set__".into()).place
|
||||
{
|
||||
let successful_call = meta_dunder_set
|
||||
.try_call(
|
||||
db,
|
||||
&CallArgumentTypes::positional([
|
||||
meta_attr_ty,
|
||||
object_ty,
|
||||
value_ty,
|
||||
]),
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
if !successful_call && emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Invalid assignment to data descriptor attribute \
|
||||
`{attribute}` on type `{}` with custom `__set__` method",
|
||||
object_ty.display(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
successful_call
|
||||
} else {
|
||||
ensure_assignable_to(meta_attr_ty)
|
||||
};
|
||||
|
||||
let assignable_to_instance_attribute = if meta_attr_boundness
|
||||
== Boundness::PossiblyUnbound
|
||||
{
|
||||
let (assignable, boundness) =
|
||||
if let Place::Type(instance_attr_ty, instance_attr_boundness) =
|
||||
object_ty.instance_member(db, attribute).place
|
||||
{
|
||||
(
|
||||
ensure_assignable_to(instance_attr_ty),
|
||||
instance_attr_boundness,
|
||||
)
|
||||
} else {
|
||||
(true, Boundness::PossiblyUnbound)
|
||||
};
|
||||
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
report_possibly_unbound_attribute(
|
||||
&self.context,
|
||||
target,
|
||||
attribute,
|
||||
object_ty,
|
||||
);
|
||||
}
|
||||
|
||||
assignable
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
assignable_to_meta_attr && assignable_to_instance_attribute
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
};
|
||||
|
||||
PlaceAndQualifiers {
|
||||
place: Place::Unbound,
|
||||
..
|
||||
} => {
|
||||
if let Place::Type(instance_attr_ty, instance_attr_boundness) =
|
||||
object_ty.instance_member(db, attribute).place
|
||||
{
|
||||
if instance_attr_boundness == Boundness::PossiblyUnbound {
|
||||
report_possibly_unbound_attribute(
|
||||
&self.context,
|
||||
target,
|
||||
attribute,
|
||||
object_ty,
|
||||
);
|
||||
match setattr_dunder_call_result {
|
||||
Ok(result) => check_setattr_return_type(result),
|
||||
Err(CallDunderError::PossiblyUnbound(result)) => {
|
||||
check_setattr_return_type(*result)
|
||||
}
|
||||
Err(CallDunderError::CallError(..)) => {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Can not assign object of type `{}` to attribute \
|
||||
`{attribute}` on type `{}` with \
|
||||
custom `__setattr__` method.",
|
||||
value_ty.display(db),
|
||||
object_ty.display(db)
|
||||
));
|
||||
}
|
||||
|
||||
if is_read_only() {
|
||||
}
|
||||
false
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
match object_ty.class_member(db, attribute.into()) {
|
||||
meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Property `{attribute}` defined in `{ty}` is read-only",
|
||||
"Cannot assign to ClassVar `{attribute}` \
|
||||
from an instance of type `{ty}`",
|
||||
ty = object_ty.display(self.db()),
|
||||
));
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
ensure_assignable_to(instance_attr_ty)
|
||||
}
|
||||
} else {
|
||||
let result = object_ty.try_call_dunder_with_policy(
|
||||
db,
|
||||
"__setattr__",
|
||||
&mut CallArgumentTypes::positional([
|
||||
Type::StringLiteral(StringLiteralType::new(
|
||||
db,
|
||||
Box::from(attribute),
|
||||
)),
|
||||
value_ty,
|
||||
]),
|
||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
|
||||
Err(CallDunderError::CallError(..)) => {
|
||||
PlaceAndQualifiers {
|
||||
place: Place::Type(meta_attr_ty, meta_attr_boundness),
|
||||
qualifiers: _,
|
||||
} => {
|
||||
if is_read_only() {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
|
||||
self.context.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Can not assign object of type `{}` to attribute \
|
||||
`{attribute}` on type `{}` with \
|
||||
custom `__setattr__` method.",
|
||||
value_ty.display(db),
|
||||
object_ty.display(db)
|
||||
));
|
||||
"Property `{attribute}` defined in `{ty}` is read-only",
|
||||
ty = object_ty.display(self.db()),
|
||||
));
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
let assignable_to_meta_attr =
|
||||
if let Place::Type(meta_dunder_set, _) =
|
||||
meta_attr_ty.class_member(db, "__set__".into()).place
|
||||
{
|
||||
let successful_call = meta_dunder_set
|
||||
.try_call(
|
||||
db,
|
||||
&CallArgumentTypes::positional([
|
||||
meta_attr_ty,
|
||||
object_ty,
|
||||
value_ty,
|
||||
]),
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
if !successful_call && emit_diagnostics {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Invalid assignment to data descriptor attribute \
|
||||
`{attribute}` on type `{}` with custom `__set__` method",
|
||||
object_ty.display(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
successful_call
|
||||
} else {
|
||||
ensure_assignable_to(meta_attr_ty)
|
||||
};
|
||||
|
||||
let assignable_to_instance_attribute =
|
||||
if meta_attr_boundness == Boundness::PossiblyUnbound {
|
||||
let (assignable, boundness) = if let Place::Type(
|
||||
instance_attr_ty,
|
||||
instance_attr_boundness,
|
||||
) =
|
||||
object_ty.instance_member(db, attribute).place
|
||||
{
|
||||
(
|
||||
ensure_assignable_to(instance_attr_ty),
|
||||
instance_attr_boundness,
|
||||
)
|
||||
} else {
|
||||
(true, Boundness::PossiblyUnbound)
|
||||
};
|
||||
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
report_possibly_unbound_attribute(
|
||||
&self.context,
|
||||
target,
|
||||
attribute,
|
||||
object_ty,
|
||||
);
|
||||
}
|
||||
|
||||
assignable
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
assignable_to_meta_attr && assignable_to_instance_attribute
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
}
|
||||
|
||||
PlaceAndQualifiers {
|
||||
place: Place::Unbound,
|
||||
..
|
||||
} => {
|
||||
if let Place::Type(instance_attr_ty, instance_attr_boundness) =
|
||||
object_ty.instance_member(db, attribute).place
|
||||
{
|
||||
if instance_attr_boundness == Boundness::PossiblyUnbound {
|
||||
report_possibly_unbound_attribute(
|
||||
&self.context,
|
||||
target,
|
||||
attribute,
|
||||
object_ty,
|
||||
);
|
||||
}
|
||||
|
||||
if is_read_only() {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_ASSIGNMENT, target)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Property `{attribute}` defined in `{ty}` is read-only",
|
||||
ty = object_ty.display(self.db()),
|
||||
));
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
ensure_assignable_to(instance_attr_ty)
|
||||
}
|
||||
} else {
|
||||
if emit_diagnostics {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
|
||||
@@ -3654,7 +3793,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
Type::ModuleLiteral(module) => {
|
||||
if let Place::Type(attr_ty, _) = module.static_member(db, attribute) {
|
||||
if let Place::Type(attr_ty, _) = module.static_member(db, attribute).place {
|
||||
let assignable = value_ty.is_assignable_to(db, attr_ty);
|
||||
if assignable {
|
||||
true
|
||||
@@ -4401,7 +4540,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
// First try loading the requested attribute from the module.
|
||||
if !import_is_self_referential {
|
||||
if let Place::Type(ty, boundness) = module_ty.member(self.db(), name).place {
|
||||
if let PlaceAndQualifiers {
|
||||
place: Place::Type(ty, boundness),
|
||||
qualifiers,
|
||||
} = module_ty.member(self.db(), name)
|
||||
{
|
||||
if &alias.name != "*" && boundness == Boundness::PossiblyUnbound {
|
||||
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
|
||||
// together if the attribute exists but is possibly-unbound.
|
||||
@@ -4417,7 +4560,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
alias.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
&DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty: TypeAndQualifiers {
|
||||
inner: ty,
|
||||
qualifiers,
|
||||
},
|
||||
inferred_ty: ty,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -4513,6 +4662,64 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_nonlocal_statement(&mut self, nonlocal: &ast::StmtNonlocal) {
|
||||
let ast::StmtNonlocal {
|
||||
node_index: _,
|
||||
range,
|
||||
names,
|
||||
} = nonlocal;
|
||||
let db = self.db();
|
||||
let scope = self.scope();
|
||||
let file_scope_id = scope.file_scope_id(db);
|
||||
let current_file = self.file();
|
||||
'names: for name in names {
|
||||
// Walk up parent scopes looking for a possible enclosing scope that may have a
|
||||
// definition of this name visible to us. Note that we skip the scope containing the
|
||||
// use that we are resolving, since we already looked for the place there up above.
|
||||
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) {
|
||||
// Class scopes are not visible to nested scopes, and `nonlocal` cannot refer to
|
||||
// globals, so check only function-like scopes.
|
||||
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
|
||||
if !enclosing_scope_id.is_function_like(db) {
|
||||
continue;
|
||||
}
|
||||
let enclosing_place_table = self.index.place_table(enclosing_scope_file_id);
|
||||
let Some(enclosing_place_id) = enclosing_place_table.place_id_by_name(name) else {
|
||||
// This scope doesn't define this name. Keep going.
|
||||
continue;
|
||||
};
|
||||
// We've found a definition for this name in an enclosing function-like scope.
|
||||
// Either this definition is the valid place this name refers to, or else we'll
|
||||
// emit a syntax error. Either way, we won't walk any more enclosing scopes. Note
|
||||
// that there are differences here compared to `infer_place_load`: A regular load
|
||||
// (e.g. `print(x)`) is allowed to refer to a global variable (e.g. `x = 1` in the
|
||||
// global scope), and similarly it's allowed to refer to a local variable in an
|
||||
// enclosing function that's declared `global` (e.g. `global x`). However, the
|
||||
// `nonlocal` keyword can't refer to global variables (that's a `SyntaxError`), and
|
||||
// it also can't refer to local variables in enclosing functions that are declared
|
||||
// `global` (also a `SyntaxError`).
|
||||
if self
|
||||
.index
|
||||
.symbol_is_global_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||
{
|
||||
// A "chain" of `nonlocal` statements is "broken" by a `global` statement. Stop
|
||||
// looping and report that this `nonlocal` statement is invalid.
|
||||
break;
|
||||
}
|
||||
// We found a definition. We've checked that the name isn't `global` in this scope,
|
||||
// but it's ok if it's `nonlocal`. If a "chain" of `nonlocal` statements fails to
|
||||
// lead to a valid binding, the outermost one will be an error; we don't need to
|
||||
// walk the whole chain for each one.
|
||||
continue 'names;
|
||||
}
|
||||
// There's no matching binding in an enclosing scope. This `nonlocal` statement is
|
||||
// invalid.
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_NONLOCAL, range) {
|
||||
builder.into_diagnostic(format_args!("no binding for nonlocal `{name}` found"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn module_type_from_name(&self, module_name: &ModuleName) -> Option<Type<'db>> {
|
||||
resolve_module(self.db(), module_name)
|
||||
.map(|module| Type::module_literal(self.db(), self.file(), &module))
|
||||
@@ -5656,6 +5863,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
};
|
||||
(place, None)
|
||||
} else {
|
||||
if expr_ref
|
||||
.as_name_expr()
|
||||
.is_some_and(|name| name.is_invalid())
|
||||
{
|
||||
return (Place::Unbound, None);
|
||||
}
|
||||
|
||||
let use_id = expr_ref.scoped_use_id(db, scope);
|
||||
let place = place_from_bindings(db, use_def.bindings_at_use(use_id));
|
||||
(place, Some(use_id))
|
||||
@@ -5695,13 +5909,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
let current_file = self.file();
|
||||
|
||||
let mut is_nonlocal_binding = false;
|
||||
if let Some(name) = expr.as_name() {
|
||||
let skip_non_global_scopes = place_table
|
||||
.place_id_by_name(name)
|
||||
.is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id));
|
||||
|
||||
if skip_non_global_scopes {
|
||||
return global_symbol(self.db(), self.file(), name);
|
||||
if let Some(symbol_id) = place_table.place_id_by_name(name) {
|
||||
if self.skip_non_global_scopes(file_scope_id, symbol_id) {
|
||||
return global_symbol(self.db(), self.file(), name);
|
||||
}
|
||||
is_nonlocal_binding = self
|
||||
.index
|
||||
.symbol_is_nonlocal_in_scope(symbol_id, file_scope_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5714,7 +5930,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// a local variable or not in function-like scopes. If a variable has any bindings in a
|
||||
// function-like scope, it is considered a local variable; it never references another
|
||||
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
|
||||
if has_bindings_in_this_scope && scope.is_function_like(db) {
|
||||
if has_bindings_in_this_scope && scope.is_function_like(db) && !is_nonlocal_binding {
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use camino::Utf8Path;
|
||||
use dir_test::{Fixture, dir_test};
|
||||
use ty_static::EnvVars;
|
||||
use ty_test::OutputFormat;
|
||||
|
||||
/// See `crates/ty_test/README.md` for documentation on these tests.
|
||||
@@ -19,7 +20,7 @@ fn mdtest(fixture: Fixture<&str>) {
|
||||
|
||||
let test_name = test_name("mdtest", absolute_fixture_path);
|
||||
|
||||
let output_format = if std::env::var("MDTEST_GITHUB_ANNOTATIONS_FORMAT").is_ok() {
|
||||
let output_format = if std::env::var(EnvVars::MDTEST_GITHUB_ANNOTATIONS_FORMAT).is_ok() {
|
||||
OutputFormat::GitHub
|
||||
} else {
|
||||
OutputFormat::Cli
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use lsp_types::request::Completion;
|
||||
use lsp_types::{CompletionItem, CompletionParams, CompletionResponse, Url};
|
||||
use lsp_types::{CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, Url};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ty_ide::completion;
|
||||
use ty_project::ProjectDatabase;
|
||||
use ty_python_semantic::CompletionKind;
|
||||
|
||||
use crate::DocumentSnapshot;
|
||||
use crate::document::PositionExt;
|
||||
@@ -55,10 +56,14 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
||||
let items: Vec<CompletionItem> = completions
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, comp)| CompletionItem {
|
||||
label: comp.name.into(),
|
||||
sort_text: Some(format!("{i:-max_index_len$}")),
|
||||
..Default::default()
|
||||
.map(|(i, comp)| {
|
||||
let kind = comp.kind(db).map(ty_kind_to_lsp_kind);
|
||||
CompletionItem {
|
||||
label: comp.name.into(),
|
||||
kind,
|
||||
sort_text: Some(format!("{i:-max_index_len$}")),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let response = CompletionResponse::Array(items);
|
||||
@@ -69,3 +74,38 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
||||
impl RetriableRequestHandler for CompletionRequestHandler {
|
||||
const RETRY_ON_CANCELLATION: bool = true;
|
||||
}
|
||||
|
||||
fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind {
|
||||
// Gimme my dang globs in tight scopes!
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use self::CompletionKind::*;
|
||||
|
||||
// ref https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
|
||||
match kind {
|
||||
Text => CompletionItemKind::TEXT,
|
||||
Method => CompletionItemKind::METHOD,
|
||||
Function => CompletionItemKind::FUNCTION,
|
||||
Constructor => CompletionItemKind::CONSTRUCTOR,
|
||||
Field => CompletionItemKind::FIELD,
|
||||
Variable => CompletionItemKind::VARIABLE,
|
||||
Class => CompletionItemKind::CLASS,
|
||||
Interface => CompletionItemKind::INTERFACE,
|
||||
Module => CompletionItemKind::MODULE,
|
||||
Property => CompletionItemKind::PROPERTY,
|
||||
Unit => CompletionItemKind::UNIT,
|
||||
Value => CompletionItemKind::VALUE,
|
||||
Enum => CompletionItemKind::ENUM,
|
||||
Keyword => CompletionItemKind::KEYWORD,
|
||||
Snippet => CompletionItemKind::SNIPPET,
|
||||
Color => CompletionItemKind::COLOR,
|
||||
File => CompletionItemKind::FILE,
|
||||
Reference => CompletionItemKind::REFERENCE,
|
||||
Folder => CompletionItemKind::FOLDER,
|
||||
EnumMember => CompletionItemKind::ENUM_MEMBER,
|
||||
Constant => CompletionItemKind::CONSTANT,
|
||||
Struct => CompletionItemKind::STRUCT,
|
||||
Event => CompletionItemKind::EVENT,
|
||||
Operator => CompletionItemKind::OPERATOR,
|
||||
TypeParameter => CompletionItemKind::TYPE_PARAMETER,
|
||||
}
|
||||
}
|
||||
|
||||
19
crates/ty_static/Cargo.toml
Normal file
19
crates/ty_static/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "ty_static"
|
||||
version = "0.0.1"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
ruff_macros = { workspace = true }
|
||||
71
crates/ty_static/src/env_vars.rs
Normal file
71
crates/ty_static/src/env_vars.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use ruff_macros::attribute_env_vars_metadata;
|
||||
|
||||
/// Declares all environment variable used throughout `ty` and its crates.
|
||||
pub struct EnvVars;
|
||||
|
||||
#[attribute_env_vars_metadata]
|
||||
impl EnvVars {
|
||||
/// If set, ty will use this value as the log level for its `--verbose` output.
|
||||
/// Accepts any filter compatible with the `tracing_subscriber` crate.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// - `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line
|
||||
/// - `TY_LOG=trace` will enable all trace-level logging.
|
||||
///
|
||||
/// See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
|
||||
/// for more.
|
||||
pub const TY_LOG: &'static str = "TY_LOG";
|
||||
|
||||
/// If set to `"1"` or `"true"`, ty will enable flamegraph profiling.
|
||||
/// This creates a `tracing.folded` file that can be used to generate flame graphs
|
||||
/// for performance analysis.
|
||||
pub const TY_LOG_PROFILE: &'static str = "TY_LOG_PROFILE";
|
||||
|
||||
/// Control memory usage reporting format after ty execution.
|
||||
///
|
||||
/// Accepted values:
|
||||
///
|
||||
/// * `short` - Display short memory report
|
||||
/// * `mypy_primer` - Display mypy_primer format and suppress workspace diagnostics
|
||||
/// * `full` - Display full memory report
|
||||
#[attr_hidden]
|
||||
pub const TY_MEMORY_REPORT: &'static str = "TY_MEMORY_REPORT";
|
||||
|
||||
/// Specifies an upper limit for the number of tasks ty is allowed to run in parallel.
|
||||
///
|
||||
/// For example, how many files should be checked in parallel.
|
||||
/// This isn't the same as a thread limit. ty may spawn additional threads
|
||||
/// when necessary, e.g. to watch for file system changes or a dedicated UI thread.
|
||||
pub const TY_MAX_PARALLELISM: &'static str = "TY_MAX_PARALLELISM";
|
||||
|
||||
/// Used to detect an activated virtual environment.
|
||||
pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV";
|
||||
|
||||
/// Used to detect an activated Conda environment location.
|
||||
/// If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
|
||||
pub const CONDA_PREFIX: &'static str = "CONDA_PREFIX";
|
||||
|
||||
/// Filter which tests to run in mdtest.
|
||||
///
|
||||
/// Only tests whose names contain this filter string will be executed.
|
||||
#[attr_hidden]
|
||||
pub const MDTEST_TEST_FILTER: &'static str = "MDTEST_TEST_FILTER";
|
||||
|
||||
/// Switch mdtest output format to GitHub Actions annotations.
|
||||
///
|
||||
/// If set (to any value), mdtest will output errors in GitHub Actions format.
|
||||
#[attr_hidden]
|
||||
pub const MDTEST_GITHUB_ANNOTATIONS_FORMAT: &'static str = "MDTEST_GITHUB_ANNOTATIONS_FORMAT";
|
||||
|
||||
// Externally defined environment variables
|
||||
|
||||
/// Specifies an upper limit for the number of threads ty uses when performing work in parallel.
|
||||
/// Equivalent to `TY_MAX_PARALLELISM`.
|
||||
///
|
||||
/// This is a standard Rayon environment variable.
|
||||
pub const RAYON_NUM_THREADS: &'static str = "RAYON_NUM_THREADS";
|
||||
|
||||
/// Path to user-level configuration directory on Unix systems.
|
||||
pub const XDG_CONFIG_HOME: &'static str = "XDG_CONFIG_HOME";
|
||||
}
|
||||
3
crates/ty_static/src/lib.rs
Normal file
3
crates/ty_static/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub use env_vars::*;
|
||||
|
||||
mod env_vars;
|
||||
@@ -19,6 +19,7 @@ ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ty_python_semantic = { workspace = true, features = ["serde", "testing"] }
|
||||
ty_static = { workspace = true }
|
||||
ty_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -29,7 +29,7 @@ mod diagnostic;
|
||||
mod matcher;
|
||||
mod parser;
|
||||
|
||||
const MDTEST_TEST_FILTER: &str = "MDTEST_TEST_FILTER";
|
||||
use ty_static::EnvVars;
|
||||
|
||||
/// Run `path` as a markdown test suite with given `title`.
|
||||
///
|
||||
@@ -53,7 +53,7 @@ pub fn run(
|
||||
|
||||
let mut db = db::Db::setup();
|
||||
|
||||
let filter = std::env::var(MDTEST_TEST_FILTER).ok();
|
||||
let filter = std::env::var(EnvVars::MDTEST_TEST_FILTER).ok();
|
||||
let mut any_failures = false;
|
||||
for test in suite.tests() {
|
||||
if filter
|
||||
@@ -105,10 +105,12 @@ pub fn run(
|
||||
|
||||
if output_format.is_cli() {
|
||||
println!(
|
||||
"\nTo rerun this specific test, set the environment variable: {MDTEST_TEST_FILTER}='{escaped_test_name}'",
|
||||
"\nTo rerun this specific test, set the environment variable: {}='{escaped_test_name}'",
|
||||
EnvVars::MDTEST_TEST_FILTER,
|
||||
);
|
||||
println!(
|
||||
"{MDTEST_TEST_FILTER}='{escaped_test_name}' cargo test -p ty_python_semantic --test mdtest -- {test_name}",
|
||||
"{}='{escaped_test_name}' cargo test -p ty_python_semantic --test mdtest -- {test_name}",
|
||||
EnvVars::MDTEST_TEST_FILTER,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -322,14 +324,14 @@ fn run_test(
|
||||
let mut diagnostics: Vec<Diagnostic> = parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| Diagnostic::syntax_error(test_file.file, &error.error, error))
|
||||
.map(|error| Diagnostic::invalid_syntax(test_file.file, &error.error, error))
|
||||
.collect();
|
||||
|
||||
diagnostics.extend(
|
||||
parsed
|
||||
.unsupported_syntax_errors()
|
||||
.iter()
|
||||
.map(|error| Diagnostic::syntax_error(test_file.file, error, error)),
|
||||
.map(|error| Diagnostic::invalid_syntax(test_file.file, error, error)),
|
||||
);
|
||||
|
||||
let mdtest_result = attempt_test(db, check_types, test_file, "run mdtest", None);
|
||||
|
||||
@@ -57,6 +57,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
|
||||
"incorrect-blank-line-after-class",
|
||||
"incorrect-blank-line-before-class",
|
||||
"indentation-with-invalid-multiple",
|
||||
"indentation-with-invalid-multiple-comment",
|
||||
"line-too-long",
|
||||
"missing-trailing-comma",
|
||||
"missing-whitespace",
|
||||
@@ -111,7 +112,6 @@ KNOWN_FORMATTING_VIOLATIONS = [
|
||||
# For some docs, Ruff is unable to parse the example code.
|
||||
KNOWN_PARSE_ERRORS = [
|
||||
"blank-line-with-whitespace",
|
||||
"indentation-with-invalid-multiple-comment",
|
||||
"indented-form-feed",
|
||||
"missing-newline-at-end-of-file",
|
||||
"mixed-spaces-and-tabs",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)"
|
||||
mkdir -p ~/.config/ty
|
||||
@@ -19,7 +20,7 @@ cd ..
|
||||
echo "Project selector: $PRIMER_SELECTOR"
|
||||
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
|
||||
uvx \
|
||||
--from="git+https://github.com/hauntsaninja/mypy_primer@e5f55447969d33ae3c7ccdb183e2a37101867270" \
|
||||
--from="git+https://github.com/hauntsaninja/mypy_primer@59509d48de6da6aaa4e3a2f5e338769bc471f2d7" \
|
||||
mypy_primer \
|
||||
--repo ruff \
|
||||
--type-checker ty \
|
||||
|
||||
10
ty.schema.json
generated
10
ty.schema.json
generated
@@ -531,6 +531,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid-nonlocal": {
|
||||
"title": "detects unmatched `nonlocal` statements",
|
||||
"description": "## What it does\nDetects `nonlocal` statements that don't match a binding in any enclosing scope.\n\n## Why is this bad?\nUnmatched `nonlocal` statements will raise a `SyntaxError` at runtime.\n\n## Example\n```python\ndef f():\n nonlocal x # error: no binding for nonlocal 'x' found\n```",
|
||||
"default": "error",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Level"
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid-overload": {
|
||||
"title": "detects invalid `@overload` usages",
|
||||
"description": "## What it does\nChecks for various invalid `@overload` usages.\n\n## Why is this bad?\nThe `@overload` decorator is used to define functions and methods that accepts different\ncombinations of arguments and return different types based on the arguments passed. This is\nmainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type\nchecker may not be able to provide correct type information.\n\n## Example\n\nDefining only one overload:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo(x: int) -> int: ...\ndef foo(x: int | None) -> int | None:\n return x\n```\n\nOr, not providing an implementation for the overloaded definition:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo() -> None: ...\n@overload\ndef foo(x: int) -> int: ...\n```\n\n## References\n- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)",
|
||||
|
||||
Reference in New Issue
Block a user