Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
1dc0a523de Separate TOC from the navigation 2024-09-05 16:08:53 +05:30
108 changed files with 1494 additions and 5711 deletions

View File

@@ -45,7 +45,7 @@ repos:
)$
- repo: https://github.com/crate-ci/typos
rev: v1.24.5
rev: v1.24.3
hooks:
- id: typos
@@ -59,7 +59,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4
rev: v0.6.3
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,34 +1,5 @@
# Changelog
## 0.6.4
### Preview features
- \[`flake8-builtins`\] Use dynamic builtins list based on Python version ([#13172](https://github.com/astral-sh/ruff/pull/13172))
- \[`pydoclint`\] Permit yielding `None` in `DOC402` and `DOC403` ([#13148](https://github.com/astral-sh/ruff/pull/13148))
- \[`pylint`\] Update diagnostic message for `PLW3201` ([#13194](https://github.com/astral-sh/ruff/pull/13194))
- \[`ruff`\] Implement `post-init-default` (`RUF033`) ([#13192](https://github.com/astral-sh/ruff/pull/13192))
- \[`ruff`\] Implement useless if-else (`RUF034`) ([#13218](https://github.com/astral-sh/ruff/pull/13218))
### Rule changes
- \[`flake8-pyi`\] Respect `pep8_naming.classmethod-decorators` settings when determining if a method is a classmethod in `custom-type-var-return-type` (`PYI019`) ([#13162](https://github.com/astral-sh/ruff/pull/13162))
- \[`flake8-pyi`\] Teach various rules that annotations might be stringized ([#12951](https://github.com/astral-sh/ruff/pull/12951))
- \[`pylint`\] Avoid `no-self-use` for `attrs`-style validators ([#13166](https://github.com/astral-sh/ruff/pull/13166))
- \[`pylint`\] Recurse into subscript subexpressions when searching for list/dict lookups (`PLR1733`, `PLR1736`) ([#13186](https://github.com/astral-sh/ruff/pull/13186))
- \[`pyupgrade`\] Detect `aiofiles.open` calls in `UP015` ([#13173](https://github.com/astral-sh/ruff/pull/13173))
- \[`pyupgrade`\] Mark `sys.version_info[0] < 3` and similar comparisons as outdated (`UP036`) ([#13175](https://github.com/astral-sh/ruff/pull/13175))
### CLI
- Enrich messages of SARIF results ([#13180](https://github.com/astral-sh/ruff/pull/13180))
- Handle singular case for incompatible rules warning in `ruff format` output ([#13212](https://github.com/astral-sh/ruff/pull/13212))
### Bug fixes
- \[`pydocstyle`\] Improve heuristics for detecting Google-style docstrings ([#13142](https://github.com/astral-sh/ruff/pull/13142))
- \[`refurb`\] Treat `sep` arguments with effects as unsafe removals (`FURB105`) ([#13165](https://github.com/astral-sh/ruff/pull/13165))
## 0.6.3
### Preview features

7
Cargo.lock generated
View File

@@ -1936,7 +1936,6 @@ dependencies = [
"smallvec",
"static_assertions",
"tempfile",
"test-case",
"thiserror",
"tracing",
"walkdir",
@@ -2092,7 +2091,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.6.4"
version = "0.6.3"
dependencies = [
"anyhow",
"argfile",
@@ -2285,7 +2284,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.6.4"
version = "0.6.3"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2605,7 +2604,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.6.4"
version = "0.6.3"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -136,8 +136,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.6.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.6.4/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.6.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.6.3/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -170,7 +170,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.6.4
rev: v0.6.3
hooks:
# Run the linter.
- id: ruff

View File

@@ -33,7 +33,6 @@ rustc-hash = { workspace = true }
hashbrown = { workspace = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
test-case = { workspace = true }
[build-dependencies]
path-slash = { workspace = true }

View File

@@ -0,0 +1,16 @@
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::semantic_index::global_scope;
use crate::semantic_index::symbol::ScopeId;
use crate::Db;
/// Salsa query to get the builtins scope.
///
/// Can return None if a custom typeshed is used that is missing `builtins.pyi`.
#[salsa::tracked]
pub(crate) fn builtins_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
let builtins_name =
ModuleName::new_static("builtins").expect("Expected 'builtins' to be a valid module name");
let builtins_file = resolve_module(db, builtins_name)?.file();
Some(global_scope(db, builtins_file))
}

View File

@@ -10,6 +10,7 @@ pub use python_version::PythonVersion;
pub use semantic_model::{HasTy, SemanticModel};
pub mod ast_node_ref;
mod builtins;
mod db;
mod module_name;
mod module_resolver;
@@ -19,7 +20,6 @@ mod python_version;
pub mod semantic_index;
mod semantic_model;
pub(crate) mod site_packages;
mod stdlib;
pub mod types;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;

View File

@@ -59,10 +59,6 @@ impl ModulePath {
self.relative_path.push(component);
}
pub(crate) fn pop(&mut self) -> bool {
self.relative_path.pop()
}
#[must_use]
pub(super) fn is_directory(&self, resolver: &ResolverContext) -> bool {
let ModulePath {

View File

@@ -569,16 +569,24 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
package_path.push(module_name);
// Check for a regular package first (highest priority)
package_path.push("__init__");
if let Some(regular_package) = resolve_file_module(&package_path, &resolver_state) {
return Some((search_path.clone(), regular_package, ModuleKind::Package));
// Must be a `__init__.pyi` or `__init__.py` or it isn't a package.
let kind = if package_path.is_directory(&resolver_state) {
package_path.push("__init__");
ModuleKind::Package
} else {
ModuleKind::Module
};
// TODO Implement full https://peps.python.org/pep-0561/#type-checker-module-resolution-order resolution
if let Some(stub) = package_path.with_pyi_extension().to_file(&resolver_state) {
return Some((search_path.clone(), stub, kind));
}
// Check for a file module next
package_path.pop();
if let Some(file_module) = resolve_file_module(&package_path, &resolver_state) {
return Some((search_path.clone(), file_module, ModuleKind::Module));
if let Some(module) = package_path
.with_py_extension()
.and_then(|path| path.to_file(&resolver_state))
{
return Some((search_path.clone(), module, kind));
}
// For regular packages, don't search the next search path. All files of that
@@ -599,23 +607,6 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
None
}
/// If `module` exists on disk with either a `.pyi` or `.py` extension,
/// return the [`File`] corresponding to that path.
///
/// `.pyi` files take priority, as they always have priority when
/// resolving modules.
fn resolve_file_module(module: &ModulePath, resolver_state: &ResolverContext) -> Option<File> {
// Stubs have precedence over source files
module
.with_pyi_extension()
.to_file(resolver_state)
.or_else(|| {
module
.with_py_extension()
.and_then(|path| path.to_file(resolver_state))
})
}
fn resolve_package<'a, 'db, I>(
module_search_path: &SearchPath,
components: I,
@@ -642,10 +633,7 @@ where
if is_regular_package {
in_namespace_package = false;
} else if package_path.is_directory(resolver_state)
// Pure modules hide namespace packages with the same name
&& resolve_file_module(&package_path, resolver_state).is_none()
{
} else if package_path.is_directory(resolver_state) {
// A directory without an `__init__.py` is a namespace package, continue with the next folder.
in_namespace_package = true;
} else if in_namespace_package {
@@ -1103,25 +1091,6 @@ mod tests {
);
}
#[test]
fn single_file_takes_priority_over_namespace_package() {
//const SRC: &[FileSpec] = &[("foo.py", "x = 1")];
const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/bar.py", "x = 2")];
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
let foo_module_name = ModuleName::new_static("foo").unwrap();
let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
// `foo.py` takes priority over the `foo` namespace package
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
assert_eq!(foo_module.file().path(&db), &src.join("foo.py"));
// `foo.bar` isn't recognised as a module
let foo_bar_module = resolve_module(&db, foo_bar_module_name.clone());
assert_eq!(foo_bar_module, None);
}
#[test]
fn typing_stub_over_module() {
const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];

View File

@@ -21,15 +21,12 @@ use crate::Db;
pub mod ast_ids;
mod builder;
pub(crate) mod constraint;
pub mod definition;
pub mod expression;
pub mod symbol;
mod use_def;
pub(crate) use self::use_def::{
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
};
pub(crate) use self::use_def::{DefinitionWithConstraints, DefinitionWithConstraintsIterator};
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
@@ -328,16 +325,16 @@ mod tests {
use crate::Db;
impl UseDefMap<'_> {
fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
self.public_bindings(symbol)
fn first_public_definition(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
self.public_definitions(symbol)
.next()
.map(|constrained_binding| constrained_binding.binding)
.map(|constrained_definition| constrained_definition.definition)
}
fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
self.bindings_at_use(use_id)
fn first_use_definition(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
self.use_definitions(use_id)
.next()
.map(|constrained_binding| constrained_binding.binding)
.map(|constrained_definition| constrained_definition.definition)
}
}
@@ -399,8 +396,8 @@ mod tests {
let foo = global_table.symbol_id_by_name("foo").unwrap();
let use_def = use_def_map(&db, scope);
let binding = use_def.first_public_binding(foo).unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Import(_)));
let definition = use_def.first_public_definition(foo).unwrap();
assert!(matches!(definition.node(&db), DefinitionKind::Import(_)));
}
#[test]
@@ -429,19 +426,22 @@ mod tests {
assert!(
global_table
.symbol_by_name("foo")
.is_some_and(|symbol| { symbol.is_bound() && !symbol.is_used() }),
.is_some_and(|symbol| { symbol.is_defined() && !symbol.is_used() }),
"symbols that are defined get the defined flag"
);
let use_def = use_def_map(&db, scope);
let binding = use_def
.first_public_binding(
let definition = use_def
.first_public_definition(
global_table
.symbol_id_by_name("foo")
.expect("symbol to exist"),
)
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::ImportFrom(_)));
assert!(matches!(
definition.node(&db),
DefinitionKind::ImportFrom(_)
));
}
#[test]
@@ -454,14 +454,17 @@ mod tests {
assert!(
global_table
.symbol_by_name("foo")
.is_some_and(|symbol| { !symbol.is_bound() && symbol.is_used() }),
.is_some_and(|symbol| { !symbol.is_defined() && symbol.is_used() }),
"a symbol used but not defined in a scope should have only the used flag"
);
let use_def = use_def_map(&db, scope);
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name("x").expect("symbol exists"))
let definition = use_def
.first_public_definition(global_table.symbol_id_by_name("x").expect("symbol exists"))
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
assert!(matches!(
definition.node(&db),
DefinitionKind::Assignment(_)
));
}
#[test]
@@ -473,12 +476,12 @@ mod tests {
assert_eq!(names(&global_table), vec!["x"]);
let use_def = use_def_map(&db, scope);
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name("x").unwrap())
let definition = use_def
.first_public_definition(global_table.symbol_id_by_name("x").unwrap())
.unwrap();
assert!(matches!(
binding.kind(&db),
definition.node(&db),
DefinitionKind::AugmentedAssignment(_)
));
}
@@ -511,10 +514,13 @@ y = 2
assert_eq!(names(&class_table), vec!["x"]);
let use_def = index.use_def_map(class_scope_id);
let binding = use_def
.first_public_binding(class_table.symbol_id_by_name("x").expect("symbol exists"))
let definition = use_def
.first_public_definition(class_table.symbol_id_by_name("x").expect("symbol exists"))
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
assert!(matches!(
definition.node(&db),
DefinitionKind::Assignment(_)
));
}
#[test]
@@ -544,14 +550,17 @@ y = 2
assert_eq!(names(&function_table), vec!["x"]);
let use_def = index.use_def_map(function_scope_id);
let binding = use_def
.first_public_binding(
let definition = use_def
.first_public_definition(
function_table
.symbol_id_by_name("x")
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
assert!(matches!(
definition.node(&db),
DefinitionKind::Assignment(_)
));
}
#[test]
@@ -583,27 +592,27 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
let use_def = index.use_def_map(function_scope_id);
for name in ["a", "b", "c", "d"] {
let binding = use_def
.first_public_binding(
let definition = use_def
.first_public_definition(
function_table
.symbol_id_by_name(name)
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
binding.kind(&db),
definition.node(&db),
DefinitionKind::ParameterWithDefault(_)
));
}
for name in ["args", "kwargs"] {
let binding = use_def
.first_public_binding(
let definition = use_def
.first_public_definition(
function_table
.symbol_id_by_name(name)
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
assert!(matches!(definition.node(&db), DefinitionKind::Parameter(_)));
}
}
@@ -631,19 +640,23 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
let use_def = index.use_def_map(lambda_scope_id);
for name in ["a", "b", "c", "d"] {
let binding = use_def
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
let definition = use_def
.first_public_definition(
lambda_table.symbol_id_by_name(name).expect("symbol exists"),
)
.unwrap();
assert!(matches!(
binding.kind(&db),
definition.node(&db),
DefinitionKind::ParameterWithDefault(_)
));
}
for name in ["args", "kwargs"] {
let binding = use_def
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
let definition = use_def
.first_public_definition(
lambda_table.symbol_id_by_name(name).expect("symbol exists"),
)
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
assert!(matches!(definition.node(&db), DefinitionKind::Parameter(_)));
}
}
@@ -681,15 +694,15 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
let use_def = index.use_def_map(comprehension_scope_id);
for name in ["x", "y"] {
let binding = use_def
.first_public_binding(
let definition = use_def
.first_public_definition(
comprehension_symbol_table
.symbol_id_by_name(name)
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(
binding.kind(&db),
definition.node(&db),
DefinitionKind::Comprehension(_)
));
}
@@ -728,8 +741,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
let element_use_id =
element.scoped_use_id(&db, comprehension_scope_id.to_scope_id(&db, file));
let binding = use_def.first_binding_at_use(element_use_id).unwrap();
let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else {
let definition = use_def.first_use_definition(element_use_id).unwrap();
let DefinitionKind::Comprehension(comprehension) = definition.node(&db) else {
panic!("expected generator definition")
};
let target = comprehension.target();
@@ -808,10 +821,12 @@ with item1 as x, item2 as y:
let use_def = index.use_def_map(FileScopeId::global());
for name in ["x", "y"] {
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
.expect("Expected with item definition for {name}");
assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
let Some(definition) = use_def.first_public_definition(
global_table.symbol_id_by_name(name).expect("symbol exists"),
) else {
panic!("Expected with item definition for {name}");
};
assert!(matches!(definition.node(&db), DefinitionKind::WithItem(_)));
}
}
@@ -831,10 +846,12 @@ with context() as (x, y):
let use_def = index.use_def_map(FileScopeId::global());
for name in ["x", "y"] {
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
.expect("Expected with item definition for {name}");
assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
let Some(definition) = use_def.first_public_definition(
global_table.symbol_id_by_name(name).expect("symbol exists"),
) else {
panic!("Expected with item definition for {name}");
};
assert!(matches!(definition.node(&db), DefinitionKind::WithItem(_)));
}
}
@@ -871,14 +888,14 @@ def func():
assert_eq!(names(&func2_table), vec!["y"]);
let use_def = index.use_def_map(FileScopeId::global());
let binding = use_def
.first_public_binding(
let definition = use_def
.first_public_definition(
global_table
.symbol_id_by_name("func")
.expect("symbol exists"),
)
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::Function(_)));
assert!(matches!(definition.node(&db), DefinitionKind::Function(_)));
}
#[test]
@@ -946,7 +963,7 @@ class C[T]:
assert!(
ann_table
.symbol_by_name("T")
.is_some_and(|s| s.is_bound() && !s.is_used()),
.is_some_and(|s| s.is_defined() && !s.is_used()),
"type parameters are defined by the scope that introduces them"
);
@@ -978,8 +995,8 @@ class C[T]:
};
let x_use_id = x_use_expr_name.scoped_use_id(&db, scope);
let use_def = use_def_map(&db, scope);
let binding = use_def.first_binding_at_use(x_use_id).unwrap();
let DefinitionKind::Assignment(assignment) = binding.kind(&db) else {
let definition = use_def.first_use_definition(x_use_id).unwrap();
let DefinitionKind::Assignment(assignment) = definition.node(&db) else {
panic!("should be an assignment definition")
};
let ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
@@ -1109,10 +1126,12 @@ match subject:
("k", 0),
("l", 1),
] {
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
let definition = use_def
.first_public_definition(
global_table.symbol_id_by_name(name).expect("symbol exists"),
)
.expect("Expected with item definition for {name}");
if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) {
if let DefinitionKind::MatchPattern(pattern) = definition.node(&db) {
assert_eq!(pattern.index(), expected_index);
} else {
panic!("Expected match pattern definition for {name}");
@@ -1139,10 +1158,12 @@ match 1:
let use_def = use_def_map(&db, global_scope_id);
for (name, expected_index) in [("first", 0), ("second", 0)] {
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
let definition = use_def
.first_public_definition(
global_table.symbol_id_by_name(name).expect("symbol exists"),
)
.expect("Expected with item definition for {name}");
if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) {
if let DefinitionKind::MatchPattern(pattern) = definition.node(&db) {
assert_eq!(pattern.index(), expected_index);
} else {
panic!("Expected match pattern definition for {name}");
@@ -1159,11 +1180,11 @@ match 1:
assert_eq!(&names(&global_table), &["a", "x"]);
let use_def = use_def_map(&db, scope);
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name("x").unwrap())
let definition = use_def
.first_public_definition(global_table.symbol_id_by_name("x").unwrap())
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
assert!(matches!(definition.node(&db), DefinitionKind::For(_)));
}
#[test]
@@ -1175,15 +1196,15 @@ match 1:
assert_eq!(&names(&global_table), &["a", "x", "y"]);
let use_def = use_def_map(&db, scope);
let x_binding = use_def
.first_public_binding(global_table.symbol_id_by_name("x").unwrap())
let x_definition = use_def
.first_public_definition(global_table.symbol_id_by_name("x").unwrap())
.unwrap();
let y_binding = use_def
.first_public_binding(global_table.symbol_id_by_name("y").unwrap())
let y_definition = use_def
.first_public_definition(global_table.symbol_id_by_name("y").unwrap())
.unwrap();
assert!(matches!(x_binding.kind(&db), DefinitionKind::For(_)));
assert!(matches!(y_binding.kind(&db), DefinitionKind::For(_)));
assert!(matches!(x_definition.node(&db), DefinitionKind::For(_)));
assert!(matches!(y_definition.node(&db), DefinitionKind::For(_)));
}
#[test]
@@ -1195,10 +1216,10 @@ match 1:
assert_eq!(&names(&global_table), &["e", "a", "b", "c", "d"]);
let use_def = use_def_map(&db, scope);
let binding = use_def
.first_public_binding(global_table.symbol_id_by_name("a").unwrap())
let definition = use_def
.first_public_definition(global_table.symbol_id_by_name("a").unwrap())
.unwrap();
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
assert!(matches!(definition.node(&db), DefinitionKind::For(_)));
}
}

View File

@@ -19,17 +19,14 @@ use crate::semantic_index::definition::{
};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolFlags,
SymbolTableBuilder,
};
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
use crate::semantic_index::SemanticIndex;
use crate::Db;
use super::constraint::{Constraint, PatternConstraint};
use super::definition::{
ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
};
use super::definition::{MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef};
pub(super) struct SemanticIndexBuilder<'db> {
// Builder state
@@ -168,93 +165,49 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_use_def_map_mut().merge(state);
}
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
let (symbol_id, added) = self.current_symbol_table().add_symbol(name);
fn add_or_update_symbol(&mut self, name: Name, flags: SymbolFlags) -> ScopedSymbolId {
let symbol_table = self.current_symbol_table();
let (symbol_id, added) = symbol_table.add_or_update_symbol(name, flags);
if added {
self.current_use_def_map_mut().add_symbol(symbol_id);
let use_def_map = self.current_use_def_map_mut();
use_def_map.add_symbol(symbol_id);
}
symbol_id
}
fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
self.current_symbol_table().mark_symbol_bound(id);
}
fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
self.current_symbol_table().mark_symbol_used(id);
}
fn add_definition<'a>(
&mut self,
symbol: ScopedSymbolId,
definition_node: impl Into<DefinitionNodeRef<'a>>,
) -> Definition<'db> {
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
#[allow(unsafe_code)]
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
let (is_declaration, is_binding) = (kind.is_declaration(), kind.is_binding());
let definition = Definition::new(
self.db,
self.file,
self.current_scope(),
symbol,
kind,
#[allow(unsafe_code)]
unsafe {
definition_node.into_owned(self.module.clone())
},
countme::Count::default(),
);
let existing_definition = self
.definitions_by_node
self.definitions_by_node
.insert(definition_node.key(), definition);
debug_assert_eq!(existing_definition, None);
if is_binding {
self.mark_symbol_bound(symbol);
}
let use_def = self.current_use_def_map_mut();
match (is_declaration, is_binding) {
(true, true) => use_def.record_declaration_and_binding(symbol, definition),
(true, false) => use_def.record_declaration(symbol, definition),
(false, true) => use_def.record_binding(symbol, definition),
(false, false) => unreachable!("definition must be declaration or binding or both"),
}
self.current_use_def_map_mut()
.record_definition(symbol, definition);
definition
}
fn add_expression_constraint(&mut self, constraint_node: &ast::Expr) -> Expression<'db> {
fn add_constraint(&mut self, constraint_node: &ast::Expr) -> Expression<'db> {
let expression = self.add_standalone_expression(constraint_node);
self.current_use_def_map_mut()
.record_constraint(Constraint::Expression(expression));
self.current_use_def_map_mut().record_constraint(expression);
expression
}
fn add_pattern_constraint(
&mut self,
subject: &ast::Expr,
pattern: &ast::Pattern,
) -> PatternConstraint<'db> {
#[allow(unsafe_code)]
let (subject, pattern) = unsafe {
(
AstNodeRef::new(self.module.clone(), subject),
AstNodeRef::new(self.module.clone(), pattern),
)
};
let pattern_constraint = PatternConstraint::new(
self.db,
self.file,
self.current_scope(),
subject,
pattern,
countme::Count::default(),
);
self.current_use_def_map_mut()
.record_constraint(Constraint::Pattern(pattern_constraint));
pattern_constraint
}
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
/// standalone (type narrowing tests, RHS of an assignment.)
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
@@ -299,9 +252,8 @@ impl<'db> SemanticIndexBuilder<'db> {
..
}) => (name, &None, default),
};
let symbol = self.add_symbol(name.id.clone());
// TODO create Definition for PEP 695 typevars
self.mark_symbol_bound(symbol);
// TODO create Definition for typevars
self.add_or_update_symbol(name.id.clone(), SymbolFlags::IS_DEFINED);
if let Some(bound) = bound {
self.visit_expr(bound);
}
@@ -366,7 +318,8 @@ impl<'db> SemanticIndexBuilder<'db> {
}
fn declare_parameter(&mut self, parameter: AnyParameterRef) {
let symbol = self.add_symbol(parameter.name().id().clone());
let symbol =
self.add_or_update_symbol(parameter.name().id().clone(), SymbolFlags::IS_DEFINED);
let definition = self.add_definition(symbol, parameter);
@@ -374,11 +327,10 @@ impl<'db> SemanticIndexBuilder<'db> {
// Insert a mapping from the parameter to the same definition.
// This ensures that calling `HasTy::ty` on the inner parameter returns
// a valid type (and doesn't panic)
let existing_definition = self.definitions_by_node.insert(
self.definitions_by_node.insert(
DefinitionNodeRef::from(AnyParameterRef::Variadic(&with_default.parameter)).key(),
definition,
);
debug_assert_eq!(existing_definition, None);
}
}
@@ -477,7 +429,8 @@ where
// The symbol for the function name itself has to be evaluated
// at the end to match the runtime evaluation of parameter defaults
// and return-type annotations.
let symbol = self.add_symbol(function_def.name.id.clone());
let symbol = self
.add_or_update_symbol(function_def.name.id.clone(), SymbolFlags::IS_DEFINED);
self.add_definition(symbol, function_def);
}
ast::Stmt::ClassDef(class) => {
@@ -485,7 +438,8 @@ where
self.visit_decorator(decorator);
}
let symbol = self.add_symbol(class.name.id.clone());
let symbol =
self.add_or_update_symbol(class.name.id.clone(), SymbolFlags::IS_DEFINED);
self.add_definition(symbol, class);
self.with_type_params(
@@ -511,7 +465,7 @@ where
Name::new(alias.name.id.split('.').next().unwrap())
};
let symbol = self.add_symbol(symbol_name);
let symbol = self.add_or_update_symbol(symbol_name, SymbolFlags::IS_DEFINED);
self.add_definition(symbol, alias);
}
}
@@ -523,7 +477,8 @@ where
&alias.name.id
};
let symbol = self.add_symbol(symbol_name.clone());
let symbol =
self.add_or_update_symbol(symbol_name.clone(), SymbolFlags::IS_DEFINED);
self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index });
}
}
@@ -539,6 +494,7 @@ where
}
ast::Stmt::AnnAssign(node) => {
debug_assert!(self.current_assignment.is_none());
// TODO deferred annotation visiting
self.visit_expr(&node.annotation);
if let Some(value) = &node.value {
self.visit_expr(value);
@@ -564,7 +520,7 @@ where
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
let pre_if = self.flow_snapshot();
self.add_expression_constraint(&node.test);
self.add_constraint(&node.test);
self.visit_body(&node.body);
let mut post_clauses: Vec<FlowSnapshot> = vec![];
for clause in &node.elif_else_clauses {
@@ -589,23 +545,14 @@ where
self.flow_merge(pre_if);
}
}
ast::Stmt::While(ast::StmtWhile {
test,
body,
orelse,
range: _,
}) => {
self.visit_expr(test);
ast::Stmt::While(node) => {
self.visit_expr(&node.test);
let pre_loop = self.flow_snapshot();
// Save aside any break states from an outer loop
let saved_break_states = std::mem::take(&mut self.loop_break_states);
// TODO: definitions created inside the body should be fully visible
// to other statements/expressions inside the body --Alex/Carl
self.visit_body(body);
self.visit_body(&node.body);
// Get the break states from the body of this loop, and restore the saved outer
// ones.
let break_states =
@@ -614,7 +561,7 @@ where
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop);
self.visit_body(orelse);
self.visit_body(&node.orelse);
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
@@ -648,35 +595,15 @@ where
orelse,
},
) => {
// TODO add control flow similar to `ast::Stmt::While` above
self.add_standalone_expression(iter);
self.visit_expr(iter);
let pre_loop = self.flow_snapshot();
let saved_break_states = std::mem::take(&mut self.loop_break_states);
debug_assert!(self.current_assignment.is_none());
self.current_assignment = Some(for_stmt.into());
self.visit_expr(target);
self.current_assignment = None;
// TODO: Definitions created by loop variables
// (and definitions created inside the body)
// are fully visible to other statements/expressions inside the body --Alex/Carl
self.visit_body(body);
let break_states =
std::mem::replace(&mut self.loop_break_states, saved_break_states);
// We may execute the `else` clause without ever executing the body, so merge in
// the pre-loop state before visiting `else`.
self.flow_merge(pre_loop);
self.visit_body(orelse);
// Breaking out of a `for` loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
self.flow_merge(break_state);
}
}
ast::Stmt::Match(ast::StmtMatch {
subject,
@@ -685,74 +612,9 @@ where
}) => {
self.add_standalone_expression(subject);
self.visit_expr(subject);
let after_subject = self.flow_snapshot();
let Some((first, remaining)) = cases.split_first() else {
return;
};
self.add_pattern_constraint(subject, &first.pattern);
self.visit_match_case(first);
let mut post_case_snapshots = vec![];
for case in remaining {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
self.add_pattern_constraint(subject, &case.pattern);
for case in cases {
self.visit_match_case(case);
}
for post_clause_state in post_case_snapshots {
self.flow_merge(post_clause_state);
}
if !cases
.last()
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
{
self.flow_merge(after_subject);
}
}
ast::Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
is_star,
range: _,
}) => {
self.visit_body(body);
for except_handler in handlers {
let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler;
let ast::ExceptHandlerExceptHandler {
name: symbol_name,
type_: handled_exceptions,
body: handler_body,
range: _,
} = except_handler;
if let Some(handled_exceptions) = handled_exceptions {
self.visit_expr(handled_exceptions);
}
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
// which is invalid syntax. However, it's still pretty obvious here that the user
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
if let Some(symbol_name) = symbol_name {
let symbol = self.add_symbol(symbol_name.id.clone());
self.add_definition(
symbol,
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
handler: except_handler,
is_star: *is_star,
}),
);
}
self.visit_body(handler_body);
}
self.visit_body(orelse);
self.visit_body(finalbody);
}
_ => {
walk_stmt(self, stmt);
@@ -767,18 +629,23 @@ where
match expr {
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
let (is_use, is_definition) = match (ctx, self.current_assignment) {
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
// For augmented assignment, the target expression is also used.
(true, true)
}
(ast::ExprContext::Load, _) => (true, false),
(ast::ExprContext::Store, _) => (false, true),
(ast::ExprContext::Del, _) => (false, true),
(ast::ExprContext::Invalid, _) => (false, false),
let mut flags = match ctx {
ast::ExprContext::Load => SymbolFlags::IS_USED,
ast::ExprContext::Store => SymbolFlags::IS_DEFINED,
ast::ExprContext::Del => SymbolFlags::IS_DEFINED,
ast::ExprContext::Invalid => SymbolFlags::empty(),
};
let symbol = self.add_symbol(id.clone());
if is_definition {
if matches!(
self.current_assignment,
Some(CurrentAssignment::AugAssign(_))
) && !ctx.is_invalid()
{
// For augmented assignment, the target expression is also used, so we should
// record that as a use.
flags |= SymbolFlags::IS_USED;
}
let symbol = self.add_or_update_symbol(id.clone(), flags);
if flags.contains(SymbolFlags::IS_DEFINED) {
match self.current_assignment {
Some(CurrentAssignment::Assign(assignment)) => {
self.add_definition(
@@ -801,7 +668,6 @@ where
ForStmtDefinitionNodeRef {
iterable: &node.iter,
target: name_node,
is_async: node.is_async,
},
);
}
@@ -818,7 +684,6 @@ where
iterable: &node.iter,
target: name_node,
first,
is_async: node.is_async,
},
);
}
@@ -835,8 +700,7 @@ where
}
}
if is_use {
self.mark_symbol_used(symbol);
if flags.contains(SymbolFlags::IS_USED) {
let use_id = self.current_ast_ids().record_use(expr);
self.current_use_def_map_mut().record_use(symbol, use_id);
}
@@ -976,7 +840,7 @@ where
range: _,
}) = pattern
{
let symbol = self.add_symbol(name.id().clone());
let symbol = self.add_or_update_symbol(name.id().clone(), SymbolFlags::IS_DEFINED);
let state = self.current_match_case.as_ref().unwrap();
self.add_definition(
symbol,
@@ -997,7 +861,7 @@ where
rest: Some(name), ..
}) = pattern
{
let symbol = self.add_symbol(name.id().clone());
let symbol = self.add_or_update_symbol(name.id().clone(), SymbolFlags::IS_DEFINED);
let state = self.current_match_case.as_ref().unwrap();
self.add_definition(
symbol,

View File

@@ -1,39 +0,0 @@
use ruff_db::files::File;
use ruff_python_ast as ast;
use crate::ast_node_ref::AstNodeRef;
use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Constraint<'db> {
Expression(Expression<'db>),
Pattern(PatternConstraint<'db>),
}
#[salsa::tracked]
pub(crate) struct PatternConstraint<'db> {
#[id]
pub(crate) file: File,
#[id]
pub(crate) file_scope: FileScopeId,
#[no_eq]
#[return_ref]
pub(crate) subject: AstNodeRef<ast::Expr>,
#[no_eq]
#[return_ref]
pub(crate) pattern: AstNodeRef<ast::Pattern>,
#[no_eq]
count: countme::Count<PatternConstraint<'static>>,
}
impl<'db> PatternConstraint<'db> {
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
}

View File

@@ -23,7 +23,7 @@ pub struct Definition<'db> {
#[no_eq]
#[return_ref]
pub(crate) kind: DefinitionKind,
pub(crate) node: DefinitionKind,
#[no_eq]
count: countme::Count<Definition<'static>>,
@@ -50,7 +50,6 @@ pub(crate) enum DefinitionNodeRef<'a> {
Parameter(ast::AnyParameterRef<'a>),
WithItem(WithItemDefinitionNodeRef<'a>),
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>),
}
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
@@ -153,13 +152,6 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
pub(crate) iterable: &'a ast::Expr,
pub(crate) target: &'a ast::ExprName,
pub(crate) is_async: bool,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> {
pub(crate) handler: &'a ast::ExceptHandlerExceptHandler,
pub(crate) is_star: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -167,7 +159,6 @@ pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
pub(crate) iterable: &'a ast::Expr,
pub(crate) target: &'a ast::ExprName,
pub(crate) first: bool,
pub(crate) is_async: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -215,25 +206,20 @@ impl DefinitionNodeRef<'_> {
DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
}
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
iterable,
target,
is_async,
}) => DefinitionKind::For(ForStmtDefinitionKind {
iterable: AstNodeRef::new(parsed.clone(), iterable),
target: AstNodeRef::new(parsed, target),
is_async,
}),
DefinitionNodeRef::For(ForStmtDefinitionNodeRef { iterable, target }) => {
DefinitionKind::For(ForStmtDefinitionKind {
iterable: AstNodeRef::new(parsed.clone(), iterable),
target: AstNodeRef::new(parsed, target),
})
}
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
iterable,
target,
first,
is_async,
}) => DefinitionKind::Comprehension(ComprehensionDefinitionKind {
iterable: AstNodeRef::new(parsed.clone(), iterable),
target: AstNodeRef::new(parsed, target),
first,
is_async,
}),
DefinitionNodeRef::Parameter(parameter) => match parameter {
ast::AnyParameterRef::Variadic(parameter) => {
@@ -258,13 +244,6 @@ impl DefinitionNodeRef<'_> {
identifier: AstNodeRef::new(parsed, identifier),
index,
}),
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
handler,
is_star,
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
handler: AstNodeRef::new(parsed.clone(), handler),
is_star,
}),
}
}
@@ -286,7 +265,6 @@ impl DefinitionNodeRef<'_> {
Self::For(ForStmtDefinitionNodeRef {
iterable: _,
target,
is_async: _,
}) => target.into(),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::Parameter(node) => match node {
@@ -297,7 +275,6 @@ impl DefinitionNodeRef<'_> {
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
identifier.into()
}
Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(),
}
}
}
@@ -318,39 +295,6 @@ pub enum DefinitionKind {
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
WithItem(WithItemDefinitionKind),
MatchPattern(MatchPatternDefinitionKind),
ExceptHandler(ExceptHandlerDefinitionKind),
}
impl DefinitionKind {
/// True if this definition establishes a "declared type" for the symbol.
///
/// If so, any assignments reached by this definition are in error if they assign a value of a
/// type not assignable to the declared type.
///
/// Annotations establish a declared type. So do function and class definition.
pub(crate) fn is_declaration(&self) -> bool {
match self {
DefinitionKind::Function(_) => true,
DefinitionKind::Class(_) => true,
DefinitionKind::Parameter(parameter) => parameter.annotation.is_some(),
DefinitionKind::ParameterWithDefault(parameter_with_default) => {
parameter_with_default.parameter.annotation.is_some()
}
DefinitionKind::AnnotatedAssignment(_) => true,
_ => false,
}
}
/// True if this definition assigns a value to the symbol.
///
/// False only for annotated assignments without a RHS.
pub(crate) fn is_binding(&self) -> bool {
if let DefinitionKind::AnnotatedAssignment(ann_assign) = self {
ann_assign.value.is_some()
} else {
true
}
}
}
#[derive(Clone, Debug)]
@@ -376,7 +320,6 @@ pub struct ComprehensionDefinitionKind {
iterable: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
impl ComprehensionDefinitionKind {
@@ -391,10 +334,6 @@ impl ComprehensionDefinitionKind {
pub(crate) fn is_first(&self) -> bool {
self.first
}
pub(crate) fn is_async(&self) -> bool {
self.is_async
}
}
#[derive(Clone, Debug)]
@@ -449,7 +388,6 @@ impl WithItemDefinitionKind {
pub struct ForStmtDefinitionKind {
iterable: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::ExprName>,
is_async: bool,
}
impl ForStmtDefinitionKind {
@@ -460,30 +398,6 @@ impl ForStmtDefinitionKind {
pub(crate) fn target(&self) -> &ast::ExprName {
self.target.node()
}
pub(crate) fn is_async(&self) -> bool {
self.is_async
}
}
#[derive(Clone, Debug)]
pub struct ExceptHandlerDefinitionKind {
handler: AstNodeRef<ast::ExceptHandlerExceptHandler>,
is_star: bool,
}
impl ExceptHandlerDefinitionKind {
pub(crate) fn node(&self) -> &ast::ExceptHandlerExceptHandler {
self.handler.node()
}
pub(crate) fn handled_exceptions(&self) -> Option<&ast::Expr> {
self.node().type_.as_deref()
}
pub(crate) fn is_star(&self) -> bool {
self.is_star
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
@@ -554,9 +468,3 @@ impl From<&ast::Identifier> for DefinitionNodeKey {
Self(NodeKey::from_node(identifier))
}
}
impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey {
fn from(handler: &ast::ExceptHandlerExceptHandler) -> Self {
Self(NodeKey::from_node(handler))
}
}

View File

@@ -44,8 +44,8 @@ impl Symbol {
}
/// Is the symbol defined in its containing scope?
pub fn is_bound(&self) -> bool {
self.flags.contains(SymbolFlags::IS_BOUND)
pub fn is_defined(&self) -> bool {
self.flags.contains(SymbolFlags::IS_DEFINED)
}
}
@@ -53,7 +53,7 @@ bitflags! {
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) struct SymbolFlags: u8 {
const IS_USED = 1 << 0;
const IS_BOUND = 1 << 1;
const IS_DEFINED = 1 << 1;
/// TODO: This flag is not yet set by anything
const MARKED_GLOBAL = 1 << 2;
/// TODO: This flag is not yet set by anything
@@ -272,7 +272,11 @@ impl SymbolTableBuilder {
}
}
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
pub(super) fn add_or_update_symbol(
&mut self,
name: Name,
flags: SymbolFlags,
) -> (ScopedSymbolId, bool) {
let hash = SymbolTable::hash_name(&name);
let entry = self
.table
@@ -281,9 +285,15 @@ impl SymbolTableBuilder {
.from_hash(hash, |id| self.table.symbols[*id].name() == &name);
match entry {
RawEntryMut::Occupied(entry) => (*entry.key(), false),
RawEntryMut::Occupied(entry) => {
let symbol = &mut self.table.symbols[*entry.key()];
symbol.insert_flags(flags);
(*entry.key(), false)
}
RawEntryMut::Vacant(entry) => {
let symbol = Symbol::new(name);
let mut symbol = Symbol::new(name);
symbol.insert_flags(flags);
let id = self.table.symbols.push(symbol);
entry.insert_with_hasher(hash, id, (), |id| {
@@ -294,14 +304,6 @@ impl SymbolTableBuilder {
}
}
pub(super) fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND);
}
pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
}
pub(super) fn finish(mut self) -> SymbolTable {
self.table.shrink_to_fit();
self.table

View File

@@ -1,75 +1,5 @@
//! First, some terminology:
//!
//! * a "binding" gives a new value to a variable. This includes many different Python statements
//! (assignment statements of course, but also imports, `def` and `class` statements, `as`
//! clauses in `with` and `except` statements, match patterns, and others) and even one
//! expression kind (named expressions). It notably does not include annotated assignment
//! statements without a right-hand side value; these do not assign any new value to the
//! variable. We consider function parameters to be bindings as well, since (from the perspective
//! of the function's internal scope), a function parameter begins the scope bound to a value.
//!
//! * a "declaration" establishes an upper bound type for the values that a variable may be
//! permitted to take on. Annotated assignment statements (with or without an RHS value) are
//! declarations; annotated function parameters are also declarations. We consider `def` and
//! `class` statements to also be declarations, so as to prohibit accidentally shadowing them.
//!
//! Annotated assignments with a right-hand side, and annotated function parameters, are both
//! bindings and declarations.
//!
//! We use [`Definition`] as the universal term (and Salsa tracked struct) encompassing both
//! bindings and declarations. (This sacrifices a bit of type safety in exchange for improved
//! performance via fewer Salsa tracked structs and queries, since most declarations -- typed
//! parameters and annotated assignments with RHS -- are both bindings and declarations.)
//!
//! At any given use of a variable, we can ask about both its "declared type" and its "inferred
//! type". These may be different, but the inferred type must always be assignable to the declared
//! type; that is, the declared type is always wider, and the inferred type may be more precise.
//!
//! The **inferred type** represents the most precise type we believe encompasses all possible
//! values for the variable at a given use. It is based on the bindings which can reach that use
//! through some control flow path, and the narrowing constraints that control flow must have
//! passed through between the binding and the use. For example, in this code:
//!
//! ```python
//! x = 1 if flag else None
//! if x is not None:
//! use(x)
//! ```
//!
//! For the use of `x` on the third line, the inferred type should be `Literal[1]`. This is based
//! on the binding on the first line, which assigns the type `Literal[1] | None`, and the narrowing
//! constraint on the second line, which rules out the type `None`, since control flow must pass
//! through this constraint to reach the use in question.
//!
//! The **declared type** represents the code author's declaration (usually through a type
//! annotation) that a given variable should not be assigned any type outside the declared type. In
//! our model, declared types are also control-flow-sensitive; we allow the code author to
//! explicitly re-declare the same variable with a different type. So for a given binding of a
//! variable, we will want to ask which declarations of that variable can reach that binding, in
//! order to determine whether the binding is permitted, or should be a type error. For example:
//!
//! ```python
//! from pathlib import Path
//! def f(path: str):
//! path: Path = Path(path)
//! ```
//!
//! In this function, the initial declared type of `path` is `str`, meaning that the assignment
//! `path = Path(path)` would be a type error, since it assigns to `path` a value whose type is not
//! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment
//! of the wrong type to a variable.
//!
//! But in some cases it is useful to "shadow" or "re-declare" a variable with a new type, and we
//! permit this, as long as it is done with an explicit re-annotation. So `path: Path =
//! Path(path)`, with the explicit `: Path` annotation, is permitted.
//!
//! The general rule is that whatever declaration(s) can reach a given binding determine the
//! validity of that binding. If multiple declarations can reach a binding, they must be
//! equivalent declarations, or we issue a type error, since we can't reconcile to a single
//! declared type.
//!
//! To support type inference, we build a map from each use of a symbol to the bindings live at
//! that use, and the type-narrowing constraints that apply to each binding.
//! Build a map from each use of a symbol to the definitions visible from that use, and the
//! type-narrowing constraints that apply to each definition.
//!
//! Let's take this code sample:
//!
@@ -77,32 +7,55 @@
//! x = 1
//! x = 2
//! y = x
//! if flag:
//! if y is not None:
//! x = 3
//! else:
//! x = 4
//! z = x
//! ```
//!
//! In this snippet, we have four bindings of `x` (the statements assigning `1`, `2`, `3`, and `4`
//! to it), and two uses of `x` (the `y = x` and `z = x` assignments). The first binding of `x`
//! does not reach any use, because it's immediately replaced by the second binding, before any use
//! happens. (A linter could thus flag the statement `x = 1` as likely superfluous.)
//! In this snippet, we have four definitions of `x` (the statements assigning `1`, `2`, `3`,
//! and `4` to it), and two uses of `x` (the `y = x` and `z = x` assignments). The first
//! [`Definition`] of `x` is never visible to any use, because it's immediately replaced by the
//! second definition, before any use happens. (A linter could thus flag the statement `x = 1`
//! as likely superfluous.)
//!
//! The first use of `x` has one live binding: the assignment `x = 2`.
//! The first use of `x` has one definition visible to it: the assignment `x = 2`.
//!
//! Things get a bit more complex when we have branches. We will definitely take either the `if` or
//! the `else` branch. Thus, the second use of `x` has two live bindings: `x = 3` and `x = 4`. The
//! `x = 2` assignment is no longer visible, because it must be replaced by either `x = 3` or `x =
//! 4`, no matter which branch was taken. We don't know which branch was taken, so we must consider
//! both bindings as live, which means eventually we would (in type inference) look at these two
//! bindings and infer a type of `Literal[3, 4]` -- the union of `Literal[3]` and `Literal[4]` --
//! for the second use of `x`.
//! the `else` branch. Thus, the second use of `x` has two definitions visible to it: `x = 3` and
//! `x = 4`. The `x = 2` definition is no longer visible, because it must be replaced by either `x
//! = 3` or `x = 4`, no matter which branch was taken. We don't know which branch was taken, so we
//! must consider both definitions as visible, which means eventually we would (in type inference)
//! look at these two definitions and infer a type of `Literal[3, 4]` -- the union of `Literal[3]`
//! and `Literal[4]` -- for the second use of `x`.
//!
//! So that's one question our use-def map needs to answer: given a specific use of a symbol, which
//! binding(s) can reach that use. In [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number
//! all uses (that means a `Name` node with `Load` context) so we have a `ScopedUseId` to
//! efficiently represent each use.
//! definition(s) is/are visible from that use. In
//! [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number all uses (that means a `Name` node
//! with `Load` context) so we have a `ScopedUseId` to efficiently represent each use.
//!
//! Another case we need to handle is when a symbol is referenced from a different scope (the most
//! obvious example of this is an import). We call this "public" use of a symbol. So the other
//! question we need to be able to answer is, what are the publicly-visible definitions of each
//! symbol?
//!
//! Technically, public use of a symbol could also occur from any point in control flow of the
//! scope where the symbol is defined (via inline imports and import cycles, in the case of an
//! import, or via a function call partway through the local scope that ends up using a symbol from
//! the scope via a global or nonlocal reference.) But modeling this fully accurately requires
//! whole-program analysis that isn't tractable for an efficient incremental compiler, since it
//! means a given symbol could have a different type every place it's referenced throughout the
//! program, depending on the shape of arbitrarily-sized call/import graphs. So we follow other
//! Python type-checkers in making the simplifying assumption that usually the scope will finish
//! execution before its symbols are made visible to other scopes; for instance, most imports will
//! import from a complete module, not a partially-executed module. (We may want to get a little
//! smarter than this in the future, in particular for closures, but for now this is where we
//! start.)
//!
//! So this means that the publicly-visible definitions of a symbol are the definitions still
//! visible at the end of the scope; effectively we have an implicit "use" of every symbol at the
//! end of the scope.
//!
//! We also need to know, for a given definition of a symbol, what type-narrowing constraints apply
//! to it. For instance, in this code sample:
@@ -110,124 +63,92 @@
//! ```python
//! x = 1 if flag else None
//! if x is not None:
//! use(x)
//! y = x
//! ```
//!
//! At the use of `x`, the live binding of `x` is `1 if flag else None`, which would infer as the
//! type `Literal[1] | None`. But the constraint `x is not None` dominates this use, which means we
//! can rule out the possibility that `x` is `None` here, which should give us the type
//! `Literal[1]` for this use.
//!
//! For declared types, we need to be able to answer the question "given a binding to a symbol,
//! which declarations of that symbol can reach the binding?" This allows us to emit a diagnostic
//! if the binding is attempting to bind a value of a type that is not assignable to the declared
//! type for that symbol, at that point in control flow.
//!
//! We also need to know, given a declaration of a symbol, what the inferred type of that symbol is
//! at that point. This allows us to emit a diagnostic in a case like `x = "foo"; x: int`. The
//! binding `x = "foo"` occurs before the declaration `x: int`, so according to our
//! control-flow-sensitive interpretation of declarations, the assignment is not an error. But the
//! declaration is an error, since it would violate the "inferred type must be assignable to
//! declared type" rule.
//!
//! Another case we need to handle is when a symbol is referenced from a different scope (for
//! example, an import or a nonlocal reference). We call this "public" use of a symbol. For public
//! use of a symbol, we prefer the declared type, if there are any declarations of that symbol; if
//! not, we fall back to the inferred type. So we also need to know which declarations and bindings
//! can reach the end of the scope.
//!
//! Technically, public use of a symbol could occur from any point in control flow of the scope
//! where the symbol is defined (via inline imports and import cycles, in the case of an import, or
//! via a function call partway through the local scope that ends up using a symbol from the scope
//! via a global or nonlocal reference.) But modeling this fully accurately requires whole-program
//! analysis that isn't tractable for an efficient analysis, since it means a given symbol could
//! have a different type every place it's referenced throughout the program, depending on the
//! shape of arbitrarily-sized call/import graphs. So we follow other Python type-checkers in
//! making the simplifying assumption that usually the scope will finish execution before its
//! symbols are made visible to other scopes; for instance, most imports will import from a
//! complete module, not a partially-executed module. (We may want to get a little smarter than
//! this in the future for some closures, but for now this is where we start.)
//! At the use of `x` in `y = x`, the visible definition of `x` is `1 if flag else None`, which
//! would infer as the type `Literal[1] | None`. But the constraint `x is not None` dominates this
//! use, which means we can rule out the possibility that `x` is `None` here, which should give us
//! the type `Literal[1]` for this use.
//!
//! The data structure we build to answer these questions is the `UseDefMap`. It has a
//! `bindings_by_use` vector of [`SymbolBindings`] indexed by [`ScopedUseId`], a
//! `declarations_by_binding` vector of [`SymbolDeclarations`] indexed by [`ScopedDefinitionId`], a
//! `bindings_by_declaration` vector of [`SymbolBindings`] indexed by [`ScopedDefinitionId`], and
//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedSymbolId`]. The values in
//! each of these vectors are (in principle) a list of live bindings at that use/definition, or at
//! the end of the scope for that symbol, with a list of the dominating constraints for each
//! binding.
//! `definitions_by_use` vector indexed by [`ScopedUseId`] and a `public_definitions` vector
//! indexed by [`ScopedSymbolId`]. The values in each of these vectors are (in principle) a list of
//! visible definitions at that use, or at the end of the scope for that symbol, with a list of the
//! dominating constraints for each of those definitions.
//!
//! In order to avoid vectors-of-vectors-of-vectors and all the allocations that would entail, we
//! don't actually store these "list of visible definitions" as a vector of [`Definition`].
//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track
//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `all_constraints`
//! indexvecs in the [`UseDefMap`].
//! Instead, the values in `definitions_by_use` and `public_definitions` are a [`SymbolState`]
//! struct which uses bit-sets to track definitions and constraints in terms of
//! [`ScopedDefinitionId`] and [`ScopedConstraintId`], which are indices into the `all_definitions`
//! and `all_constraints` indexvecs in the [`UseDefMap`].
//!
//! There is another special kind of possible "definition" for a symbol: there might be a path from
//! the scope entry to a given use in which the symbol is never bound.
//!
//! The simplest way to model "unbound" would be as a "binding" itself: the initial "binding" for
//! each symbol in a scope. But actually modeling it this way would unnecessarily increase the
//! number of [`Definition`] that Salsa must track. Since "unbound" is special in that all symbols
//! share it, and it doesn't have any additional per-symbol state, and constraints are irrelevant
//! to it, we can represent it more efficiently: we use the `may_be_unbound` boolean on the
//! [`SymbolBindings`] struct. If this flag is `true` for a use of a symbol, it means the symbol
//! has a path to the use in which it is never bound. If this flag is `false`, it means we've
//! eliminated the possibility of unbound: every control flow path to the use includes a binding
//! for this symbol.
//! The simplest way to model "unbound" would be as an actual [`Definition`] itself: the initial
//! visible [`Definition`] for each symbol in a scope. But actually modeling it this way would
//! unnecessarily increase the number of [`Definition`] that Salsa must track. Since "unbound" is a
//! special definition in that all symbols share it, and it doesn't have any additional per-symbol
//! state, and constraints are irrelevant to it, we can represent it more efficiently: we use the
//! `may_be_unbound` boolean on the [`SymbolState`] struct. If this flag is `true`, it means the
//! symbol/use really has one additional visible "definition", which is the unbound state. If this
//! flag is `false`, it means we've eliminated the possibility of unbound: every path we've
//! followed includes a definition for this symbol.
//!
//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and
//! constraint as they are encountered by the
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder) AST visit. For
//! each symbol, the builder tracks the `SymbolState` (`SymbolBindings` and `SymbolDeclarations`)
//! for that symbol. When we hit a use or definition of a symbol, we record the necessary parts of
//! the current state for that symbol that we need for that use or definition. When we reach the
//! end of the scope, it records the state for each symbol as the public definitions of that
//! symbol.
//! each symbol, the builder tracks the `SymbolState` for that symbol. When we hit a use of a
//! symbol, it records the current state for that symbol for that use. When we reach the end of the
//! scope, it records the state for each symbol as the public definitions of that symbol.
//!
//! Let's walk through the above example. Initially we record for `x` that it has no bindings, and
//! may be unbound. When we see `x = 1`, we record that as the sole live binding of `x`, and flip
//! `may_be_unbound` to `false`. Then we see `x = 2`, and we replace `x = 1` as the sole live
//! binding of `x`. When we get to `y = x`, we record that the live bindings for that use of `x`
//! are just the `x = 2` definition.
//! Let's walk through the above example. Initially we record for `x` that it has no visible
//! definitions, and may be unbound. When we see `x = 1`, we record that as the sole visible
//! definition of `x`, and flip `may_be_unbound` to `false`. Then we see `x = 2`, and it replaces
//! `x = 1` as the sole visible definition of `x`. When we get to `y = x`, we record that the
//! visible definitions for that use of `x` are just the `x = 2` definition.
//!
//! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will
//! happen regardless. Then we take a pre-branch snapshot of the current state for all symbols,
//! which we'll need later. Then we record `flag` as a possible constraint on the current binding
//! (`x = 2`), and go ahead and visit the `if` body. When we see `x = 3`, it replaces `x = 2`
//! (constrained by `flag`) as the sole live binding of `x`. At the end of the `if` body, we take
//! another snapshot of the current symbol state; we'll call this the post-if-body snapshot.
//! happen regardless. Then we take a pre-branch snapshot of the currently visible definitions for
//! all symbols, which we'll need later. Then we record `flag` as a possible constraint on the
//! currently visible definition (`x = 2`), and go ahead and visit the `if` body. When we see `x =
//! 3`, it replaces `x = 2` (constrained by `flag`) as the sole visible definition of `x`. At the
//! end of the `if` body, we take another snapshot of the currently-visible definitions; we'll call
//! this the post-if-body snapshot.
//!
//! Now we need to visit the `else` clause. The conditions when entering the `else` clause should
//! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test
//! failed and we didn't execute the `if` body. So we first reset the builder to the pre-if state,
//! using the snapshot we took previously (meaning we now have `x = 2` as the sole binding for `x`
//! again), then visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding
//! of `x`.
//! using the snapshot we took previously (meaning we now have `x = 2` as the sole visible
//! definition for `x` again), then visit the `else` clause, where `x = 4` replaces `x = 2` as the
//! sole visible definition of `x`.
//!
//! Now we reach the end of the if/else, and want to visit the following code. The state here needs
//! to reflect that we might have gone through the `if` branch, or we might have gone through the
//! `else` branch, and we don't know which. So we need to "merge" our current builder state
//! (reflecting the end-of-else state, with `x = 4` as the only live binding) with our post-if-body
//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now
//! have two live bindings of `x`: `x = 3` and `x = 4`.
//! (reflecting the end-of-else state, with `x = 4` as the only visible definition) with our
//! post-if-body snapshot (which has `x = 3` as the only visible definition). The result of this
//! merge is that we now have two visible definitions of `x`: `x = 3` and `x = 4`.
//!
//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
//! visits a `StmtIf` node.
//!
//! (In the future we may have some other questions we want to answer as well, such as "is this
//! definition used?", which will require tracking a bit more info in our map, e.g. a "used" bit
//! for each [`Definition`] which is flipped to true when we record that definition for a use.)
use self::symbol_state::{
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
ScopedConstraintId, ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
ConstraintIdIterator, DefinitionIdWithConstraintsIterator, ScopedConstraintId,
ScopedDefinitionId, SymbolState,
};
use crate::semantic_index::ast_ids::ScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::ScopedSymbolId;
use ruff_index::IndexVec;
use rustc_hash::FxHashMap;
use super::constraint::Constraint;
mod bitset;
mod symbol_state;
@@ -238,133 +159,63 @@ pub(crate) struct UseDefMap<'db> {
/// Array of [`Definition`] in this scope.
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
/// Array of [`Constraint`] in this scope.
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
/// Array of constraints (as [`Expression`]) in this scope.
all_constraints: IndexVec<ScopedConstraintId, Expression<'db>>,
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
/// [`SymbolBindings`] or [`SymbolDeclarations`] reaching a given [`Definition`].
///
/// If the definition is a binding (only) -- `x = 1` for example -- then we need
/// [`SymbolDeclarations`] to know whether this binding is permitted by the live declarations.
///
/// If the definition is a declaration (only) -- `x: int` for example -- then we need
/// [`SymbolBindings`] to know whether this declaration is consistent with the previously
/// inferred type.
///
/// 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.
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
/// [`SymbolState`] visible at a [`ScopedUseId`].
definitions_by_use: IndexVec<ScopedUseId, SymbolState>,
/// [`SymbolState`] visible at end of scope for each symbol.
public_symbols: IndexVec<ScopedSymbolId, SymbolState>,
public_definitions: IndexVec<ScopedSymbolId, SymbolState>,
}
impl<'db> UseDefMap<'db> {
pub(crate) fn bindings_at_use(
pub(crate) fn use_definitions(
&self,
use_id: ScopedUseId,
) -> BindingWithConstraintsIterator<'_, 'db> {
self.bindings_iterator(&self.bindings_by_use[use_id])
) -> DefinitionWithConstraintsIterator<'_, 'db> {
DefinitionWithConstraintsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
inner: self.definitions_by_use[use_id].visible_definitions(),
}
}
pub(crate) fn use_may_be_unbound(&self, use_id: ScopedUseId) -> bool {
self.bindings_by_use[use_id].may_be_unbound()
self.definitions_by_use[use_id].may_be_unbound()
}
pub(crate) fn public_bindings(
pub(crate) fn public_definitions(
&self,
symbol: ScopedSymbolId,
) -> BindingWithConstraintsIterator<'_, 'db> {
self.bindings_iterator(self.public_symbols[symbol].bindings())
) -> DefinitionWithConstraintsIterator<'_, 'db> {
DefinitionWithConstraintsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
inner: self.public_definitions[symbol].visible_definitions(),
}
}
pub(crate) fn public_may_be_unbound(&self, symbol: ScopedSymbolId) -> bool {
self.public_symbols[symbol].may_be_unbound()
self.public_definitions[symbol].may_be_unbound()
}
pub(crate) fn bindings_at_declaration(
&self,
declaration: Definition<'db>,
) -> BindingWithConstraintsIterator<'_, 'db> {
if let SymbolDefinitions::Bindings(bindings) = &self.definitions_by_definition[&declaration]
{
self.bindings_iterator(bindings)
} else {
unreachable!("Declaration has non-Bindings in definitions_by_definition");
}
}
pub(crate) fn declarations_at_binding(
&self,
binding: Definition<'db>,
) -> DeclarationsIterator<'_, 'db> {
if let SymbolDefinitions::Declarations(declarations) =
&self.definitions_by_definition[&binding]
{
self.declarations_iterator(declarations)
} else {
unreachable!("Binding has non-Declarations in definitions_by_definition");
}
}
pub(crate) fn public_declarations(
&self,
symbol: ScopedSymbolId,
) -> DeclarationsIterator<'_, 'db> {
self.declarations_iterator(self.public_symbols[symbol].declarations())
}
pub(crate) fn has_public_declarations(&self, symbol: ScopedSymbolId) -> bool {
!self.public_symbols[symbol].declarations().is_empty()
}
fn bindings_iterator<'a>(
&'a self,
bindings: &'a SymbolBindings,
) -> BindingWithConstraintsIterator<'a, 'db> {
BindingWithConstraintsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
inner: bindings.iter(),
}
}
fn declarations_iterator<'a>(
&'a self,
declarations: &'a SymbolDeclarations,
) -> DeclarationsIterator<'a, 'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
inner: declarations.iter(),
}
}
}
/// Either live bindings or live declarations for a symbol.
#[derive(Debug, PartialEq, Eq)]
enum SymbolDefinitions {
Bindings(SymbolBindings),
Declarations(SymbolDeclarations),
}
#[derive(Debug)]
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
pub(crate) struct DefinitionWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
inner: BindingIdWithConstraintsIterator<'map>,
all_constraints: &'map IndexVec<ScopedConstraintId, Expression<'db>>,
inner: DefinitionIdWithConstraintsIterator<'map>,
}
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
type Item = BindingWithConstraints<'map, 'db>;
impl<'map, 'db> Iterator for DefinitionWithConstraintsIterator<'map, 'db> {
type Item = DefinitionWithConstraints<'map, 'db>;
fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
.map(|def_id_with_constraints| BindingWithConstraints {
binding: self.all_definitions[def_id_with_constraints.definition],
.map(|def_id_with_constraints| DefinitionWithConstraints {
definition: self.all_definitions[def_id_with_constraints.definition],
constraints: ConstraintsIterator {
all_constraints: self.all_constraints,
constraint_ids: def_id_with_constraints.constraint_ids,
@@ -373,20 +224,20 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
}
}
impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
impl std::iter::FusedIterator for DefinitionWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: Definition<'db>,
pub(crate) struct DefinitionWithConstraints<'map, 'db> {
pub(crate) definition: Definition<'db>,
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
}
pub(crate) struct ConstraintsIterator<'map, 'db> {
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
all_constraints: &'map IndexVec<ScopedConstraintId, Expression<'db>>,
constraint_ids: ConstraintIdIterator<'map>,
}
impl<'map, 'db> Iterator for ConstraintsIterator<'map, 'db> {
type Item = Constraint<'db>;
type Item = Expression<'db>;
fn next(&mut self) -> Option<Self::Item> {
self.constraint_ids
@@ -397,43 +248,25 @@ impl<'map, 'db> Iterator for ConstraintsIterator<'map, 'db> {
impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
inner: DeclarationIdIterator<'map>,
}
impl<'map, 'db> Iterator for DeclarationsIterator<'map, 'db> {
type Item = Definition<'db>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|def_id| self.all_definitions[def_id])
}
}
impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
/// A snapshot of the definitions and constraints state at a particular point in control flow.
#[derive(Clone, Debug)]
pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
definitions_by_symbol: IndexVec<ScopedSymbolId, SymbolState>,
}
#[derive(Debug, Default)]
pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Definition`].
/// Append-only array of [`Definition`]; None is unbound.
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
/// Append-only array of [`Constraint`].
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
/// Append-only array of constraints (as [`Expression`]).
all_constraints: IndexVec<ScopedConstraintId, Expression<'db>>,
/// Live bindings at each so-far-recorded use.
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
/// Visible definitions at each so-far-recorded use.
definitions_by_use: IndexVec<ScopedUseId, SymbolState>,
/// Live bindings or declarations for each so-far-recorded definition.
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
/// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
/// Currently visible definitions for each symbol.
definitions_by_symbol: IndexVec<ScopedSymbolId, SymbolState>,
}
impl<'db> UseDefMapBuilder<'db> {
@@ -442,103 +275,86 @@ impl<'db> UseDefMapBuilder<'db> {
}
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
let new_symbol = self.symbol_states.push(SymbolState::undefined());
let new_symbol = self.definitions_by_symbol.push(SymbolState::unbound());
debug_assert_eq!(symbol, new_symbol);
}
pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
let def_id = self.all_definitions.push(binding);
let symbol_state = &mut self.symbol_states[symbol];
self.definitions_by_definition.insert(
binding,
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
);
symbol_state.record_binding(def_id);
}
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
let constraint_id = self.all_constraints.push(constraint);
for state in &mut self.symbol_states {
state.record_constraint(constraint_id);
}
}
pub(super) fn record_declaration(
&mut self,
symbol: ScopedSymbolId,
declaration: Definition<'db>,
) {
let def_id = self.all_definitions.push(declaration);
let symbol_state = &mut self.symbol_states[symbol];
self.definitions_by_definition.insert(
declaration,
SymbolDefinitions::Bindings(symbol_state.bindings().clone()),
);
symbol_state.record_declaration(def_id);
}
pub(super) fn record_declaration_and_binding(
pub(super) fn record_definition(
&mut self,
symbol: ScopedSymbolId,
definition: Definition<'db>,
) {
// We don't need to store anything in self.definitions_by_definition.
// We have a new definition of a symbol; this replaces any previous definitions in this
// path.
let def_id = self.all_definitions.push(definition);
let symbol_state = &mut self.symbol_states[symbol];
symbol_state.record_declaration(def_id);
symbol_state.record_binding(def_id);
self.definitions_by_symbol[symbol] = SymbolState::with(def_id);
}
pub(super) fn record_constraint(&mut self, constraint: Expression<'db>) {
let constraint_id = self.all_constraints.push(constraint);
for definitions in &mut self.definitions_by_symbol {
definitions.add_constraint(constraint_id);
}
}
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
// We have a use of a symbol; clone the current bindings for that symbol, and record them
// as the live bindings for this use.
// We have a use of a symbol; clone the currently visible definitions for that symbol, and
// record them as the visible definitions for this use.
let new_use = self
.bindings_by_use
.push(self.symbol_states[symbol].bindings().clone());
.definitions_by_use
.push(self.definitions_by_symbol[symbol].clone());
debug_assert_eq!(use_id, new_use);
}
/// Take a snapshot of the current visible-symbols state.
pub(super) fn snapshot(&self) -> FlowSnapshot {
FlowSnapshot {
symbol_states: self.symbol_states.clone(),
definitions_by_symbol: self.definitions_by_symbol.clone(),
}
}
/// Restore the current builder symbols state to the given snapshot.
/// Restore the current builder visible-definitions state to the given snapshot.
pub(super) fn restore(&mut self, snapshot: FlowSnapshot) {
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
// We never remove symbols from `definitions_by_symbol` (it's an IndexVec, and the symbol
// IDs must line up), so the current number of known symbols must always be equal to or
// greater than the number of known symbols in a previously-taken snapshot.
let num_symbols = self.symbol_states.len();
debug_assert!(num_symbols >= snapshot.symbol_states.len());
let num_symbols = self.definitions_by_symbol.len();
debug_assert!(num_symbols >= snapshot.definitions_by_symbol.len());
// Restore the current visible-definitions state to the given snapshot.
self.symbol_states = snapshot.symbol_states;
self.definitions_by_symbol = snapshot.definitions_by_symbol;
// If the snapshot we are restoring is missing some symbols we've recorded since, we need
// to fill them in so the symbol IDs continue to line up. Since they don't exist in the
// snapshot, the correct state to fill them in with is "undefined".
self.symbol_states
.resize(num_symbols, SymbolState::undefined());
// snapshot, the correct state to fill them in with is "unbound".
self.definitions_by_symbol
.resize(num_symbols, SymbolState::unbound());
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.
/// path to get here. The new visible-definitions state for each symbol should include
/// definitions from both the prior state and the snapshot.
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
// The tricky thing about merging two Ranges pointing into `all_definitions` is that if the
// two Ranges aren't already adjacent in `all_definitions`, we will have to copy at least
// one or the other of the ranges to the end of `all_definitions` so as to make them
// adjacent. We can't ever move things around in `all_definitions` because previously
// recorded uses may still have ranges pointing to any part of it; all we can do is append.
// It's possible we may end up with some old entries in `all_definitions` that nobody is
// pointing to, but that's OK.
// We never remove symbols from `definitions_by_symbol` (it's an IndexVec, and the symbol
// IDs must line up), so the current number of known symbols must always be equal to or
// greater than the number of known symbols in a previously-taken snapshot.
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
debug_assert!(self.definitions_by_symbol.len() >= snapshot.definitions_by_symbol.len());
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
for current in &mut self.symbol_states {
let mut snapshot_definitions_iter = snapshot.definitions_by_symbol.into_iter();
for current in &mut self.definitions_by_symbol {
if let Some(snapshot) = snapshot_definitions_iter.next() {
current.merge(snapshot);
} else {
// Symbol not present in snapshot, so it's unbound from that path.
current.set_may_be_unbound();
current.add_unbound();
}
}
}
@@ -546,16 +362,14 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn finish(mut self) -> UseDefMap<'db> {
self.all_definitions.shrink_to_fit();
self.all_constraints.shrink_to_fit();
self.symbol_states.shrink_to_fit();
self.bindings_by_use.shrink_to_fit();
self.definitions_by_definition.shrink_to_fit();
self.definitions_by_symbol.shrink_to_fit();
self.definitions_by_use.shrink_to_fit();
UseDefMap {
all_definitions: self.all_definitions,
all_constraints: self.all_constraints,
bindings_by_use: self.bindings_by_use,
public_symbols: self.symbol_states,
definitions_by_definition: self.definitions_by_definition,
definitions_by_use: self.definitions_by_use,
public_definitions: self.definitions_by_symbol,
}
}
}

View File

@@ -32,25 +32,17 @@ impl<const B: usize> BitSet<B> {
bitset
}
pub(super) fn is_empty(&self) -> bool {
self.blocks().iter().all(|&b| b == 0)
}
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
fn resize(&mut self, value: u32) {
let num_blocks_needed = (value / 64) + 1;
self.resize_blocks(num_blocks_needed as usize);
}
fn resize_blocks(&mut self, num_blocks_needed: usize) {
match self {
Self::Inline(blocks) => {
let mut vec = blocks.to_vec();
vec.resize(num_blocks_needed, 0);
vec.resize(num_blocks_needed as usize, 0);
*self = Self::Heap(vec);
}
Self::Heap(vec) => {
vec.resize(num_blocks_needed, 0);
vec.resize(num_blocks_needed as usize, 0);
}
}
}
@@ -97,20 +89,6 @@ impl<const B: usize> BitSet<B> {
}
}
/// Union in-place with another [`BitSet`].
pub(super) fn union(&mut self, other: &BitSet<B>) {
let mut max_len = self.blocks().len();
let other_len = other.blocks().len();
if other_len > max_len {
max_len = other_len;
self.resize_blocks(max_len);
}
let other_blocks = other.blocks();
for (i, my_block) in self.blocks_mut().iter_mut().enumerate() {
*my_block |= other_blocks.get(i).unwrap_or(&0);
}
}
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
let blocks = self.blocks();
@@ -240,59 +218,6 @@ mod tests {
assert_bitset(&b1, &[89]);
}
#[test]
fn union() {
let mut b1 = BitSet::<1>::with(2);
let b2 = BitSet::<1>::with(4);
b1.union(&b2);
assert_bitset(&b1, &[2, 4]);
}
#[test]
fn union_mixed_1() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(89);
b2.insert(5);
b1.union(&b2);
assert_bitset(&b1, &[4, 5, 89]);
}
#[test]
fn union_mixed_2() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(23);
b2.insert(89);
b1.union(&b2);
assert_bitset(&b1, &[4, 23, 89]);
}
#[test]
fn union_heap() {
let mut b1 = BitSet::<1>::with(4);
let mut b2 = BitSet::<1>::with(4);
b1.insert(89);
b2.insert(90);
b1.union(&b2);
assert_bitset(&b1, &[4, 89, 90]);
}
#[test]
fn union_heap_2() {
let mut b1 = BitSet::<1>::with(89);
let mut b2 = BitSet::<1>::with(89);
b1.insert(91);
b2.insert(90);
b1.union(&b2);
assert_bitset(&b1, &[89, 90, 91]);
}
#[test]
fn multiple_blocks() {
let mut b = BitSet::<2>::with(120);
@@ -300,11 +225,4 @@ mod tests {
assert!(matches!(b, BitSet::Inline(_)));
assert_bitset(&b, &[45, 120]);
}
#[test]
fn empty() {
let b = BitSet::<1>::default();
assert!(b.is_empty());
}
}

View File

@@ -53,178 +53,93 @@ pub(super) struct ScopedDefinitionId;
pub(super) struct ScopedConstraintId;
/// Can reference this * 64 total definitions inline; more will fall back to the heap.
const INLINE_BINDING_BLOCKS: usize = 3;
const INLINE_DEFINITION_BLOCKS: usize = 3;
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live bindings of a symbol in a scope.
type Bindings = BitSet<INLINE_BINDING_BLOCKS>;
type BindingsIterator<'a> = BitSetIterator<'a, INLINE_BINDING_BLOCKS>;
/// Can reference this * 64 total declarations inline; more will fall back to the heap.
const INLINE_DECLARATION_BLOCKS: usize = 3;
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live declarations of a symbol in a scope.
type Declarations = BitSet<INLINE_DECLARATION_BLOCKS>;
type DeclarationsIterator<'a> = BitSetIterator<'a, INLINE_DECLARATION_BLOCKS>;
/// A [`BitSet`] of [`ScopedDefinitionId`], representing visible definitions of a symbol in a scope.
type Definitions = BitSet<INLINE_DEFINITION_BLOCKS>;
type DefinitionsIterator<'a> = BitSetIterator<'a, INLINE_DEFINITION_BLOCKS>;
/// Can reference this * 64 total constraints inline; more will fall back to the heap.
const INLINE_CONSTRAINT_BLOCKS: usize = 2;
/// Can keep inline this many live bindings per symbol at a given time; more will go to heap.
const INLINE_BINDINGS_PER_SYMBOL: usize = 4;
/// Can keep inline this many visible definitions per symbol at a given time; more will go to heap.
const INLINE_VISIBLE_DEFINITIONS_PER_SYMBOL: usize = 4;
/// One [`BitSet`] of applicable [`ScopedConstraintId`] per live binding.
type InlineConstraintArray = [BitSet<INLINE_CONSTRAINT_BLOCKS>; INLINE_BINDINGS_PER_SYMBOL];
/// One [`BitSet`] of applicable [`ScopedConstraintId`] per visible definition.
type InlineConstraintArray =
[BitSet<INLINE_CONSTRAINT_BLOCKS>; INLINE_VISIBLE_DEFINITIONS_PER_SYMBOL];
type Constraints = SmallVec<InlineConstraintArray>;
type ConstraintsIterator<'a> = std::slice::Iter<'a, BitSet<INLINE_CONSTRAINT_BLOCKS>>;
type ConstraintsIntoIterator = smallvec::IntoIter<InlineConstraintArray>;
/// Live declarations for a single symbol at some point in control flow.
/// Visible definitions and narrowing constraints for a single symbol at some point in control flow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SymbolDeclarations {
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
live_declarations: Declarations,
}
pub(super) struct SymbolState {
/// [`BitSet`]: which [`ScopedDefinitionId`] are visible for this symbol?
visible_definitions: Definitions,
impl SymbolDeclarations {
fn undeclared() -> Self {
Self {
live_declarations: Declarations::default(),
}
}
/// Record a newly-encountered declaration for this symbol.
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.live_declarations = Declarations::with(declaration_id.into());
}
/// Return an iterator over live declarations for this symbol.
pub(super) fn iter(&self) -> DeclarationIdIterator {
DeclarationIdIterator {
inner: self.live_declarations.iter(),
}
}
pub(super) fn is_empty(&self) -> bool {
self.live_declarations.is_empty()
}
}
/// Live bindings and narrowing constraints for a single symbol at some point in control flow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SymbolBindings {
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
live_bindings: Bindings,
/// For each live binding, which [`ScopedConstraintId`] apply?
/// For each definition, which [`ScopedConstraintId`] apply?
///
/// This is a [`smallvec::SmallVec`] which should always have one [`BitSet`] of constraints per
/// binding in `live_bindings`.
/// definition in `visible_definitions`.
constraints: Constraints,
/// Could the symbol be unbound at this point?
may_be_unbound: bool,
}
impl SymbolBindings {
fn unbound() -> Self {
/// A single [`ScopedDefinitionId`] with an iterator of its applicable [`ScopedConstraintId`].
#[derive(Debug)]
pub(super) struct DefinitionIdWithConstraints<'a> {
pub(super) definition: ScopedDefinitionId,
pub(super) constraint_ids: ConstraintIdIterator<'a>,
}
impl SymbolState {
/// Return a new [`SymbolState`] representing an unbound symbol.
pub(super) fn unbound() -> Self {
Self {
live_bindings: Bindings::default(),
visible_definitions: Definitions::default(),
constraints: Constraints::default(),
may_be_unbound: true,
}
}
/// Return a new [`SymbolState`] representing a symbol with a single visible definition.
pub(super) fn with(definition_id: ScopedDefinitionId) -> Self {
let mut constraints = Constraints::with_capacity(1);
constraints.push(BitSet::default());
Self {
visible_definitions: Definitions::with(definition_id.into()),
constraints,
may_be_unbound: false,
}
}
/// Add Unbound as a possibility for this symbol.
fn set_may_be_unbound(&mut self) {
pub(super) fn add_unbound(&mut self) {
self.may_be_unbound = true;
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
let mut constraints = Constraints::with_capacity(1);
constraints.push(BitSet::default());
self.constraints = constraints;
self.may_be_unbound = false;
}
/// Add given constraint to all live bindings.
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
/// Add given constraint to all currently-visible definitions.
pub(super) fn add_constraint(&mut self, constraint_id: ScopedConstraintId) {
for bitset in &mut self.constraints {
bitset.insert(constraint_id.into());
}
}
/// Iterate over currently live bindings for this symbol.
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
BindingIdWithConstraintsIterator {
definitions: self.live_bindings.iter(),
constraints: self.constraints.iter(),
}
}
pub(super) fn may_be_unbound(&self) -> bool {
self.may_be_unbound
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SymbolState {
declarations: SymbolDeclarations,
bindings: SymbolBindings,
}
impl SymbolState {
/// Return a new [`SymbolState`] representing an unbound, undeclared symbol.
pub(super) fn undefined() -> Self {
Self {
declarations: SymbolDeclarations::undeclared(),
bindings: SymbolBindings::unbound(),
}
}
/// Add Unbound as a possibility for this symbol.
pub(super) fn set_may_be_unbound(&mut self) {
self.bindings.set_may_be_unbound();
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
self.bindings.record_binding(binding_id);
}
/// Add given constraint to all live bindings.
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
self.bindings.record_constraint(constraint_id);
}
/// Record a newly-encountered declaration of this symbol.
pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.declarations.record_declaration(declaration_id);
}
/// Merge another [`SymbolState`] into this one.
pub(super) fn merge(&mut self, b: SymbolState) {
let mut a = Self {
bindings: SymbolBindings {
live_bindings: Bindings::default(),
constraints: Constraints::default(),
may_be_unbound: self.bindings.may_be_unbound || b.bindings.may_be_unbound,
},
declarations: self.declarations.clone(),
visible_definitions: Definitions::default(),
constraints: Constraints::default(),
may_be_unbound: self.may_be_unbound || b.may_be_unbound,
};
std::mem::swap(&mut a, self);
self.declarations
.live_declarations
.union(&b.declarations.live_declarations);
let mut a_defs_iter = a.bindings.live_bindings.iter();
let mut b_defs_iter = b.bindings.live_bindings.iter();
let mut a_constraints_iter = a.bindings.constraints.into_iter();
let mut b_constraints_iter = b.bindings.constraints.into_iter();
let mut a_defs_iter = a.visible_definitions.iter();
let mut b_defs_iter = b.visible_definitions.iter();
let mut a_constraints_iter = a.constraints.into_iter();
let mut b_constraints_iter = b.constraints.into_iter();
let mut opt_a_def: Option<u32> = a_defs_iter.next();
let mut opt_b_def: Option<u32> = b_defs_iter.next();
@@ -237,7 +152,7 @@ impl SymbolState {
// Helper to push `def`, with constraints in `constraints_iter`, onto `self`.
let push = |def, constraints_iter: &mut ConstraintsIntoIterator, merged: &mut Self| {
merged.bindings.live_bindings.insert(def);
merged.visible_definitions.insert(def);
// SAFETY: we only ever create SymbolState with either no definitions and no constraint
// bitsets (`::unbound`) or one definition and one constraint bitset (`::with`), and
// `::merge` always pushes one definition and one constraint bitset together (just
@@ -246,7 +161,7 @@ impl SymbolState {
let constraints = constraints_iter
.next()
.expect("definitions and constraints length mismatch");
merged.bindings.constraints.push(constraints);
merged.constraints.push(constraints);
};
loop {
@@ -276,8 +191,7 @@ impl SymbolState {
// If the same definition is visible through both paths, any constraint
// that applies on only one path is irrelevant to the resulting type from
// unioning the two paths, so we intersect the constraints.
self.bindings
.constraints
self.constraints
.last_mut()
.unwrap()
.intersect(&a_constraints);
@@ -300,49 +214,40 @@ impl SymbolState {
}
}
pub(super) fn bindings(&self) -> &SymbolBindings {
&self.bindings
}
pub(super) fn declarations(&self) -> &SymbolDeclarations {
&self.declarations
/// Get iterator over visible definitions with constraints.
pub(super) fn visible_definitions(&self) -> DefinitionIdWithConstraintsIterator {
DefinitionIdWithConstraintsIterator {
definitions: self.visible_definitions.iter(),
constraints: self.constraints.iter(),
}
}
/// Could the symbol be unbound?
pub(super) fn may_be_unbound(&self) -> bool {
self.bindings.may_be_unbound()
self.may_be_unbound
}
}
/// The default state of a symbol, if we've seen no definitions of it, is undefined (that is,
/// both unbound and undeclared).
/// The default state of a symbol (if we've seen no definitions of it) is unbound.
impl Default for SymbolState {
fn default() -> Self {
SymbolState::undefined()
SymbolState::unbound()
}
}
/// A single binding (as [`ScopedDefinitionId`]) with an iterator of its applicable
/// [`ScopedConstraintId`].
#[derive(Debug)]
pub(super) struct BindingIdWithConstraints<'a> {
pub(super) definition: ScopedDefinitionId,
pub(super) constraint_ids: ConstraintIdIterator<'a>,
}
#[derive(Debug)]
pub(super) struct BindingIdWithConstraintsIterator<'a> {
definitions: BindingsIterator<'a>,
pub(super) struct DefinitionIdWithConstraintsIterator<'a> {
definitions: DefinitionsIterator<'a>,
constraints: ConstraintsIterator<'a>,
}
impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
type Item = BindingIdWithConstraints<'a>;
impl<'a> Iterator for DefinitionIdWithConstraintsIterator<'a> {
type Item = DefinitionIdWithConstraints<'a>;
fn next(&mut self) -> Option<Self::Item> {
match (self.definitions.next(), self.constraints.next()) {
(None, None) => None,
(Some(def), Some(constraints)) => Some(BindingIdWithConstraints {
(Some(def), Some(constraints)) => Some(DefinitionIdWithConstraints {
definition: ScopedDefinitionId::from_u32(def),
constraint_ids: ConstraintIdIterator {
wrapped: constraints.iter(),
@@ -354,7 +259,7 @@ impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
}
}
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
impl std::iter::FusedIterator for DefinitionIdWithConstraintsIterator<'_> {}
#[derive(Debug)]
pub(super) struct ConstraintIdIterator<'a> {
@@ -371,29 +276,15 @@ impl Iterator for ConstraintIdIterator<'_> {
impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
#[derive(Debug)]
pub(super) struct DeclarationIdIterator<'a> {
inner: DeclarationsIterator<'a>,
}
impl<'a> Iterator for DeclarationIdIterator<'a> {
type Item = ScopedDefinitionId;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(ScopedDefinitionId::from_u32)
}
}
#[cfg(test)]
mod tests {
use super::{ScopedConstraintId, ScopedDefinitionId, SymbolState};
impl SymbolState {
pub(crate) fn assert_bindings(&self, may_be_unbound: bool, expected: &[&str]) {
pub(crate) fn assert(&self, may_be_unbound: bool, expected: &[&str]) {
assert_eq!(self.may_be_unbound(), may_be_unbound);
let actual = self
.bindings()
.iter()
.visible_definitions()
.map(|def_id_with_constraints| {
format!(
"{}<{}>",
@@ -409,122 +300,75 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
pub(crate) fn assert_declarations(&self, expected: &[u32]) {
let actual = self
.declarations()
.iter()
.map(ScopedDefinitionId::as_u32)
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
}
#[test]
fn unbound() {
let sym = SymbolState::undefined();
let cd = SymbolState::unbound();
sym.assert_bindings(true, &[]);
cd.assert(true, &[]);
}
#[test]
fn with() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
let cd = SymbolState::with(ScopedDefinitionId::from_u32(0));
sym.assert_bindings(false, &["0<>"]);
cd.assert(false, &["0<>"]);
}
#[test]
fn set_may_be_unbound() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
sym.set_may_be_unbound();
fn add_unbound() {
let mut cd = SymbolState::with(ScopedDefinitionId::from_u32(0));
cd.add_unbound();
sym.assert_bindings(true, &["0<>"]);
cd.assert(true, &["0<>"]);
}
#[test]
fn record_constraint() {
let mut sym = SymbolState::undefined();
sym.record_binding(ScopedDefinitionId::from_u32(0));
sym.record_constraint(ScopedConstraintId::from_u32(0));
fn add_constraint() {
let mut cd = SymbolState::with(ScopedDefinitionId::from_u32(0));
cd.add_constraint(ScopedConstraintId::from_u32(0));
sym.assert_bindings(false, &["0<0>"]);
cd.assert(false, &["0<0>"]);
}
#[test]
fn merge() {
// merging the same definition with the same constraint keeps the constraint
let mut sym0a = SymbolState::undefined();
sym0a.record_binding(ScopedDefinitionId::from_u32(0));
sym0a.record_constraint(ScopedConstraintId::from_u32(0));
let mut cd0a = SymbolState::with(ScopedDefinitionId::from_u32(0));
cd0a.add_constraint(ScopedConstraintId::from_u32(0));
let mut sym0b = SymbolState::undefined();
sym0b.record_binding(ScopedDefinitionId::from_u32(0));
sym0b.record_constraint(ScopedConstraintId::from_u32(0));
let mut cd0b = SymbolState::with(ScopedDefinitionId::from_u32(0));
cd0b.add_constraint(ScopedConstraintId::from_u32(0));
sym0a.merge(sym0b);
let mut sym0 = sym0a;
sym0.assert_bindings(false, &["0<0>"]);
cd0a.merge(cd0b);
let mut cd0 = cd0a;
cd0.assert(false, &["0<0>"]);
// merging the same definition with differing constraints drops all constraints
let mut sym1a = SymbolState::undefined();
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
sym1a.record_constraint(ScopedConstraintId::from_u32(1));
let mut cd1a = SymbolState::with(ScopedDefinitionId::from_u32(1));
cd1a.add_constraint(ScopedConstraintId::from_u32(1));
let mut sym1b = SymbolState::undefined();
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
let mut cd1b = SymbolState::with(ScopedDefinitionId::from_u32(1));
cd1b.add_constraint(ScopedConstraintId::from_u32(2));
sym1a.merge(sym1b);
let sym1 = sym1a;
sym1.assert_bindings(false, &["1<>"]);
cd1a.merge(cd1b);
let cd1 = cd1a;
cd1.assert(false, &["1<>"]);
// merging a constrained definition with unbound keeps both
let mut sym2a = SymbolState::undefined();
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
sym2a.record_constraint(ScopedConstraintId::from_u32(3));
let mut cd2a = SymbolState::with(ScopedDefinitionId::from_u32(2));
cd2a.add_constraint(ScopedConstraintId::from_u32(3));
let sym2b = SymbolState::undefined();
let cd2b = SymbolState::unbound();
sym2a.merge(sym2b);
let sym2 = sym2a;
sym2.assert_bindings(true, &["2<3>"]);
cd2a.merge(cd2b);
let cd2 = cd2a;
cd2.assert(true, &["2<3>"]);
// merging different definitions keeps them each with their existing constraints
sym0.merge(sym2);
let sym = sym0;
sym.assert_bindings(true, &["0<0>", "2<3>"]);
}
#[test]
fn record_declaration() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
sym.assert_declarations(&[1]);
}
#[test]
fn record_declaration_override() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
sym.record_declaration(ScopedDefinitionId::from_u32(2));
sym.assert_declarations(&[2]);
}
#[test]
fn record_declaration_merge() {
let mut sym = SymbolState::undefined();
sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut sym2 = SymbolState::undefined();
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
sym.merge(sym2);
sym.assert_declarations(&[1, 2]);
cd0.merge(cd2);
let cd = cd0;
cd.assert(true, &["0<0>", "2<3>"]);
}
}

View File

@@ -8,7 +8,7 @@ use crate::module_name::ModuleName;
use crate::module_resolver::{resolve_module, Module};
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::semantic_index;
use crate::types::{binding_ty, global_symbol_ty, infer_scope_types, Type};
use crate::types::{definition_ty, global_symbol_ty_by_name, infer_scope_types, Type};
use crate::Db;
pub struct SemanticModel<'db> {
@@ -40,7 +40,7 @@ impl<'db> SemanticModel<'db> {
}
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
global_symbol_ty(self.db, module.file(), symbol_name)
global_symbol_ty_by_name(self.db, module.file(), symbol_name)
}
}
@@ -147,24 +147,24 @@ impl HasTy for ast::Expr {
}
}
macro_rules! impl_binding_has_ty {
macro_rules! impl_definition_has_ty {
($ty: ty) => {
impl HasTy for $ty {
#[inline]
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let index = semantic_index(model.db, model.file);
let binding = index.definition(self);
binding_ty(model.db, binding)
let definition = index.definition(self);
definition_ty(model.db, definition)
}
}
};
}
impl_binding_has_ty!(ast::StmtFunctionDef);
impl_binding_has_ty!(ast::StmtClassDef);
impl_binding_has_ty!(ast::Alias);
impl_binding_has_ty!(ast::Parameter);
impl_binding_has_ty!(ast::ParameterWithDefault);
impl_definition_has_ty!(ast::StmtFunctionDef);
impl_definition_has_ty!(ast::StmtClassDef);
impl_definition_has_ty!(ast::Alias);
impl_definition_has_ty!(ast::Parameter);
impl_definition_has_ty!(ast::ParameterWithDefault);
#[cfg(test)]
mod tests {

View File

@@ -1,77 +0,0 @@
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::semantic_index::global_scope;
use crate::semantic_index::symbol::ScopeId;
use crate::types::{global_symbol_ty, Type};
use crate::Db;
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CoreStdlibModule {
Builtins,
Types,
Typeshed,
}
impl CoreStdlibModule {
fn name(self) -> ModuleName {
let module_name = match self {
Self::Builtins => "builtins",
Self::Types => "types",
Self::Typeshed => "_typeshed",
};
ModuleName::new_static(module_name)
.unwrap_or_else(|| panic!("{module_name} should be a valid module name!"))
}
}
/// Lookup the type of `symbol` in a given core module
///
/// Returns `Unbound` if the given core module cannot be resolved for some reason
fn core_module_symbol_ty<'db>(
db: &'db dyn Db,
core_module: CoreStdlibModule,
symbol: &str,
) -> Type<'db> {
resolve_module(db, core_module.name())
.map(|module| global_symbol_ty(db, module.file(), symbol))
.unwrap_or(Type::Unbound)
}
/// Lookup the type of `symbol` in the builtins namespace.
///
/// Returns `Unbound` if the `builtins` module isn't available for some reason.
#[inline]
pub(crate) fn builtins_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
core_module_symbol_ty(db, CoreStdlibModule::Builtins, symbol)
}
/// Lookup the type of `symbol` in the `types` module namespace.
///
/// Returns `Unbound` if the `types` module isn't available for some reason.
#[inline]
pub(crate) fn types_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
core_module_symbol_ty(db, CoreStdlibModule::Types, symbol)
}
/// Lookup the type of `symbol` in the `_typeshed` module namespace.
///
/// Returns `Unbound` if the `_typeshed` module isn't available for some reason.
#[inline]
pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
}
/// Get the scope of a core stdlib module.
///
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<ScopeId<'_>> {
resolve_module(db, core_module.name()).map(|module| global_scope(db, module.file()))
}
/// Get the `builtins` module scope.
///
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
core_module_scope(db, CoreStdlibModule::Builtins)
}

View File

@@ -1,16 +1,14 @@
use infer::TypeInferenceBuilder;
use ruff_db::files::File;
use ruff_python_ast as ast;
use crate::module_resolver::file_to_module;
use crate::builtins::builtins_scope;
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
use crate::semantic_index::{
global_scope, semantic_index, symbol_table, use_def_map, BindingWithConstraints,
BindingWithConstraintsIterator, DeclarationsIterator,
global_scope, semantic_index, symbol_table, use_def_map, DefinitionWithConstraints,
DefinitionWithConstraintsIterator,
};
use crate::stdlib::{builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet};
@@ -18,6 +16,7 @@ pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::diagnostic::TypeCheckDiagnostics;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
TypeInference,
};
mod builder;
@@ -41,52 +40,54 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
}
/// Infer the public type of a symbol (its type as seen from outside its scope).
pub(crate) fn symbol_ty_by_id<'db>(
pub(crate) fn symbol_ty<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
symbol: ScopedSymbolId,
) -> Type<'db> {
let _span = tracing::trace_span!("symbol_ty_by_id", ?symbol).entered();
let _span = tracing::trace_span!("symbol_ty", ?symbol).entered();
let use_def = use_def_map(db, scope);
if use_def.has_public_declarations(symbol) {
declarations_ty(db, use_def.public_declarations(symbol))
} else {
bindings_ty(
db,
use_def.public_bindings(symbol),
use_def
.public_may_be_unbound(symbol)
.then_some(Type::Unbound),
)
}
definitions_ty(
db,
use_def.public_definitions(symbol),
use_def
.public_may_be_unbound(symbol)
.then_some(Type::Unbound),
)
}
/// Shorthand for `symbol_ty` that takes a symbol name instead of an ID.
pub(crate) fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
pub(crate) fn symbol_ty_by_name<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
name: &str,
) -> Type<'db> {
let table = symbol_table(db, scope);
table
.symbol_id_by_name(name)
.map(|symbol| symbol_ty_by_id(db, scope, symbol))
.map(|symbol| symbol_ty(db, scope, symbol))
.unwrap_or(Type::Unbound)
}
/// Shorthand for `symbol_ty` that looks up a module-global symbol by name in a file.
pub(crate) fn global_symbol_ty<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
symbol_ty(db, global_scope(db, file), name)
pub(crate) fn global_symbol_ty_by_name<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
symbol_ty_by_name(db, global_scope(db, file), name)
}
/// Infer the type of a binding.
pub(crate) fn binding_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
let inference = infer_definition_types(db, definition);
inference.binding_ty(definition)
/// Shorthand for `symbol_ty` that looks up a symbol in the builtins.
///
/// Returns `Unbound` if the builtins module isn't available for some reason.
pub(crate) fn builtins_symbol_ty_by_name<'db>(db: &'db dyn Db, name: &str) -> Type<'db> {
builtins_scope(db)
.map(|builtins| symbol_ty_by_name(db, builtins, name))
.unwrap_or(Type::Unbound)
}
/// Infer the type of a declaration.
pub(crate) fn declaration_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
/// Infer the type of a [`Definition`].
pub(crate) fn definition_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
let inference = infer_definition_types(db, definition);
inference.declaration_ty(definition)
inference.definition_ty(definition)
}
/// Infer the type of a (possibly deferred) sub-expression of a [`Definition`].
@@ -122,30 +123,30 @@ pub(crate) fn definition_expression_ty<'db>(
/// Will panic if called with zero definitions and no `unbound_ty`. This is a logic error,
/// as any symbol with zero visible definitions clearly may be unbound, and the caller should
/// provide an `unbound_ty`.
pub(crate) fn bindings_ty<'db>(
pub(crate) fn definitions_ty<'db>(
db: &'db dyn Db,
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
definitions_with_constraints: DefinitionWithConstraintsIterator<'_, 'db>,
unbound_ty: Option<Type<'db>>,
) -> Type<'db> {
let def_types = bindings_with_constraints.map(
|BindingWithConstraints {
binding,
let def_types = definitions_with_constraints.map(
|DefinitionWithConstraints {
definition,
constraints,
}| {
let mut constraint_tys =
constraints.filter_map(|constraint| narrowing_constraint(db, constraint, binding));
let binding_ty = binding_ty(db, binding);
constraints.filter_map(|test| narrowing_constraint(db, test, definition));
let definition_ty = definition_ty(db, definition);
if let Some(first_constraint_ty) = constraint_tys.next() {
let mut builder = IntersectionBuilder::new(db);
builder = builder
.add_positive(binding_ty)
.add_positive(definition_ty)
.add_positive(first_constraint_ty);
for constraint_ty in constraint_tys {
builder = builder.add_positive(constraint_ty);
}
builder.build()
} else {
binding_ty
definition_ty
}
},
);
@@ -153,23 +154,22 @@ pub(crate) fn bindings_ty<'db>(
let first = all_types
.next()
.expect("bindings_ty should never be called with zero definitions and no unbound_ty.");
.expect("definitions_ty should never be called with zero definitions and no unbound_ty.");
if let Some(second) = all_types.next() {
UnionType::from_elements(db, [first, second].into_iter().chain(all_types))
let mut builder = UnionBuilder::new(db);
builder = builder.add(first).add(second);
for variant in all_types {
builder = builder.add(variant);
}
builder.build()
} else {
first
}
}
/// Union an iterable of declared types.
pub(crate) fn declarations_ty<'db>(
db: &'db dyn Db,
declarations: DeclarationsIterator<'_, 'db>,
) -> Type<'db> {
UnionType::from_elements(db, declarations.map(|decl| declaration_ty(db, decl)))
}
/// Unique ID for a type.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Type<'db> {
@@ -208,9 +208,6 @@ pub enum Type<'db> {
LiteralString,
/// A bytes literal
BytesLiteral(BytesLiteralType<'db>),
/// A heterogeneous tuple type, with elements of the given types in source order.
// TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`.
Tuple(TupleType<'db>),
// TODO protocols, callable types, overloads, generics, type vars
}
@@ -309,47 +306,17 @@ impl<'db> Type<'db> {
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> {
match self {
Type::Unbound => replacement,
Type::Union(union) => {
union.map(db, |element| element.replace_unbound_with(db, replacement))
}
Type::Union(union) => union
.elements(db)
.into_iter()
.fold(UnionBuilder::new(db), |builder, ty| {
builder.add(ty.replace_unbound_with(db, replacement))
})
.build(),
ty => *ty,
}
}
/// Return true if this type is assignable to type `other`.
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
if self.is_equivalent_to(db, other) {
return true;
}
match (self, other) {
(Type::Unknown | Type::Any | Type::Never, _) => true,
(_, Type::Unknown | Type::Any) => true,
(Type::IntLiteral(_), Type::Instance(class)) if class.is_builtin_named(db, "int") => {
true
}
(Type::StringLiteral(_), Type::LiteralString) => true,
(Type::StringLiteral(_) | Type::LiteralString, Type::Instance(class))
if class.is_builtin_named(db, "str") =>
{
true
}
(Type::BytesLiteral(_), Type::Instance(class))
if class.is_builtin_named(db, "bytes") =>
{
true
}
// TODO
_ => false,
}
}
/// Return true if this type is equivalent to type `other`.
pub(crate) fn is_equivalent_to(self, _db: &'db dyn Db, other: Type<'db>) -> bool {
// TODO equivalent but not identical structural types, differently-ordered unions and
// intersections, other cases?
self == other
}
/// Resolve a member access of a type.
///
/// For example, if `foo` is `Type::Instance(<Bar>)`,
@@ -364,7 +331,7 @@ impl<'db> Type<'db> {
/// us to explicitly consider whether to handle an error or propagate
/// it up the call stack.
#[must_use]
pub fn member(&self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn member(&self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
match self {
Type::Any => Type::Any,
Type::Never => {
@@ -381,13 +348,19 @@ impl<'db> Type<'db> {
// TODO: attribute lookup on function type
Type::Unknown
}
Type::Module(file) => global_symbol_ty(db, *file, name),
Type::Module(file) => global_symbol_ty_by_name(db, *file, name),
Type::Class(class) => class.class_member(db, name),
Type::Instance(_) => {
// TODO MRO? get_own_instance_member, get_instance_member
Type::Unknown
}
Type::Union(union) => union.map(db, |element| element.member(db, name)),
Type::Union(union) => union
.elements(db)
.iter()
.fold(UnionBuilder::new(db), |builder, element_ty| {
builder.add(element_ty.member(db, name))
})
.build(),
Type::Intersection(_) => {
// TODO perform the get_member on each type in the intersection
// TODO return the intersection of those results
@@ -412,10 +385,6 @@ impl<'db> Type<'db> {
// TODO defer to Type::Instance(<bytes from typeshed>).member
Type::Unknown
}
Type::Tuple(_) => {
// TODO: implement tuple methods
Type::Unknown
}
}
}
@@ -446,138 +415,13 @@ impl<'db> Type<'db> {
}
}
/// Given the type of an object that is iterated over in some way,
/// return the type of objects that are yielded by that iteration.
///
/// E.g., for the following loop, given the type of `x`, infer the type of `y`:
/// ```python
/// for y in x:
/// pass
/// ```
fn iterate(&self, db: &'db dyn Db) -> IterationOutcome<'db> {
if let Type::Tuple(tuple_type) = self {
return IterationOutcome::Iterable {
element_ty: UnionType::from_elements(db, &**tuple_type.elements(db)),
};
}
// `self` represents the type of the iterable;
// `__iter__` and `__next__` are both looked up on the class of the iterable:
let iterable_meta_type = self.to_meta_type(db);
let dunder_iter_method = iterable_meta_type.member(db, "__iter__");
if !dunder_iter_method.is_unbound() {
let Some(iterator_ty) = dunder_iter_method.call(db) else {
return IterationOutcome::NotIterable {
not_iterable_ty: *self,
};
};
let dunder_next_method = iterator_ty.to_meta_type(db).member(db, "__next__");
return dunder_next_method
.call(db)
.map(|element_ty| IterationOutcome::Iterable { element_ty })
.unwrap_or(IterationOutcome::NotIterable {
not_iterable_ty: *self,
});
}
// Although it's not considered great practice,
// classes that define `__getitem__` are also iterable,
// even if they do not define `__iter__`.
//
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
// accepting `int` or `SupportsIndex`
let dunder_get_item_method = iterable_meta_type.member(db, "__getitem__");
dunder_get_item_method
.call(db)
.map(|element_ty| IterationOutcome::Iterable { element_ty })
.unwrap_or(IterationOutcome::NotIterable {
not_iterable_ty: *self,
})
}
#[must_use]
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
pub fn to_instance(&self) -> Type<'db> {
match self {
Type::Any => Type::Any,
Type::Unknown => Type::Unknown,
Type::Unbound => Type::Unknown,
Type::Never => Type::Never,
Type::Class(class) => Type::Instance(*class),
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => Type::Unknown,
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
// since they already indicate that the object is an instance of some kind:
Type::BooleanLiteral(_)
| Type::BytesLiteral(_)
| Type::Function(_)
| Type::Instance(_)
| Type::Module(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::Tuple(_)
| Type::LiteralString
| Type::None => Type::Unknown,
}
}
/// Given a type that is assumed to represent an instance of a class,
/// return a type that represents that class itself.
#[must_use]
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Unbound => Type::Unbound,
Type::Never => Type::Never,
Type::Instance(class) => Type::Class(*class),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => builtins_symbol_ty(db, "bool"),
Type::BytesLiteral(_) => builtins_symbol_ty(db, "bytes"),
Type::IntLiteral(_) => builtins_symbol_ty(db, "int"),
Type::Function(_) => types_symbol_ty(db, "FunctionType"),
Type::Module(_) => types_symbol_ty(db, "ModuleType"),
Type::None => typeshed_symbol_ty(db, "NoneType"),
// TODO not accurate if there's a custom metaclass...
Type::Class(_) => builtins_symbol_ty(db, "type"),
// TODO can we do better here? `type[LiteralString]`?
Type::StringLiteral(_) | Type::LiteralString => builtins_symbol_ty(db, "str"),
// TODO: `type[Any]`?
Type::Any => Type::Any,
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
// TODO intersections
Type::Intersection(_) => Type::Unknown,
Type::Tuple(_) => builtins_symbol_ty(db, "tuple"),
}
}
}
impl<'db> From<&Type<'db>> for Type<'db> {
fn from(value: &Type<'db>) -> Self {
*value
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IterationOutcome<'db> {
Iterable { element_ty: Type<'db> },
NotIterable { not_iterable_ty: Type<'db> },
}
impl<'db> IterationOutcome<'db> {
fn unwrap_with_diagnostic(
self,
iterable_node: ast::AnyNodeRef,
inference_builder: &mut TypeInferenceBuilder<'db>,
) -> Type<'db> {
match self {
Self::Iterable { element_ty } => element_ty,
Self::NotIterable { not_iterable_ty } => {
inference_builder.not_iterable_diagnostic(iterable_node, not_iterable_ty);
Type::Unknown
}
_ => Type::Unknown, // TODO type errors
}
}
}
@@ -602,7 +446,7 @@ impl<'db> FunctionType<'db> {
/// inferred return type for this function
pub fn return_type(&self, db: &'db dyn Db) -> Type<'db> {
let definition = self.definition(db);
let DefinitionKind::Function(function_stmt_node) = definition.kind(db) else {
let DefinitionKind::Function(function_stmt_node) = definition.node(db) else {
panic!("Function type definition must have `DefinitionKind::Function`")
};
@@ -642,21 +486,13 @@ pub struct ClassType<'db> {
}
impl<'db> ClassType<'db> {
pub(crate) fn is_builtin_named(self, db: &'db dyn Db, name: &str) -> bool {
name == self.name(db).as_str()
&& file_to_module(db, self.body_scope(db).file(db))
// Builtin module names are special-cased in the resolver, so there can't be a
// module named builtins other than the actual builtins.
.is_some_and(|module| module.name().as_str() == "builtins")
}
/// Return an iterator over the types of this class's bases.
///
/// # Panics:
/// If `definition` is not a `DefinitionKind::Class`.
pub fn bases(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
let definition = self.definition(db);
let DefinitionKind::Class(class_stmt_node) = definition.kind(db) else {
let DefinitionKind::Class(class_stmt_node) = definition.node(db) else {
panic!("Class type definition must have DefinitionKind::Class");
};
class_stmt_node
@@ -668,7 +504,7 @@ impl<'db> ClassType<'db> {
/// Returns the class member of this class named `name`.
///
/// The member resolves to a member of the class itself or any of its bases.
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
let member = self.own_class_member(db, name);
if !member.is_unbound() {
return member;
@@ -678,12 +514,12 @@ impl<'db> ClassType<'db> {
}
/// Returns the inferred type of the class member named `name`.
pub fn own_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn own_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
let scope = self.body_scope(db);
symbol_ty(db, scope, name)
symbol_ty_by_name(db, scope, name)
}
pub fn inherited_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
pub fn inherited_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
for base in self.bases(db) {
let member = base.member(db, name);
if !member.is_unbound() {
@@ -706,30 +542,6 @@ impl<'db> UnionType<'db> {
pub fn contains(&self, db: &'db dyn Db, ty: Type<'db>) -> bool {
self.elements(db).contains(&ty)
}
/// Create a union from a list of elements
/// (which may be eagerly simplified into a different variant of [`Type`] altogether)
pub fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
elements: impl IntoIterator<Item = T>,
) -> Type<'db> {
elements
.into_iter()
.fold(UnionBuilder::new(db), |builder, element| {
builder.add(element.into())
})
.build()
}
/// Apply a transformation function to all elements of the union,
/// and create a new union from the resulting set of types
pub fn map(
&self,
db: &'db dyn Db,
transform_fn: impl Fn(&Type<'db>) -> Type<'db>,
) -> Type<'db> {
Self::from_elements(db, self.elements(db).into_iter().map(transform_fn))
}
}
#[salsa::interned]
@@ -759,37 +571,29 @@ pub struct BytesLiteralType<'db> {
value: Box<[u8]>,
}
#[salsa::interned]
pub struct TupleType<'db> {
#[return_ref]
elements: Box<[Type<'db>]>,
}
#[cfg(test)]
mod tests {
#![allow(clippy::needless_pass_by_value)]
use anyhow::Context;
use super::{builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::ProgramSettings;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use test_case::test_case;
use crate::db::tests::TestDb;
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
use super::TypeCheckDiagnostics;
fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.create_directory_all("/src")
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
search_paths: SearchPathSettings::new(SystemPathBuf::from("/src")),
},
)
.expect("Valid search path settings");
@@ -797,71 +601,91 @@ mod tests {
db
}
/// A test representation of a type that can be transformed unambiguously into a real Type,
/// given a db.
#[derive(Debug)]
enum Ty {
Never,
Unknown,
Any,
IntLiteral(i64),
StringLiteral(&'static str),
LiteralString,
BytesLiteral(&'static str),
BuiltinInstance(&'static str),
Union(Box<[Ty]>),
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
let messages: Vec<&str> = diagnostics
.iter()
.map(|diagnostic| diagnostic.message())
.collect();
assert_eq!(&messages, expected);
}
impl Ty {
fn to_type<'db>(&self, db: &'db TestDb) -> Type<'db> {
match self {
Ty::Never => Type::Never,
Ty::Unknown => Type::Unknown,
Ty::Any => Type::Any,
Ty::IntLiteral(n) => Type::IntLiteral(*n),
Ty::StringLiteral(s) => {
Type::StringLiteral(StringLiteralType::new(db, (*s).into()))
}
Ty::LiteralString => Type::LiteralString,
Ty::BytesLiteral(s) => {
Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes().into()))
}
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
Ty::Union(tys) => UnionType::from_elements(db, tys.iter().map(|ty| ty.to_type(db))),
}
}
#[test]
fn unresolved_import_statement() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_file("src/foo.py", "import bar\n")
.context("Failed to write foo.py")?;
let foo = system_path_to_file(&db, "src/foo.py").context("Failed to resolve foo.py")?;
let diagnostics = super::check_types(&db, foo);
assert_diagnostic_messages(&diagnostics, &["Cannot resolve import 'bar'."]);
Ok(())
}
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
#[test_case(Ty::Any, Ty::IntLiteral(1))]
#[test_case(Ty::Never, Ty::IntLiteral(1))]
#[test_case(Ty::IntLiteral(1), Ty::Unknown)]
#[test_case(Ty::IntLiteral(1), Ty::Any)]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))]
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::StringLiteral("foo"), Ty::LiteralString)]
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))]
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
fn is_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.to_type(&db).is_assignable_to(&db, to.to_type(&db)));
#[test]
fn unresolved_import_from_statement() {
let mut db = setup_db();
db.write_file("src/foo.py", "from bar import baz\n")
.unwrap();
let foo = system_path_to_file(&db, "src/foo.py").unwrap();
let diagnostics = super::check_types(&db, foo);
assert_diagnostic_messages(&diagnostics, &["Cannot resolve import 'bar'."]);
}
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
fn is_not_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.to_type(&db).is_assignable_to(&db, to.to_type(&db)));
#[test]
fn unresolved_import_from_resolved_module() {
let mut db = setup_db();
db.write_files([("/src/a.py", ""), ("/src/b.py", "from a import thing")])
.unwrap();
let b_file = system_path_to_file(&db, "/src/b.py").unwrap();
let b_file_diagnostics = super::check_types(&db, b_file);
assert_diagnostic_messages(&b_file_diagnostics, &["Module 'a' has no member 'thing'"]);
}
#[test_case(
Ty::Union(Box::new([Ty::IntLiteral(1), Ty::IntLiteral(2)])),
Ty::Union(Box::new([Ty::IntLiteral(1), Ty::IntLiteral(2)]))
)]
fn is_equivalent_to(from: Ty, to: Ty) {
let db = setup_db();
#[test]
fn resolved_import_of_symbol_from_unresolved_import() {
let mut db = setup_db();
assert!(from.to_type(&db).is_equivalent_to(&db, to.to_type(&db)));
db.write_files([
("/src/a.py", "import foo as foo"),
("/src/b.py", "from a import foo"),
])
.unwrap();
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
let a_file_diagnostics = super::check_types(&db, a_file);
assert_diagnostic_messages(&a_file_diagnostics, &["Cannot resolve import 'foo'."]);
// Importing the unresolved import into a second first-party file should not trigger
// an additional "unresolved import" violation
let b_file = system_path_to_file(&db, "/src/b.py").unwrap();
let b_file_diagnostics = super::check_types(&db, b_file);
assert_eq!(&*b_file_diagnostics, &[]);
}
#[test]
fn invalid_callable() {
let mut db = setup_db();
db.write_dedented(
"src/a.py",
"
nonsense = 123
x = nonsense()
",
)
.unwrap();
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
let a_file_diagnostics = super::check_types(&db, a_file);
assert_diagnostic_messages(
&a_file_diagnostics,
&["Object of type 'Literal[123]' is not callable"],
);
}
}

View File

@@ -25,10 +25,13 @@
//! * No type in an intersection can be a supertype of any other type in the intersection (just
//! eliminate the supertype from the intersection).
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
use crate::types::{IntersectionType, Type, UnionType};
use crate::{Db, FxOrderSet};
use ordermap::set::MutableValues;
use super::builtins_symbol_ty_by_name;
pub(crate) struct UnionBuilder<'db> {
elements: FxOrderSet<Type<'db>>,
db: &'db dyn Db,
@@ -65,7 +68,7 @@ impl<'db> UnionBuilder<'db> {
if let Some(true_index) = self.elements.get_index_of(&Type::BooleanLiteral(true)) {
if self.elements.contains(&Type::BooleanLiteral(false)) {
*self.elements.get_index_mut2(true_index).unwrap() =
builtins_symbol_ty(self.db, "bool");
builtins_symbol_ty_by_name(self.db, "bool");
self.elements.remove(&Type::BooleanLiteral(false));
}
}
@@ -169,12 +172,11 @@ impl<'db> IntersectionBuilder<'db> {
if self.intersections.len() == 1 {
self.intersections.pop().unwrap().build(self.db)
} else {
UnionType::from_elements(
self.db,
self.intersections
.into_iter()
.map(|inner| inner.build(self.db)),
)
let mut builder = UnionBuilder::new(self.db);
for inner in self.intersections {
builder = builder.add(inner.build(self.db));
}
builder.build()
}
}
}
@@ -272,11 +274,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
#[cfg(test)]
mod tests {
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
use super::{IntersectionBuilder, IntersectionType, Type, UnionBuilder, UnionType};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::{builtins_symbol_ty, UnionBuilder};
use crate::types::builtins_symbol_ty_by_name;
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -311,7 +313,11 @@ mod tests {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
let union = UnionBuilder::new(&db)
.add(t0)
.add(t1)
.build()
.expect_union();
assert_eq!(union.elements_vec(&db), &[t0, t1]);
}
@@ -320,7 +326,8 @@ mod tests {
fn build_union_single() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ty = UnionType::from_elements(&db, [t0]);
let ty = UnionBuilder::new(&db).add(t0).build();
assert_eq!(ty, t0);
}
@@ -328,6 +335,7 @@ mod tests {
fn build_union_empty() {
let db = setup_db();
let ty = UnionBuilder::new(&db).build();
assert_eq!(ty, Type::Never);
}
@@ -335,24 +343,36 @@ mod tests {
fn build_union_never() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
let ty = UnionBuilder::new(&db).add(t0).add(Type::Never).build();
assert_eq!(ty, t0);
}
#[test]
fn build_union_bool() {
let db = setup_db();
let bool_ty = builtins_symbol_ty(&db, "bool");
let bool_ty = builtins_symbol_ty_by_name(&db, "bool");
let t0 = Type::BooleanLiteral(true);
let t1 = Type::BooleanLiteral(true);
let t2 = Type::BooleanLiteral(false);
let t3 = Type::IntLiteral(17);
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
let union = UnionBuilder::new(&db)
.add(t0)
.add(t1)
.add(t3)
.build()
.expect_union();
assert_eq!(union.elements_vec(&db), &[t0, t3]);
let union = UnionBuilder::new(&db)
.add(t0)
.add(t1)
.add(t2)
.add(t3)
.build()
.expect_union();
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
assert_eq!(union.elements_vec(&db), &[bool_ty, t3]);
}
@@ -362,8 +382,12 @@ mod tests {
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let t2 = Type::IntLiteral(2);
let u1 = UnionType::from_elements(&db, [t0, t1]);
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
let u1 = UnionBuilder::new(&db).add(t0).add(t1).build();
let union = UnionBuilder::new(&db)
.add(u1)
.add(t2)
.build()
.expect_union();
assert_eq!(union.elements_vec(&db), &[t0, t1, t2]);
}
@@ -439,7 +463,7 @@ mod tests {
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let ta = Type::Any;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u0 = UnionBuilder::new(&db).add(t0).add(t1).build();
let union = IntersectionBuilder::new(&db)
.add_positive(ta)

View File

@@ -86,23 +86,6 @@ impl std::fmt::Display for DisplayRepresentation<'_> {
escape.bytes_repr().write(f)
}
Type::Tuple(tuple) => {
f.write_str("tuple[")?;
let elements = tuple.elements(self.db);
if elements.is_empty() {
f.write_str("()")?;
} else {
let mut first = true;
for element in &**elements {
if !first {
f.write_str(", ")?;
}
first = false;
element.display(self.db).fmt(f)?;
}
}
f.write_str("]")
}
}
}
}
@@ -253,7 +236,9 @@ mod tests {
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use crate::db::tests::TestDb;
use crate::types::{global_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType};
use crate::types::{
global_symbol_ty_by_name, BytesLiteralType, StringLiteralType, Type, UnionBuilder,
};
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
fn setup_db() -> TestDb {
@@ -295,23 +280,26 @@ mod tests {
)?;
let mod_file = system_path_to_file(&db, "src/main.py").expect("Expected file to exist.");
let union_elements = &[
let vec: Vec<Type<'_>> = vec![
Type::Unknown,
Type::IntLiteral(-1),
global_symbol_ty(&db, mod_file, "A"),
global_symbol_ty_by_name(&db, mod_file, "A"),
Type::StringLiteral(StringLiteralType::new(&db, Box::from("A"))),
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([0]))),
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([7]))),
Type::IntLiteral(0),
Type::IntLiteral(1),
Type::StringLiteral(StringLiteralType::new(&db, Box::from("B"))),
global_symbol_ty(&db, mod_file, "foo"),
global_symbol_ty(&db, mod_file, "bar"),
global_symbol_ty(&db, mod_file, "B"),
global_symbol_ty_by_name(&db, mod_file, "foo"),
global_symbol_ty_by_name(&db, mod_file, "bar"),
global_symbol_ty_by_name(&db, mod_file, "B"),
Type::BooleanLiteral(true),
Type::None,
];
let union = UnionType::from_elements(&db, union_elements).expect_union();
let builder = vec.iter().fold(UnionBuilder::new(&db), |builder, literal| {
builder.add(*literal)
});
let union = builder.build().expect_union();
let display = format!("{}", union.display(&db));
assert_eq!(
display,

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::constraint::{Constraint, PatternConstraint};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
use crate::semantic_index::symbol_table;
use crate::types::{infer_expression_types, IntersectionBuilder, Type};
use crate::types::{infer_expression_types, IntersectionBuilder, Type, TypeInference};
use crate::Db;
use ruff_python_ast as ast;
use rustc_hash::FxHashMap;
@@ -28,114 +27,62 @@ use std::sync::Arc;
/// constraint is applied to that definition, so we'd just return `None`.
pub(crate) fn narrowing_constraint<'db>(
db: &'db dyn Db,
constraint: Constraint<'db>,
test: Expression<'db>,
definition: Definition<'db>,
) -> Option<Type<'db>> {
match constraint {
Constraint::Expression(expression) => {
all_narrowing_constraints_for_expression(db, expression)
.get(&definition.symbol(db))
.copied()
}
Constraint::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern)
.get(&definition.symbol(db))
.copied(),
}
all_narrowing_constraints(db, test)
.get(&definition.symbol(db))
.copied()
}
#[salsa::tracked(return_ref)]
fn all_narrowing_constraints_for_pattern<'db>(
fn all_narrowing_constraints<'db>(
db: &'db dyn Db,
pattern: PatternConstraint<'db>,
test: Expression<'db>,
) -> NarrowingConstraints<'db> {
NarrowingConstraintsBuilder::new(db, Constraint::Pattern(pattern)).finish()
}
#[salsa::tracked(return_ref)]
fn all_narrowing_constraints_for_expression<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> NarrowingConstraints<'db> {
NarrowingConstraintsBuilder::new(db, Constraint::Expression(expression)).finish()
NarrowingConstraintsBuilder::new(db, test).finish()
}
type NarrowingConstraints<'db> = FxHashMap<ScopedSymbolId, Type<'db>>;
struct NarrowingConstraintsBuilder<'db> {
db: &'db dyn Db,
constraint: Constraint<'db>,
expression: Expression<'db>,
constraints: NarrowingConstraints<'db>,
}
impl<'db> NarrowingConstraintsBuilder<'db> {
fn new(db: &'db dyn Db, constraint: Constraint<'db>) -> Self {
fn new(db: &'db dyn Db, expression: Expression<'db>) -> Self {
Self {
db,
constraint,
expression,
constraints: NarrowingConstraints::default(),
}
}
fn finish(mut self) -> NarrowingConstraints<'db> {
match self.constraint {
Constraint::Expression(expression) => self.evaluate_expression_constraint(expression),
Constraint::Pattern(pattern) => self.evaluate_pattern_constraint(pattern),
if let ast::Expr::Compare(expr_compare) = self.expression.node_ref(self.db).node() {
self.add_expr_compare(expr_compare);
}
// TODO other test expression kinds
self.constraints.shrink_to_fit();
self.constraints
}
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>) {
if let ast::Expr::Compare(expr_compare) = expression.node_ref(self.db).node() {
self.add_expr_compare(expr_compare, expression);
}
// TODO other test expression kinds
}
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
let subject = pattern.subject(self.db);
match pattern.pattern(self.db).node() {
ast::Pattern::MatchValue(_) => {
// TODO
}
ast::Pattern::MatchSingleton(singleton_pattern) => {
self.add_match_pattern_singleton(subject, singleton_pattern);
}
ast::Pattern::MatchSequence(_) => {
// TODO
}
ast::Pattern::MatchMapping(_) => {
// TODO
}
ast::Pattern::MatchClass(_) => {
// TODO
}
ast::Pattern::MatchStar(_) => {
// TODO
}
ast::Pattern::MatchAs(_) => {
// TODO
}
ast::Pattern::MatchOr(_) => {
// TODO
}
}
}
fn symbols(&self) -> Arc<SymbolTable> {
symbol_table(self.db, self.scope())
}
fn scope(&self) -> ScopeId<'db> {
match self.constraint {
Constraint::Expression(expression) => expression.scope(self.db),
Constraint::Pattern(pattern) => pattern.scope(self.db),
}
self.expression.scope(self.db)
}
fn add_expr_compare(&mut self, expr_compare: &ast::ExprCompare, expression: Expression<'db>) {
fn inference(&self) -> &'db TypeInference<'db> {
infer_expression_types(self.db, self.expression)
}
fn add_expr_compare(&mut self, expr_compare: &ast::ExprCompare) {
let ast::ExprCompare {
range: _,
left,
@@ -152,7 +99,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let scope = self.scope();
let inference = infer_expression_types(self.db, expression);
let inference = self.inference();
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
if matches!(op, ast::CmpOp::IsNot) {
@@ -165,22 +112,4 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
}
fn add_match_pattern_singleton(
&mut self,
subject: &ast::Expr,
pattern: &ast::PatternMatchSingleton,
) {
if let Some(ast::ExprName { id, .. }) = subject.as_name_expr() {
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let ty = match pattern.value {
ast::Singleton::None => Type::None,
ast::Singleton::True => Type::BooleanLiteral(true),
ast::Singleton::False => Type::BooleanLiteral(false),
};
self.constraints.insert(symbol, ty);
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.6.4"
version = "0.6.3"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -66,4 +66,4 @@ codspeed = ["codspeed-criterion-compat"]
mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true, features = ["unprefixed_malloc_on_supported_platforms"] }
tikv-jemallocator = { workspace = true }

View File

@@ -28,24 +28,6 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
// Disable decay after 10s because it can show up as *random* slow allocations
// in benchmarks. We don't need purging in benchmarks because it isn't important
// to give unallocated pages back to the OS.
// https://jemalloc.net/jemalloc.3.html#opt.dirty_decay_ms
#[cfg(all(
not(target_os = "windows"),
not(target_os = "openbsd"),
any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "powerpc64"
)
))]
#[allow(non_upper_case_globals)]
#[export_name = "malloc_conf"]
#[allow(unsafe_code)]
pub static malloc_conf: &[u8] = b"dirty_decay_ms:-1,muzzy_decay_ms:-1\0";
fn create_test_cases() -> Result<Vec<TestCase>, TestFileDownloadError> {
Ok(vec![
TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?),

View File

@@ -23,7 +23,6 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
// The failed import from 'collections.abc' is due to lack of support for 'import *'.
static EXPECTED_DIAGNOSTICS: &[&str] = &[
"/src/tomllib/_parser.py:5:24: Module '__future__' has no member 'annotations'",
"/src/tomllib/_parser.py:7:29: Module 'collections.abc' has no member 'Iterable'",
"Line 69 is too long (89 characters)",
"Use double quotes for strings",
@@ -33,6 +32,7 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"Use double quotes for strings",
"Use double quotes for strings",
"Use double quotes for strings",
"/src/tomllib/_parser.py:628:75: Name 'e' used when not defined.",
];
fn get_test_file(name: &str) -> TestFile {

View File

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

View File

@@ -57,18 +57,7 @@ dictionary = {
# ]
# ///
# Script tag with multiple closing tags (OK)
# /// script
# [tool.uv]
# extra-index-url = ["https://pypi.org/simple", """\
# https://example.com/
# ///
# """
# ]
# ///
print(1)
# Script tag without a closing tag (Error)
# Script tag without a closing tag (OK)
# /// script
# requires-python = ">=3.11"

View File

@@ -207,10 +207,3 @@ def foo(s: str) -> str | None:
s (str): A string.
"""
return None
class Spam:
# OK
def __new__(cls) -> 'Spam':
"""New!!"""
return cls()

View File

@@ -94,9 +94,6 @@ class Apples:
def __mro_entries__(self, bases):
pass
# Removed with Python 3
def __unicode__(self):
pass
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@@ -1,154 +0,0 @@
# Test suite from Refurb
# See https://github.com/dosisod/refurb/blob/db02242b142285e615a664a8d3324470bb711306/test/data/err_188.py
# these should match
def remove_extension_via_slice(filename: str) -> str:
if filename.endswith(".txt"):
filename = filename[:-4]
return filename
def remove_extension_via_slice_len(filename: str, extension: str) -> str:
if filename.endswith(extension):
filename = filename[:-len(extension)]
return filename
def remove_extension_via_ternary(filename: str) -> str:
return filename[:-4] if filename.endswith(".txt") else filename
def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
return filename[:-len(extension)] if filename.endswith(extension) else filename
def remove_prefix(filename: str) -> str:
return filename[4:] if filename.startswith("abc-") else filename
def remove_prefix_via_len(filename: str, prefix: str) -> str:
return filename[len(prefix):] if filename.startswith(prefix) else filename
# these should not
def remove_extension_with_mismatched_len(filename: str) -> str:
if filename.endswith(".txt"):
filename = filename[:3]
return filename
def remove_extension_assign_to_different_var(filename: str) -> str:
if filename.endswith(".txt"):
other_var = filename[:-4]
return filename
def remove_extension_with_multiple_stmts(filename: str) -> str:
if filename.endswith(".txt"):
print("do some work")
filename = filename[:-4]
if filename.endswith(".txt"):
filename = filename[:-4]
print("do some work")
return filename
def remove_extension_from_unrelated_var(filename: str) -> str:
xyz = "abc.txt"
if filename.endswith(".txt"):
filename = xyz[:-4]
return filename
def remove_extension_in_elif(filename: str) -> str:
if filename:
pass
elif filename.endswith(".txt"):
filename = filename[:-4]
return filename
def remove_extension_in_multiple_elif(filename: str) -> str:
if filename:
pass
elif filename:
pass
elif filename.endswith(".txt"):
filename = filename[:-4]
return filename
def remove_extension_in_if_with_else(filename: str) -> str:
if filename.endswith(".txt"):
filename = filename[:-4]
else:
pass
return filename
def remove_extension_ternary_name_mismatch(filename: str):
xyz = ""
_ = xyz[:-4] if filename.endswith(".txt") else filename
_ = filename[:-4] if xyz.endswith(".txt") else filename
_ = filename[:-4] if filename.endswith(".txt") else xyz
def remove_extension_slice_amount_mismatch(filename: str) -> None:
extension = ".txt"
_ = filename[:-1] if filename.endswith(".txt") else filename
_ = filename[:-1] if filename.endswith(extension) else filename
_ = filename[:-len("")] if filename.endswith(extension) else filename
def remove_prefix_size_mismatch(filename: str) -> str:
return filename[3:] if filename.startswith("abc-") else filename
def remove_prefix_name_mismatch(filename: str) -> None:
xyz = ""
_ = xyz[4:] if filename.startswith("abc-") else filename
_ = filename[4:] if xyz.startswith("abc-") else filename
_ = filename[4:] if filename.startswith("abc-") else xyz
# ---- End of refurb test suite ---- #
# ---- Begin ruff specific test suite --- #
# these should be linted
def remove_suffix_multiple_attribute_expr() -> None:
import foo.bar
SUFFIX = "suffix"
x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
def remove_prefix_comparable_literal_expr() -> None:
return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
def shadow_builtins(filename: str, extension: str) -> None:
from builtins import len as builtins_len
return filename[:-builtins_len(extension)] if filename.endswith(extension) else filename

View File

@@ -50,13 +50,6 @@ a = 10.0
val = Decimal(a)
# See https://github.com/astral-sh/ruff/issues/13258
val = Decimal(~4.0) # Skip
val = Decimal(++4.0) # Suggest `Decimal("4.0")`
val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
# Tests with shadowed name
class Decimal():

View File

@@ -1,11 +0,0 @@
# Valid
x = 1 if True else 2
# Invalid
x = 1 if True else 1
# Invalid
x = "a" if True else "a"
# Invalid
x = 0.1 if False else 0.1

View File

@@ -1404,12 +1404,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::IfExpInsteadOfOrOperator) {
refurb::rules::if_exp_instead_of_or_operator(checker, if_exp);
}
if checker.enabled(Rule::UselessIfElse) {
ruff::rules::useless_if_else(checker, if_exp);
}
if checker.enabled(Rule::SliceToRemovePrefixOrSuffix) {
refurb::rules::slice_to_remove_affix_expr(checker, if_exp);
}
}
Expr::ListComp(
comp @ ast::ExprListComp {

View File

@@ -1178,9 +1178,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::CheckAndRemoveFromSet) {
refurb::rules::check_and_remove_from_set(checker, if_);
}
if checker.enabled(Rule::SliceToRemovePrefixOrSuffix) {
refurb::rules::slice_to_remove_affix_stmt(checker, if_);
}
if checker.enabled(Rule::TooManyBooleanExpressions) {
pylint::rules::too_many_boolean_expressions(checker, if_);
}

View File

@@ -1,6 +1,5 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_semantic::Exceptions;
use ruff_python_stdlib::builtins::version_builtin_was_added;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
@@ -36,12 +35,9 @@ pub(crate) fn unresolved_references(checker: &mut Checker) {
}
}
let symbol_name = reference.name(checker.locator);
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: symbol_name.to_string(),
minor_version_builtin_added: version_builtin_was_added(symbol_name),
name: reference.name(checker.locator).to_string(),
},
reference.range(),
));

View File

@@ -1951,25 +1951,20 @@ impl<'a> Checker<'a> {
}
fn bind_builtins(&mut self) {
let mut bind_builtin = |builtin| {
// Add the builtin to the scope.
let binding_id = self.semantic.push_builtin();
let scope = self.semantic.global_scope_mut();
scope.add(builtin, binding_id);
};
let standard_builtins = python_builtins(
self.settings.target_version.minor(),
self.source_type.is_ipynb(),
);
for builtin in standard_builtins {
bind_builtin(builtin);
}
for builtin in MAGIC_GLOBALS {
bind_builtin(builtin);
}
for builtin in &self.settings.builtins {
bind_builtin(builtin);
for builtin in standard_builtins
.iter()
.chain(MAGIC_GLOBALS.iter())
.copied()
.chain(self.settings.builtins.iter().map(String::as_str))
{
// Add the builtin to the scope.
let binding_id = self.semantic.push_builtin();
let scope = self.semantic.global_scope_mut();
scope.add(builtin, binding_id);
}
}

View File

@@ -63,7 +63,7 @@ pub(crate) fn check_tokens(
ruff::rules::ambiguous_unicode_character_comment(
&mut diagnostics,
locator,
range,
*range,
settings,
);
}
@@ -154,13 +154,7 @@ pub(crate) fn check_tokens(
Rule::ShebangNotFirstLine,
Rule::ShebangMissingPython,
]) {
flake8_executable::rules::from_tokens(
&mut diagnostics,
path,
locator,
comment_ranges,
settings,
);
flake8_executable::rules::from_tokens(&mut diagnostics, path, locator, comment_ranges);
}
if settings.rules.any_enabled(&[

View File

@@ -961,7 +961,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript),
(Ruff, "032") => (RuleGroup::Preview, rules::ruff::rules::DecimalFromFloatLiteral),
(Ruff, "033") => (RuleGroup::Preview, rules::ruff::rules::PostInitDefault),
(Ruff, "034") => (RuleGroup::Preview, rules::ruff::rules::UselessIfElse),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
@@ -1067,7 +1066,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta),
(Refurb, "181") => (RuleGroup::Stable, rules::refurb::rules::HashlibDigestHex),
(Refurb, "187") => (RuleGroup::Stable, rules::refurb::rules::ListReverseCopy),
(Refurb, "188") => (RuleGroup::Preview, rules::refurb::rules::SliceToRemovePrefixOrSuffix),
(Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax),
// flake8-logging

View File

@@ -1,9 +1,9 @@
use std::collections::HashSet;
use std::io::Write;
use anyhow::Result;
use serde::{Serialize, Serializer};
use serde_json::json;
use strum::IntoEnumIterator;
use ruff_source_file::OneIndexed;
@@ -27,10 +27,6 @@ impl Emitter for SarifEmitter {
.map(SarifResult::from_message)
.collect::<Result<Vec<_>>>()?;
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.rule).collect();
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
rules.sort_by(|a, b| a.code.cmp(&b.code));
let output = json!({
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
@@ -39,7 +35,7 @@ impl Emitter for SarifEmitter {
"driver": {
"name": "ruff",
"informationUri": "https://github.com/astral-sh/ruff",
"rules": rules,
"rules": Rule::iter().map(SarifRule::from).collect::<Vec<_>>(),
"version": VERSION.to_string(),
}
},
@@ -220,23 +216,9 @@ mod tests {
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 3);
assert_eq!(
results
.iter()
.map(|r| r["message"]["text"].as_str().unwrap())
.collect::<Vec<_>>(),
vec![
"`os` imported but unused",
"Local variable `x` is assigned to but never used",
"Undefined name `a`",
]
);
assert_eq!(rules.len(), 3);
assert_eq!(
rules
.iter()
.map(|r| r["id"].as_str().unwrap())
.collect::<Vec<_>>(),
vec!["F401", "F821", "F841"],
results[0]["message"]["text"].as_str().unwrap(),
"`os` imported but unused"
);
assert!(rules.len() > 3);
}
}

View File

@@ -361,7 +361,7 @@ impl<'a> FileNoqaDirectives<'a> {
let mut lines = vec![];
for range in comment_ranges {
match ParsedFileExemption::try_extract(&contents[range]) {
match ParsedFileExemption::try_extract(&contents[*range]) {
Err(err) => {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
@@ -403,7 +403,7 @@ impl<'a> FileNoqaDirectives<'a> {
};
lines.push(FileNoqaDirectiveLine {
range,
range: *range,
parsed_file_exemption: exemption,
matches,
});
@@ -922,7 +922,7 @@ impl<'a> NoqaDirectives<'a> {
let mut directives = Vec::new();
for range in comment_ranges {
match Directive::try_extract(locator.slice(range), range.start()) {
match Directive::try_extract(locator.slice(*range), range.start()) {
Err(err) => {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());

View File

@@ -1,9 +1,9 @@
use crate::settings::LinterSettings;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{Locator, UniversalNewlineIterator};
use ruff_text_size::TextRange;
use ruff_source_file::Locator;
use crate::settings::LinterSettings;
use super::super::detection::comment_contains_code;
@@ -50,110 +50,40 @@ pub(crate) fn commented_out_code(
comment_ranges: &CommentRanges,
settings: &LinterSettings,
) {
let mut comments = comment_ranges.into_iter().peekable();
// Iterate over all comments in the document.
while let Some(range) = comments.next() {
let line = locator.line(range.start());
// Skip comments within `/// script` tags.
let mut in_script_tag = false;
if is_script_tag_start(line) {
if skip_script_comments(range, &mut comments, locator) {
continue;
// Iterate over all comments in the document.
for range in comment_ranges {
let line = locator.lines(*range);
// Detect `/// script` tags.
if in_script_tag {
if is_script_tag_end(line) {
in_script_tag = false;
}
} else {
if is_script_tag_start(line) {
in_script_tag = true;
}
}
// Skip comments within `/// script` tags.
if in_script_tag {
continue;
}
// Verify that the comment is on its own line, and that it contains code.
if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, range);
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
diagnostic.set_fix(Fix::display_only_edit(Edit::range_deletion(
locator.full_lines_range(range),
locator.full_lines_range(*range),
)));
diagnostics.push(diagnostic);
}
}
}
/// Parses the rest of a [PEP 723](https://peps.python.org/pep-0723/)
/// script comment and moves `comments` past the script comment's end unless
/// the script comment is invalid.
///
/// Returns `true` if it is a valid script comment.
fn skip_script_comments<I>(
script_start: TextRange,
comments: &mut std::iter::Peekable<I>,
locator: &Locator,
) -> bool
where
I: Iterator<Item = TextRange>,
{
let line_end = locator.full_line_end(script_start.end());
let rest = locator.after(line_end);
let mut end_offset = None;
let mut lines = UniversalNewlineIterator::with_offset(rest, line_end).peekable();
while let Some(line) = lines.next() {
let Some(content) = script_line_content(&line) else {
break;
};
if content == "///" {
// > Precedence for an ending line # /// is given when the next line is not a valid
// > embedded content line as described above.
// > For example, the following is a single fully valid block:
// > ```python
// > # /// some-toml
// > # embedded-csharp = """
// > # /// <summary>
// > # /// text
// > # ///
// > # /// </summary>
// > # public class MyClass { }
// > # """
// > # ///
// ````
if lines.next().is_some_and(|line| is_valid_script_line(&line)) {
continue;
}
end_offset = Some(line.full_end());
break;
}
}
// > Unclosed blocks MUST be ignored.
let Some(end_offset) = end_offset else {
return false;
};
// Skip over all script-comments.
while let Some(comment) = comments.peek() {
if comment.start() >= end_offset {
break;
}
comments.next();
}
true
}
fn script_line_content(line: &str) -> Option<&str> {
let Some(rest) = line.strip_prefix('#') else {
// Not a comment
return None;
};
// An empty line
if rest.is_empty() {
return Some("");
}
// > If there are characters after the # then the first character MUST be a space.
rest.strip_prefix(' ')
}
fn is_valid_script_line(line: &str) -> bool {
script_line_content(line).is_some()
}
/// Returns `true` if line contains an own-line comment.
fn is_own_line_comment(line: &str) -> bool {
for char in line.chars() {
@@ -174,77 +104,9 @@ fn is_script_tag_start(line: &str) -> bool {
line == "# /// script"
}
#[cfg(test)]
mod tests {
use crate::rules::eradicate::rules::commented_out_code::skip_script_comments;
use ruff_python_parser::parse_module;
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
use ruff_text_size::TextSize;
#[test]
fn script_comment() {
let code = r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
a = 10 # abc
"#;
let parsed = parse_module(code).unwrap();
let locator = Locator::new(code);
let comments = CommentRanges::from(parsed.tokens());
let mut comments = comments.into_iter().peekable();
let script_start = code.find("# /// script").unwrap();
let script_start_range = locator.full_line_range(TextSize::try_from(script_start).unwrap());
let valid = skip_script_comments(script_start_range, &mut comments, &Locator::new(code));
assert!(valid);
let next_comment = comments.next();
assert!(next_comment.is_some());
assert_eq!(&code[next_comment.unwrap()], "# abc");
}
#[test]
fn script_comment_end_precedence() {
let code = r#"
# /// script
# [tool.uv]
# extra-index-url = ["https://pypi.org/simple", """\
# https://example.com/
# ///
# """
# ]
# ///
a = 10 # abc
"#;
let parsed = parse_module(code).unwrap();
let locator = Locator::new(code);
let comments = CommentRanges::from(parsed.tokens());
let mut comments = comments.into_iter().peekable();
let script_start = code.find("# /// script").unwrap();
let script_start_range = locator.full_line_range(TextSize::try_from(script_start).unwrap());
let valid = skip_script_comments(script_start_range, &mut comments, &Locator::new(code));
assert!(valid);
let next_comment = comments.next();
assert!(next_comment.is_some());
assert_eq!(&code[next_comment.unwrap()], "# abc");
}
/// Returns `true` if the line appears to start a script tag.
///
/// See: <https://peps.python.org/pep-0723/>
fn is_script_tag_end(line: &str) -> bool {
line == "# ///"
}

View File

@@ -321,38 +321,3 @@ ERA001.py:47:1: ERA001 Found commented-out code
48 47 | # ///
49 48 |
50 49 | # Script tag (OK)
ERA001.py:75:1: ERA001 Found commented-out code
|
73 | # /// script
74 | # requires-python = ">=3.11"
75 | # dependencies = [
| ^^^^^^^^^^^^^^^^^^ ERA001
76 | # "requests<3",
77 | # "rich",
|
= help: Remove commented-out code
Display-only fix
72 72 |
73 73 | # /// script
74 74 | # requires-python = ">=3.11"
75 |-# dependencies = [
76 75 | # "requests<3",
77 76 | # "rich",
78 77 | # ]
ERA001.py:78:1: ERA001 Found commented-out code
|
76 | # "requests<3",
77 | # "rich",
78 | # ]
| ^^^ ERA001
|
= help: Remove commented-out code
Display-only fix
75 75 | # dependencies = [
76 76 | # "requests<3",
77 77 | # "rich",
78 |-# ]

View File

@@ -1,8 +1,5 @@
use std::path::Path;
use crate::codes::Rule;
use crate::comments::shebang::ShebangDirective;
use crate::settings::LinterSettings;
use ruff_diagnostics::Diagnostic;
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
@@ -12,6 +9,8 @@ pub(crate) use shebang_missing_python::*;
pub(crate) use shebang_not_executable::*;
pub(crate) use shebang_not_first_line::*;
use crate::comments::shebang::ShebangDirective;
mod shebang_leading_whitespace;
mod shebang_missing_executable_file;
mod shebang_missing_python;
@@ -23,39 +22,34 @@ pub(crate) fn from_tokens(
path: &Path,
locator: &Locator,
comment_ranges: &CommentRanges,
settings: &LinterSettings,
) {
let mut has_any_shebang = false;
for range in comment_ranges {
let comment = locator.slice(range);
let comment = locator.slice(*range);
if let Some(shebang) = ShebangDirective::try_extract(comment) {
has_any_shebang = true;
if let Some(diagnostic) = shebang_missing_python(range, &shebang) {
if let Some(diagnostic) = shebang_missing_python(*range, &shebang) {
diagnostics.push(diagnostic);
}
if settings.rules.enabled(Rule::ShebangNotExecutable) {
if let Some(diagnostic) = shebang_not_executable(path, range) {
diagnostics.push(diagnostic);
}
}
if let Some(diagnostic) = shebang_leading_whitespace(range, locator) {
if let Some(diagnostic) = shebang_not_executable(path, *range) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_first_line(range, locator) {
if let Some(diagnostic) = shebang_leading_whitespace(*range, locator) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_not_first_line(*range, locator) {
diagnostics.push(diagnostic);
}
}
}
if !has_any_shebang {
if settings.rules.enabled(Rule::ShebangMissingExecutableFile) {
if let Some(diagnostic) = shebang_missing_executable_file(path) {
diagnostics.push(diagnostic);
}
if let Some(diagnostic) = shebang_missing_executable_file(path) {
diagnostics.push(diagnostic);
}
}
}

View File

@@ -41,10 +41,10 @@ pub(crate) fn type_comment_in_stub(
comment_ranges: &CommentRanges,
) {
for range in comment_ranges {
let comment = locator.slice(range);
let comment = locator.slice(*range);
if TYPE_COMMENT_REGEX.is_match(comment) && !TYPE_IGNORE_REGEX.is_match(comment) {
diagnostics.push(Diagnostic::new(TypeCommentInStub, range));
diagnostics.push(Diagnostic::new(TypeCommentInStub, *range));
}
}
}

View File

@@ -35,9 +35,6 @@ use crate::settings::LinterSettings;
///
/// import typing
/// ```
///
/// ## Options
/// - `lint.isort.required-imports`
#[violation]
pub struct MissingRequiredImport(pub String);

View File

@@ -741,10 +741,6 @@ fn returns_documented(
|| (matches!(convention, Some(Convention::Google)) && starts_with_returns(docstring))
}
fn should_document_returns(function_def: &ast::StmtFunctionDef) -> bool {
!matches!(function_def.name.as_str(), "__new__")
}
fn starts_with_yields(docstring: &Docstring) -> bool {
if let Some(first_word) = docstring.body().as_str().split(' ').next() {
return matches!(first_word, "Yield" | "Yields");
@@ -872,9 +868,7 @@ pub(crate) fn check_docstring(
// DOC201
if checker.enabled(Rule::DocstringMissingReturns) {
if should_document_returns(function_def)
&& !returns_documented(docstring, &docstring_sections, convention)
{
if !returns_documented(docstring, &docstring_sections, convention) {
let extra_property_decorators = checker.settings.pydocstyle.property_decorators();
if !definition.is_property(extra_property_decorators, semantic) {
if let Some(body_return) = body_entries.returns.first() {

View File

@@ -67,7 +67,9 @@ impl Violation for TripleSingleQuotes {
pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
let leading_quote = docstring.leading_quote();
let prefixes = leading_quote.trim_end_matches(['\'', '"']).to_owned();
let prefixes = leading_quote
.trim_end_matches(|c| c == '\'' || c == '"')
.to_owned();
let expected_quote = if docstring.body().contains("\"\"\"") {
if docstring.body().contains("\'\'\'") {

View File

@@ -208,18 +208,6 @@ mod tests {
Ok(())
}
#[test]
fn f821_with_builtin_added_on_new_py_version_but_old_target_version_specified() {
let diagnostics = test_snippet(
"PythonFinalizationError",
&LinterSettings {
target_version: crate::settings::types::PythonVersion::Py312,
..LinterSettings::for_rule(Rule::UndefinedName)
},
);
assert_messages!(diagnostics);
}
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
#[test_case(Rule::UnusedImport, Path::new("__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]

View File

@@ -19,35 +19,17 @@ use ruff_macros::{derive_message_formats, violation};
/// return n * 2
/// ```
///
/// ## Options
/// - [`target-version`]: Can be used to configure which symbols Ruff will understand
/// as being available in the `builtins` namespace.
///
/// ## References
/// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)
#[violation]
pub struct UndefinedName {
pub(crate) name: String,
pub(crate) minor_version_builtin_added: Option<u8>,
}
impl Violation for UndefinedName {
#[derive_message_formats]
fn message(&self) -> String {
let UndefinedName {
name,
minor_version_builtin_added,
} = self;
let tip = minor_version_builtin_added.map(|version_added| {
format!(
r#"Consider specifying `requires-python = ">= 3.{version_added}"` or `tool.ruff.target-version = "py3{version_added}"` in your `pyproject.toml` file."#
)
});
if let Some(tip) = tip {
format!("Undefined name `{name}`. {tip}")
} else {
format!("Undefined name `{name}`")
}
let UndefinedName { name } = self;
format!("Undefined name `{name}`")
}
}

View File

@@ -1,8 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
<filename>:1:1: F821 Undefined name `PythonFinalizationError`. Consider specifying `requires-python = ">= 3.13"` or `tool.ruff.target-version = "py313"` in your `pyproject.toml` file.
|
1 | PythonFinalizationError
| ^^^^^^^^^^^^^^^^^^^^^^^ F821
|

View File

@@ -55,7 +55,7 @@ pub(crate) fn blanket_type_ignore(
locator: &Locator,
) {
for range in comment_ranges {
let line = locator.slice(range);
let line = locator.slice(*range);
// Match, e.g., `# type: ignore` or `# type: ignore[attr-defined]`.
// See: https://github.com/python/mypy/blob/b43e0d34247a6d1b3b9d9094d184bbfcb9808bb9/mypy/fastparse.py#L248

View File

@@ -8,10 +8,10 @@ use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::is_known_dunder_method;
/// ## What it does
/// Checks for dunder methods that have no special meaning in Python 3.
/// Checks for misspelled and unknown dunder names in method definitions.
///
/// ## Why is this bad?
/// Misspelled or no longer supported dunder name methods may cause your code to not function
/// Misspelled dunder name methods may cause your code to not function
/// as expected.
///
/// Since dunder methods are associated with customizing the behavior
@@ -51,7 +51,7 @@ impl Violation for BadDunderMethodName {
#[derive_message_formats]
fn message(&self) -> String {
let BadDunderMethodName { name } = self;
format!("Dunder method `{name}` has no special meaning in Python 3")
format!("Bad or misspelled dunder method name `{name}`")
}
}

View File

@@ -56,7 +56,7 @@ pub(crate) fn empty_comments(
}
// If the line contains an empty comment, add a diagnostic.
if let Some(diagnostic) = empty_comment(range, locator) {
if let Some(diagnostic) = empty_comment(*range, locator) {
diagnostics.push(diagnostic);
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
bad_dunder_method_name.py:5:9: PLW3201 Dunder method `_init_` has no special meaning in Python 3
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
4 | class Apples:
5 | def _init_(self): # [bad-dunder-name]
@@ -9,7 +9,7 @@ bad_dunder_method_name.py:5:9: PLW3201 Dunder method `_init_` has no special mea
6 | pass
|
bad_dunder_method_name.py:8:9: PLW3201 Dunder method `__hello__` has no special meaning in Python 3
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__hello__`
|
6 | pass
7 |
@@ -18,7 +18,7 @@ bad_dunder_method_name.py:8:9: PLW3201 Dunder method `__hello__` has no special
9 | print("hello")
|
bad_dunder_method_name.py:11:9: PLW3201 Dunder method `__init_` has no special meaning in Python 3
bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__init_`
|
9 | print("hello")
10 |
@@ -28,7 +28,7 @@ bad_dunder_method_name.py:11:9: PLW3201 Dunder method `__init_` has no special m
13 | pass
|
bad_dunder_method_name.py:15:9: PLW3201 Dunder method `_init_` has no special meaning in Python 3
bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
13 | pass
14 |
@@ -38,7 +38,7 @@ bad_dunder_method_name.py:15:9: PLW3201 Dunder method `_init_` has no special me
17 | pass
|
bad_dunder_method_name.py:19:9: PLW3201 Dunder method `___neg__` has no special meaning in Python 3
bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `___neg__`
|
17 | pass
18 |
@@ -48,7 +48,7 @@ bad_dunder_method_name.py:19:9: PLW3201 Dunder method `___neg__` has no special
21 | pass
|
bad_dunder_method_name.py:23:9: PLW3201 Dunder method `__inv__` has no special meaning in Python 3
bad_dunder_method_name.py:23:9: PLW3201 Bad or misspelled dunder method name `__inv__`
|
21 | pass
22 |
@@ -58,10 +58,4 @@ bad_dunder_method_name.py:23:9: PLW3201 Dunder method `__inv__` has no special m
25 | pass
|
bad_dunder_method_name.py:98:9: PLW3201 Dunder method `__unicode__` has no special meaning in Python 3
|
97 | # Removed with Python 3
98 | def __unicode__(self):
| ^^^^^^^^^^^ PLW3201
99 | pass
|

View File

@@ -13,7 +13,7 @@ use crate::checkers::ast::Checker;
///
/// ## Why is this bad?
/// The `unittest` module has deprecated aliases for some of its methods.
/// The deprecated aliases were removed in Python 3.12. Instead of aliases,
/// The aliases may be removed in future versions of Python. Instead,
/// use their non-deprecated counterparts.
///
/// ## Example
@@ -37,7 +37,7 @@ use crate::checkers::ast::Checker;
/// ```
///
/// ## References
/// - [Python 3.11 documentation: Deprecated aliases](https://docs.python.org/3.11/library/unittest.html#deprecated-aliases)
/// - [Python documentation: Deprecated aliases](https://docs.python.org/3/library/unittest.html#deprecated-aliases)
#[violation]
pub struct DeprecatedUnittestAlias {
alias: String,

View File

@@ -46,7 +46,6 @@ mod tests {
#[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))]
#[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))]
#[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))]
#[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -23,7 +23,6 @@ pub(crate) use repeated_append::*;
pub(crate) use repeated_global::*;
pub(crate) use single_item_membership_test::*;
pub(crate) use slice_copy::*;
pub(crate) use slice_to_remove_prefix_or_suffix::*;
pub(crate) use sorted_min_max::*;
pub(crate) use type_none_comparison::*;
pub(crate) use unnecessary_enumerate::*;
@@ -56,7 +55,6 @@ mod repeated_append;
mod repeated_global;
mod single_item_membership_test;
mod slice_copy;
mod slice_to_remove_prefix_or_suffix;
mod sorted_min_max;
mod type_none_comparison;
mod unnecessary_enumerate;

View File

@@ -1,474 +0,0 @@
use crate::{checkers::ast::Checker, settings::types::PythonVersion};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen};
/// ## What it does
/// Checks for the removal of a prefix or suffix from a string by assigning
/// the string to a slice after checking `.startswith()` or `.endswith()`, respectively.
///
/// ## Why is this bad?
/// The methods [`str.removeprefix`] and [`str.removesuffix`],
/// introduced in Python 3.9, have the same behavior
/// and are more readable and efficient.
///
/// ## Example
/// ```python
/// filename[:-4] if filename.endswith(".txt") else filename
/// ```
///
/// ```python
/// if text.startswith("pre"):
/// text = text[3:]
/// ```
///
/// Use instead:
/// ```python
/// filename = filename.removesuffix(".txt")
/// ```
///
/// ```python
/// text = text.removeprefix("pre")
/// ```
///
/// [`str.removeprefix`]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix
/// [`str.removesuffix`]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix
#[violation]
pub struct SliceToRemovePrefixOrSuffix {
string: String,
affix_kind: AffixKind,
stmt_or_expression: StmtOrExpr,
}
impl AlwaysFixableViolation for SliceToRemovePrefixOrSuffix {
#[derive_message_formats]
fn message(&self) -> String {
match self.affix_kind {
AffixKind::StartsWith => {
format!("Prefer `removeprefix` over conditionally replacing with slice.")
}
AffixKind::EndsWith => {
format!("Prefer `removesuffix` over conditionally replacing with slice.")
}
}
}
fn fix_title(&self) -> String {
let method_name = self.affix_kind.as_str();
let replacement = self.affix_kind.replacement();
let context = match self.stmt_or_expression {
StmtOrExpr::Statement => "assignment",
StmtOrExpr::Expression => "ternary expression",
};
format!("Use {replacement} instead of {context} conditional upon {method_name}.")
}
}
/// FURB188
pub(crate) fn slice_to_remove_affix_expr(checker: &mut Checker, if_expr: &ast::ExprIf) {
if checker.settings.target_version < PythonVersion::Py39 {
return;
}
if let Some(removal_data) = affix_removal_data_expr(if_expr) {
if affix_matches_slice_bound(&removal_data, checker.semantic()) {
let kind = removal_data.affix_query.kind;
let text = removal_data.text;
let mut diagnostic = Diagnostic::new(
SliceToRemovePrefixOrSuffix {
affix_kind: kind,
string: checker.locator().slice(text).to_string(),
stmt_or_expression: StmtOrExpr::Expression,
},
if_expr.range,
);
let replacement =
generate_removeaffix_expr(text, &removal_data.affix_query, checker.locator());
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
replacement,
if_expr.start(),
if_expr.end(),
)));
checker.diagnostics.push(diagnostic);
}
}
}
/// FURB188
pub(crate) fn slice_to_remove_affix_stmt(checker: &mut Checker, if_stmt: &ast::StmtIf) {
if checker.settings.target_version < PythonVersion::Py39 {
return;
}
if let Some(removal_data) = affix_removal_data_stmt(if_stmt) {
if affix_matches_slice_bound(&removal_data, checker.semantic()) {
let kind = removal_data.affix_query.kind;
let text = removal_data.text;
let mut diagnostic = Diagnostic::new(
SliceToRemovePrefixOrSuffix {
affix_kind: kind,
string: checker.locator().slice(text).to_string(),
stmt_or_expression: StmtOrExpr::Statement,
},
if_stmt.range,
);
let replacement = generate_assignment_with_removeaffix(
text,
&removal_data.affix_query,
checker.locator(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
replacement,
if_stmt.start(),
if_stmt.end(),
)));
checker.diagnostics.push(diagnostic);
}
}
}
/// Given an expression of the form:
///
/// ```python
/// text[slice] if text.func(affix) else text
/// ```
///
/// where `func` is either `startswith` or `endswith`,
/// this function collects `text`,`func`, `affix`, and the non-null
/// bound of the slice. Otherwise, returns `None`.
fn affix_removal_data_expr(if_expr: &ast::ExprIf) -> Option<RemoveAffixData> {
let ast::ExprIf {
test,
body,
orelse,
range: _,
} = if_expr;
let ast::ExprSubscript { value, slice, .. } = body.as_subscript_expr()?;
// Variable names correspond to:
// ```python
// value[slice] if test else orelse
// ```
affix_removal_data(value, test, orelse, slice)
}
/// Given a statement of the form:
///
/// ```python
/// if text.func(affix):
/// text = text[slice]
/// ```
///
/// where `func` is either `startswith` or `endswith`,
/// this function collects `text`,`func`, `affix`, and the non-null
/// bound of the slice. Otherwise, returns `None`.
fn affix_removal_data_stmt(if_stmt: &ast::StmtIf) -> Option<RemoveAffixData> {
let ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
} = if_stmt;
// Cannot safely transform, e.g.,
// ```python
// if text.startswith(prefix):
// text = text[len(prefix):]
// else:
// text = "something completely different"
// ```
if !elif_else_clauses.is_empty() {
return None;
};
// Cannot safely transform, e.g.,
// ```python
// if text.startswith(prefix):
// text = f"{prefix} something completely different"
// text = text[len(prefix):]
// ```
let [statement] = body.as_slice() else {
return None;
};
// Variable names correspond to:
// ```python
// if test:
// else_or_target_name = value[slice]
// ```
let ast::StmtAssign {
value,
targets,
range: _,
} = statement.as_assign_stmt()?;
let [target] = targets.as_slice() else {
return None;
};
let ast::ExprSubscript { value, slice, .. } = value.as_subscript_expr()?;
affix_removal_data(value, test, target, slice)
}
/// Suppose given a statement of the form:
/// ```python
/// if test:
/// else_or_target_name = value[slice]
/// ```
/// or an expression of the form:
/// ```python
/// value[slice] if test else else_or_target_name
/// ```
/// This function verifies that
/// - `value` and `else_or_target_name`
/// are equal to a common name `text`
/// - `test` is of the form `text.startswith(prefix)`
/// or `text.endswith(suffix)`
/// - `slice` has no upper bound in the case of a prefix,
/// and no lower bound in the case of a suffix
///
/// If these conditions are satisfied, the function
/// returns the corresponding `RemoveAffixData` object;
/// otherwise it returns `None`.
fn affix_removal_data<'a>(
value: &'a ast::Expr,
test: &'a ast::Expr,
else_or_target: &'a ast::Expr,
slice: &'a ast::Expr,
) -> Option<RemoveAffixData<'a>> {
let compr_value = ast::comparable::ComparableExpr::from(value);
let compr_else_or_target = ast::comparable::ComparableExpr::from(else_or_target);
if compr_value != compr_else_or_target {
return None;
}
let slice = slice.as_slice_expr()?;
let compr_test_expr = ast::comparable::ComparableExpr::from(
&test.as_call_expr()?.func.as_attribute_expr()?.value,
);
let func_name = test
.as_call_expr()?
.func
.as_attribute_expr()?
.attr
.id
.as_str();
let func_args = &test.as_call_expr()?.arguments.args;
let [affix] = func_args.as_ref() else {
return None;
};
if compr_value != compr_test_expr || compr_test_expr != compr_else_or_target {
return None;
}
let (affix_kind, bound) = match func_name {
"startswith" if slice.upper.is_none() => (AffixKind::StartsWith, slice.lower.as_ref()?),
"endswith" if slice.lower.is_none() => (AffixKind::EndsWith, slice.upper.as_ref()?),
_ => return None,
};
Some(RemoveAffixData {
text: value,
bound,
affix_query: AffixQuery {
kind: affix_kind,
affix,
},
})
}
/// Tests whether the slice of the given string actually removes the
/// detected affix.
///
/// For example, in the situation
///
/// ```python
/// text[:bound] if text.endswith(suffix) else text
/// ```
///
/// This function verifies that `bound == -len(suffix)` in two cases:
/// - `suffix` is a string literal and `bound` is a number literal
/// - `suffix` is an expression and `bound` is
/// exactly `-len(suffix)` (as AST nodes, prior to evaluation.)
fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) -> bool {
let RemoveAffixData {
text: _,
bound,
affix_query: AffixQuery { kind, affix },
} = *data;
match (kind, bound, affix) {
(
AffixKind::StartsWith,
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: num,
range: _,
}),
ast::Expr::StringLiteral(ast::ExprStringLiteral {
range: _,
value: string_val,
}),
) => num
.as_int()
.and_then(ast::Int::as_u32) // Only support prefix removal for size at most `u32::MAX`
.is_some_and(|x| x == string_val.to_str().text_len().to_u32()),
(
AffixKind::StartsWith,
ast::Expr::Call(ast::ExprCall {
range: _,
func,
arguments,
}),
_,
) => {
arguments.len() == 1
&& arguments.find_positional(0).is_some_and(|arg| {
let compr_affix = ast::comparable::ComparableExpr::from(affix);
let compr_arg = ast::comparable::ComparableExpr::from(arg);
compr_affix == compr_arg
})
&& semantic.match_builtin_expr(func, "len")
}
(
AffixKind::EndsWith,
ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::USub,
operand,
range: _,
}),
ast::Expr::StringLiteral(ast::ExprStringLiteral {
range: _,
value: string_val,
}),
) => operand.as_number_literal_expr().is_some_and(
|ast::ExprNumberLiteral { value, .. }| {
// Only support prefix removal for size at most `u32::MAX`
value
.as_int()
.and_then(ast::Int::as_u32)
.is_some_and(|x| x == string_val.to_str().text_len().to_u32())
},
),
(
AffixKind::EndsWith,
ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::USub,
operand,
range: _,
}),
_,
) => operand.as_call_expr().is_some_and(
|ast::ExprCall {
range: _,
func,
arguments,
}| {
arguments.len() == 1
&& arguments.find_positional(0).is_some_and(|arg| {
let compr_affix = ast::comparable::ComparableExpr::from(affix);
let compr_arg = ast::comparable::ComparableExpr::from(arg);
compr_affix == compr_arg
})
&& semantic.match_builtin_expr(func, "len")
},
),
_ => false,
}
}
/// Generates the source code string
/// ```python
/// text = text.removeprefix(prefix)
/// ```
/// or
/// ```python
/// text = text.removesuffix(prefix)
/// ```
/// as appropriate.
fn generate_assignment_with_removeaffix(
text: &ast::Expr,
affix_query: &AffixQuery,
locator: &Locator,
) -> String {
let text_str = locator.slice(text);
let affix_str = locator.slice(affix_query.affix);
let replacement = affix_query.kind.replacement();
format!("{text_str} = {text_str}.{replacement}({affix_str})")
}
/// Generates the source code string
/// ```python
/// text.removeprefix(prefix)
/// ```
/// or
///
/// ```python
/// text.removesuffix(suffix)
/// ```
/// as appropriate.
fn generate_removeaffix_expr(
text: &ast::Expr,
affix_query: &AffixQuery,
locator: &Locator,
) -> String {
let text_str = locator.slice(text);
let affix_str = locator.slice(affix_query.affix);
let replacement = affix_query.kind.replacement();
format!("{text_str}.{replacement}({affix_str})")
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StmtOrExpr {
Statement,
Expression,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum AffixKind {
StartsWith,
EndsWith,
}
impl AffixKind {
const fn as_str(self) -> &'static str {
match self {
Self::StartsWith => "startswith",
Self::EndsWith => "endswith",
}
}
const fn replacement(self) -> &'static str {
match self {
Self::StartsWith => "removeprefix",
Self::EndsWith => "removesuffix",
}
}
}
/// Components of `startswith(prefix)` or `endswith(suffix)`.
#[derive(Debug)]
struct AffixQuery<'a> {
/// Whether the method called is `startswith` or `endswith`.
kind: AffixKind,
/// Node representing the prefix or suffix being passed to the string method.
affix: &'a ast::Expr,
}
/// Ingredients for a statement or expression
/// which potentially removes a prefix or suffix from a string.
///
/// Specifically
#[derive(Debug)]
struct RemoveAffixData<'a> {
/// Node representing the string whose prefix or suffix we want to remove
text: &'a ast::Expr,
/// Node representing the bound used to slice the string
bound: &'a ast::Expr,
/// Contains the prefix or suffix used in `text.startswith(prefix)` or `text.endswith(suffix)`
affix_query: AffixQuery<'a>,
}

View File

@@ -1,177 +0,0 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB188.py:7:5: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
6 | def remove_extension_via_slice(filename: str) -> str:
7 | if filename.endswith(".txt"):
| _____^
8 | | filename = filename[:-4]
| |________________________________^ FURB188
9 |
10 | return filename
|
= help: Use removesuffix instead of assignment conditional upon endswith.
Safe fix
4 4 | # these should match
5 5 |
6 6 | def remove_extension_via_slice(filename: str) -> str:
7 |- if filename.endswith(".txt"):
8 |- filename = filename[:-4]
7 |+ filename = filename.removesuffix(".txt")
9 8 |
10 9 | return filename
11 10 |
FURB188.py:14:5: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
13 | def remove_extension_via_slice_len(filename: str, extension: str) -> str:
14 | if filename.endswith(extension):
| _____^
15 | | filename = filename[:-len(extension)]
| |_____________________________________________^ FURB188
16 |
17 | return filename
|
= help: Use removesuffix instead of assignment conditional upon endswith.
Safe fix
11 11 |
12 12 |
13 13 | def remove_extension_via_slice_len(filename: str, extension: str) -> str:
14 |- if filename.endswith(extension):
15 |- filename = filename[:-len(extension)]
14 |+ filename = filename.removesuffix(extension)
16 15 |
17 16 | return filename
18 17 |
FURB188.py:21:12: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
20 | def remove_extension_via_ternary(filename: str) -> str:
21 | return filename[:-4] if filename.endswith(".txt") else filename
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
= help: Use removesuffix instead of ternary expression conditional upon endswith.
Safe fix
18 18 |
19 19 |
20 20 | def remove_extension_via_ternary(filename: str) -> str:
21 |- return filename[:-4] if filename.endswith(".txt") else filename
21 |+ return filename.removesuffix(".txt")
22 22 |
23 23 |
24 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
FURB188.py:25:12: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
25 | return filename[:-len(extension)] if filename.endswith(extension) else filename
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
= help: Use removesuffix instead of ternary expression conditional upon endswith.
Safe fix
22 22 |
23 23 |
24 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
25 |- return filename[:-len(extension)] if filename.endswith(extension) else filename
25 |+ return filename.removesuffix(extension)
26 26 |
27 27 |
28 28 | def remove_prefix(filename: str) -> str:
FURB188.py:29:12: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
28 | def remove_prefix(filename: str) -> str:
29 | return filename[4:] if filename.startswith("abc-") else filename
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
= help: Use removeprefix instead of ternary expression conditional upon startswith.
Safe fix
26 26 |
27 27 |
28 28 | def remove_prefix(filename: str) -> str:
29 |- return filename[4:] if filename.startswith("abc-") else filename
29 |+ return filename.removeprefix("abc-")
30 30 |
31 31 |
32 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
FURB188.py:33:12: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
33 | return filename[len(prefix):] if filename.startswith(prefix) else filename
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
= help: Use removeprefix instead of ternary expression conditional upon startswith.
Safe fix
30 30 |
31 31 |
32 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
33 |- return filename[len(prefix):] if filename.startswith(prefix) else filename
33 |+ return filename.removeprefix(prefix)
34 34 |
35 35 |
36 36 | # these should not
FURB188.py:146:9: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
144 | SUFFIX = "suffix"
145 |
146 | x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
147 |
148 | def remove_prefix_comparable_literal_expr() -> None:
|
= help: Use removesuffix instead of ternary expression conditional upon endswith.
Safe fix
143 143 |
144 144 | SUFFIX = "suffix"
145 145 |
146 |- x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
146 |+ x = foo.bar.baz.removesuffix(SUFFIX)
147 147 |
148 148 | def remove_prefix_comparable_literal_expr() -> None:
149 149 | return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
FURB188.py:149:12: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
148 | def remove_prefix_comparable_literal_expr() -> None:
149 | return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
150 |
151 | def shadow_builtins(filename: str, extension: str) -> None:
|
= help: Use removeprefix instead of ternary expression conditional upon startswith.
Safe fix
146 146 | x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
147 147 |
148 148 | def remove_prefix_comparable_literal_expr() -> None:
149 |- return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
149 |+ return "abc" "def".removeprefix("abc")
150 150 |
151 151 | def shadow_builtins(filename: str, extension: str) -> None:
152 152 | from builtins import len as builtins_len
FURB188.py:154:12: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
152 | from builtins import len as builtins_len
153 |
154 | return filename[:-builtins_len(extension)] if filename.endswith(extension) else filename
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
= help: Use removesuffix instead of ternary expression conditional upon endswith.
Safe fix
151 151 | def shadow_builtins(filename: str, extension: str) -> None:
152 152 | from builtins import len as builtins_len
153 153 |
154 |- return filename[:-builtins_len(extension)] if filename.endswith(extension) else filename
154 |+ return filename.removesuffix(extension)

View File

@@ -58,7 +58,6 @@ mod tests {
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
#[test_case(Rule::IncorrectlyParenthesizedTupleInSubscript, Path::new("RUF031.py"))]
#[test_case(Rule::DecimalFromFloatLiteral, Path::new("RUF032.py"))]
#[test_case(Rule::UselessIfElse, Path::new("RUF034.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
#[test_case(Rule::PostInitDefault, Path::new("RUF033.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -1,10 +1,7 @@
use std::fmt;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::{self as ast};
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -52,90 +49,37 @@ pub(crate) fn decimal_from_float_literal_syntax(checker: &mut Checker, call: &as
return;
};
if let Some(float) = extract_float_literal(arg, Sign::Positive) {
if checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["decimal", "Decimal"])
})
{
let diagnostic = Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix(
fix_float_literal(arg.range(), float, checker.locator(), checker.stylist()),
);
checker.diagnostics.push(diagnostic);
}
if !is_arg_float_literal(arg) {
return;
}
if checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["decimal", "Decimal"]))
{
let diagnostic =
Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix(fix_float_literal(
arg.range(),
&checker.generator().expr(arg),
checker.stylist(),
));
checker.diagnostics.push(diagnostic);
}
}
#[derive(Debug, Clone, Copy)]
enum Sign {
Positive,
Negative,
}
impl Sign {
const fn as_str(self) -> &'static str {
match self {
Self::Positive => "",
Self::Negative => "-",
}
}
const fn flip(self) -> Self {
match self {
Self::Negative => Self::Positive,
Self::Positive => Self::Negative,
}
}
}
impl fmt::Display for Sign {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Copy, Clone)]
struct Float {
/// The range of the float excluding the sign.
/// E.g. for `+--+-+-4.3`, this will be the range of `4.3`
value_range: TextRange,
/// The resolved sign of the float (either `-` or `+`)
sign: Sign,
}
fn extract_float_literal(arg: &ast::Expr, sign: Sign) -> Option<Float> {
fn is_arg_float_literal(arg: &ast::Expr) -> bool {
match arg {
ast::Expr::NumberLiteral(number_literal_expr) if number_literal_expr.value.is_float() => {
Some(Float {
value_range: arg.range(),
sign,
})
}
ast::Expr::UnaryOp(ast::ExprUnaryOp {
operand,
op: ast::UnaryOp::UAdd,
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Float(_),
..
}) => extract_float_literal(operand, sign),
ast::Expr::UnaryOp(ast::ExprUnaryOp {
operand,
op: ast::UnaryOp::USub,
..
}) => extract_float_literal(operand, sign.flip()),
_ => None,
}) => true,
ast::Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_arg_float_literal(operand),
_ => false,
}
}
fn fix_float_literal(
original_range: TextRange,
float: Float,
locator: &Locator,
stylist: &Stylist,
) -> Fix {
let quote = stylist.quote();
let Float { value_range, sign } = float;
let float_value = locator.slice(value_range);
let content = format!("{quote}{sign}{float_value}{quote}");
Fix::unsafe_edit(Edit::range_replacement(content, original_range))
fn fix_float_literal(range: TextRange, float_literal: &str, stylist: &Stylist) -> Fix {
let content = format!("{quote}{float_literal}{quote}", quote = stylist.quote());
Fix::unsafe_edit(Edit::range_replacement(content, range))
}

View File

@@ -75,7 +75,7 @@ pub(crate) fn ignored_formatter_suppression_comment(checker: &mut Checker, suite
.into_iter()
.filter_map(|range| {
Some(SuppressionComment {
range,
range: *range,
kind: SuppressionKind::from_comment(locator.slice(range))?,
})
})

View File

@@ -29,7 +29,6 @@ pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
pub(crate) use unused_async::*;
pub(crate) use unused_noqa::*;
pub(crate) use useless_if_else::*;
pub(crate) use zip_instead_of_pairwise::*;
mod ambiguous_unicode_character;
@@ -67,7 +66,6 @@ mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
mod unused_async;
mod unused_noqa;
mod useless_if_else;
mod zip_instead_of_pairwise;
#[derive(Clone, Copy)]

View File

@@ -1,55 +0,0 @@
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
/// ## What it does
/// Checks for useless if-else conditions with identical arms.
///
/// ## Why is this bad?
/// Useless if-else conditions add unnecessary complexity to the code without
/// providing any logical benefit.
///
/// Assigning the value directly is clearer and more explicit, and
/// should be preferred.
///
/// ## Example
/// ```python
/// # Bad
/// foo = x if y else x
/// ```
///
/// Use instead:
/// ```python
/// # Good
/// foo = x
/// ```
#[violation]
pub struct UselessIfElse;
impl Violation for UselessIfElse {
#[derive_message_formats]
fn message(&self) -> String {
format!("Useless if-else condition")
}
}
/// RUF031
pub(crate) fn useless_if_else(checker: &mut Checker, if_expr: &ast::ExprIf) {
let ast::ExprIf {
body,
orelse,
range,
..
} = if_expr;
// Skip if the body and orelse are not the same
if ComparableExpr::from(body) != ComparableExpr::from(orelse) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(UselessIfElse, *range));
}

View File

@@ -127,105 +127,65 @@ RUF032.py:45:15: RUF032 [*] `Decimal()` called with float literal argument
47 47 | val = Decimal("-10.0")
48 48 |
RUF032.py:56:15: RUF032 [*] `Decimal()` called with float literal argument
RUF032.py:81:23: RUF032 [*] `Decimal()` called with float literal argument
|
54 | val = Decimal(~4.0) # Skip
55 |
56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")`
| ^^^^^ RUF032
57 |
58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
|
= help: Use a string literal instead
Unsafe fix
53 53 | # See https://github.com/astral-sh/ruff/issues/13258
54 54 | val = Decimal(~4.0) # Skip
55 55 |
56 |-val = Decimal(++4.0) # Suggest `Decimal("4.0")`
56 |+val = Decimal("4.0") # Suggest `Decimal("4.0")`
57 57 |
58 58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
59 59 |
RUF032.py:58:15: RUF032 [*] `Decimal()` called with float literal argument
|
56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")`
57 |
58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
| ^^^^^^^^^^^ RUF032
|
= help: Use a string literal instead
Unsafe fix
55 55 |
56 56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")`
57 57 |
58 |-val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
58 |+val = Decimal("-4.0") # Suggest `Decimal("-4.0")`
59 59 |
60 60 |
61 61 | # Tests with shadowed name
RUF032.py:88:23: RUF032 [*] `Decimal()` called with float literal argument
|
86 | # Retest with fully qualified import
87 |
88 | val = decimal.Decimal(0.0) # Should error
79 | # Retest with fully qualified import
80 |
81 | val = decimal.Decimal(0.0) # Should error
| ^^^ RUF032
89 |
90 | val = decimal.Decimal("0.0")
82 |
83 | val = decimal.Decimal("0.0")
|
= help: Use a string literal instead
Unsafe fix
85 85 |
86 86 | # Retest with fully qualified import
87 87 |
88 |-val = decimal.Decimal(0.0) # Should error
88 |+val = decimal.Decimal("0.0") # Should error
89 89 |
90 90 | val = decimal.Decimal("0.0")
91 91 |
78 78 |
79 79 | # Retest with fully qualified import
80 80 |
81 |-val = decimal.Decimal(0.0) # Should error
81 |+val = decimal.Decimal("0.0") # Should error
82 82 |
83 83 | val = decimal.Decimal("0.0")
84 84 |
RUF032.py:92:23: RUF032 [*] `Decimal()` called with float literal argument
RUF032.py:85:23: RUF032 [*] `Decimal()` called with float literal argument
|
90 | val = decimal.Decimal("0.0")
91 |
92 | val = decimal.Decimal(10.0) # Should error
83 | val = decimal.Decimal("0.0")
84 |
85 | val = decimal.Decimal(10.0) # Should error
| ^^^^ RUF032
93 |
94 | val = decimal.Decimal("10.0")
86 |
87 | val = decimal.Decimal("10.0")
|
= help: Use a string literal instead
Unsafe fix
89 89 |
90 90 | val = decimal.Decimal("0.0")
91 91 |
92 |-val = decimal.Decimal(10.0) # Should error
92 |+val = decimal.Decimal("10.0") # Should error
93 93 |
94 94 | val = decimal.Decimal("10.0")
95 95 |
82 82 |
83 83 | val = decimal.Decimal("0.0")
84 84 |
85 |-val = decimal.Decimal(10.0) # Should error
85 |+val = decimal.Decimal("10.0") # Should error
86 86 |
87 87 | val = decimal.Decimal("10.0")
88 88 |
RUF032.py:96:23: RUF032 [*] `Decimal()` called with float literal argument
RUF032.py:89:23: RUF032 [*] `Decimal()` called with float literal argument
|
94 | val = decimal.Decimal("10.0")
95 |
96 | val = decimal.Decimal(-10.0) # Should error
87 | val = decimal.Decimal("10.0")
88 |
89 | val = decimal.Decimal(-10.0) # Should error
| ^^^^^ RUF032
97 |
98 | val = decimal.Decimal("-10.0")
90 |
91 | val = decimal.Decimal("-10.0")
|
= help: Use a string literal instead
Unsafe fix
93 93 |
94 94 | val = decimal.Decimal("10.0")
95 95 |
96 |-val = decimal.Decimal(-10.0) # Should error
96 |+val = decimal.Decimal("-10.0") # Should error
97 97 |
98 98 | val = decimal.Decimal("-10.0")
99 99 |
86 86 |
87 87 | val = decimal.Decimal("10.0")
88 88 |
89 |-val = decimal.Decimal(-10.0) # Should error
89 |+val = decimal.Decimal("-10.0") # Should error
90 90 |
91 91 | val = decimal.Decimal("-10.0")
92 92 |

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF034.py:5:5: RUF034 Useless if-else condition
|
4 | # Invalid
5 | x = 1 if True else 1
| ^^^^^^^^^^^^^^^^ RUF034
6 |
7 | # Invalid
|
RUF034.py:8:5: RUF034 Useless if-else condition
|
7 | # Invalid
8 | x = "a" if True else "a"
| ^^^^^^^^^^^^^^^^^^^^ RUF034
9 |
10 | # Invalid
|
RUF034.py:11:5: RUF034 Useless if-else condition
|
10 | # Invalid
11 | x = 0.1 if False else 0.1
| ^^^^^^^^^^^^^^^^^^^^^ RUF034
|

View File

@@ -3124,29 +3124,6 @@ impl Pattern {
_ => false,
}
}
/// Checks if the [`Pattern`] is a [wildcard pattern].
///
/// The following are wildcard patterns:
/// ```python
/// match subject:
/// case _ as x: ...
/// case _ | _: ...
/// case _: ...
/// ```
///
/// [wildcard pattern]: https://docs.python.org/3/reference/compound_stmts.html#wildcard-patterns
pub fn is_wildcard(&self) -> bool {
match self {
Pattern::MatchAs(PatternMatchAs { pattern, .. }) => {
pattern.as_deref().map_or(true, Pattern::is_wildcard)
}
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
patterns.iter().all(Pattern::is_wildcard)
}
_ => false,
}
}
}
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)

