From 4617c997bb7eb7834aed5e6a6c54af1d7a44f22e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 30 Aug 2022 21:42:21 -0400 Subject: [PATCH] Experiment with LibCST parsing --- Cargo.lock | 238 ++++++++++------ Cargo.toml | 1 + src/{visitor.rs => ast_visitor.rs} | 0 src/check_cst.rs | 437 +++++++++++++++++++++++++++++ src/cst_visitor.rs | 125 +++++++++ src/lib.rs | 4 +- src/linter.rs | 52 +--- 7 files changed, 731 insertions(+), 126 deletions(-) rename src/{visitor.rs => ast_visitor.rs} (100%) create mode 100644 src/check_cst.rs create mode 100644 src/cst_visitor.rs diff --git a/Cargo.lock b/Cargo.lock index 6976dd7c5d..a31250def0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,18 +24,24 @@ dependencies = [ [[package]] name = "android_system_properties" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] -name = "anyhow" -version = "1.0.60" +name = "annotate-snippets" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794e162a5eff65c72ef524dfe393eb923c354e350bb78b9c7383df13f3bc142" +checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7" + +[[package]] +name = "anyhow" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a" [[package]] name = "ascii-canvas" @@ -48,9 +54,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b31b87a3367ed04dbcbc252bce3f2a8172fef861d47177524c503c908dff2c6" +checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ "concurrent-queue", "event-listener", @@ -73,9 +79,9 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" +checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca" dependencies = [ "async-channel", "async-executor", @@ -83,16 +89,16 @@ dependencies = [ "async-lock", "blocking", "futures-lite", - "num_cpus", "once_cell", ] [[package]] name = "async-io" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" dependencies = [ + "autocfg", "concurrent-queue", "futures-lite", "libc", @@ -117,11 +123,12 @@ dependencies = [ [[package]] name = "async-process" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" dependencies = [ "async-io", + "autocfg", "blocking", "cfg-if 1.0.0", "event-listener", @@ -284,9 +291,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "byte-tools" @@ -348,10 +355,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "chrono" -version = "0.4.21" +name = "chic" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f725f340c3854e3cb3ab736dc21f0cca183303acea3b3ffec30f141503ac8eb" +checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c" +dependencies = [ + "annotate-snippets", +] + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ "iana-time-zone", "js-sys", @@ -364,9 +380,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.16" +version = "3.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" dependencies = [ "atty", "bitflags", @@ -381,9 +397,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.15" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -440,16 +456,6 @@ dependencies = [ "cache-padded", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -458,9 +464,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" dependencies = [ "libc", ] @@ -603,9 +609,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "ena" @@ -707,9 +713,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", @@ -722,9 +728,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", "futures-sink", @@ -732,15 +738,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" dependencies = [ "futures-core", "futures-task", @@ -749,9 +755,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-lite" @@ -770,9 +776,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", @@ -781,21 +787,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-channel", "futures-core", @@ -903,13 +909,14 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.41" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1779539f58004e5dba1c1f093d44325ebeb244bfc04b791acdc0aaeca9c04570" +checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" dependencies = [ "android_system_properties", - "core-foundation", + "core-foundation-sys", "js-sys", + "once_cell", "wasm-bindgen", "winapi 0.3.9", ] @@ -1051,15 +1058,37 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.127" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "libcst" +version = "0.1.0" +dependencies = [ + "chic", + "itertools", + "libcst_derive", + "once_cell", + "paste", + "peg", + "regex", + "thiserror", +] + +[[package]] +name = "libcst_derive" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ "autocfg", "scopeguard", @@ -1092,9 +1121,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a79b39c93a7a5a27eeaf9a23b5ff43f1b9e0ad6b1cdd441140ae53c35613fc7" +checksum = "95af15f345b17af2efc8ead6080fb8bc376f8cec1b35277b935637595fe77498" dependencies = [ "libc", ] @@ -1260,9 +1289,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "opaque-debug" @@ -1278,9 +1307,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "os_str_bytes" -version = "6.2.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "parking" @@ -1311,6 +1340,39 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "paste" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" + +[[package]] +name = "peg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af728fe826811af3b38c37e93de6d104485953ea373d656eebae53d6987fcd2c" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4536be147b770b824895cbad934fccce8e49f14b4c4946eaa46a6e4a12fcdc16" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f" + [[package]] name = "petgraph" version = "0.6.2" @@ -1417,10 +1479,11 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "polling" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" dependencies = [ + "autocfg", "cfg-if 1.0.0", "libc", "log", @@ -1655,6 +1718,7 @@ dependencies = [ "fern", "filetime", "glob", + "libcst", "log", "notify", "once_cell", @@ -1746,18 +1810,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.143" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.143" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -1766,9 +1830,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -1867,9 +1931,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi 0.3.9", @@ -1981,18 +2045,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" dependencies = [ "proc-macro2", "quote", @@ -2110,9 +2174,9 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "unicode_names2" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec8e807a365e5c972debc47b8f06d361b37b94cfd18d48f7adc715fb86404dd" +checksum = "029df4cc8238cefc911704ff8fa210853a0f3bce2694d8f51181dd41ee0f3301" [[package]] name = "value-bag" @@ -2252,13 +2316,13 @@ dependencies = [ [[package]] name = "which" -version = "4.2.5" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b2649ca214..d40a01f410 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" name = "ruff" [dependencies] +libcst = { path = "../LibCST/native/libcst" } anyhow = { version = "1.0.60" } bincode = { version = "1.3.3" } cacache = { version = "10.0.1" } diff --git a/src/visitor.rs b/src/ast_visitor.rs similarity index 100% rename from src/visitor.rs rename to src/ast_visitor.rs diff --git a/src/check_cst.rs b/src/check_cst.rs new file mode 100644 index 0000000000..d556f3c9ff --- /dev/null +++ b/src/check_cst.rs @@ -0,0 +1,437 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::format; + +use rustpython_parser::ast::{ + Arg, Arguments, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind, Suite, +}; + +use crate::ast_visitor; +use crate::ast_visitor::ASTVisitor; +use crate::check_cst::ScopeKind::{Class, Function, Generator, Module}; +use crate::checks::{Check, CheckKind}; +use crate::settings::Settings; + +enum ScopeKind { + Class, + Function, + Generator, + Module, +} + +struct Scope { + kind: ScopeKind, + values: BTreeMap, +} + +enum BindingKind { + Argument, + Assignment, + ClassDefinition, + Definition, + FutureImportation, + Importation, + StarImportation, + SubmoduleImportation, +} + +struct Binding { + kind: BindingKind, + name: String, + location: Location, + used: bool, +} + +struct Checker<'a> { + settings: &'a Settings, + checks: Vec, + scopes: Vec, + dead_scopes: Vec, + in_f_string: bool, +} + +impl Checker<'_> { + pub fn new(settings: &Settings) -> Checker { + Checker { + settings, + checks: vec![], + scopes: vec![], + dead_scopes: vec![], + in_f_string: false, + } + } +} + +impl ASTVisitor for Checker<'_> { + fn visit_stmt(&mut self, stmt: &Stmt) { + match &stmt.node { + StmtKind::FunctionDef { name, .. } => { + self.push_scope(Scope { + kind: Function, + values: BTreeMap::new(), + }); + self.add_binding(Binding { + kind: BindingKind::ClassDefinition, + name: name.clone(), + used: false, + location: stmt.location, + }) + } + StmtKind::AsyncFunctionDef { name, .. } => { + self.push_scope(Scope { + kind: Function, + values: BTreeMap::new(), + }); + self.add_binding(Binding { + kind: BindingKind::ClassDefinition, + name: name.clone(), + used: false, + location: stmt.location, + }) + } + StmtKind::Return { .. } => { + if self + .settings + .select + .contains(CheckKind::ReturnOutsideFunction.code()) + { + if let Some(scope) = self.scopes.last() { + match scope.kind { + Class | Module => { + self.checks.push(Check { + kind: CheckKind::ReturnOutsideFunction, + location: stmt.location, + }); + } + _ => {} + } + } + } + } + StmtKind::ClassDef { .. } => self.push_scope(Scope { + kind: Class, + values: BTreeMap::new(), + }), + StmtKind::Import { names } => { + for alias in names { + if alias.node.name.contains('.') && alias.node.asname.is_none() { + self.add_binding(Binding { + kind: BindingKind::SubmoduleImportation, + name: alias.node.name.clone(), + used: false, + location: stmt.location, + }) + } else { + self.add_binding(Binding { + kind: BindingKind::Importation, + name: alias + .node + .asname + .clone() + .unwrap_or_else(|| alias.node.name.clone()), + used: false, + location: stmt.location, + }) + } + } + } + StmtKind::ImportFrom { names, module, .. } => { + for alias in names { + let name = alias + .node + .asname + .clone() + .unwrap_or_else(|| alias.node.name.clone()); + if module + .clone() + .map(|name| name == "future") + .unwrap_or_default() + { + self.add_binding(Binding { + kind: BindingKind::FutureImportation, + name, + used: true, + location: stmt.location, + }); + } else if alias.node.name == "*" { + self.add_binding(Binding { + kind: BindingKind::StarImportation, + name, + used: false, + location: stmt.location, + }); + + if self + .settings + .select + .contains(CheckKind::ImportStarUsage.code()) + { + self.checks.push(Check { + kind: CheckKind::ImportStarUsage, + location: stmt.location, + }); + } + } else { + self.add_binding(Binding { + kind: BindingKind::Importation, + name: match module { + None => name, + Some(parent) => format!("{}.{}", parent, name), + }, + used: false, + location: stmt.location, + }) + } + } + } + StmtKind::If { test, .. } => { + if self.settings.select.contains(CheckKind::IfTuple.code()) { + if let ExprKind::Tuple { .. } = test.node { + self.checks.push(Check { + kind: CheckKind::IfTuple, + location: stmt.location, + }); + } + } + } + StmtKind::Raise { exc, .. } => { + if self + .settings + .select + .contains(CheckKind::RaiseNotImplemented.code()) + { + if let Some(expr) = exc { + match &expr.node { + ExprKind::Call { func, .. } => { + if let ExprKind::Name { id, .. } = &func.node { + if id == "NotImplemented" { + self.checks.push(Check { + kind: CheckKind::RaiseNotImplemented, + location: stmt.location, + }); + } + } + } + ExprKind::Name { id, .. } => { + if id == "NotImplemented" { + self.checks.push(Check { + kind: CheckKind::RaiseNotImplemented, + location: stmt.location, + }); + } + } + _ => {} + } + } + } + } + StmtKind::AugAssign { target, .. } => { + self.handle_node_load(target); + } + _ => {} + } + + ast_visitor::walk_stmt(self, stmt); + + match &stmt.node { + StmtKind::ClassDef { .. } + | StmtKind::FunctionDef { .. } + | StmtKind::AsyncFunctionDef { .. } => { + self.pop_scope(); + } + _ => {} + }; + + if let StmtKind::ClassDef { name, .. } = &stmt.node { + self.add_binding(Binding { + kind: BindingKind::Definition, + name: name.clone(), + used: false, + location: stmt.location, + }); + } + } + + fn visit_expr(&mut self, expr: &Expr) { + let initial = self.in_f_string; + match &expr.node { + ExprKind::Name { ctx, .. } => match ctx { + ExprContext::Load => self.handle_node_load(expr), + ExprContext::Store => self.handle_node_store(expr), + ExprContext::Del => {} + }, + ExprKind::GeneratorExp { .. } => self.push_scope(Scope { + kind: Generator, + values: BTreeMap::new(), + }), + ExprKind::Lambda { .. } => self.push_scope(Scope { + kind: Function, + values: BTreeMap::new(), + }), + ExprKind::JoinedStr { values } => { + if !self.in_f_string + && self + .settings + .select + .contains(CheckKind::FStringMissingPlaceholders.code()) + && !values + .iter() + .any(|value| matches!(value.node, ExprKind::FormattedValue { .. })) + { + self.checks.push(Check { + kind: CheckKind::FStringMissingPlaceholders, + location: expr.location, + }); + } + self.in_f_string = true; + } + _ => {} + }; + + ast_visitor::walk_expr(self, expr); + + match &expr.node { + ExprKind::GeneratorExp { .. } | ExprKind::Lambda { .. } => { + if let Some(scope) = self.scopes.pop() { + self.dead_scopes.push(scope); + } + } + ExprKind::JoinedStr { .. } => { + self.in_f_string = initial; + } + _ => {} + }; + } + + fn visit_arguments(&mut self, arguments: &Arguments) { + if self + .settings + .select + .contains(CheckKind::DuplicateArgumentName.code()) + { + // Collect all the arguments into a single vector. + let mut all_arguments: Vec<&Arg> = arguments + .args + .iter() + .chain(arguments.posonlyargs.iter()) + .chain(arguments.kwonlyargs.iter()) + .collect(); + if let Some(arg) = &arguments.vararg { + all_arguments.push(arg); + } + if let Some(arg) = &arguments.kwarg { + all_arguments.push(arg); + } + + // Search for duplicates. + let mut idents: BTreeSet = BTreeSet::new(); + for arg in all_arguments { + let ident = &arg.node.arg; + if idents.contains(ident) { + self.checks.push(Check { + kind: CheckKind::DuplicateArgumentName, + location: arg.location, + }); + break; + } + idents.insert(ident.clone()); + } + } + + ast_visitor::walk_arguments(self, arguments); + } + + fn visit_arg(&mut self, arg: &Arg) { + self.add_binding(Binding { + kind: BindingKind::Argument, + name: arg.node.arg.clone(), + used: false, + location: arg.location, + }); + ast_visitor::walk_arg(self, arg); + } +} + +impl Checker<'_> { + fn push_scope(&mut self, scope: Scope) { + self.scopes.push(scope); + } + + fn pop_scope(&mut self) { + self.dead_scopes + .push(self.scopes.pop().expect("Attempted to pop without scope.")); + } + + fn add_binding(&mut self, binding: Binding) { + // TODO(charlie): Don't treat annotations as assignments if there is an existing value. + let scope = self.scopes.last_mut().expect("No current scope found."); + scope.values.insert( + binding.name.clone(), + match scope.values.get(&binding.name) { + None => binding, + Some(existing) => Binding { + kind: binding.kind, + name: binding.name, + location: binding.location, + used: existing.used, + }, + }, + ); + } + + fn handle_node_load(&mut self, expr: &Expr) { + if let ExprKind::Name { id, .. } = &expr.node { + for scope in self.scopes.iter_mut().rev() { + if matches!(scope.kind, Class) { + if id == "__class__" { + return; + } else { + continue; + } + } + if let Some(binding) = scope.values.get_mut(id) { + binding.used = true; + } + } + } + } + + fn handle_node_store(&mut self, expr: &Expr) { + if let ExprKind::Name { id, .. } = &expr.node { + // TODO(charlie): Handle alternate binding types (like `Annotation`). + self.add_binding(Binding { + kind: BindingKind::Assignment, + name: id.to_string(), + used: false, + location: expr.location, + }); + } + } + + fn check_dead_scopes(&mut self) { + // TODO(charlie): Handle `__all__`. + for scope in &self.dead_scopes { + for (name, binding) in scope.values.iter().rev() { + if !binding.used && matches!(binding.kind, BindingKind::Importation) { + self.checks.push(Check { + kind: CheckKind::UnusedImport(name.clone()), + location: binding.location, + }); + } + } + } + } +} + +pub fn check_ast(python_ast: &Suite, settings: &Settings) -> Vec { + let mut checker = Checker::new(settings); + checker.push_scope(Scope { + kind: Module, + values: BTreeMap::new(), + }); + for stmt in python_ast { + checker.visit_stmt(stmt); + } + checker.pop_scope(); + checker.check_dead_scopes(); + checker.checks +} diff --git a/src/cst_visitor.rs b/src/cst_visitor.rs new file mode 100644 index 0000000000..32555d74c7 --- /dev/null +++ b/src/cst_visitor.rs @@ -0,0 +1,125 @@ +use libcst_native::{CompoundStatement, Expression, SimpleStatementLine, Statement}; +use libcst_native::{Expr, SmallStatement}; + +pub trait CSTVisitor { + fn visit_statement(&mut self, statement: &Statement) { + walk_statement(self, statement); + } + fn visit_simple_statement_line(&mut self, simple_statement_line: &SimpleStatementLine) { + walk_simple_statement_line(self, simple_statement_line); + } + fn visit_compound_statement(&mut self, compound_statement: &CompoundStatement) { + walk_compound_statement(self, compound_statement); + } + fn visit_small_statement(&mut self, small_statement: &SmallStatement) { + walk_small_statement(self, small_statement); + } + fn visit_expression(&mut self, expression: &Expression) { + walk_expression(self, expression); + } +} + +pub fn walk_statement(visitor: &mut V, statement: &Statement) { + match statement { + Statement::Simple(simple_statement_line) => { + visitor.visit_simple_statement_line(simple_statement_line) + } + Statement::Compound(compound_statement) => { + visitor.visit_compound_statement(compound_statement) + } + } +} + +pub fn walk_simple_statement_line( + visitor: &mut V, + simple_statement_line: &SimpleStatementLine, +) { + for small_statement in &simple_statement_line.body { + visitor.visit_small_statement(small_statement); + } +} + +pub fn walk_compound_statement( + visitor: &mut V, + compound_statement: &CompoundStatement, +) { + match compound_statement { + CompoundStatement::FunctionDef(_) => {} + CompoundStatement::If(_) => {} + CompoundStatement::For(_) => {} + CompoundStatement::While(_) => {} + CompoundStatement::ClassDef(_) => {} + CompoundStatement::Try(_) => {} + CompoundStatement::TryStar(_) => {} + CompoundStatement::With(_) => {} + CompoundStatement::Match(_) => {} + } +} + +pub fn walk_small_statement( + visitor: &mut V, + small_statement: &SmallStatement, +) { + match small_statement { + SmallStatement::Pass(_) => {} + SmallStatement::Break(_) => {} + SmallStatement::Continue(_) => {} + SmallStatement::Return(inner) => { + if let Some(expression) = &inner.value { + visitor.visit_expression(expression); + } + } + SmallStatement::Expr(inner) => { + visitor.visit_expression(&inner.value); + } + SmallStatement::Assert(inner) => { + visitor.visit_expression(&inner.test); + if let Some(expression) = &inner.msg { + visitor.visit_expression(expression); + } + } + SmallStatement::Import(_) => {} + SmallStatement::ImportFrom(_) => {} + SmallStatement::Assign(_) => {} + SmallStatement::AnnAssign(_) => {} + SmallStatement::Raise(_) => {} + SmallStatement::Global(_) => {} + SmallStatement::Nonlocal(_) => {} + SmallStatement::AugAssign(_) => {} + SmallStatement::Del(_) => {} + } +} + +pub fn walk_expression(visitor: &mut V, expression: &Expression) { + match expression { + Expression::Name(_) => {} + Expression::Ellipsis(_) => {} + Expression::Integer(_) => {} + Expression::Float(_) => {} + Expression::Imaginary(_) => {} + Expression::Comparison(_) => {} + Expression::UnaryOperation(_) => {} + Expression::BinaryOperation(_) => {} + Expression::BooleanOperation(_) => {} + Expression::Attribute(_) => {} + Expression::Tuple(_) => {} + Expression::Call(_) => {} + Expression::GeneratorExp(_) => {} + Expression::ListComp(_) => {} + Expression::SetComp(_) => {} + Expression::DictComp(_) => {} + Expression::List(_) => {} + Expression::Set(_) => {} + Expression::Dict(_) => {} + Expression::Subscript(_) => {} + Expression::StarredElement(_) => {} + Expression::IfExp(_) => {} + Expression::Lambda(_) => {} + Expression::Yield(_) => {} + Expression::Await(_) => {} + Expression::SimpleString(_) => {} + Expression::ConcatenatedString(_) => {} + Expression::FormattedString(_) => {} + Expression::NamedExpr(_) => {} + } +} diff --git a/src/lib.rs b/src/lib.rs index bfb1a8b0d6..f5ffc88c14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,15 @@ mod ast_ops; +mod ast_visitor; mod builtins; mod cache; pub mod check_ast; +mod check_cst; mod check_lines; pub mod checks; +mod cst_visitor; pub mod fs; pub mod linter; pub mod logging; pub mod message; mod pyproject; pub mod settings; -mod visitor; diff --git a/src/linter.rs b/src/linter.rs index a1e9bef9c5..7b61c40210 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -1,55 +1,31 @@ use std::path::Path; use anyhow::Result; -use log::debug; -use rustpython_parser::parser; +use libcst_native::{parse_module, Codegen}; -use crate::check_ast::check_ast; -use crate::check_lines::check_lines; -use crate::checks::{Check, LintSource}; use crate::message::Message; use crate::settings::Settings; use crate::{cache, fs}; pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Result> { - // Check the cache. - if let Some(messages) = cache::get(path, settings, mode) { - debug!("Cache hit for: {}", path.to_string_lossy()); - return Ok(messages); - } - // Read the file from disk. - let contents = fs::read_file(path)?; + let contents = fs::read_file(path).unwrap(); - // Aggregate all checks. - let mut checks: Vec = vec![]; + // Parse the module. + let mut m = match parse_module(&contents, None) { + Ok(m) => m, + Err(e) => panic!("foo"), + }; - // Run the AST-based checks. - if settings - .select - .iter() - .any(|check_code| matches!(check_code.lint_source(), LintSource::AST)) - { - let path = path.to_string_lossy(); - let python_ast = parser::parse_program(&contents, &path)?; - checks.extend(check_ast(&python_ast, settings, &path)); - } + // Remove the first statement. + m.body.remove(0); - // Run the lines-based checks. - check_lines(&mut checks, &contents, settings); + let mut state = Default::default(); + m.codegen(&mut state); + let generated = state.to_string(); + println!("{}", generated); - // Convert to messages. - let messages: Vec = checks - .into_iter() - .map(|check| Message { - kind: check.kind, - location: check.location, - filename: path.to_string_lossy().to_string(), - }) - .collect(); - cache::set(path, settings, &messages, mode); - - Ok(messages) + Ok(vec![]) } #[cfg(test)]