Compare commits
1 Commits
0.12.11
...
david/map-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c605221c0 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,32 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.11
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
|
||||
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
|
||||
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
|
||||
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
|
||||
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
|
||||
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
|
||||
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
|
||||
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
|
||||
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
|
||||
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
|
||||
|
||||
## 0.12.10
|
||||
|
||||
### Preview features
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -2743,7 +2743,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2870,7 +2870,6 @@ dependencies = [
|
||||
"insta",
|
||||
"matchit",
|
||||
"path-slash",
|
||||
"pathdiff",
|
||||
"quick-junit",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
@@ -2999,7 +2998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3021,6 +3020,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"natord",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
"pep440_rs",
|
||||
"pyproject-toml",
|
||||
"regex",
|
||||
@@ -3337,7 +3337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.12.11/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.11/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.10/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.10/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.11
|
||||
rev: v0.12.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -15,7 +15,8 @@ use ruff_db::diagnostic::{
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{
|
||||
Emitter, EmitterContext, GithubEmitter, GroupedEmitter, SarifEmitter, TextEmitter,
|
||||
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, SarifEmitter,
|
||||
TextEmitter,
|
||||
};
|
||||
use ruff_linter::notify_user;
|
||||
use ruff_linter::settings::flags::{self};
|
||||
@@ -295,11 +296,7 @@ impl Printer {
|
||||
GithubEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
}
|
||||
OutputFormat::Gitlab => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Gitlab)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
GitlabEmitter::default().emit(writer, &diagnostics.inner, &context)?;
|
||||
}
|
||||
OutputFormat::Pylint => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
|
||||
@@ -20,59 +20,59 @@ exit_code: 1
|
||||
{
|
||||
"check_name": "F401",
|
||||
"description": "F401: `os` imported but unused",
|
||||
"severity": "major",
|
||||
"fingerprint": "4dbad37161e65c72",
|
||||
"location": {
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 8
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 10
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "F821",
|
||||
"description": "F821: Undefined name `y`",
|
||||
"severity": "major",
|
||||
"fingerprint": "7af59862a085230",
|
||||
"location": {
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 2,
|
||||
"column": 5
|
||||
"column": 5,
|
||||
"line": 2
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"column": 6
|
||||
"column": 6,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "invalid-syntax",
|
||||
"description": "invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"severity": "major",
|
||||
"fingerprint": "e558cec859bb66e8",
|
||||
"location": {
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 3,
|
||||
"column": 1
|
||||
"column": 1,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"line": 3,
|
||||
"column": 6
|
||||
"column": 6,
|
||||
"line": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
||||
----- stderr -----
|
||||
|
||||
@@ -450,6 +450,9 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
|
||||
r#"
|
||||
class C:
|
||||
def f(self: "C"):
|
||||
self.a = ""
|
||||
self.b = ""
|
||||
|
||||
if isinstance(self.a, str):
|
||||
return
|
||||
|
||||
@@ -463,56 +466,6 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
|
||||
return
|
||||
if isinstance(self.b, str):
|
||||
return
|
||||
if isinstance(self.b, str):
|
||||
return
|
||||
if isinstance(self.b, str):
|
||||
return
|
||||
|
||||
self.a = ""
|
||||
self.b = ""
|
||||
"#,
|
||||
)
|
||||
},
|
||||
|case| {
|
||||
let Case { db, .. } = case;
|
||||
let result = db.check();
|
||||
assert_eq!(result.len(), 0);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_complex_constrained_attributes_3(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
criterion.bench_function("ty_micro[complex_constrained_attributes_3]", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| {
|
||||
// This is a regression test for https://github.com/astral-sh/ty/issues/758
|
||||
setup_micro_case(
|
||||
r#"
|
||||
class GridOut:
|
||||
def __init__(self: "GridOut") -> None:
|
||||
self._buffer = b""
|
||||
|
||||
def _read_size_or_line(self: "GridOut", size: int = -1):
|
||||
if size > self._position:
|
||||
size = self._position
|
||||
pass
|
||||
if size == 0:
|
||||
return bytes()
|
||||
|
||||
while size > 0:
|
||||
if self._buffer:
|
||||
buf = self._buffer
|
||||
self._buffer = b""
|
||||
else:
|
||||
buf = b""
|
||||
|
||||
if len(buf) > size:
|
||||
self._buffer = buf
|
||||
self._position -= len(self._buffer)
|
||||
"#,
|
||||
)
|
||||
},
|
||||
@@ -715,7 +668,6 @@ criterion_group!(
|
||||
benchmark_tuple_implicit_instance_attributes,
|
||||
benchmark_complex_constrained_attributes_1,
|
||||
benchmark_complex_constrained_attributes_2,
|
||||
benchmark_complex_constrained_attributes_3,
|
||||
benchmark_many_enum_members,
|
||||
);
|
||||
criterion_group!(project, anyio, attrs, hydra, datetype);
|
||||
|
||||
@@ -34,7 +34,6 @@ glob = { workspace = true }
|
||||
ignore = { workspace = true, optional = true }
|
||||
matchit = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
pathdiff = { workspace = true }
|
||||
quick-junit = { workspace = true, optional = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
@@ -54,7 +53,7 @@ web-time = { version = "1.1.0" }
|
||||
etcetera = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1435,11 +1435,6 @@ pub enum DiagnosticFormat {
|
||||
/// Print diagnostics in the format expected by JUnit.
|
||||
#[cfg(feature = "junit")]
|
||||
Junit,
|
||||
/// Print diagnostics in the JSON format used by GitLab [Code Quality] reports.
|
||||
///
|
||||
/// [Code Quality]: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
#[cfg(feature = "serde")]
|
||||
Gitlab,
|
||||
}
|
||||
|
||||
/// A representation of the kinds of messages inside a diagnostic.
|
||||
|
||||
@@ -31,8 +31,6 @@ mod azure;
|
||||
mod concise;
|
||||
mod full;
|
||||
#[cfg(feature = "serde")]
|
||||
mod gitlab;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json_lines;
|
||||
@@ -138,10 +136,6 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
DiagnosticFormat::Junit => {
|
||||
junit::JunitRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
#[cfg(feature = "serde")]
|
||||
DiagnosticFormat::Gitlab => {
|
||||
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use ruff_source_file::LineColumn;
|
||||
use serde::{Serialize, Serializer, ser::SerializeSeq};
|
||||
|
||||
use crate::diagnostic::{Diagnostic, Severity};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct GitlabRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
}
|
||||
|
||||
impl<'a> GitlabRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver) -> Self {
|
||||
Self { resolver }
|
||||
}
|
||||
}
|
||||
|
||||
impl GitlabRenderer<'_> {
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&SerializedMessages {
|
||||
diagnostics,
|
||||
resolver: self.resolver,
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "We don't have access to a `System` here, \
|
||||
and this is only intended for use by GitLab CI, \
|
||||
which runs on a real `System`."
|
||||
)]
|
||||
project_dir: std::env::var("CI_PROJECT_DIR").ok().as_deref(),
|
||||
})
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializedMessages<'a> {
|
||||
diagnostics: &'a [Diagnostic],
|
||||
resolver: &'a dyn FileResolver,
|
||||
project_dir: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Serialize for SerializedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
|
||||
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
|
||||
|
||||
for diagnostic in self.diagnostics {
|
||||
let location = diagnostic
|
||||
.primary_span()
|
||||
.map(|span| {
|
||||
let file = span.file();
|
||||
let positions = if self.resolver.is_notebook(file) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
Default::default()
|
||||
} else {
|
||||
let diagnostic_source = file.diagnostic_source(self.resolver);
|
||||
let source_code = diagnostic_source.as_source_code();
|
||||
span.range()
|
||||
.map(|range| Positions {
|
||||
begin: source_code.line_column(range.start()),
|
||||
end: source_code.line_column(range.end()),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
|| file.relative_path(self.resolver).display().to_string(),
|
||||
|project_dir| relativize_path_to(file.path(self.resolver), project_dir),
|
||||
);
|
||||
|
||||
Location { path, positions }
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut message_fingerprint = fingerprint(diagnostic, &location.path, 0);
|
||||
|
||||
// Make sure that we do not get a fingerprint that is already in use
|
||||
// by adding in the previously generated one.
|
||||
while fingerprints.contains(&message_fingerprint) {
|
||||
message_fingerprint = fingerprint(diagnostic, &location.path, message_fingerprint);
|
||||
}
|
||||
fingerprints.insert(message_fingerprint);
|
||||
|
||||
let description = diagnostic.body();
|
||||
let check_name = diagnostic.secondary_code_or_id();
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => "info",
|
||||
Severity::Warning => "minor",
|
||||
Severity::Error => "major",
|
||||
// Another option here is `blocker`
|
||||
Severity::Fatal => "critical",
|
||||
};
|
||||
|
||||
let value = Message {
|
||||
check_name,
|
||||
// GitLab doesn't display the separate `check_name` field in a Code Quality report,
|
||||
// so prepend it to the description too.
|
||||
description: format!("{check_name}: {description}"),
|
||||
severity,
|
||||
fingerprint: format!("{:x}", message_fingerprint),
|
||||
location,
|
||||
};
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Message<'a> {
|
||||
check_name: &'a str,
|
||||
description: String,
|
||||
severity: &'static str,
|
||||
fingerprint: String,
|
||||
location: Location,
|
||||
}
|
||||
|
||||
/// The place in the source code where the issue was discovered.
|
||||
///
|
||||
/// According to the CodeClimate report format [specification] linked from the GitLab [docs], this
|
||||
/// field is required, so we fall back on a default `path` and position if the diagnostic doesn't
|
||||
/// have a primary span.
|
||||
///
|
||||
/// [specification]: https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
|
||||
/// [docs]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
|
||||
#[derive(Default, Serialize)]
|
||||
struct Location {
|
||||
path: String,
|
||||
positions: Positions,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
struct Positions {
|
||||
begin: LineColumn,
|
||||
end: LineColumn,
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(diagnostic: &Diagnostic, project_path: &str, salt: u64) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
salt.hash(&mut hasher);
|
||||
diagnostic.name().hash(&mut hasher);
|
||||
project_path.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// Convert an absolute path to be relative to the specified project root.
|
||||
fn relativize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root: R) -> String {
|
||||
format!(
|
||||
"{}",
|
||||
pathdiff::diff_paths(&path, project_root)
|
||||
.expect("Could not diff paths")
|
||||
.display()
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
|
||||
};
|
||||
|
||||
const FINGERPRINT_FILTERS: [(&str, &str); 1] = [(
|
||||
r#""fingerprint": "[a-z0-9]+","#,
|
||||
r#""fingerprint": "<redacted>","#,
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Gitlab);
|
||||
insta::with_settings!({filters => FINGERPRINT_FILTERS}, {
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Gitlab);
|
||||
insta::with_settings!({filters => FINGERPRINT_FILTERS}, {
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -51,6 +51,7 @@ path-absolutize = { workspace = true, features = [
|
||||
"once_cell_cache",
|
||||
"use_unix_paths_on_wasm",
|
||||
] }
|
||||
pathdiff = { workspace = true }
|
||||
pep440_rs = { workspace = true }
|
||||
pyproject-toml = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
Test: allowed-unused-imports-top-level-module
|
||||
"""
|
||||
|
||||
# No errors
|
||||
|
||||
def f():
|
||||
import hvplot
|
||||
def f():
|
||||
import hvplot.pandas
|
||||
def f():
|
||||
import hvplot.pandas.plots
|
||||
def f():
|
||||
from hvplot.pandas import scatter_matrix
|
||||
def f():
|
||||
from hvplot.pandas.plots import scatter_matrix
|
||||
@@ -58,3 +58,13 @@ pub fn relativize_path<P: AsRef<Path>>(path: P) -> String {
|
||||
}
|
||||
format!("{}", path.display())
|
||||
}
|
||||
|
||||
/// Convert an absolute path to be relative to the specified project root.
|
||||
pub fn relativize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root: R) -> String {
|
||||
format!(
|
||||
"{}",
|
||||
pathdiff::diff_paths(&path, project_root)
|
||||
.expect("Could not diff paths")
|
||||
.display()
|
||||
)
|
||||
}
|
||||
|
||||
174
crates/ruff_linter/src/message/gitlab.rs
Normal file
174
crates/ruff_linter/src/message/gitlab.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
|
||||
use crate::fs::{relativize_path, relativize_path_to};
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
|
||||
/// Generate JSON with violations in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
pub struct GitlabEmitter {
|
||||
project_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GitlabEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
project_dir: std::env::var("CI_PROJECT_DIR").ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Emitter for GitlabEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
serde_json::to_writer_pretty(
|
||||
writer,
|
||||
&SerializedMessages {
|
||||
diagnostics,
|
||||
context,
|
||||
project_dir: self.project_dir.as_deref(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializedMessages<'a> {
|
||||
diagnostics: &'a [Diagnostic],
|
||||
context: &'a EmitterContext<'a>,
|
||||
project_dir: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Serialize for SerializedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
|
||||
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
|
||||
|
||||
for diagnostic in self.diagnostics {
|
||||
let filename = diagnostic.expect_ruff_filename();
|
||||
|
||||
let (start_location, end_location) = if self.context.is_notebook(&filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
Default::default()
|
||||
} else {
|
||||
(
|
||||
diagnostic.expect_ruff_start_location(),
|
||||
diagnostic.expect_ruff_end_location(),
|
||||
)
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
|| relativize_path(&filename),
|
||||
|project_dir| relativize_path_to(&filename, project_dir),
|
||||
);
|
||||
|
||||
let mut message_fingerprint = fingerprint(diagnostic, &path, 0);
|
||||
|
||||
// Make sure that we do not get a fingerprint that is already in use
|
||||
// by adding in the previously generated one.
|
||||
while fingerprints.contains(&message_fingerprint) {
|
||||
message_fingerprint = fingerprint(diagnostic, &path, message_fingerprint);
|
||||
}
|
||||
fingerprints.insert(message_fingerprint);
|
||||
|
||||
let description = diagnostic.body();
|
||||
let check_name = diagnostic.secondary_code_or_id();
|
||||
|
||||
let value = json!({
|
||||
"check_name": check_name,
|
||||
// GitLab doesn't display the separate `check_name` field in a Code Quality report,
|
||||
// so prepend it to the description too.
|
||||
"description": format!("{check_name}: {description}"),
|
||||
"severity": "major",
|
||||
"fingerprint": format!("{:x}", message_fingerprint),
|
||||
"location": {
|
||||
"path": path,
|
||||
"positions": {
|
||||
"begin": start_location,
|
||||
"end": end_location,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(message: &Diagnostic, project_path: &str, salt: u64) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
salt.hash(&mut hasher);
|
||||
message.name().hash(&mut hasher);
|
||||
project_path.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::GitlabEmitter;
|
||||
use crate::message::tests::{
|
||||
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = GitlabEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(redact_fingerprint(&content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let mut emitter = GitlabEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
||||
|
||||
assert_snapshot!(redact_fingerprint(&content));
|
||||
}
|
||||
|
||||
// Redact the fingerprint because the default hasher isn't stable across platforms.
|
||||
fn redact_fingerprint(content: &str) -> String {
|
||||
static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#;
|
||||
|
||||
let mut output = String::with_capacity(content.len());
|
||||
let mut last = 0;
|
||||
|
||||
for (start, _) in content.match_indices(FINGERPRINT_HAY_KEY) {
|
||||
let fingerprint_hash_start = start + FINGERPRINT_HAY_KEY.len();
|
||||
output.push_str(&content[last..fingerprint_hash_start]);
|
||||
output.push_str("<redacted>");
|
||||
last = fingerprint_hash_start
|
||||
+ content[fingerprint_hash_start..]
|
||||
.find('"')
|
||||
.expect("Expected terminating quote");
|
||||
}
|
||||
|
||||
output.push_str(&content[last..]);
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use ruff_db::diagnostic::{
|
||||
use ruff_db::files::File;
|
||||
|
||||
pub use github::GithubEmitter;
|
||||
pub use gitlab::GitlabEmitter;
|
||||
pub use grouped::GroupedEmitter;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::SourceFile;
|
||||
@@ -21,6 +22,7 @@ use crate::Fix;
|
||||
use crate::registry::Rule;
|
||||
|
||||
mod github;
|
||||
mod gitlab;
|
||||
mod grouped;
|
||||
mod sarif;
|
||||
mod text;
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
---
|
||||
source: crates/ruff_db/src/diagnostic/render/gitlab.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
source: crates/ruff_linter/src/message/gitlab.rs
|
||||
expression: redact_fingerprint(&content)
|
||||
---
|
||||
[
|
||||
{
|
||||
"check_name": "F401",
|
||||
"description": "F401: `os` imported but unused",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 8
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 10
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "F841",
|
||||
"description": "F841: Local variable `x` is assigned to but never used",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 6,
|
||||
"column": 5
|
||||
"column": 5,
|
||||
"line": 6
|
||||
},
|
||||
"end": {
|
||||
"line": 6,
|
||||
"column": 6
|
||||
"column": 6,
|
||||
"line": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "F821",
|
||||
"description": "F821: Undefined name `a`",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "undef.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 4
|
||||
"column": 4,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 5
|
||||
"column": 5,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
||||
@@ -1,44 +1,44 @@
|
||||
---
|
||||
source: crates/ruff_db/src/diagnostic/render/gitlab.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
source: crates/ruff_linter/src/message/gitlab.rs
|
||||
expression: redact_fingerprint(&content)
|
||||
---
|
||||
[
|
||||
{
|
||||
"check_name": "invalid-syntax",
|
||||
"description": "invalid-syntax: Expected one or more symbol names after import",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 15
|
||||
"column": 15,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"column": 1
|
||||
"column": 1,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "invalid-syntax",
|
||||
"description": "invalid-syntax: Expected ')', found newline",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 3,
|
||||
"column": 12
|
||||
"column": 12,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"line": 4,
|
||||
"column": 1
|
||||
"column": 1,
|
||||
"line": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
||||
@@ -376,22 +376,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_35.py"))]
|
||||
fn f401_allowed_unused_imports_top_level_module(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyflakes").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
pyflakes: pyflakes::settings::Settings {
|
||||
allowed_unused_imports: vec!["hvplot".to_string()],
|
||||
..pyflakes::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f841_dummy_variable_rgx() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -334,7 +334,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) {
|
||||
.allowed_unused_imports
|
||||
.iter()
|
||||
.any(|allowed_unused_import| {
|
||||
let allowed_unused_import = QualifiedName::user_defined(allowed_unused_import);
|
||||
let allowed_unused_import = QualifiedName::from_dotted_name(allowed_unused_import);
|
||||
import.qualified_name().starts_with(&allowed_unused_import)
|
||||
})
|
||||
{
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -2288,26 +2288,6 @@ class H:
|
||||
self.x = other.x or self.x
|
||||
```
|
||||
|
||||
An attribute definition can be guarded by a condition involving that attribute. This is a regression
|
||||
test for <https://github.com/astral-sh/ty/issues/692>:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def check(x) -> Literal[False]:
|
||||
return False
|
||||
|
||||
class Toggle:
|
||||
def __init__(self: "Toggle"):
|
||||
if not self.x:
|
||||
self.x: Literal[True] = True
|
||||
if check(self.y):
|
||||
self.y = True
|
||||
|
||||
reveal_type(Toggle().x) # revealed: Literal[True]
|
||||
reveal_type(Toggle().y) # revealed: Unknown | Literal[True]
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
|
||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||
|
||||
@@ -1564,24 +1564,6 @@ if True:
|
||||
from module import symbol
|
||||
```
|
||||
|
||||
## Non-definitely bound symbols in conditions
|
||||
|
||||
When a non-definitely bound symbol is used as a (part of a) condition, we always infer an ambiguous
|
||||
truthiness. If we didn't do that, `x` would be considered definitely bound in the following example:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
ALWAYS_TRUE_IF_BOUND = True
|
||||
|
||||
# error: [possibly-unresolved-reference] "Name `ALWAYS_TRUE_IF_BOUND` used when possibly not defined"
|
||||
if True and ALWAYS_TRUE_IF_BOUND:
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference] "Name `x` used when possibly not defined"
|
||||
x
|
||||
```
|
||||
|
||||
## Unreachable code
|
||||
|
||||
A closely related feature is the ability to detect unreachable code. For example, we do not emit a
|
||||
|
||||
@@ -135,10 +135,6 @@ impl<'db> Place<'db> {
|
||||
Place::Unbound => Place::Unbound,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_definitely_bound(&self) -> bool {
|
||||
matches!(self, Place::Type(_, Boundness::Bound))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<LookupResult<'db>> for PlaceAndQualifiers<'db> {
|
||||
|
||||
@@ -209,7 +209,6 @@ use crate::semantic_index::predicate::{
|
||||
};
|
||||
use crate::types::{
|
||||
IntersectionBuilder, Truthiness, Type, UnionBuilder, UnionType, infer_expression_type,
|
||||
static_expression_truthiness,
|
||||
};
|
||||
|
||||
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
|
||||
@@ -822,7 +821,8 @@ impl ReachabilityConstraints {
|
||||
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
|
||||
match predicate.node {
|
||||
PredicateNode::Expression(test_expr) => {
|
||||
static_expression_truthiness(db, test_expr).negate_if(!predicate.is_positive)
|
||||
let ty = infer_expression_type(db, test_expr);
|
||||
ty.bool(db).negate_if(!predicate.is_positive)
|
||||
}
|
||||
PredicateNode::ReturnsNever(CallableAndCallExpr {
|
||||
callable,
|
||||
|
||||
@@ -598,7 +598,7 @@ impl<'db> UseDefMap<'db> {
|
||||
.is_always_false()
|
||||
}
|
||||
|
||||
pub(crate) fn declaration_reachability(
|
||||
pub(crate) fn is_declaration_reachable(
|
||||
&self,
|
||||
db: &dyn crate::Db,
|
||||
declaration: &DeclarationWithConstraint<'db>,
|
||||
@@ -610,7 +610,7 @@ impl<'db> UseDefMap<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn binding_reachability(
|
||||
pub(crate) fn is_binding_reachable(
|
||||
&self,
|
||||
db: &dyn crate::Db,
|
||||
binding: &BindingWithConstraints<'_, 'db>,
|
||||
|
||||
@@ -24,7 +24,7 @@ pub use self::diagnostic::TypeCheckDiagnostics;
|
||||
pub(crate) use self::diagnostic::register_lints;
|
||||
pub(crate) use self::infer::{
|
||||
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
|
||||
infer_scope_types, static_expression_truthiness,
|
||||
infer_scope_types,
|
||||
};
|
||||
pub(crate) use self::signatures::{CallableSignature, Signature};
|
||||
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
|
||||
@@ -6910,10 +6910,6 @@ bitflags! {
|
||||
const NOT_REQUIRED = 1 << 4;
|
||||
/// `typing_extensions.ReadOnly`
|
||||
const READ_ONLY = 1 << 5;
|
||||
/// An implicit instance attribute which is possibly unbound according
|
||||
/// to local control flow within the method it is defined in. This flag
|
||||
/// overrules the `Boundness` information on `PlaceAndQualifiers`.
|
||||
const POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE = 1 << 6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8624,7 +8620,7 @@ impl TypeRelation {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Truthiness {
|
||||
/// For an object `x`, `bool(x)` will always return `True`
|
||||
AlwaysTrue,
|
||||
@@ -9549,7 +9545,7 @@ impl<'db> UnionType<'db> {
|
||||
place: ty_member,
|
||||
qualifiers: new_qualifiers,
|
||||
} = transform_fn(ty);
|
||||
qualifiers |= new_qualifiers;
|
||||
qualifiers &= new_qualifiers;
|
||||
match ty_member {
|
||||
Place::Unbound => {
|
||||
possibly_unbound = true;
|
||||
|
||||
@@ -2736,20 +2736,17 @@ impl<'db> ClassLiteral<'db> {
|
||||
// self.name: <annotation>
|
||||
// self.name: <annotation> = …
|
||||
|
||||
let reachability = use_def_map(db, method_scope)
|
||||
.declaration_reachability(db, &attribute_declaration);
|
||||
|
||||
if reachability.is_always_false() {
|
||||
if use_def_map(db, method_scope)
|
||||
.is_declaration_reachable(db, &attribute_declaration)
|
||||
.is_always_false()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let annotation = declaration_type(db, declaration);
|
||||
let mut annotation =
|
||||
let annotation =
|
||||
Place::bound(annotation.inner).with_qualifiers(annotation.qualifiers);
|
||||
|
||||
if reachability.is_ambiguous() {
|
||||
annotation.qualifiers |= TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE;
|
||||
}
|
||||
if let Some(all_qualifiers) = annotation.is_bare_final() {
|
||||
if let Some(value) = assignment.value(&module) {
|
||||
// If we see an annotated assignment with a bare `Final` as in
|
||||
@@ -2792,7 +2789,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
.all_reachable_symbol_bindings(method_place)
|
||||
.find_map(|bind| {
|
||||
(bind.binding.is_defined_and(|def| def == method))
|
||||
.then(|| class_map.binding_reachability(db, &bind))
|
||||
.then(|| class_map.is_binding_reachable(db, &bind))
|
||||
})
|
||||
.unwrap_or(Truthiness::AlwaysFalse)
|
||||
} else {
|
||||
@@ -2821,16 +2818,12 @@ impl<'db> ClassLiteral<'db> {
|
||||
continue;
|
||||
};
|
||||
match method_map
|
||||
.binding_reachability(db, &attribute_assignment)
|
||||
.is_binding_reachable(db, &attribute_assignment)
|
||||
.and(is_method_reachable)
|
||||
{
|
||||
Truthiness::AlwaysTrue => {
|
||||
Truthiness::AlwaysTrue | Truthiness::Ambiguous => {
|
||||
is_attribute_bound = true;
|
||||
}
|
||||
Truthiness::Ambiguous => {
|
||||
is_attribute_bound = true;
|
||||
qualifiers |= TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE;
|
||||
}
|
||||
Truthiness::AlwaysFalse => {
|
||||
continue;
|
||||
}
|
||||
@@ -2841,7 +2834,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
// TODO: this is incomplete logic since the attributes bound after termination are considered reachable.
|
||||
let unbound_reachability = unbound_binding
|
||||
.as_ref()
|
||||
.map(|binding| method_map.binding_reachability(db, binding))
|
||||
.map(|binding| method_map.is_binding_reachable(db, binding))
|
||||
.unwrap_or(Truthiness::AlwaysFalse);
|
||||
|
||||
if unbound_reachability
|
||||
|
||||
@@ -337,45 +337,6 @@ fn single_expression_cycle_initial<'db>(
|
||||
Type::Never
|
||||
}
|
||||
|
||||
/// Returns the statically-known truthiness of a given expression.
|
||||
///
|
||||
/// Returns [`Truthiness::Ambiguous`] in case any non-definitely bound places
|
||||
/// were encountered while inferring the type of the expression.
|
||||
#[salsa::tracked(cycle_fn=static_expression_truthiness_cycle_recover, cycle_initial=static_expression_truthiness_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)]
|
||||
pub(crate) fn static_expression_truthiness<'db>(
|
||||
db: &'db dyn Db,
|
||||
expression: Expression<'db>,
|
||||
) -> Truthiness {
|
||||
let inference = infer_expression_types(db, expression);
|
||||
|
||||
if !inference.all_places_definitely_bound() {
|
||||
return Truthiness::Ambiguous;
|
||||
}
|
||||
|
||||
let file = expression.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
let node = expression.node_ref(db, &module);
|
||||
|
||||
inference.expression_type(node).bool(db)
|
||||
}
|
||||
|
||||
#[expect(clippy::trivially_copy_pass_by_ref)]
|
||||
fn static_expression_truthiness_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Truthiness,
|
||||
_count: u32,
|
||||
_expression: Expression<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Truthiness> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn static_expression_truthiness_cycle_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_expression: Expression<'db>,
|
||||
) -> Truthiness {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
|
||||
/// Infer the types for an [`Unpack`] operation.
|
||||
///
|
||||
/// This infers the expression type and performs structural match against the target expression
|
||||
@@ -696,9 +657,6 @@ struct ExpressionInferenceExtra<'db> {
|
||||
///
|
||||
/// Falls back to `Type::Never` if an expression is missing.
|
||||
cycle_fallback: bool,
|
||||
|
||||
/// `true` if all places in this expression are definitely bound
|
||||
all_definitely_bound: bool,
|
||||
}
|
||||
|
||||
impl<'db> ExpressionInference<'db> {
|
||||
@@ -707,7 +665,6 @@ impl<'db> ExpressionInference<'db> {
|
||||
Self {
|
||||
extra: Some(Box::new(ExpressionInferenceExtra {
|
||||
cycle_fallback: true,
|
||||
all_definitely_bound: true,
|
||||
..ExpressionInferenceExtra::default()
|
||||
})),
|
||||
expressions: FxHashMap::default(),
|
||||
@@ -741,14 +698,6 @@ impl<'db> ExpressionInference<'db> {
|
||||
fn fallback_type(&self) -> Option<Type<'db>> {
|
||||
self.is_cycle_callback().then_some(Type::Never)
|
||||
}
|
||||
|
||||
/// Returns true if all places in this expression are definitely bound.
|
||||
pub(crate) fn all_places_definitely_bound(&self) -> bool {
|
||||
self.extra
|
||||
.as_ref()
|
||||
.map(|e| e.all_definitely_bound)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the intersection type is on the left or right side of the comparison.
|
||||
@@ -898,9 +847,6 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
|
||||
///
|
||||
/// This is used only when constructing a cycle-recovery `TypeInference`.
|
||||
cycle_fallback: bool,
|
||||
|
||||
/// `true` if all places in this expression are definitely bound
|
||||
all_definitely_bound: bool,
|
||||
}
|
||||
|
||||
impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
@@ -934,7 +880,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
deferred: VecSet::default(),
|
||||
undecorated_type: None,
|
||||
cycle_fallback: false,
|
||||
all_definitely_bound: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6669,7 +6614,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let (resolved, constraint_keys) =
|
||||
self.infer_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node));
|
||||
|
||||
let resolved_after_fallback = resolved
|
||||
resolved
|
||||
// Not found in the module's explicitly declared global symbols?
|
||||
// Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc.
|
||||
// These are looked up as attributes on `types.ModuleType`.
|
||||
@@ -6705,14 +6650,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
} else {
|
||||
Place::Unbound.into()
|
||||
}
|
||||
});
|
||||
|
||||
if !resolved_after_fallback.place.is_definitely_bound() {
|
||||
self.all_definitely_bound = false;
|
||||
}
|
||||
|
||||
let ty =
|
||||
resolved_after_fallback.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
})
|
||||
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound(qualifiers) => {
|
||||
self.report_unresolved_reference(name_node);
|
||||
TypeAndQualifiers::new(Type::unknown(), qualifiers)
|
||||
@@ -6723,9 +6662,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
type_when_bound
|
||||
}
|
||||
});
|
||||
|
||||
ty.inner_type()
|
||||
})
|
||||
.inner_type()
|
||||
}
|
||||
|
||||
fn infer_local_place_load(
|
||||
@@ -7155,7 +7093,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
fn narrow_expr_with_applicable_constraints<'r>(
|
||||
&mut self,
|
||||
&self,
|
||||
target: impl Into<ast::ExprRef<'r>>,
|
||||
target_ty: Type<'db>,
|
||||
constraint_keys: &[(FileScopeId, ConstraintKey)],
|
||||
@@ -7198,19 +7136,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
assigned_type = Some(ty);
|
||||
}
|
||||
}
|
||||
let fallback_place = value_type.member(db, &attr.id);
|
||||
if !fallback_place.place.is_definitely_bound()
|
||||
|| fallback_place
|
||||
.qualifiers
|
||||
.contains(TypeQualifiers::POSSIBLY_UNBOUND_IMPLICIT_ATTRIBUTE)
|
||||
{
|
||||
self.all_definitely_bound = false;
|
||||
}
|
||||
|
||||
let resolved_type =
|
||||
fallback_place.map_type(|ty| {
|
||||
self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys)
|
||||
}).unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
let resolved_type = value_type
|
||||
.member(db, &attr.id)
|
||||
.map_type(|ty| self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys))
|
||||
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound(_) => {
|
||||
let report_unresolved_attribute = self.is_reachable(attribute);
|
||||
|
||||
@@ -9318,7 +9248,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
declarations,
|
||||
deferred,
|
||||
cycle_fallback,
|
||||
all_definitely_bound,
|
||||
|
||||
// Ignored; only relevant to definition regions
|
||||
undecorated_type: _,
|
||||
@@ -9345,7 +9274,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
|
||||
let extra =
|
||||
(cycle_fallback || !bindings.is_empty() || !diagnostics.is_empty() || !all_definitely_bound).then(|| {
|
||||
(cycle_fallback || !bindings.is_empty() || !diagnostics.is_empty()).then(|| {
|
||||
if bindings.len() > 20 {
|
||||
tracing::debug!(
|
||||
"Inferred expression region `{:?}` contains {} bindings. Lookups by linear scan might be slow.",
|
||||
@@ -9358,7 +9287,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
bindings: bindings.into_boxed_slice(),
|
||||
diagnostics,
|
||||
cycle_fallback,
|
||||
all_definitely_bound,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -9384,7 +9312,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
deferred,
|
||||
cycle_fallback,
|
||||
undecorated_type,
|
||||
all_definitely_bound: _,
|
||||
|
||||
// builder only state
|
||||
typevar_binding_context: _,
|
||||
deferred_state: _,
|
||||
@@ -9451,7 +9379,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
deferred: _,
|
||||
bindings: _,
|
||||
declarations: _,
|
||||
all_definitely_bound: _,
|
||||
|
||||
// Ignored; only relevant to definition regions
|
||||
undecorated_type: _,
|
||||
|
||||
@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
|
||||
stage: build
|
||||
interruptible: true
|
||||
image:
|
||||
name: ghcr.io/astral-sh/ruff:0.12.11-alpine
|
||||
name: ghcr.io/astral-sh/ruff:0.12.10-alpine
|
||||
before_script:
|
||||
- cd $CI_PROJECT_DIR
|
||||
- ruff --version
|
||||
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.11
|
||||
rev: v0.12.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.11
|
||||
rev: v0.12.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.11
|
||||
rev: v0.12.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.11
|
||||
rev: v0.12.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "scripts"
|
||||
version = "0.12.11"
|
||||
version = "0.12.10"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user