View File

@@ -1,16 +0,0 @@
use ruff_python_parser::parse_module;
#[test]
fn pattern_is_wildcard() {
let source_code = r"
match subject:
case _ as x: ...
case _ | _: ...
case _: ...
";
let parsed = parse_module(source_code).unwrap();
let cases = &parsed.syntax().body[0].as_match_stmt().unwrap().cases;
for case in cases {
assert!(case.pattern.is_wildcard());
}
}

View File

@@ -97,8 +97,9 @@ impl StringParser {
#[inline]
fn next_char(&mut self) -> Option<char> {
self.source[self.cursor..].chars().next().inspect(|c| {
self.source[self.cursor..].chars().next().map(|c| {
self.cursor += c.len_utf8();
c
})
}

View File

@@ -23,197 +23,182 @@ pub const MAGIC_GLOBALS: &[&str] = &[
"__file__",
];
static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[
"ArithmeticError",
"AssertionError",
"AttributeError",
"BaseException",
"BlockingIOError",
"BrokenPipeError",
"BufferError",
"BytesWarning",
"ChildProcessError",
"ConnectionAbortedError",
"ConnectionError",
"ConnectionRefusedError",
"ConnectionResetError",
"DeprecationWarning",
"EOFError",
"Ellipsis",
"EnvironmentError",
"Exception",
"False",
"FileExistsError",
"FileNotFoundError",
"FloatingPointError",
"FutureWarning",
"GeneratorExit",
"IOError",
"ImportError",
"ImportWarning",
"IndentationError",
"IndexError",
"InterruptedError",
"IsADirectoryError",
"KeyError",
"KeyboardInterrupt",
"LookupError",
"MemoryError",
"ModuleNotFoundError",
"NameError",
"None",
"NotADirectoryError",
"NotImplemented",
"NotImplementedError",
"OSError",
"OverflowError",
"PendingDeprecationWarning",
"PermissionError",
"ProcessLookupError",
"RecursionError",
"ReferenceError",
"ResourceWarning",
"RuntimeError",
"RuntimeWarning",
"StopAsyncIteration",
"StopIteration",
"SyntaxError",
"SyntaxWarning",
"SystemError",
"SystemExit",
"TabError",
"TimeoutError",
"True",
"TypeError",
"UnboundLocalError",
"UnicodeDecodeError",
"UnicodeEncodeError",
"UnicodeError",
"UnicodeTranslateError",
"UnicodeWarning",
"UserWarning",
"ValueError",
"Warning",
"ZeroDivisionError",
"__build_class__",
"__debug__",
"__doc__",
"__import__",
"__loader__",
"__name__",
"__package__",
"__spec__",
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"copyright",
"credits",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"exit",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"license",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"quit",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
];
static PY310_PLUS_BUILTINS: &[&str] = &["EncodingWarning", "aiter", "anext"];
static PY311_PLUS_BUILTINS: &[&str] = &["BaseExceptionGroup", "ExceptionGroup"];
static PY313_PLUS_BUILTINS: &[&str] = &["PythonFinalizationError"];
/// Return the list of builtins for the given Python minor version.
///
/// Intended to be kept in sync with [`is_python_builtin`].
pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator<Item = &'static str> {
let py310_builtins = if minor_version >= 10 {
Some(PY310_PLUS_BUILTINS)
} else {
None
};
let py311_builtins = if minor_version >= 11 {
Some(PY311_PLUS_BUILTINS)
} else {
None
};
let py313_builtins = if minor_version >= 13 {
Some(PY313_PLUS_BUILTINS)
} else {
None
};
let ipython_builtins = if is_notebook {
Some(IPYTHON_BUILTINS)
} else {
None
};
pub fn python_builtins(minor_version: u8, is_notebook: bool) -> Vec<&'static str> {
let mut builtins = vec![
"ArithmeticError",
"AssertionError",
"AttributeError",
"BaseException",
"BlockingIOError",
"BrokenPipeError",
"BufferError",
"BytesWarning",
"ChildProcessError",
"ConnectionAbortedError",
"ConnectionError",
"ConnectionRefusedError",
"ConnectionResetError",
"DeprecationWarning",
"EOFError",
"Ellipsis",
"EnvironmentError",
"Exception",
"False",
"FileExistsError",
"FileNotFoundError",
"FloatingPointError",
"FutureWarning",
"GeneratorExit",
"IOError",
"ImportError",
"ImportWarning",
"IndentationError",
"IndexError",
"InterruptedError",
"IsADirectoryError",
"KeyError",
"KeyboardInterrupt",
"LookupError",
"MemoryError",
"ModuleNotFoundError",
"NameError",
"None",
"NotADirectoryError",
"NotImplemented",
"NotImplementedError",
"OSError",
"OverflowError",
"PendingDeprecationWarning",
"PermissionError",
"ProcessLookupError",
"RecursionError",
"ReferenceError",
"ResourceWarning",
"RuntimeError",
"RuntimeWarning",
"StopAsyncIteration",
"StopIteration",
"SyntaxError",
"SyntaxWarning",
"SystemError",
"SystemExit",
"TabError",
"TimeoutError",
"True",
"TypeError",
"UnboundLocalError",
"UnicodeDecodeError",
"UnicodeEncodeError",
"UnicodeError",
"UnicodeTranslateError",
"UnicodeWarning",
"UserWarning",
"ValueError",
"Warning",
"ZeroDivisionError",
"__build_class__",
"__debug__",
"__doc__",
"__import__",
"__loader__",
"__name__",
"__package__",
"__spec__",
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"breakpoint",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"compile",
"complex",
"copyright",
"credits",
"delattr",
"dict",
"dir",
"divmod",
"enumerate",
"eval",
"exec",
"exit",
"filter",
"float",
"format",
"frozenset",
"getattr",
"globals",
"hasattr",
"hash",
"help",
"hex",
"id",
"input",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"license",
"list",
"locals",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"open",
"ord",
"pow",
"print",
"property",
"quit",
"range",
"repr",
"reversed",
"round",
"set",
"setattr",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"super",
"tuple",
"type",
"vars",
"zip",
];
py310_builtins
.into_iter()
.chain(py311_builtins)
.chain(py313_builtins)
.chain(ipython_builtins)
.flatten()
.chain(ALWAYS_AVAILABLE_BUILTINS)
.copied()
if minor_version >= 10 {
builtins.extend(&["EncodingWarning", "aiter", "anext"]);
}
if minor_version >= 11 {
builtins.extend(&["BaseExceptionGroup", "ExceptionGroup"]);
}
if minor_version >= 13 {
builtins.push("PythonFinalizationError");
}
if is_notebook {
builtins.extend(IPYTHON_BUILTINS);
}
builtins
}
/// Returns `true` if the given name is that of a Python builtin.
@@ -385,22 +370,6 @@ pub fn is_python_builtin(name: &str, minor_version: u8, is_notebook: bool) -> bo
)
}
/// Return `Some(version)`, where `version` corresponds to the Python minor version
/// in which the builtin was added
pub fn version_builtin_was_added(name: &str) -> Option<u8> {
if PY310_PLUS_BUILTINS.contains(&name) {
Some(10)
} else if PY311_PLUS_BUILTINS.contains(&name) {
Some(11)
} else if PY313_PLUS_BUILTINS.contains(&name) {
Some(13)
} else if ALWAYS_AVAILABLE_BUILTINS.contains(&name) {
Some(0)
} else {
None
}
}
/// Returns `true` if the given name is that of a Python builtin iterator.
pub fn is_iterator(name: &str) -> bool {
matches!(

View File

@@ -215,10 +215,10 @@ impl Debug for CommentRanges {
}
impl<'a> IntoIterator for &'a CommentRanges {
type Item = TextRange;
type IntoIter = std::iter::Copied<std::slice::Iter<'a, TextRange>>;
type Item = &'a TextRange;
type IntoIter = std::slice::Iter<'a, TextRange>;
fn into_iter(self) -> Self::IntoIter {
self.raw.iter().copied()
self.raw.iter()
}
}

View File

@@ -11,8 +11,8 @@ use lsp_types::{PositionEncodingKind, Url};
pub use notebook::NotebookDocument;
pub(crate) use range::{NotebookRange, RangeExt, ToRangeExt};
pub(crate) use replacement::Replacement;
pub(crate) use text_document::DocumentVersion;
pub use text_document::TextDocument;
pub(crate) use text_document::{DocumentVersion, LanguageId};
use crate::{fix::Fixes, session::ResolvedClientCapabilities};

View File

@@ -20,23 +20,6 @@ pub struct TextDocument {
/// The latest version of the document, set by the LSP client. The server will panic in
/// debug mode if we attempt to update the document with an 'older' version.
version: DocumentVersion,
/// The language ID of the document as provided by the client.
language_id: Option<LanguageId>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum LanguageId {
Python,
Other,
}
impl From<&str> for LanguageId {
fn from(language_id: &str) -> Self {
match language_id {
"python" => Self::Python,
_ => Self::Other,
}
}
}
impl TextDocument {
@@ -46,16 +29,9 @@ impl TextDocument {
contents,
index,
version,
language_id: None,
}
}
#[must_use]
pub fn with_language_id(mut self, language_id: &str) -> Self {
self.language_id = Some(LanguageId::from(language_id));
self
}
pub fn into_contents(self) -> String {
self.contents
}
@@ -72,10 +48,6 @@ impl TextDocument {
self.version
}
pub fn language_id(&self) -> Option<LanguageId> {
self.language_id
}
pub fn apply_changes(
&mut self,
changes: Vec<lsp_types::TextDocumentContentChangeEvent>,

View File

@@ -38,7 +38,6 @@ pub(crate) fn fix_all(
file_resolver_settings,
Some(linter_settings),
None,
query.text_document_language_id(),
) {
return Ok(Fixes::default());
}

View File

@@ -77,7 +77,6 @@ pub(crate) fn check(
file_resolver_settings,
Some(linter_settings),
None,
query.text_document_language_id(),
) {
return DiagnosticsMap::default();
}

View File

@@ -4,8 +4,6 @@ use ruff_linter::settings::LinterSettings;
use ruff_workspace::resolver::{match_any_exclusion, match_any_inclusion};
use ruff_workspace::{FileResolverSettings, FormatterSettings};
use crate::edit::LanguageId;
/// Return `true` if the document at the given [`Path`] should be excluded.
///
/// The tool-specific settings should be provided if the request for the document is specific to
@@ -21,7 +19,6 @@ pub(crate) fn is_document_excluded(
resolver_settings: &FileResolverSettings,
linter_settings: Option<&LinterSettings>,
formatter_settings: Option<&FormatterSettings>,
language_id: Option<LanguageId>,
) -> bool {
if let Some(exclusion) = match_any_exclusion(
path,
@@ -41,14 +38,8 @@ pub(crate) fn is_document_excluded(
) {
tracing::debug!("Included path via `{}`: {}", inclusion, path.display());
false
} else if let Some(LanguageId::Python) = language_id {
tracing::debug!("Included path via Python language ID: {}", path.display());
false
} else {
tracing::debug!(
"Ignored path as it's not in the inclusion set: {}",
path.display()
);
// Path is excluded by not being in the inclusion set.
true
}
}

View File

@@ -21,14 +21,11 @@ impl super::SyncNotificationHandler for DidOpen {
types::DidOpenTextDocumentParams {
text_document:
types::TextDocumentItem {
uri,
text,
version,
language_id,
uri, text, version, ..
},
}: types::DidOpenTextDocumentParams,
) -> Result<()> {
let document = TextDocument::new(text, version).with_language_id(&language_id);
let document = TextDocument::new(text, version);
session.open_text_document(uri.clone(), document);

View File

@@ -90,7 +90,6 @@ fn format_text_document(
file_resolver_settings,
None,
Some(formatter_settings),
text_document.language_id(),
) {
return Ok(None);
}

View File

@@ -54,7 +54,6 @@ fn format_text_document_range(
file_resolver_settings,
None,
Some(formatter_settings),
text_document.language_id(),
) {
return Ok(None);
}

View File

@@ -9,7 +9,6 @@ use rustc_hash::FxHashMap;
pub(crate) use ruff_settings::RuffSettings;
use crate::edit::LanguageId;
use crate::{
edit::{DocumentKey, DocumentVersion, NotebookDocument},
PositionEncoding, TextDocument,
@@ -604,12 +603,4 @@ impl DocumentQuery {
.and_then(|cell_uri| notebook.cell_document_by_uri(cell_uri)),
}
}
pub(crate) fn text_document_language_id(&self) -> Option<LanguageId> {
if let DocumentQuery::Text { document, .. } = self {
document.language_id()
} else {
None
}
}
}

View File

@@ -340,7 +340,7 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
// Merge in the editor-specified configuration file, if it exists.
let editor_configuration = if let Some(config_file_path) = configuration {
match open_configuration_file(&config_file_path) {
match open_configuration_file(&config_file_path, project_root) {
Ok(config_from_file) => editor_configuration.combine(config_from_file),
Err(err) => {
tracing::error!("Unable to find editor-specified configuration file: {err}");
@@ -363,18 +363,11 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
}
}
fn open_configuration_file(config_path: &Path) -> crate::Result<Configuration> {
ruff_workspace::resolver::resolve_configuration(
config_path,
Relativity::Parent,
&IdentityTransformer,
)
}
fn open_configuration_file(
config_path: &Path,
project_root: &Path,
) -> crate::Result<Configuration> {
let options = ruff_workspace::pyproject::load_options(config_path)?;
struct IdentityTransformer;
impl ConfigurationTransformer for IdentityTransformer {
fn transform(&self, config: Configuration) -> Configuration {
config
}
Configuration::from_options(options, Some(config_path), project_root)
}

View File

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

View File

@@ -140,7 +140,7 @@ pub fn find_user_settings_toml() -> Option<PathBuf> {
}
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
if path.as_ref().ends_with("pyproject.toml") {
let pyproject = parse_pyproject_toml(&path)?;
let mut ruff = pyproject

View File

@@ -263,7 +263,7 @@ pub trait ConfigurationTransformer {
// configuration file extends another in the same path, we'll re-parse the same
// file at least twice (possibly more than twice, since we'll also parse it when
// resolving the "default" configuration).
pub fn resolve_configuration(
fn resolve_configuration(
pyproject: &Path,
relativity: Relativity,
transformer: &dyn ConfigurationTransformer,

View File

@@ -458,21 +458,19 @@ parentheses:
```python
# Input
for a, [b, d,] in c:
for a, f(b,) in c:
pass
# Black
for a, [
for a, f(
b,
d,
] in c:
) in c:
pass
# Ruff
for a, [
for a, f(
b,
d,
] in c:
) in c:
pass
```

View File

@@ -78,7 +78,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.6.4
rev: v0.6.3
hooks:
# Run the linter.
- id: ruff
@@ -91,7 +91,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.6.4
rev: v0.6.3
hooks:
# Run the linter.
- id: ruff
@@ -105,7 +105,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.6.4
rev: v0.6.3
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.6.4
ruff==0.6.3
mkdocs==1.6.1
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@38c0b8187325c3bab386b666daf3518ac036f2f4
mkdocs-redirects==1.2.1

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.6.4
ruff==0.6.3
mkdocs==1.6.1
mkdocs-material==9.1.18
mkdocs-redirects==1.2.1

View File

@@ -109,3 +109,16 @@
user-select: none;
}
/* Omits the nav title "Ruff" entirely unless on a small screen, in which case
the nav title is needed for backwards navigation in the collapsible
nav variant.
See https://github.com/astral-sh/uv/issues/5130 */
.md-nav__title {
display: none;
}
@media screen and (max-width: 1219px) {
.md-nav__title {
display: flex ;
}
}

View File

@@ -4,16 +4,16 @@ theme:
logo: assets/bolt.svg
favicon: assets/favicon.ico
features:
- navigation.instant
- navigation.instant.prefetch
- navigation.tracking
- content.code.annotate
- toc.integrate
- toc.follow
- navigation.path
- navigation.top
- content.code.copy
- content.tabs.link
- navigation.footer
- navigation.instant
- navigation.instant.prefetch
- navigation.path
- navigation.top
- navigation.tracking
- toc.follow
palette:
# Note: Using the system theme works with the insiders version
# https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#automatic-light-dark-mode
@@ -37,7 +37,6 @@ repo_name: ruff
site_author: charliermarsh
site_url: https://docs.astral.sh/ruff/
site_dir: site/ruff
site_description: An extremely fast Python linter and code formatter, written in Rust.
markdown_extensions:
- admonition
- pymdownx.details
@@ -72,6 +71,15 @@ not_in_nav: |
extra:
analytics:
provider: fathom
social:
- icon: fontawesome/brands/github
link: https://github.com/astral-sh/ruff
- icon: fontawesome/brands/discord
link: https://discord.com/invite/astral-sh
- icon: fontawesome/brands/python
link: https://pypi.org/project/ruff/
- icon: fontawesome/brands/x-twitter
link: https://x.com/astral_sh
validation:
omitted_files: warn
absolute_links: warn

View File

@@ -16,7 +16,7 @@
"@cloudflare/workers-types": "^4.20230801.0",
"miniflare": "^3.20230801.1",
"typescript": "^5.1.6",
"wrangler": "3.75.0"
"wrangler": "3.73.0"
}
},
"node_modules/@cloudflare/kv-asset-handler": {
@@ -128,9 +128,9 @@
}
},
"node_modules/@cloudflare/workers-types": {
"version": "4.20240903.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240903.0.tgz",
"integrity": "sha512-a4mqgtVsPWg3JNNlQdLRE0Z6/mHr/uXa1ANDw6Zd7in438UCbeb+j7Z954Sf93G24jExpAn9VZ8kUUml0RwZbQ==",
"version": "4.20240821.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240821.1.tgz",
"integrity": "sha512-icAkbnAqgVl6ef9lgLTom8na+kj2RBw2ViPAQ586hbdj0xZcnrjK7P46Eu08OU9D/lNDgN2sKU/sxhe2iK/gIg==",
"dev": true,
"license": "MIT OR Apache-2.0"
},
@@ -1105,9 +1105,9 @@
}
},
"node_modules/miniflare": {
"version": "3.20240821.1",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240821.1.tgz",
"integrity": "sha512-81qdiryDG7VXzZuoa0EwhkaIYYrn7+StRIrd/2i7SPqPUNICUBjbhFFKqTnvE1+fqIPPB6l8ShKFaFvmnZOASg==",
"version": "3.20240821.0",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240821.0.tgz",
"integrity": "sha512-4BhLGpssQxM/O6TZmJ10GkT3wBJK6emFkZ3V87/HyvQmVt8zMxEBvyw5uv6kdtp+7F54Nw6IKFJjPUL8rFVQrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1591,9 +1591,9 @@
}
},
"node_modules/wrangler": {
"version": "3.75.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.75.0.tgz",
"integrity": "sha512-CitNuNj0O1z6qbonUXmpUbxeWpU3nx28Kc4ZT33tMdeooQssb063Ie7+ZCdfS3kPhRHSwGdtOV22xFYytHON8w==",
"version": "3.73.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.73.0.tgz",
"integrity": "sha512-VrdDR2OpvsCQp+r5Of3rDP1W64cNN/LHLVx1roULOlPS8PZiv7rUYgkwhdCQ61+HICAaeSxWYIzkL5+B9+8W3g==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
@@ -1605,7 +1605,7 @@
"chokidar": "^3.5.3",
"date-fns": "^3.6.0",
"esbuild": "0.17.19",
"miniflare": "3.20240821.1",
"miniflare": "3.20240821.0",
"nanoid": "^3.3.3",
"path-to-regexp": "^6.2.0",
"resolve": "^1.22.8",

View File

@@ -5,7 +5,7 @@
"@cloudflare/workers-types": "^4.20230801.0",
"miniflare": "^3.20230801.1",
"typescript": "^5.1.6",
"wrangler": "3.75.0"
"wrangler": "3.73.0"
},
"private": true,
"scripts": {

View File

@@ -873,13 +873,6 @@
"win32"
]
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@swc/core": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.24.tgz",
@@ -1145,17 +1138,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz",
"integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
"integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.4.0",
"@typescript-eslint/type-utils": "8.4.0",
"@typescript-eslint/utils": "8.4.0",
"@typescript-eslint/visitor-keys": "8.4.0",
"@typescript-eslint/scope-manager": "8.3.0",
"@typescript-eslint/type-utils": "8.3.0",
"@typescript-eslint/utils": "8.3.0",
"@typescript-eslint/visitor-keys": "8.3.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1179,16 +1172,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz",
"integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
"integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.4.0",
"@typescript-eslint/types": "8.4.0",
"@typescript-eslint/typescript-estree": "8.4.0",
"@typescript-eslint/visitor-keys": "8.4.0",
"@typescript-eslint/scope-manager": "8.3.0",
"@typescript-eslint/types": "8.3.0",
"@typescript-eslint/typescript-estree": "8.3.0",
"@typescript-eslint/visitor-keys": "8.3.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1208,14 +1201,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz",
"integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
"integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.4.0",
"@typescript-eslint/visitor-keys": "8.4.0"
"@typescript-eslint/types": "8.3.0",
"@typescript-eslint/visitor-keys": "8.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1226,14 +1219,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz",
"integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
"integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.4.0",
"@typescript-eslint/utils": "8.4.0",
"@typescript-eslint/typescript-estree": "8.3.0",
"@typescript-eslint/utils": "8.3.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1251,9 +1244,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz",
"integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
"integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1265,14 +1258,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz",
"integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
"integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.4.0",
"@typescript-eslint/visitor-keys": "8.4.0",
"@typescript-eslint/types": "8.3.0",
"@typescript-eslint/visitor-keys": "8.3.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1320,16 +1313,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz",
"integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
"integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.4.0",
"@typescript-eslint/types": "8.4.0",
"@typescript-eslint/typescript-estree": "8.4.0"
"@typescript-eslint/scope-manager": "8.3.0",
"@typescript-eslint/types": "8.3.0",
"@typescript-eslint/typescript-estree": "8.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1343,13 +1336,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz",
"integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
"integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.4.0",
"@typescript-eslint/types": "8.3.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -2393,11 +2386,10 @@
}
},
"node_modules/eslint-module-utils": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz",
"integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
"integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^3.2.7"
},
@@ -2420,28 +2412,26 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
"integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
"array.prototype.findlastindex": "^1.2.5",
"array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.3",
"array.prototype.flat": "^1.3.2",
"array.prototype.flatmap": "^1.3.2",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.9.0",
"hasown": "^2.0.2",
"is-core-module": "^2.15.1",
"eslint-module-utils": "^2.8.0",
"hasown": "^2.0.0",
"is-core-module": "^2.13.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"object.groupby": "^1.0.3",
"object.values": "^1.2.0",
"object.fromentries": "^2.0.7",
"object.groupby": "^1.0.1",
"object.values": "^1.1.7",
"semver": "^6.3.1",
"tsconfig-paths": "^3.15.0"
},
@@ -2457,7 +2447,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
@@ -2467,7 +2456,6 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
@@ -2480,15 +2468,14 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-react": {
"version": "7.35.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz",
"integrity": "sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ==",
"version": "7.35.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz",
"integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3197,16 +3184,12 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4028,9 +4011,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.45",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
"integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
"version": "8.4.43",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz",
"integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==",
"dev": true,
"funding": [
{
@@ -5039,14 +5022,14 @@
"dev": true
},
"node_modules/vite": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz",
"integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"postcss": "^8.4.41",
"rollup": "^4.20.0"
},
"bin": {

View File

@@ -1,4 +1,4 @@
import MonacoEditor from "@monaco-editor/react";
import Editor from "@monaco-editor/react";
import { Theme } from "./theme";
export enum SecondaryTool {
@@ -72,7 +72,7 @@ function Content({
}
return (
<MonacoEditor
<Editor
options={{
readOnly: true,
minimap: { enabled: false },

View File

@@ -2,7 +2,7 @@
* Editor for the settings JSON.
*/
import MonacoEditor, { useMonaco } from "@monaco-editor/react";
import Editor, { useMonaco } from "@monaco-editor/react";
import { useCallback, useEffect } from "react";
import schema from "../../../ruff.schema.json";
import { Theme } from "./theme";
@@ -39,7 +39,7 @@ export default function SettingsEditor({
[onChange],
);
return (
<MonacoEditor
<Editor
options={{
readOnly: false,
minimap: { enabled: false },

View File

@@ -2,25 +2,11 @@
* Editor for the Python source code.
*/
import MonacoEditor, { Monaco, OnMount } from "@monaco-editor/react";
import {
editor,
IDisposable,
languages,
MarkerSeverity,
MarkerTag,
Range,
} from "monaco-editor";
import Editor, { BeforeMount, Monaco } from "@monaco-editor/react";
import { MarkerSeverity, MarkerTag } from "monaco-editor";
import { useCallback, useEffect, useRef } from "react";
import { Diagnostic } from "../pkg";
import { Theme } from "./theme";
import CodeActionProvider = languages.CodeActionProvider;
type MonacoEditorState = {
monaco: Monaco;
codeActionProvider: RuffCodeActionProvider;
disposeCodeActionProvider: IDisposable;
};
export default function SourceEditor({
visible,
@@ -35,32 +21,80 @@ export default function SourceEditor({
theme: Theme;
onChange: (pythonSource: string) => void;
}) {
const monacoRef = useRef<MonacoEditorState | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const monaco = monacoRef.current;
// Update the diagnostics in the editor.
useEffect(() => {
const editorState = monacoRef.current;
if (editorState == null) {
const editor = monaco?.editor;
const model = editor?.getModels()[0];
if (!editor || !model) {
return;
}
editorState.codeActionProvider.diagnostics = diagnostics;
editor.setModelMarkers(
model,
"owner",
diagnostics.map((diagnostic) => ({
startLineNumber: diagnostic.location.row,
startColumn: diagnostic.location.column,
endLineNumber: diagnostic.end_location.row,
endColumn: diagnostic.end_location.column,
message: diagnostic.code
? `${diagnostic.code}: ${diagnostic.message}`
: diagnostic.message,
severity: MarkerSeverity.Error,
tags:
diagnostic.code === "F401" || diagnostic.code === "F841"
? [MarkerTag.Unnecessary]
: [],
})),
);
updateMarkers(editorState.monaco, diagnostics);
}, [diagnostics]);
// Dispose the code action provider on unmount.
useEffect(() => {
const disposeActionProvider = monacoRef.current?.disposeCodeActionProvider;
if (disposeActionProvider == null) {
return;
}
const codeActionProvider = monaco?.languages.registerCodeActionProvider(
"python",
{
provideCodeActions: function (model, position) {
const actions = diagnostics
.filter((check) => position.startLineNumber === check.location.row)
.filter(({ fix }) => fix)
.map((check) => ({
title: check.fix
? check.fix.message
? `${check.code}: ${check.fix.message}`
: `Fix ${check.code}`
: "Fix",
id: `fix-${check.code}`,
kind: "quickfix",
edit: check.fix
? {
edits: check.fix.edits.map((edit) => ({
resource: model.uri,
versionId: model.getVersionId(),
textEdit: {
range: {
startLineNumber: edit.location.row,
startColumn: edit.location.column,
endLineNumber: edit.end_location.row,
endColumn: edit.end_location.column,
},
text: edit.content || "",
},
})),
}
: undefined,
}));
return {
actions,
dispose: () => {},
};
},
},
);
return () => {
disposeActionProvider.dispose();
codeActionProvider?.dispose();
};
}, []);
}, [diagnostics, monaco]);
const handleChange = useCallback(
(value: string | undefined) => {
@@ -69,30 +103,14 @@ export default function SourceEditor({
[onChange],
);
const handleMount: OnMount = useCallback(
(_editor, instance) => {
const ruffActionsProvider = new RuffCodeActionProvider(diagnostics);
const disposeCodeActionProvider =
instance.languages.registerCodeActionProvider(
"python",
ruffActionsProvider,
);
updateMarkers(instance, diagnostics);
monacoRef.current = {
monaco: instance,
codeActionProvider: ruffActionsProvider,
disposeCodeActionProvider,
};
},
[diagnostics],
const handleMount: BeforeMount = useCallback(
(instance) => (monacoRef.current = instance),
[],
);
return (
<MonacoEditor
onMount={handleMount}
<Editor
beforeMount={handleMount}
options={{
fixedOverflowWidgets: true,
readOnly: false,
@@ -110,76 +128,3 @@ export default function SourceEditor({
/>
);
}
class RuffCodeActionProvider implements CodeActionProvider {
constructor(public diagnostics: Array<Diagnostic>) {}
provideCodeActions(
model: editor.ITextModel,
range: Range,
): languages.ProviderResult<languages.CodeActionList> {
const actions = this.diagnostics
.filter((check) => range.startLineNumber === check.location.row)
.filter(({ fix }) => fix)
.map((check) => ({
title: check.fix
? check.fix.message
? `${check.code}: ${check.fix.message}`
: `Fix ${check.code}`
: "Fix",
id: `fix-${check.code}`,
kind: "quickfix",
edit: check.fix
? {
edits: check.fix.edits.map((edit) => ({
resource: model.uri,
versionId: model.getVersionId(),
textEdit: {
range: {
startLineNumber: edit.location.row,
startColumn: edit.location.column,
endLineNumber: edit.end_location.row,
endColumn: edit.end_location.column,
},
text: edit.content || "",
},
})),
}
: undefined,
}));
return {
actions,
dispose: () => {},
};
}
}
function updateMarkers(monaco: Monaco, diagnostics: Array<Diagnostic>) {
const editor = monaco.editor;
const model = editor?.getModels()[0];
if (!model) {
return;
}
editor.setModelMarkers(
model,
"owner",
diagnostics.map((diagnostic) => ({
startLineNumber: diagnostic.location.row,
startColumn: diagnostic.location.column,
endLineNumber: diagnostic.end_location.row,
endColumn: diagnostic.end_location.column,
message: diagnostic.code
? `${diagnostic.code}: ${diagnostic.message}`
: diagnostic.message,
severity: MarkerSeverity.Error,
tags:
diagnostic.code === "F401" || diagnostic.code === "F841"
? [MarkerTag.Unnecessary]
: [],
})),
);
}

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.6.4"
version = "0.6.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

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