Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
728 changes: 548 additions & 180 deletions Cargo.lock

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ lto = false

[workspace.dependencies]
# ruff, ty and related crates
ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_python_parser", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_python_stdlib = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_python_stdlib", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_python_ast", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_text_size", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_db = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_db", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d", features = ["serde"] }
ty_python_semantic = { git = "https://github.com/astral-sh/ruff.git", package = "ty_python_semantic", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ty_module_resolver = { git = "https://github.com/astral-sh/ruff.git", package = "ty_module_resolver", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ty_vendored = { git = "https://github.com/astral-sh/ruff.git", package = "ty_vendored", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_python_parser = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_parser", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ruff_python_stdlib = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_stdlib", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ruff_python_ast = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_ast", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ruff_text_size = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_text_size", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ruff_db = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_db", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae", features = ["serde"] }
ty_python_semantic = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_semantic", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ty_python_core = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_core", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ty_module_resolver = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_module_resolver", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
ty_vendored = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_vendored", rev = "c7d3f28cb21f1520d156301eb5ec0d13857105ae" }
Comment thread
samuelcolvin marked this conversation as resolved.
Outdated
# salsa version matches current main of ruff
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "53421c2fff87426fa0bb51cab06632b87646de13", default-features = false, features = [
salsa = { version="0.26.1", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
Expand Down
1 change: 1 addition & 0 deletions crates/monty-type-checking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ruff_python_ast = { workspace = true }
ruff_db = { workspace = true }
ruff_text_size = { workspace = true }
ty_python_semantic = { workspace = true }
ty_python_core = { workspace = true }
ty_module_resolver = { workspace = true }
salsa = { workspace = true }

Expand Down
38 changes: 30 additions & 8 deletions crates/monty-type-checking/src/db.rs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Stale check_types references in unchanged docstrings

The docstring at lines 68-69 of crates/monty-type-checking/src/db.rs still references check_types ("Program settings needed by check_types") even though the PR replaces all usage with check_file_unwrap. Similarly, crates/monty-type-checking/src/pool.rs:125 says "e.g. for check_types or rendering". These are pre-existing context lines not in the diff hunks, but they became stale as a direct result of this PR's changes. Per the CLAUDE.md rule about updating stale comments, these should be updated in a follow-up.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ use std::{fmt, sync::Arc};

use ruff_db::{
Db as SourceDb,
diagnostic::Diagnostic,
files::{File, FileRootKind, Files},
system::{DbWithTestSystem, System, SystemPathBuf, TestSystem},
vendored::VendoredFileSystem,
};
use ruff_python_ast::PythonVersion;
use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings, SearchPaths};
use ty_module_resolver::{Db as ModuleResolverDb, FallibleStrategy, SearchPathSettings, SearchPaths};
use ty_python_core::{
Db as PythonCoreDb,
platform::PythonPlatform,
program::{Program, ProgramSettings},
};
use ty_python_semantic::{
AnalysisSettings, Db, Program, ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource,
default_lint_registry,
AnalysisSettings, Db, PythonVersionSource, PythonVersionWithSource, check_file_unwrap, default_lint_registry,
lint::{LintRegistry, RuleSelection},
};

Expand All @@ -21,10 +26,13 @@ use ty_python_semantic::{
///
/// ## Lifetime invariant
///
/// Each `MemoryDb` owns a unique Salsa storage. It must never be cloned or shared
/// with another live handle because Salsa setters require exclusive access to the
/// underlying `Arc<Zalsa>`.
/// Each `MemoryDb` owns a unique Salsa storage. The pool must never clone a pooled
/// instance or hand out parallel live handles because Salsa setters require
/// exclusive access to the underlying `Arc<Zalsa>`. `Clone` is only derived to
/// satisfy `ty_python_semantic::Db::dyn_clone`, which is reached only by ty's
/// autofix pipeline — a code path monty never drives.
#[salsa::db]
#[derive(Clone)]
pub(crate) struct MemoryDb {
storage: salsa::Storage<Self>,
files: Files,
Expand Down Expand Up @@ -74,7 +82,7 @@ impl Default for MemoryDb {
db.files().try_add_root(&db, &src_root, FileRootKind::Project);

let search_paths = SearchPathSettings::new(vec![src_root.to_path_buf()])
.to_search_paths(db.system(), db.vendored())
.to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
.expect("vendored typeshed search paths always resolve");

Program::from_settings(
Expand Down Expand Up @@ -123,10 +131,20 @@ impl SourceDb for MemoryDb {
}

#[salsa::db]
impl Db for MemoryDb {
impl PythonCoreDb for MemoryDb {
fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}
}

#[salsa::db]
impl Db for MemoryDb {
fn check_file(&self, file: File) -> Vec<Diagnostic> {
if !self.should_check_file(file) {
return Vec::new();
}
check_file_unwrap(self, file)
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

fn rule_selection(&self, _file: File) -> &RuleSelection {
&self.rule_selection
Expand All @@ -143,6 +161,10 @@ impl Db for MemoryDb {
fn verbose(&self) -> bool {
false
}

fn dyn_clone(&self) -> Box<dyn Db> {
Box::new(self.clone())
}
}

#[salsa::db]
Expand Down
8 changes: 6 additions & 2 deletions crates/monty-type-checking/src/type_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use ruff_db::{
system::SystemPathBuf,
};
use ruff_text_size::{TextRange, TextSize};
use ty_python_semantic::types::check_types;
use ty_python_semantic::check_file_unwrap;

use crate::{
db::SRC_ROOT,
Expand Down Expand Up @@ -81,7 +81,11 @@ pub fn type_check(
(main_file, 0)
};

let mut diagnostics = check_types(pooled_db.db(), main_file);
// Use `check_file_unwrap` (not `check_types` alone) so that parser errors
// and unsupported-syntax errors are included — otherwise malformed input
// (e.g. deeply nested parentheses that ruff's parser rejects) would silently
// type-check clean.
let mut diagnostics = check_file_unwrap(pooled_db.db(), main_file);
diagnostics.retain(filter_diagnostics);

if diagnostics.is_empty() {
Expand Down
24 changes: 22 additions & 2 deletions crates/monty-type-checking/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ result = add(1, '2')
| ^^^ ------ Parameter declared here
2 | return x + y
|
info: rule `invalid-argument-type` is enabled by default
"#);
}

Expand Down Expand Up @@ -87,7 +86,6 @@ result = add(1, '2')";
| ^^^ ------ Parameter declared here
2 | return x + y
|
info: rule `invalid-argument-type` is enabled by default
"#);
}

Expand Down Expand Up @@ -299,3 +297,25 @@ fn test_reveal_types() {

assert_snapshot!(actual);
}

#[test]
fn deeply_nested_parentheses_do_not_stack_overflow() {
let depth = 500;
let mut code = String::with_capacity(depth * 2 + 1);
for _ in 0..depth {
code.push('(');
}
code.push('1');
for _ in 0..depth {
code.push(')');
}

let r = type_check(&SourceFile::new(&code, "main.py"), None).unwrap().unwrap();
assert_snapshot!(
r.format(DiagnosticFormat::Concise).to_string(),
@r"
main.py:1:200: error[invalid-syntax] Source is too deeply nested for the parser
main.py:1:1002: error[invalid-syntax] Expected `)`, found end of file
"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ reveal_types.py:10:13: info[revealed-type] Revealed type: `int`
reveal_types.py:11:13: info[revealed-type] Revealed type: `float`
reveal_types.py:14:13: info[revealed-type] Revealed type: `Literal["hello"]`
reveal_types.py:15:13: info[revealed-type] Revealed type: `Literal[b"foobar"]`
reveal_types.py:18:13: info[revealed-type] Revealed type: `list[Unknown | int]`
reveal_types.py:18:13: info[revealed-type] Revealed type: `list[int]`
reveal_types.py:19:13: info[revealed-type] Revealed type: `tuple[Literal[1], Literal[2]]`
reveal_types.py:20:13: info[revealed-type] Revealed type: `dict[Unknown | int, Unknown | int]`
reveal_types.py:21:13: info[revealed-type] Revealed type: `set[Unknown | int]`
reveal_types.py:20:13: info[revealed-type] Revealed type: `dict[int, int]`
reveal_types.py:21:13: info[revealed-type] Revealed type: `set[int]`
reveal_types.py:22:13: info[revealed-type] Revealed type: `frozenset[int]`
reveal_types.py:23:13: info[revealed-type] Revealed type: `range`
reveal_types.py:26:13: info[revealed-type] Revealed type: `enumerate[int]`
reveal_types.py:27:13: info[revealed-type] Revealed type: `reversed[int]`
reveal_types.py:28:13: info[revealed-type] Revealed type: `zip[Unknown]`
reveal_types.py:31:13: info[revealed-type] Revealed type: `slice[Any, Any, Any]`
reveal_types.py:27:13: info[revealed-type] Revealed type: `Iterator[int]`
reveal_types.py:28:13: info[revealed-type] Revealed type: `zip[tuple[int, int]]`
reveal_types.py:31:13: info[revealed-type] Revealed type: `slice[Literal[1], Literal[2], Any]`
reveal_types.py:34:13: info[revealed-type] Revealed type: `BaseException`
reveal_types.py:35:13: info[revealed-type] Revealed type: `Exception`
reveal_types.py:36:13: info[revealed-type] Revealed type: `SystemExit`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ expression: actual
bad_types.py:22:11: error[invalid-argument-type] Argument to function `takes_int` is incorrect: Expected `int`, found `Literal["hello"]`
bad_types.py:23:11: error[invalid-argument-type] Argument to function `takes_int` is incorrect: Expected `int`, found `float`
bad_types.py:24:11: error[invalid-argument-type] Argument to function `takes_str` is incorrect: Expected `str`, found `Literal[42]`
bad_types.py:25:11: error[invalid-argument-type] Argument to function `takes_str` is incorrect: Expected `str`, found `list[Unknown | int]`
bad_types.py:25:11: error[invalid-argument-type] Argument to function `takes_str` is incorrect: Expected `str`, found `list[int]`
bad_types.py:28:16: error[invalid-argument-type] Argument to function `takes_list_int` is incorrect: Expected `list[int]`, found `list[int | str]`
bad_types.py:29:16: error[invalid-argument-type] Argument to function `takes_list_int` is incorrect: Expected `list[int]`, found `list[int | float]`
bad_types.py:36:12: error[invalid-return-type] Return type does not match returned value: expected `int`, found `Literal["oops"]`
Expand All @@ -14,8 +14,8 @@ bad_types.py:44:12: error[invalid-return-type] Return type does not match return
bad_types.py:48:12: error[invalid-return-type] Return type does not match returned value: expected `None`, found `Literal[42]`
bad_types.py:63:11: error[unsupported-operator] Operator `+` is not supported between objects of type `int` and `str`
bad_types.py:64:11: error[unsupported-operator] Operator `-` is not supported between objects of type `str` and `int`
bad_types.py:70:1: error[type-assertion-failure] Type `str` does not match asserted type `Literal[42]`
bad_types.py:73:1: error[type-assertion-failure] Type `list[str]` does not match asserted type `list[int]`
bad_types.py:70:1: error[type-assertion-failure] Type `Literal[42]` does not match asserted type `str`
bad_types.py:73:1: error[type-assertion-failure] Type `list[int]` does not match asserted type `list[str]`
bad_types.py:85:5: error[unresolved-attribute] Object of type `MyClass` has no attribute `nonexistent_attr`
bad_types.py:95:1: error[missing-argument] No argument provided for required parameter `b` of function `takes_two`
bad_types.py:96:23: error[too-many-positional-arguments] Too many positional arguments to function `takes_two`: expected 2, got 3
Expand Down
18 changes: 18 additions & 0 deletions crates/monty/tests/security.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use insta::assert_snapshot;
use monty::MontyRun;

#[test]
fn deeply_nested_parentheses_do_not_stack_overflow() {
let depth = 5000;
let mut code = String::with_capacity(depth * 2 + 1);
for _ in 0..depth {
code.push('(');
}
code.push('1');
for _ in 0..depth {
code.push(')');
}
let result = MontyRun::new(code, "test.py", vec![]);
let err = result.expect_err("expected parse error for deeply nested parentheses");
assert_snapshot!(err.message().unwrap_or(""), @"Source is too deeply nested for the parser at byte range 199..200");
}
Loading