diff --git a/Cargo.lock b/Cargo.lock index f4594eec..2143a81a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -425,6 +425,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -687,6 +698,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -922,6 +942,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "drop_bomb" version = "0.1.5" @@ -979,12 +1010,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equator" version = "0.4.2" @@ -1237,9 +1262,9 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a" +checksum = "dfd774e8175d3adb09c1742cb4697fb08490607fc02acfaa3b66b88254239d1d" dependencies = [ "attribute-derive", "quote", @@ -1248,13 +1273,13 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb" +checksum = "d5b6f7d040889b1980e31d03585f0150223f44eeada7a69c525cbb74c38266f6" dependencies = [ "compact_str", "get-size-derive2", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "indexmap", "ordermap", "smallvec", @@ -1288,10 +1313,24 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1352,6 +1391,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "equivalent", +] + [[package]] name = "hashlink" version = "0.10.0" @@ -1429,6 +1477,74 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1437,12 +1553,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1482,7 +1598,7 @@ checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ "console", "once_cell", - "similar", + "similar 2.7.0", "tempfile", ] @@ -1503,9 +1619,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] @@ -1533,15 +1649,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "is_executable" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1613,6 +1720,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lexical-parse-float" version = "1.0.6" @@ -1699,6 +1812,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" version = "0.4.14" @@ -1737,12 +1856,6 @@ dependencies = [ "quote", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "matchit" version = "0.9.1" @@ -1809,7 +1922,7 @@ dependencies = [ "serde_json", "smallvec", "speedate", - "strum", + "strum 0.27.2", "tempfile", ] @@ -1846,7 +1959,7 @@ dependencies = [ "monty", "pyo3", "pyo3-build-config", - "similar", + "similar 2.7.0", "tempfile", ] @@ -1888,6 +2001,7 @@ dependencies = [ "ruff_text_size", "salsa", "ty_module_resolver", + "ty_python_core", "ty_python_semantic", ] @@ -2082,9 +2196,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "ordermap" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa78c92071bbd3628c22b1a964f7e0eb201dc1456555db072beb1662ecd6715" +checksum = "7f7476a5b122ff1fce7208e7ee9dccd0a516e835f5b8b19b8f3c98a34cf757c1" dependencies = [ "indexmap", ] @@ -2250,6 +2364,15 @@ dependencies = [ "serde", ] +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2298,6 +2421,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -2433,9 +2566,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2468,6 +2601,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2505,6 +2644,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2543,6 +2693,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.11.0" @@ -2622,7 +2778,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" version = "0.1.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "anstyle", "memchr", @@ -2632,7 +2788,7 @@ dependencies = [ [[package]] name = "ruff_db" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "anstyle", "arc-swap", @@ -2641,8 +2797,6 @@ dependencies = [ "dunce", "filetime", "get-size2", - "glob", - "is_executable", "matchit", "path-slash", "pathdiff", @@ -2659,20 +2813,19 @@ dependencies = [ "salsa", "serde", "serde_json", - "similar", + "similar 3.1.0", "supports-hyperlinks", "thiserror", "tracing", "ty_static", "web-time", - "which", "zip", ] [[package]] name = "ruff_diagnostics" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "get-size2", "is-macro", @@ -2683,7 +2836,7 @@ dependencies = [ [[package]] name = "ruff_index" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "get-size2", "ruff_macros", @@ -2693,7 +2846,7 @@ dependencies = [ [[package]] name = "ruff_macros" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "heck", "itertools 0.14.0", @@ -2707,7 +2860,7 @@ dependencies = [ [[package]] name = "ruff_memory_usage" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "get-size2", ] @@ -2715,11 +2868,11 @@ dependencies = [ [[package]] name = "ruff_notebook" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "anyhow", "itertools 0.14.0", - "rand 0.9.2", + "rand 0.10.1", "ruff_diagnostics", "ruff_source_file", "ruff_text_size", @@ -2733,7 +2886,7 @@ dependencies = [ [[package]] name = "ruff_python_ast" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "aho-corasick", "bitflags 2.10.0", @@ -2752,18 +2905,18 @@ dependencies = [ [[package]] name = "ruff_python_literal" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "bitflags 2.10.0", + "icu_properties", "itertools 0.14.0", "ruff_python_ast", - "unic-ucd-category", ] [[package]] name = "ruff_python_parser" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "bitflags 2.10.0", "bstr", @@ -2783,7 +2936,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "bitflags 2.10.0", "unicode-ident", @@ -2792,7 +2945,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "itertools 0.14.0", "ruff_source_file", @@ -2803,7 +2956,7 @@ dependencies = [ [[package]] name = "ruff_source_file" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "get-size2", "memchr", @@ -2814,7 +2967,7 @@ dependencies = [ [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "get-size2", "serde", @@ -2890,8 +3043,9 @@ checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "salsa" -version = "0.26.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=53421c2fff87426fa0bb51cab06632b87646de13#53421c2fff87426fa0bb51cab06632b87646de13" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07bc2a7df3f8e2306434a172a694d44d14fda738d08aad5f2f7f747d2f06fdc" dependencies = [ "boxcar", "compact_str", @@ -2915,13 +3069,15 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.26.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=53421c2fff87426fa0bb51cab06632b87646de13#53421c2fff87426fa0bb51cab06632b87646de13" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec256ece77895f4a8d624cecc133dd798c7961a861439740b1c7410a613ee7ba" [[package]] name = "salsa-macros" -version = "0.26.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=53421c2fff87426fa0bb51cab06632b87646de13#53421c2fff87426fa0bb51cab06632b87646de13" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978e5d5c9533ce19b6a58ad91024e1d136f6eec83c4ba98b5ce94c87986c41d8" dependencies = [ "proc-macro2", "quote", @@ -3028,7 +3184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3039,7 +3195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3061,6 +3217,15 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "similar" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d93e861ede2e497b47833469b8ec9d5c07fa4c78ce7a00f6eb7dd8168b4b3f" +dependencies = [ + "bstr", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -3089,8 +3254,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aba069c070b5e213f2a094deb7e5ed50ecb092be36102a4f4042e8d2056d060e" dependencies = [ "lexical-parse-float", - "strum", - "strum_macros", + "strum 0.27.2", + "strum_macros 0.27.2", ] [[package]] @@ -3151,7 +3316,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -3166,6 +3340,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3248,39 +3434,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "test-case" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" -dependencies = [ - "test-case-macros", -] - -[[package]] -name = "test-case-core" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "test-case-macros" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "test-case-core", -] - [[package]] name = "thin-vec" version = "0.2.14" @@ -3326,6 +3479,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3394,7 +3557,7 @@ dependencies = [ [[package]] name = "ty_combine" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "ordermap", "ruff_db", @@ -3404,8 +3567,9 @@ dependencies = [ [[package]] name = "ty_module_resolver" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ + "anyhow", "camino", "compact_str", "get-size2", @@ -3417,28 +3581,56 @@ dependencies = [ "ruff_python_stdlib", "rustc-hash", "salsa", - "strum", - "strum_macros", + "strum 0.28.0", + "strum_macros 0.28.0", "thiserror", "tracing", ] [[package]] -name = "ty_python_semantic" +name = "ty_python_core" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ - "anyhow", "bitflags 2.10.0", "bitvec", + "get-size2", + "hashbrown 0.17.0", + "itertools 0.14.0", + "ruff_db", + "ruff_index", + "ruff_macros", + "ruff_memory_usage", + "ruff_python_ast", + "ruff_python_parser", + "ruff_python_trivia", + "ruff_text_size", + "rustc-hash", + "salsa", + "serde", + "smallvec", + "static_assertions", + "tracing", + "ty_combine", + "ty_module_resolver", + "ty_site_packages", + "ty_vendored", +] + +[[package]] +name = "ty_python_semantic" +version = "0.0.0" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" +dependencies = [ + "bitflags 2.10.0", "compact_str", "drop_bomb", "get-size2", - "hashbrown 0.16.1", "indexmap", "itertools 0.14.0", "memchr", "ordermap", + "rayon", "ruff_db", "ruff_diagnostics", "ruff_index", @@ -3455,21 +3647,19 @@ dependencies = [ "salsa", "smallvec", "static_assertions", - "strsim", - "strum", - "strum_macros", - "test-case", + "strum 0.28.0", + "strum_macros 0.28.0", "thiserror", "tracing", - "ty_combine", "ty_module_resolver", + "ty_python_core", "ty_site_packages", ] [[package]] name = "ty_site_packages" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "camino", "colored 3.0.0", @@ -3481,68 +3671,37 @@ dependencies = [ "ruff_python_trivia", "ruff_source_file", "ruff_text_size", - "strum", - "strum_macros", + "strum 0.28.0", + "strum_macros 0.28.0", "tracing", "ty_static", - "which", ] [[package]] name = "ty_static" version = "0.0.1" -source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ "ruff_macros", ] [[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-category" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" +name = "ty_vendored" +version = "0.0.0" +source = "git+https://github.com/samuelcolvin/ruff.git?rev=af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1#af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" dependencies = [ - "matches", - "unic-char-property", - "unic-char-range", - "unic-ucd-version", + "path-slash", + "ruff_db", + "static_assertions", + "walkdir", + "zip", ] [[package]] -name = "unic-ucd-version" -version = "0.9.0" +name = "typenum" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" @@ -3571,6 +3730,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_names2" version = "1.3.0" @@ -3593,6 +3758,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3651,7 +3822,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -3699,6 +3879,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -3719,17 +3933,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "which" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" -dependencies = [ - "env_home", - "rustix", - "winsafe", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3977,16 +4180,104 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "winsafe" -version = "0.0.19" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -4003,6 +4294,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.33" @@ -4023,6 +4337,60 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 6f1135c5..ccb7b475 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ruff_python_stdlib = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_stdlib", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ruff_python_ast = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_ast", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ruff_text_size = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_text_size", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ruff_db = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_db", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1", features = ["serde"] } +ty_python_semantic = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_semantic", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ty_python_core = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_core", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ty_module_resolver = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_module_resolver", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } +ty_vendored = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_vendored", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" } # 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", diff --git a/crates/monty-python/python/pydantic_monty/_monty.pyi b/crates/monty-python/python/pydantic_monty/_monty.pyi index e4ca1479..643b27cd 100644 --- a/crates/monty-python/python/pydantic_monty/_monty.pyi +++ b/crates/monty-python/python/pydantic_monty/_monty.pyi @@ -840,7 +840,10 @@ class MontySyntaxError(MontyError): Inherits exception(), __str__() from MontyError. """ - def display(self, format: Literal['type-msg', 'msg'] = 'msg') -> str: + def traceback(self) -> list[Frame]: + """Returns the Monty traceback as a list of Frame objects.""" + + def display(self, format: Literal['traceback', 'type-msg', 'msg'] = 'traceback') -> str: """Returns formatted exception string. Args: diff --git a/crates/monty-python/src/exceptions.rs b/crates/monty-python/src/exceptions.rs index 3afffb30..6509ec2c 100644 --- a/crates/monty-python/src/exceptions.rs +++ b/crates/monty-python/src/exceptions.rs @@ -96,62 +96,6 @@ impl MontyError { } } -/// Raised when Python code has syntax errors or cannot be parsed by Monty. -/// -/// Inherits from `MontyError`. The inner exception is always a `SyntaxError`. -#[pyclass(extends=MontyError, module="pydantic_monty", skip_from_py_object)] -#[derive(Clone)] -pub struct MontySyntaxError; - -impl MontySyntaxError { - /// Creates a new `MontySyntaxError` with the given message. - #[must_use] - pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr { - let base_error = MontyError::new(exc); - let init = PyClassInitializer::from(base_error).add_subclass(Self); - match Py::new(py, init) { - Ok(err) => PyErr::from_value(err.into_bound(py).into_any()), - Err(e) => e, - } - } -} - -#[pymethods] -impl MontySyntaxError { - /// Returns formatted exception string. - /// - /// Args: - /// format: 'type-msg' - 'ExceptionType: message' format - /// 'msg' - just the message - #[pyo3(signature = (format = "msg"))] - #[expect(clippy::needless_pass_by_value, reason = "required by macro")] - fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult { - let parent = slf.as_super(); - match format { - "msg" => Ok(parent.message().unwrap_or_default().to_string()), - "type-msg" => Ok(parent.exc.summary()), - _ => Err(exceptions::PyValueError::new_err(format!( - "Invalid display format: '{format}'. Expected 'type-msg', or 'msg'" - ))), - } - } - - #[expect(clippy::needless_pass_by_value, reason = "required by macro")] - fn __str__(slf: PyRef<'_, Self>) -> String { - slf.as_super().message().unwrap_or_default().to_string() - } - - #[expect(clippy::needless_pass_by_value, reason = "required by macro")] - fn __repr__(slf: PyRef<'_, Self>) -> String { - let parent = slf.as_super(); - if let Some(msg) = parent.message() { - format!("MontySyntaxError({msg})") - } else { - "MontySyntaxError()".to_string() - } - } -} - /// Raised when type checking finds errors in the code. /// /// Inherits from `MontyError`. This exception is raised when static type @@ -202,37 +146,93 @@ impl MontyTypingError { } } +/// Raised when Python code has syntax errors or cannot be parsed by Monty. +/// +/// Inherits from `MontyError`. The inner exception is always a `SyntaxError`. +#[pyclass(extends=MontyError, module="pydantic_monty", skip_from_py_object)] +pub struct MontySyntaxError { + traceback: Traceback, +} + +impl MontySyntaxError { + /// Creates a new `MontySyntaxError` with the given message. + #[must_use] + pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr { + let traceback = match Traceback::new(py, &exc) { + Ok(frames) => frames, + Err(e) => return e, + }; + + let base_error = MontyError::new(exc); + let syntax_error = Self { traceback }; + let init = PyClassInitializer::from(base_error).add_subclass(syntax_error); + match Py::new(py, init) { + Ok(err) => PyErr::from_value(err.into_bound(py).into_any()), + Err(e) => e, + } + } +} + +#[pymethods] +impl MontySyntaxError { + /// Returns the Monty traceback as a list of Frame objects. + fn traceback(&self, py: Python<'_>) -> Py { + self.traceback.py_list(py) + } + + /// Returns formatted exception string. + #[pyo3(signature = (format = "traceback"))] + #[expect(clippy::needless_pass_by_value, reason = "required by macro")] + fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult { + match format { + "traceback" => Ok(slf.as_super().exc.to_string()), + "type-msg" => Ok(slf.as_super().exc.summary()), + "msg" => Ok(slf.as_super().message().unwrap_or_default().to_string()), + _ => Err(exceptions::PyValueError::new_err(format!( + "Invalid display format: '{format}'. Expected 'traceback', 'type-msg', or 'msg'" + ))), + } + } + + #[expect(clippy::needless_pass_by_value, reason = "required by macro")] + fn __str__(slf: PyRef<'_, Self>) -> String { + slf.as_super().message().unwrap_or_default().to_string() + } + + #[expect(clippy::needless_pass_by_value, reason = "required by macro")] + fn __repr__(slf: PyRef<'_, Self>) -> String { + let parent = slf.as_super(); + if let Some(msg) = parent.message() { + format!("MontySyntaxError({msg})") + } else { + "MontySyntaxError()".to_string() + } + } +} + /// Raised when Monty code fails during execution. /// /// Inherits from `MontyError`. Additionally provides `traceback()` to access /// the Monty stack frames where the error occurred. #[pyclass(extends=MontyError, module="pydantic_monty")] pub struct MontyRuntimeError { - /// The traceback frames where the error occurred (pre-converted to Python objects). - frames: Vec>, + traceback: Traceback, } impl MontyRuntimeError { /// Creates a new `MontyRuntimeError` from the given exception data. #[must_use] pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr { - // Convert stack frames to PyFrame objects - let frames_result: PyResult>> = exc - .traceback() - .iter() - .map(|f| Py::new(py, PyFrame::from_stack_frame(f))) - .collect(); - - let frames = match frames_result { + let traceback = match Traceback::new(py, &exc) { Ok(frames) => frames, Err(e) => return e, }; let base_error = MontyError::new(exc); // Create the MontyRuntimeError with proper initialization - let runtime_error = Self { frames }; + let runtime_error = Self { traceback }; - let init = pyo3::PyClassInitializer::from(base_error).add_subclass(runtime_error); + let init = PyClassInitializer::from(base_error).add_subclass(runtime_error); match Py::new(py, init) { Ok(err) => PyErr::from_value(err.into_bound(py).into_any()), Err(e) => e, @@ -244,14 +244,10 @@ impl MontyRuntimeError { impl MontyRuntimeError { /// Returns the Monty traceback as a list of Frame objects. fn traceback(&self, py: Python<'_>) -> Py { - PyList::new(py, &self.frames) - .expect("failed to create frames list") - .unbind() + self.traceback.py_list(py) } /// Returns formatted exception string. - /// - /// Overrides the base class to provide the full traceback when format='traceback'. #[pyo3(signature = (format = "traceback"))] #[expect(clippy::needless_pass_by_value, reason = "required by macro")] fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult { @@ -290,6 +286,24 @@ impl MontyRuntimeError { } } +/// The traceback frames where the error occurred (pre-converted to Python objects). +struct Traceback(Vec>); + +impl Traceback { + fn new(py: Python<'_>, exc: &MontyException) -> PyResult { + // Convert stack frames to PyFrame objects + exc.traceback() + .iter() + .map(|f| Py::new(py, PyFrame::from_stack_frame(f))) + .collect::>>>() + .map(Self) + } + + fn py_list(&self, py: Python<'_>) -> Py { + PyList::new(py, &self.0).expect("failed to create frames list").unbind() + } +} + /// A single frame in a Monty traceback. /// /// Contains all the information needed to display a traceback line: diff --git a/crates/monty-python/tests/test_exceptions.py b/crates/monty-python/tests/test_exceptions.py index ecf1979b..57696d8f 100644 --- a/crates/monty-python/tests/test_exceptions.py +++ b/crates/monty-python/tests/test_exceptions.py @@ -293,8 +293,15 @@ def test_str_returns_msg(): def test_syntax_error_display(): with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info: pydantic_monty.Monty('def') - assert exc_info.value.display() == snapshot('Expected an identifier at byte range 3..3') - assert exc_info.value.display('type-msg') == snapshot('SyntaxError: Expected an identifier at byte range 3..3') + assert exc_info.value.display() == snapshot("""\ +Traceback (most recent call last): + File "main.py", line 1 + def + ~ +SyntaxError: Expected an identifier\ +""") + assert exc_info.value.display('type-msg') == snapshot('SyntaxError: Expected an identifier') + assert exc_info.value.display('msg') == snapshot('Expected an identifier') def test_syntax_error_str(): @@ -419,7 +426,7 @@ def test_runtime_error_repr(): def test_syntax_error_repr(): with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info: pydantic_monty.Monty('def') - assert repr(exc_info.value) == snapshot('MontySyntaxError(Expected an identifier at byte range 3..3)') + assert repr(exc_info.value) == snapshot('MontySyntaxError(Expected an identifier)') def test_frame_repr(): diff --git a/crates/monty-python/tests/test_type_check.py b/crates/monty-python/tests/test_type_check.py index 9f9d9d37..317f6c57 100644 --- a/crates/monty-python/tests/test_type_check.py +++ b/crates/monty-python/tests/test_type_check.py @@ -47,7 +47,6 @@ def test_type_check_stubs_not_leaked_to_later_call(): 1 | result = call1_stub_var + 1 | ^^^^^^^^^^^^^^ | -info: rule `unresolved-reference` is enabled by default """) @@ -67,7 +66,6 @@ def test_type_check_with_errors(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -90,7 +88,6 @@ def foo() -> int: 3 | return "not an int" | ^^^^^^^^^^^^ expected `int`, found `Literal["not an int"]` | -info: rule `invalid-return-type` is enabled by default """) @@ -107,7 +104,6 @@ def test_type_check_undefined_variable(): 1 | print(undefined_var) | ^^^^^^^^^^^^^ | -info: rule `unresolved-reference` is enabled by default """) @@ -183,7 +179,6 @@ def test_monty_typing_error_repr(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default )\ """) @@ -231,7 +226,6 @@ def test_constructor_type_check_explicit_true(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -298,7 +292,6 @@ def test_constructor_type_check_stubs_invalid(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -351,7 +344,6 @@ async def agent(prompt: str, messages: Messages): 2 | while True: 3 | print(f'messages so far: {messages}') | -info: rule `unresolved-reference` is enabled by default """) @@ -381,7 +373,6 @@ async def agent(prompt: str, messages: Messages): | ^^^^^^^^ ------------------ Parameter declared here 6 | ... | -info: rule `invalid-argument-type` is enabled by default """) @@ -434,7 +425,6 @@ def test_repl_feed_run_type_check_enabled(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -502,7 +492,6 @@ def test_repl_type_check_line_numbers(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -526,7 +515,6 @@ def test_repl_type_check_line_numbers_multiline(): | | Has type `Literal[1]` | Has type `Literal["hi"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -569,7 +557,6 @@ def fetch(url: str) -> str: | ^^^^^ -------- Parameter declared here 2 | return '' | -info: rule `invalid-argument-type` is enabled by default """) @@ -589,7 +576,6 @@ def test_repl_type_check_accumulated_catches_type_mismatch(): | | | Declared type | -info: rule `invalid-assignment` is enabled by default """) @@ -660,7 +646,6 @@ def fetch(url: str) -> str: | ^^^^^ -------- Parameter declared here 2 | return '' | -info: rule `invalid-argument-type` is enabled by default """) @@ -709,7 +694,6 @@ def greet(name: str) -> str: | ^^^^^ --------- Parameter declared here 3 | return 'hello ' + name | -info: rule `invalid-argument-type` is enabled by default """) @@ -735,7 +719,6 @@ def get_count() -> int: | | | Declared type | -info: rule `invalid-assignment` is enabled by default """) @@ -794,7 +777,6 @@ def transform(x: str) -> str: | ^^^^^^^^^ ------ Parameter declared here 7 | return x + '!' | -info: rule `invalid-argument-type` is enabled by default """) @@ -894,7 +876,6 @@ def test_repl_type_check_script_name(): | | Has type `Literal[1]` | Has type `Literal["hello"]` | -info: rule `unsupported-operator` is enabled by default """) @@ -936,7 +917,6 @@ def foo(x: int) -> int: 1 | foo("x") | ^^^ | -info: rule `unresolved-reference` is enabled by default """) @@ -970,7 +950,6 @@ def fetch(x: int) -> int: | | | Declared type | -info: rule `invalid-assignment` is enabled by default """) @@ -1015,7 +994,6 @@ def foo(x: int) -> int: | ^^^ ------ Parameter declared here 5 | return x | -info: rule `invalid-argument-type` is enabled by default """) @@ -1053,7 +1031,6 @@ def fetch(url: str) -> str: | ^^^^^ -------- Parameter declared here 2 | return '' | -info: rule `invalid-argument-type` is enabled by default """) @@ -1088,7 +1065,6 @@ def test_repl_type_check_feed_run_skip_does_not_accumulate(): 1 | x + 1 | ^ | -info: rule `unresolved-reference` is enabled by default """) @@ -1108,7 +1084,6 @@ def test_repl_type_check_feed_start_skip_does_not_accumulate(): 1 | x + 1 | ^ | -info: rule `unresolved-reference` is enabled by default """) @@ -1166,7 +1141,6 @@ async def go(): 1 | x + 1 | ^ | -info: rule `unresolved-reference` is enabled by default """) @@ -1194,6 +1168,5 @@ def foo(x: int) -> int: 1 | foo("x") | ^^^ | -info: rule `unresolved-reference` is enabled by default """) diff --git a/crates/monty-type-checking/Cargo.toml b/crates/monty-type-checking/Cargo.toml index 24bd4fbd..9d0550d4 100644 --- a/crates/monty-type-checking/Cargo.toml +++ b/crates/monty-type-checking/Cargo.toml @@ -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 } diff --git a/crates/monty-type-checking/src/db.rs b/crates/monty-type-checking/src/db.rs index 00bb47bb..c5969207 100644 --- a/crates/monty-type-checking/src/db.rs +++ b/crates/monty-type-checking/src/db.rs @@ -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}, }; @@ -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`. +/// 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`. `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, files: Files, @@ -57,8 +65,8 @@ impl Default for MemoryDb { /// Create a fresh database wired up for type checking under `SRC_ROOT`. /// /// Registers `SRC_ROOT` as a Salsa-tracked project root and installs the - /// `Program` settings needed by `check_types`. Returning a db without this - /// setup would unwrap-panic the first time `check_types` is called, so this + /// `Program` settings needed by `check_file_unwrap`. Returning a db without this + /// setup would unwrap-panic the first time `check_file_unwrap` is called, so this /// constructor is the only sanctioned way to build a `MemoryDb`. fn default() -> Self { let src_root = SystemPathBuf::from(SRC_ROOT); @@ -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( @@ -123,10 +131,21 @@ 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 { + if self.should_check_file(file) { + check_file_unwrap(self, file) + } else { + Vec::new() + } + } fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection @@ -143,6 +162,10 @@ impl Db for MemoryDb { fn verbose(&self) -> bool { false } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } } #[salsa::db] diff --git a/crates/monty-type-checking/src/type_check.rs b/crates/monty-type-checking/src/type_check.rs index f6c59c36..c2e94c5e 100644 --- a/crates/monty-type-checking/src/type_check.rs +++ b/crates/monty-type-checking/src/type_check.rs @@ -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, @@ -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() { diff --git a/crates/monty-type-checking/tests/main.rs b/crates/monty-type-checking/tests/main.rs index f20e5c09..6797a540 100644 --- a/crates/monty-type-checking/tests/main.rs +++ b/crates/monty-type-checking/tests/main.rs @@ -44,7 +44,6 @@ result = add(1, '2') | ^^^ ------ Parameter declared here 2 | return x + y | - info: rule `invalid-argument-type` is enabled by default "#); } @@ -87,7 +86,6 @@ result = add(1, '2')"; | ^^^ ------ Parameter declared here 2 | return x + y | - info: rule `invalid-argument-type` is enabled by default "#); } @@ -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(), + @" + main.py:1:200: error[invalid-syntax] Source is too deeply nested + main.py:1:1002: error[invalid-syntax] Expected `)`, found end of file + " + ); +} diff --git a/crates/monty-type-checking/tests/snapshots/main__reveal_types.snap b/crates/monty-type-checking/tests/snapshots/main__reveal_types.snap index 2e88b772..612b53e7 100644 --- a/crates/monty-type-checking/tests/snapshots/main__reveal_types.snap +++ b/crates/monty-type-checking/tests/snapshots/main__reveal_types.snap @@ -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` diff --git a/crates/monty-type-checking/tests/snapshots/main__type_bad_types.snap b/crates/monty-type-checking/tests/snapshots/main__type_bad_types.snap index e54409ef..afb24a47 100644 --- a/crates/monty-type-checking/tests/snapshots/main__type_bad_types.snap +++ b/crates/monty-type-checking/tests/snapshots/main__type_bad_types.snap @@ -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"]` @@ -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 diff --git a/crates/monty/src/exception_public.rs b/crates/monty/src/exception_public.rs index 5e1c2887..fa3894ee 100644 --- a/crates/monty/src/exception_public.rs +++ b/crates/monty/src/exception_public.rs @@ -87,6 +87,22 @@ impl MontyException { } } + pub(crate) fn new_full(exc_type: ExcType, message: Option, traceback: Vec) -> Self { + Self { + exc_type, + message, + traceback, + } + } + + pub(crate) fn runtime_error(err: impl fmt::Display) -> Self { + Self { + exc_type: ExcType::RuntimeError, + message: Some(err.to_string()), + traceback: vec![], + } + } + /// The exception type raised. #[must_use] pub fn exc_type(&self) -> ExcType { @@ -143,22 +159,6 @@ impl MontyException { format!("{type_str}()") } } - - pub(crate) fn new_full(exc_type: ExcType, message: Option, traceback: Vec) -> Self { - Self { - exc_type, - message, - traceback, - } - } - - pub(crate) fn runtime_error(err: impl fmt::Display) -> Self { - Self { - exc_type: ExcType::RuntimeError, - message: Some(err.to_string()), - traceback: vec![], - } - } } /// Check if two stack frames are identical for the purpose of collapsing repeated frames. @@ -235,7 +235,10 @@ impl fmt::Display for StackFrame { 4 }; f.write_str(&" ".repeat(caret_start))?; - writeln!(f, "{}", "~".repeat((self.end.column - self.start.column) as usize))?; + // Always render at least one caret, even for zero-length ranges + // (e.g. a SyntaxError pointing just past the end of a truncated token). + let caret_len = (self.end.column - self.start.column).max(1) as usize; + writeln!(f, "{}", "~".repeat(caret_len))?; } } else { f.write_char('\n')?; diff --git a/crates/monty/src/parse.rs b/crates/monty/src/parse.rs index 88951de3..e4f1573d 100644 --- a/crates/monty/src/parse.rs +++ b/crates/monty/src/parse.rs @@ -146,7 +146,8 @@ pub(crate) fn parse_with_interner( interner: InternerBuilder, ) -> Result { let mut parser = Parser::new(code, filename, interner); - let parsed = parse_module(code).map_err(|e| ParseError::syntax(e.to_string(), parser.convert_range(e.range())))?; + let parsed = + parse_module(code).map_err(|e| ParseError::syntax(e.error.to_string(), parser.convert_range(e.range())))?; let module = parsed.into_syntax(); let nodes = parser.parse_statements(module.body)?; Ok(ParseResult { @@ -167,7 +168,7 @@ pub struct Parser<'a> { pub interner: InternerBuilder, /// Remaining nesting depth budget for recursive structures. /// Starts at MAX_NESTING_DEPTH and decrements on each nested level. - /// When it reaches zero, we return a "too many nested parentheses" error. + /// When it reaches zero, we return a "Source is too deeply nested" syntax error. depth_remaining: u16, } @@ -1556,7 +1557,7 @@ impl<'a> Parser<'a> { Ok(()) } else { let position = self.convert_range(get_range()); - Err(ParseError::syntax("too many nested parentheses", position)) + Err(ParseError::syntax("Source is too deeply nested", position)) } } } diff --git a/crates/monty/tests/parse_errors.rs b/crates/monty/tests/parse_errors.rs index ef575065..bf84acae 100644 --- a/crates/monty/tests/parse_errors.rs +++ b/crates/monty/tests/parse_errors.rs @@ -1,46 +1,33 @@ use std::fmt::Write; +use insta::assert_snapshot; use monty::{ExcType, MontyException, MontyRun}; -/// Helper to extract the exception type from a parse error. -fn get_exc_type(result: Result) -> ExcType { - let err = result.expect_err("expected parse error"); - err.exc_type() +/// Helper to extract the exception from a parse error. +fn get_parse_err(code: impl Into) -> MontyException { + let result = MontyRun::new(code.into(), "test.py", vec![]); + result.expect_err("expected parse error") } #[test] fn complex_numbers_return_not_implemented_error() { - let result = MontyRun::new("1 + 2j".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::NotImplementedError); -} - -#[test] -fn complex_numbers_have_descriptive_message() { - let result = MontyRun::new("1 + 2j".to_owned(), "test.py", vec![]); - let exc = result.expect_err("expected parse error"); - assert!( - exc.message().is_some_and(|m| m.contains("complex")), - "message should mention 'complex', got: {exc}" - ); + let err = get_parse_err("1 + 2j"); + assert_eq!(err.exc_type(), ExcType::NotImplementedError); + assert_snapshot!(err.message().unwrap(), @"The monty syntax parser does not yet support complex constants"); } #[test] fn yield_expressions_return_not_implemented_error() { - // Yield expressions are not supported and fail at parse time - let result = MontyRun::new("def foo():\n yield 1".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::NotImplementedError); - let result = MontyRun::new("def foo():\n yield 1".to_owned(), "test.py", vec![]); - let exc = result.expect_err("expected parse error"); - assert!( - exc.message().is_some_and(|m| m.contains("yield")), - "message should mention 'yield', got: {exc}" - ); + let err = get_parse_err("def foo():\n yield 1"); + assert_eq!(err.exc_type(), ExcType::NotImplementedError); + assert_snapshot!(err.message().unwrap(), @"The monty syntax parser does not yet support yield expressions"); } #[test] fn classes_return_not_implemented_error() { - let result = MontyRun::new("class Foo: pass".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::NotImplementedError); + let err = get_parse_err("class Foo: pass"); + assert_eq!(err.exc_type(), ExcType::NotImplementedError); + assert_snapshot!(err.message().unwrap(), @"The monty syntax parser does not yet support class definitions"); } #[test] @@ -54,60 +41,50 @@ fn unknown_imports_compile_successfully_error_deferred_to_runtime() { #[test] fn with_statement_returns_not_implemented_error() { - let result = MontyRun::new("with open('f') as f: pass".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::NotImplementedError); + let err = get_parse_err("with open('f') as f: pass"); + assert_eq!(err.exc_type(), ExcType::NotImplementedError); + assert_snapshot!(err.message().unwrap(), @"The monty syntax parser does not yet support context managers (with statements)"); } #[test] fn error_display_format() { // Verify the Display format matches Python's exception output with traceback - let result = MontyRun::new("1 + 2j".to_owned(), "test.py", vec![]); - let err = result.expect_err("expected parse error"); - let display = err.to_string(); - // Should start with traceback header - assert!( - display.starts_with("Traceback (most recent call last):"), - "display should start with 'Traceback': got: {display}" - ); - // Should contain the file/line info - assert!( - display.contains("File \"test.py\", line 1"), - "display should contain file location, got: {display}" - ); - // Should end with NotImplementedError message - assert!( - display.contains("NotImplementedError:"), - "display should contain 'NotImplementedError:', got: {display}" - ); - assert!( - display.contains("monty syntax parser"), - "display should mention 'monty syntax parser', got: {display}" - ); + let err = get_parse_err("1 + 2j"); + assert_snapshot!(err, @r#" + Traceback (most recent call last): + File "test.py", line 1, in + 1 + 2j + ~~ + NotImplementedError: The monty syntax parser does not yet support complex constants + "#); } /// Tests that syntax errors return `SyntaxError` exceptions. #[test] fn invalid_fstring_format_spec_returns_syntax_error() { - let result = MontyRun::new("f'{1:10xyz}'".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err("f'{1:10xyz}'"); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Invalid format specifier '10xyz'"); } #[test] fn invalid_fstring_format_spec_str_returns_syntax_error() { - let result = MontyRun::new("f'{\"hello\":abc}'".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err("f'{\"hello\":abc}'"); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Invalid format specifier 'abc'"); } #[test] fn syntax_error_display_format() { - let result = MontyRun::new("f'{1:10xyz}'".to_owned(), "test.py", vec![]); - let err = result.expect_err("expected parse error"); - let display = err.to_string(); - assert!( - display.contains("SyntaxError:"), - "display should contain 'SyntaxError:', got: {display}" - ); + let err = get_parse_err("f'{1:10xyz}'"); + assert_snapshot!(err, @r#" + Traceback (most recent call last): + File "test.py", line 1 + f'{1:10xyz}' + ~~~~~ + SyntaxError: Invalid format specifier '10xyz' + "#); } #[test] @@ -117,15 +94,9 @@ fn deeply_nested_tuples_exceed_limit() { for _ in 0..250 { code = format!("({code},)"); } - let result = MontyRun::new(code, "test.py", vec![]); - let err = result.expect_err("expected parse error"); + let err = get_parse_err(code); assert_eq!(err.exc_type(), ExcType::SyntaxError); - assert_eq!( - err.message(), - Some("too many nested parentheses"), - "error message should match CPython, got: {:?}", - err.message() - ); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -148,16 +119,9 @@ fn deeply_nested_unpack_assignment_exceeds_limit() { for _ in 0..250 { target = format!("({target},)"); } - let code = format!("{target} = (1,)"); - let result = MontyRun::new(code, "test.py", vec![]); - let err = result.expect_err("expected parse error"); + let err = get_parse_err(format!("{target} = (1,)")); assert_eq!(err.exc_type(), ExcType::SyntaxError); - assert_eq!( - err.message(), - Some("too many nested parentheses"), - "error message should match CPython, got: {:?}", - err.message() - ); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -167,8 +131,9 @@ fn deeply_nested_lists_exceed_limit() { for _ in 0..250 { code = format!("[{code}]"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -178,8 +143,9 @@ fn deeply_nested_dicts_exceed_limit() { for _ in 0..250 { code = format!("{{'a': {code}}}"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -189,9 +155,9 @@ fn deeply_nested_function_calls_exceed_limit() { for _ in 0..250 { code = format!("f({code})"); } - let code = format!("def f(x): return x\n{code}"); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(format!("def f(x): return x\n{code}")); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -201,8 +167,9 @@ fn deeply_nested_binary_ops_exceed_limit() { for _ in 0..250 { code = format!("({code} + 1)"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -212,8 +179,9 @@ fn deeply_nested_ternary_if_exceed_limit() { for _ in 0..250 { code = format!("(1 if {code} else 0)"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -223,9 +191,9 @@ fn deeply_nested_subscripts_exceed_limit() { for _ in 0..250 { code = format!("a[{code}]"); } - let code = format!("a = [1]\n{code}"); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(format!("a = [1]\n{code}")); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -235,8 +203,9 @@ fn deeply_nested_list_comprehension_exceed_limit() { for _ in 0..250 { code = format!("[x for x in {code}]"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -248,8 +217,9 @@ fn deeply_nested_if_statements_exceed_limit() { writeln!(code, "{indent}if 1:").unwrap(); } write!(code, "{}pass", " ".repeat(250)).unwrap(); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -261,8 +231,9 @@ fn deeply_nested_while_loops_exceed_limit() { writeln!(code, "{indent}while True:").unwrap(); } write!(code, "{}break", " ".repeat(250)).unwrap(); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -274,8 +245,9 @@ fn deeply_nested_for_loops_exceed_limit() { writeln!(code, "{indent}for x in [1]:").unwrap(); } write!(code, "{}pass", " ".repeat(250)).unwrap(); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -291,8 +263,9 @@ fn deeply_nested_try_except_exceed_limit() { let indent = " ".repeat(i); writeln!(code, "{indent}except: pass").unwrap(); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -304,8 +277,9 @@ fn deeply_nested_function_defs_exceed_limit() { writeln!(code, "{indent}def f():").unwrap(); } write!(code, "{}pass", " ".repeat(250)).unwrap(); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -315,8 +289,9 @@ fn deeply_nested_attribute_access_exceed_limit() { for _ in 0..250 { code.push_str(".x"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -326,8 +301,9 @@ fn deeply_nested_lambdas_exceed_limit() { for _ in 0..250 { code = format!("(lambda: {code})"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -337,8 +313,9 @@ fn deeply_nested_unary_not_exceed_limit() { for _ in 0..250 { code = format!("not ({code})"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -348,8 +325,9 @@ fn deeply_nested_unary_minus_exceed_limit() { for _ in 0..250 { code = format!("-({code})"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -359,8 +337,9 @@ fn deeply_nested_walrus_operator_exceed_limit() { for i in 0..250 { code = format!("(x{i} := {code})"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -371,9 +350,9 @@ fn deeply_nested_await_exceed_limit() { for _ in 0..250 { code = format!("await ({code})"); } - let code = format!("async def f():\n {code}"); - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(format!("async def f():\n {code}")); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -383,8 +362,9 @@ fn deeply_nested_boolean_and_exceed_limit() { for _ in 0..250 { code = format!("(True and {code})"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } #[test] @@ -394,50 +374,41 @@ fn deeply_nested_boolean_or_exceed_limit() { for _ in 0..250 { code = format!("(False or {code})"); } - let result = MontyRun::new(code, "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); + let err = get_parse_err(code); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"Source is too deeply nested"); } // === Runtime NotImplementedError tests === // These test that unimplemented features return proper errors instead of panicking. -/// Helper to run code and get the exception type from a runtime error. -fn run_and_get_exc_type(code: &str) -> ExcType { +/// Helper to run code and get the exception from a runtime error. +fn run_and_get_err(code: &str) -> MontyException { let runner = MontyRun::new(code.to_owned(), "test.py", vec![]).expect("should parse"); - let err = runner.run_no_limits(vec![]).expect_err("expected runtime error"); - err.exc_type() + runner.run_no_limits(vec![]).expect_err("expected runtime error") } #[test] fn matrix_multiplication_returns_not_implemented_error() { // The @ operator (matrix multiplication) is not supported at runtime - assert_eq!(run_and_get_exc_type("1 @ 2"), ExcType::NotImplementedError); + let err = run_and_get_err("1 @ 2"); + assert_eq!(err.exc_type(), ExcType::NotImplementedError); } #[test] fn matrix_multiplication_augmented_assignment_returns_syntax_error() { // The @= operator (augmented matrix multiplication) is not supported at compile time - let result = MontyRun::new("a = 1\na @= 2".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::SyntaxError); -} - -#[test] -fn matrix_multiplication_augmented_assignment_has_descriptive_message() { - // Verify the error message is helpful - let result = MontyRun::new("a = 1\na @= 2".to_owned(), "test.py", vec![]); - let exc = result.expect_err("expected compile error"); - assert!( - exc.message().is_some_and(|m| m.contains("@=")), - "message should mention '@=', got: {:?}", - exc.message() - ); + let err = get_parse_err("a = 1\na @= 2"); + assert_eq!(err.exc_type(), ExcType::SyntaxError); + assert_snapshot!(err.message().unwrap(), @"matrix multiplication augmented assignment (@=) is not yet supported"); } #[test] fn del_statement_returns_not_implemented_error() { // The del statement is not supported at parse time - let result = MontyRun::new("x = 1\ndel x".to_owned(), "test.py", vec![]); - assert_eq!(get_exc_type(result), ExcType::NotImplementedError); + let err = get_parse_err("x = 1\ndel x"); + assert_eq!(err.exc_type(), ExcType::NotImplementedError); + assert_snapshot!(err.message().unwrap(), @"The monty syntax parser does not yet support the 'del' statement"); } #[test] diff --git a/crates/monty/tests/security.rs b/crates/monty/tests/security.rs new file mode 100644 index 00000000..15fb92c1 --- /dev/null +++ b/crates/monty/tests/security.rs @@ -0,0 +1,35 @@ +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"); +} + +/// Ruff parses postfix attribute access iteratively, so `a.x.x.x...` is not caught +/// by ruff's recursion limit even though the resulting AST is deeply nested. +/// Monty's own AST walk must reject it to avoid a stack overflow when recursing +/// through the `Attribute` chain. +#[test] +fn deeply_nested_attribute_access_does_not_stack_overflow() { + let depth = 200; + let mut code = String::with_capacity(depth * 2 + 1); + code.push('a'); + for _ in 0..depth { + code.push_str(".x"); + } + let result = MontyRun::new(code, "test.py", vec![]); + let err = result.expect_err("expected parse error for deeply nested attribute access"); + assert_snapshot!(err.message().unwrap_or(""), @"Source is too deeply nested"); +}