diff --git a/.github/renovate.json b/.github/renovate.json index a86816298edf8..5af0c3b7ed3aa 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,7 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>Boshen/renovate", "helpers:pinGitHubActionDigests"], - "ignorePaths": [ - "crates/oxc_linter/fixtures/**" - ] + "ignorePaths": ["crates/oxc_linter/fixtures/**"], + "ignoreDeps": ["@types/vscode"] } diff --git a/Cargo.lock b/Cargo.lock index cab6130bbc748..02afbcce22142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "assert-unchecked" version = "0.1.2" @@ -174,6 +180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", + "regex-automata", "serde", ] @@ -287,15 +294,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width 0.1.14", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] @@ -482,9 +489,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" @@ -515,9 +522,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "env_filter", "humantime", @@ -542,9 +549,12 @@ dependencies = [ [[package]] name = "fast-glob" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb10ed0f8a3dca52477be37ac0fb8f9d1fd4cd8d311b4484bdd45c1c56e0c9ec" +checksum = "0eca69ef247d19faa15ac0156968637440824e5ff22baa5ee0cd35b2f7ea6a0f" +dependencies = [ + "arrayvec", +] [[package]] name = "fastrand" @@ -701,9 +711,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "globset" @@ -753,7 +763,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1082,7 +1092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1404,9 +1414,9 @@ dependencies = [ [[package]] name = "oxc-browserslist" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d240f6572a29895f324ad834a42a0c4bad0739269a08d0181ad7d6db7537e0d1" +checksum = "1b585351ba1b93f0bf9683df23285cc0978ec35a6e3074794815756086770c14" dependencies = [ "js-sys", "nom", @@ -1414,7 +1424,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", - "thiserror", + "thiserror 2.0.9", "time", "wasm-bindgen", ] @@ -1429,7 +1439,7 @@ dependencies = [ "owo-colors", "oxc-miette-derive", "textwrap", - "thiserror", + "thiserror 1.0.69", "unicode-width 0.2.0", ] @@ -1683,7 +1693,7 @@ dependencies = [ [[package]] name = "oxc_linter" -version = "0.15.3" +version = "0.15.4" dependencies = [ "bitflags 2.6.0", "convert_case", @@ -1787,6 +1797,7 @@ dependencies = [ name = "oxc_minsize" version = "0.0.0" dependencies = [ + "cow-utils", "flate2", "humansize", "oxc_allocator", @@ -1798,6 +1809,7 @@ dependencies = [ "oxc_tasks_common", "oxc_transformer", "rustc-hash", + "similar-asserts", ] [[package]] @@ -1907,9 +1919,9 @@ dependencies = [ [[package]] name = "oxc_resolver" -version = "3.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f7b4604348e24a53f114dabe3001306ca720cb83d007b1140afc26c1fdfa7b" +checksum = "bed381b6ab4bbfebfc7a011ad43b110ace8d201d02a39c0e09855f16b8f3f741" dependencies = [ "cfg-if", "dashmap 6.1.0", @@ -1920,7 +1932,7 @@ dependencies = [ "serde", "serde_json", "simdutf8", - "thiserror", + "thiserror 1.0.69", "tracing", ] @@ -1950,9 +1962,9 @@ dependencies = [ [[package]] name = "oxc_sourcemap" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d811a7c2879984b0fbb70ddbf0818f80247dccfcb5014be997019683aa5dfc" +checksum = "48557f779d04c8bfa8a930db5a3c35c0a86ff4e6bf1552ce446b9596a6e77c42" dependencies = [ "base64-simd", "cfg-if", @@ -1984,6 +1996,7 @@ version = "0.44.0" dependencies = [ "assert-unchecked", "bitflags 2.6.0", + "cow-utils", "nonmax", "oxc_allocator", "oxc_ast_macros", @@ -2115,7 +2128,7 @@ dependencies = [ [[package]] name = "oxlint" -version = "0.15.3" +version = "0.15.4" dependencies = [ "bpaf", "glob", @@ -2167,7 +2180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] @@ -2328,9 +2341,9 @@ checksum = "8bccbff07d5ed689c4087d20d7307a52ab6141edeedf487c3876a55b86cf63df" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2617,9 +2630,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -2637,9 +2650,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2670,9 +2683,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "indexmap", "itoa", @@ -2749,6 +2762,20 @@ name = "similar" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe85670573cd6f0fa97940f26e7e6601213c3b0555246c24234131f88c5709e" +dependencies = [ + "console", + "similar", +] [[package]] name = "siphasher" @@ -2840,9 +2867,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", @@ -2890,7 +2917,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", ] [[package]] @@ -2904,6 +2940,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" diff --git a/Cargo.toml b/Cargo.toml index 1bd59663b3e21..1eff0ff6634e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,6 +189,7 @@ serde-wasm-bindgen = "0.6.5" sha1 = "0.10.6" simdutf8 = { version = "0.1.5", features = ["aarch64_neon"] } similar = "2.6.0" +similar-asserts = "1.6.0" string_wizard = "0.0.25" tempfile = "3.14.0" tokio = "1.42.0" diff --git a/apps/oxlint/CHANGELOG.md b/apps/oxlint/CHANGELOG.md index f54162f99ba5b..1088d6aa57e42 100644 --- a/apps/oxlint/CHANGELOG.md +++ b/apps/oxlint/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) until v1.0.0. +## [0.15.4] - 2024-12-30 + +### Bug Fixes + +- f3050d4 linter: Exclude svelte files from `no_unused_vars` rule (#8170) (Yuichiro Yamashita) + +### Refactor + +- 6da0b21 oxlint: Remove unused `git.rs` (#7990) (Boshen) +- 58e7777 oxlint: Remove extra if check in `Walkdir` (#7989) (Boshen) + ## [0.15.3] - 2024-12-17 ### Styling diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml index c4948db0cbba0..6f3611b7ac023 100644 --- a/apps/oxlint/Cargo.toml +++ b/apps/oxlint/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxlint" -version = "0.15.3" +version = "0.15.4" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index ffe842a861374..0e1b55444292e 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -628,7 +628,7 @@ mod test { let args = &["fixtures/svelte/debugger.svelte"]; let result = test(args); assert_eq!(result.number_of_files, 1); - assert_eq!(result.number_of_warnings, 2); + assert_eq!(result.number_of_warnings, 1); assert_eq!(result.number_of_errors, 0); } diff --git a/crates/oxc_codegen/src/lib.rs b/crates/oxc_codegen/src/lib.rs index 5fb02ce4306b6..9c15e0f9fea66 100644 --- a/crates/oxc_codegen/src/lib.rs +++ b/crates/oxc_codegen/src/lib.rs @@ -589,9 +589,9 @@ impl<'a> Codegen<'a> { fn print_quoted_utf16(&mut self, s: &str, allow_backtick: bool) { let quote = if self.options.minify { - let mut single_cost: u32 = 0; - let mut double_cost: u32 = 0; - let mut backtick_cost: u32 = 0; + let mut single_cost: i32 = 0; + let mut double_cost: i32 = 0; + let mut backtick_cost: i32 = 0; let mut bytes = s.as_bytes().iter().peekable(); while let Some(b) = bytes.next() { match b { @@ -642,7 +642,13 @@ impl<'a> Codegen<'a> { '\u{8}' => self.print_str("\\b"), // \b '\u{b}' => self.print_str("\\v"), // \v '\u{c}' => self.print_str("\\f"), // \f - '\n' => self.print_str("\\n"), + '\n' => { + if quote == b'`' { + self.print_ascii_byte(b'\n'); + } else { + self.print_str("\\n"); + } + } '\r' => self.print_str("\\r"), '\x1B' => self.print_str("\\x1B"), '\\' => self.print_str("\\\\"), diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index a05ef1f408bf4..007e41c80da27 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -39,11 +39,6 @@ fn expr() { test("delete 2e308", "delete (0, Infinity);\n"); test_minify("delete 2e308", "delete(1/0);"); - test_minify( - r#";'eval("\'\\vstr\\ving\\v\'") === "\\vstr\\ving\\v"'"#, - r#";`eval("'\\vstr\\ving\\v'") === "\\vstr\\ving\\v"`;"#, - ); - test_minify_same(r#"({"http://a\r\" \n<'b:b@c\r\nd/e?f":{}});"#); } @@ -438,3 +433,12 @@ fn getter_setter() { test_minify("({ get [foo]() {} })", "({get[foo](){}});"); test_minify("({ set [foo]() {} })", "({set[foo](){}});"); } + +#[test] +fn string() { + test_minify( + r#";'eval("\'\\vstr\\ving\\v\'") === "\\vstr\\ving\\v"'"#, + r#";`eval("'\\vstr\\ving\\v'") === "\\vstr\\ving\\v"`;"#, + ); + test_minify(r#"foo("\n")"#, "foo(`\n`);"); +} diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index ab47066c7de25..79722b5aedbf9 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, cmp::Ordering}; use num_bigint::BigInt; -use num_traits::Zero; +use num_traits::{ToPrimitive, Zero}; use oxc_ast::ast::*; @@ -193,6 +193,7 @@ pub trait ConstantEvaluation<'a> { Expression::StringLiteral(lit) => { Some(ConstantValue::String(Cow::Borrowed(lit.value.as_str()))) } + Expression::StaticMemberExpression(e) => self.eval_static_member_expression(e), _ => None, } } @@ -458,6 +459,31 @@ pub trait ConstantEvaluation<'a> { } } + fn eval_static_member_expression( + &self, + expr: &StaticMemberExpression<'a>, + ) -> Option> { + match expr.property.name.as_str() { + "length" => { + if let Some(ConstantValue::String(s)) = self.eval_expression(&expr.object) { + // TODO(perf): no need to actually convert, only need the length + Some(ConstantValue::Number(s.encode_utf16().count().to_f64().unwrap())) + } else { + if expr.object.may_have_side_effects() { + return None; + } + + if let Expression::ArrayExpression(arr) = &expr.object { + Some(ConstantValue::Number(arr.elements.len().to_f64().unwrap())) + } else { + None + } + } + } + _ => None, + } + } + /// fn is_less_than( &self, diff --git a/crates/oxc_linter/CHANGELOG.md b/crates/oxc_linter/CHANGELOG.md index c8b188a8d0535..9beb0ea8a295d 100644 --- a/crates/oxc_linter/CHANGELOG.md +++ b/crates/oxc_linter/CHANGELOG.md @@ -4,6 +4,48 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) until v1.0.0. +## [0.15.4] - 2024-12-30 + +- ed75e42 semantic: [**BREAKING**] Make SymbolTable fields `pub(crate)` instead of `pub` (#7999) (Boshen) + +### Features + +- 47cea9a linter: Implement `eslint/no-extra-label` (#8181) (Anson Heung) +- ef76e28 linter: Implement `eslint/no-multi-assign` (#8158) (Anson Heung) +- 384858b linter: Implement `jsx-a11y/no-noninteractive-tabindex` (#8167) (Tyler Earls) +- afc21a6 linter: Implement `eslint/vars-on-top` (#8157) (Yuichiro Yamashita) +- 65796c4 linter: Implement `eslint/prefer-rest-params` (#8155) (Yuichiro Yamashita) +- 5234d96 linter: Implement `eslint/no-nested-ternary` (#8150) (Yuichiro Yamashita) +- 1c5db72 linter: Implement eslint/no-labels (#8131) (Anson Heung) +- 0b04288 linter: Move `import/named` to nursery (#8068) (Boshen) + +### Bug Fixes + +- f3050d4 linter: Exclude svelte files from `no_unused_vars` rule (#8170) (Yuichiro Yamashita) +- faf7464 linter: Disable rule `react/rules-of-hook` by file extension (#8168) (Alexander S.) +- 1171e00 linter: Disable `react/rules-of-hooks` for vue and svelte files (#8165) (Alexander S.) +- 1b9a5ba linter: False positiver in private member expr in oxc/const-comparison (#8164) (camc314) +- 6bd9ddb linter: False positive in `typescript/ban-tslint-comment` (#8094) (dalaoshu) +- 10a1fd5 linter: Rule: `no-restricted-imports` support option `patterns` with `group` key (#8050) (Alexander S.) +- b3f38ae linter: Rule `no-restricted-imports`: support option `allowImportNames` (#8002) (Alexander S.) +- 340cc90 linter: Rule `no-restricted-imports`: fix option "importNames" (#7943) (Alexander S.) +- ec2128e linter: Fix line calculation for `eslint/max-lines` in diagnostics (#7962) (Dmitry Zakharov) +- 79af100 semantic: Reference flags not correctly resolved when after an export stmt (#8134) (camc314) + +### Performance + +- d8d2ec6 linter: Run rules which require typescript syntax only when source type is actually typescript (#8166) (Alexander S.) +- 2736657 semantic: Allocate `UnresolvedReferences` in allocator (#8046) (Boshen) + +### Refactor + +- 774babb linter: Read `exported_bindings_from_star_export` lazily (#8062) (Boshen) +- 547c102 linter: Use `RwLock` instead of `FxDashMap` for module record data (#8061) (Boshen) +- 952d7e4 linter: Rename `flat.rs` to `config.rs` (#8033) (camc314) +- 50848ed linter: Simplify `ConfigStore` to prep for nested configs (#8032) (camc314) +- b2a4a78 linter: Remove unused `with_rules` and `set_rule` methods (#8029) (camc314) +- 02f968d semantic: Change `Bindings` to a plain `FxHashMap` (#8019) (Boshen) + ## [0.15.3] - 2024-12-17 ### Features diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 10105e422f152..5a9afa00ad115 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_linter" -version = "0.15.3" +version = "0.15.4" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_linter/src/config/rules.rs b/crates/oxc_linter/src/config/rules.rs index e661f876f9fe9..251026b5f088c 100644 --- a/crates/oxc_linter/src/config/rules.rs +++ b/crates/oxc_linter/src/config/rules.rs @@ -160,6 +160,10 @@ fn transform_rule_and_plugin_name<'a>( return (rule_name, "eslint"); } + if plugin_name == "unicorn" && rule_name == "no-negated-condition" { + return ("no-negated-condition", "eslint"); + } + (rule_name, plugin_name) } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 4b3571dca696d..9fae5f8eaeb23 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -81,6 +81,7 @@ mod eslint { pub mod no_ex_assign; pub mod no_extend_native; pub mod no_extra_boolean_cast; + pub mod no_extra_label; pub mod no_fallthrough; pub mod no_func_assign; pub mod no_global_assign; @@ -93,7 +94,9 @@ mod eslint { pub mod no_labels; pub mod no_loss_of_precision; pub mod no_magic_numbers; + pub mod no_multi_assign; pub mod no_multi_str; + pub mod no_negated_condition; pub mod no_nested_ternary; pub mod no_new; pub mod no_new_func; @@ -142,6 +145,7 @@ mod eslint { pub mod prefer_exponentiation_operator; pub mod prefer_numeric_literals; pub mod prefer_object_has_own; + pub mod prefer_rest_params; pub mod prefer_spread; pub mod radix; pub mod require_await; @@ -153,6 +157,7 @@ mod eslint { pub mod unicode_bom; pub mod use_isnan; pub mod valid_typeof; + pub mod vars_on_top; pub mod yoda; } @@ -321,7 +326,6 @@ mod unicorn { pub mod no_length_as_slice_end; pub mod no_lonely_if; pub mod no_magic_array_flat_depth; - pub mod no_negated_condition; pub mod no_negation_in_equality_check; pub mod no_nested_ternary; pub mod no_new_array; @@ -412,6 +416,7 @@ mod jsx_a11y { pub mod no_aria_hidden_on_focusable; pub mod no_autofocus; pub mod no_distracting_elements; + pub mod no_noninteractive_tabindex; pub mod no_redundant_roles; pub mod prefer_tag_over_role; pub mod role_has_required_aria_props; @@ -538,6 +543,8 @@ oxc_macros::declare_all_lint_rules! { eslint::max_lines, eslint::max_params, eslint::new_cap, + eslint::no_extra_label, + eslint::no_multi_assign, eslint::no_nested_ternary, eslint::no_labels, eslint::no_restricted_imports, @@ -589,6 +596,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_label_var, eslint::no_loss_of_precision, eslint::no_magic_numbers, + eslint::no_negated_condition, eslint::no_multi_str, eslint::no_new_func, eslint::no_new_native_nonconstructor, @@ -632,6 +640,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_var, eslint::no_void, eslint::no_with, + eslint::prefer_rest_params, eslint::prefer_exponentiation_operator, eslint::prefer_numeric_literals, eslint::prefer_object_has_own, @@ -646,6 +655,7 @@ oxc_macros::declare_all_lint_rules! { eslint::unicode_bom, eslint::use_isnan, eslint::valid_typeof, + eslint::vars_on_top, eslint::yoda, import::default, import::export, @@ -749,6 +759,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::lang, jsx_a11y::media_has_caption, jsx_a11y::mouse_events_have_key_events, + jsx_a11y::no_noninteractive_tabindex, jsx_a11y::no_access_key, jsx_a11y::no_aria_hidden_on_focusable, jsx_a11y::no_autofocus, @@ -920,7 +931,6 @@ oxc_macros::declare_all_lint_rules! { unicorn::no_length_as_slice_end, unicorn::no_lonely_if, unicorn::no_magic_array_flat_depth, - unicorn::no_negated_condition, unicorn::no_negation_in_equality_check, unicorn::no_nested_ternary, unicorn::no_new_array, diff --git a/crates/oxc_linter/src/rules/eslint/no_extra_label.rs b/crates/oxc_linter/src/rules/eslint/no_extra_label.rs new file mode 100644 index 0000000000000..e2868599fc06c --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_extra_label.rs @@ -0,0 +1,278 @@ +use oxc_ast::{ast::LabelIdentifier, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_extra_label_diagnostic(label: &LabelIdentifier) -> OxcDiagnostic { + let label_name = &label.name; + OxcDiagnostic::warn(format!("This label '{label_name}' is unnecessary")) + .with_help(format!("Remove this label. It will have the same result because the labeled statement '{label_name}' has no nested loops or switches",)) + .with_label(label.span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoExtraLabel; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow unnecessary labels. + /// + /// ### Why is this bad? + /// + /// If a loop contains no nested loops or switches, labeling the loop is unnecessary. + /// ```js + /// A: while (a) { + /// break A; + /// } + /// ``` + /// You can achieve the same result by removing the label and using `break` or `continue` without a label. + /// Probably those labels would confuse developers because they expect labels to jump to further. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// A: while (a) { + /// break A; + /// } + /// + /// B: for (let i = 0; i < 10; ++i) { + /// break B; + /// } + /// + /// C: switch (a) { + /// case 0: + /// break C; + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// while (a) { + /// break; + /// } + /// + /// for (let i = 0; i < 10; ++i) { + /// break; + /// } + /// + /// switch (a) { + /// case 0: + /// break; + /// } + /// + /// A: { + /// break A; + /// } + /// + /// B: while (a) { + /// while (b) { + /// break B; + /// } + /// } + /// + /// C: switch (a) { + /// case 0: + /// while (b) { + /// break C; + /// } + /// break; + /// } + /// ``` + NoExtraLabel, + style, + fix +); + +impl Rule for NoExtraLabel { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::BreakStatement(break_stmt) = node.kind() { + if let Some(label) = &break_stmt.label { + report_label_if_extra(label, node, ctx); + } + } + if let AstKind::ContinueStatement(cont_stmt) = node.kind() { + if let Some(label) = &cont_stmt.label { + report_label_if_extra(label, node, ctx); + } + } + } +} + +fn report_label_if_extra(label: &LabelIdentifier, node: &AstNode, ctx: &LintContext) { + let nodes = ctx.nodes(); + for ancestor_id in nodes.ancestor_ids(node.id()) { + if !is_breakable_statement(nodes.kind(ancestor_id)) { + continue; + } + let Some(AstKind::LabeledStatement(labeled_stmt)) = nodes.parent_kind(ancestor_id) else { + return; // no need to check outer loops/switches + }; + if labeled_stmt.label.name != label.name { + return; + } + + let keyword_len: u32 = match node.kind() { + AstKind::BreakStatement(_) => 5, + AstKind::ContinueStatement(_) => 8, + _ => unreachable!(), + }; + + let keyword_end = node.span().start + keyword_len; + let delete_span = Span::new(keyword_end, label.span.end); + + let diagnostic = no_extra_label_diagnostic(label); + if ctx.comments().iter().any(|comment| delete_span.contains_inclusive(comment.span)) { + // No autofix to avoid deleting comments between keyword and label + // e.g. `break /* comment */ label;` + ctx.diagnostic(diagnostic); + } else { + // e.g. `break label;` -> `break;` + ctx.diagnostic_with_fix(diagnostic, |fixer| fixer.delete_range(delete_span)); + } + return; + } +} + +fn is_breakable_statement(kind: AstKind) -> bool { + match kind { + kind if kind.is_iteration_statement() => true, + AstKind::SwitchStatement(_) => true, + _ => false, + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "A: break A;", + "A: { if (a) break A; }", + "A: { while (b) { break A; } }", + "A: { switch (b) { case 0: break A; } }", + "A: while (a) { while (b) { break; } break; }", + "A: while (a) { while (b) { break A; } }", + "A: while (a) { while (b) { continue A; } }", + "A: while (a) { switch (b) { case 0: break A; } }", + "A: while (a) { switch (b) { case 0: continue A; } }", + "A: switch (a) { case 0: while (b) { break A; } }", + "A: switch (a) { case 0: switch (b) { case 0: break A; } }", + "A: for (;;) { while (b) { break A; } }", + "A: do { switch (b) { case 0: break A; break; } } while (a);", + "A: for (a in obj) { while (b) { break A; } }", + "A: for (a of ary) { switch (b) { case 0: break A; } }", // { "ecmaVersion": 6 } + ]; + + let fail = vec![ + "A: while (a) break A;", + "A: while (a) { B: { continue A; } }", + "X: while (x) { A: while (a) { B: { break A; break B; continue X; } } }", + "A: do { break A; } while (a);", + "A: for (;;) { break A; }", + "A: for (a in obj) { break A; }", + "A: for (a of ary) { break A; }", // { "ecmaVersion": 6 }, + "A: switch (a) { case 0: break A; }", + "X: while (x) { A: switch (a) { case 0: break A; } }", + "X: switch (a) { case 0: A: while (b) break A; }", + " + A: while (true) { + break A; + while (true) { + break A; + } + } + ", + "A: while(true) { /*comment*/break A; }", + "A: while(true) { break/**/ A; }", + "A: while(true) { continue /**/ A; }", + "A: while(true) { break /**/A; }", + "A: while(true) { continue/**/A; }", + "A: while(true) { continue A/*comment*/; }", + "A: while(true) { break A//comment + }", + "A: while(true) { break A/*comment*/ + foo() }", + ]; + + let fix = vec![ + ("A: while (a) break A;", "A: while (a) break;", None), + ("A: while (a) { B: { continue A; } }", "A: while (a) { B: { continue; } }", None), + ( + "X: while (x) { A: while (a) { B: { break A; break B; continue X; } } }", + "X: while (x) { A: while (a) { B: { break; break B; continue X; } } }", + None, + ), + ("A: do { break A; } while (a);", "A: do { break; } while (a);", None), + ("A: for (;;) { break A; }", "A: for (;;) { break; }", None), + ("A: for (a in obj) { break A; }", "A: for (a in obj) { break; }", None), + ("A: for (a of ary) { break A; }", "A: for (a of ary) { break; }", None), + ("A: switch (a) { case 0: break A; }", "A: switch (a) { case 0: break; }", None), + ( + "X: while (x) { A: switch (a) { case 0: break A; } }", + "X: while (x) { A: switch (a) { case 0: break; } }", + None, + ), + ( + "X: switch (a) { case 0: A: while (b) break A; }", + "X: switch (a) { case 0: A: while (b) break; }", + None, + ), + ( + " + A: while (true) { + break A; + while (true) { + break A; + } + } + ", + " + A: while (true) { + break; + while (true) { + break A; + } + } + ", + None, + ), + ("A: while(true) { /*comment*/break A; }", "A: while(true) { /*comment*/break; }", None), + ( + "A: while(true) { continue A/*comment*/; }", + "A: while(true) { continue/*comment*/; }", + None, + ), + ( + "A: while(true) { break A//comment + }", + "A: while(true) { break//comment + }", + None, + ), + ( + "A: while(true) { break A/*comment*/ + foo() }", + "A: while(true) { break/*comment*/ + foo() }", + None, + ), + // Do not fix if a comment sits between break/continue and label + ( + r"A: while(true) { break /*comment*/ A; }", + r"A: while(true) { break /*comment*/ A; }", + None, + ), + ( + r"A: while(true) { continue /*comment*/ A; }", + r"A: while(true) { continue /*comment*/ A; }", + None, + ), + ]; + Tester::new(NoExtraLabel::NAME, NoExtraLabel::CATEGORY, pass, fail) + .expect_fix(fix) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/eslint/no_multi_assign.rs b/crates/oxc_linter/src/rules/eslint/no_multi_assign.rs new file mode 100644 index 0000000000000..72a1b548ed5cf --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_multi_assign.rs @@ -0,0 +1,226 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_multi_assign_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Do not use chained assignment") + .with_help("Separate each assignment into its own statement") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoMultiAssign { + ignore_non_declaration: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow use of chained assignment expressions. + /// + /// ### Why is this bad? + /// + /// Chaining the assignment of variables can lead to unexpected results and be difficult to read. + /// ```js + /// (function() { + /// const foo = bar = 0; // Did you mean `foo = bar == 0`? + /// bar = 1; // This will not fail since `bar` is not constant. + /// })(); + /// console.log(bar); // This will output 1 since `bar` is not scoped. + /// ``` + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// var a = b = c = 5; + /// + /// const foo = bar = "baz"; + /// + /// let d = + /// e = + /// f; + /// + /// class Foo { + /// a = b = 10; + /// } + /// + /// a = b = "quux"; + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// var a = 5; + /// var b = 5; + /// var c = 5; + /// + /// const foo = "baz"; + /// const bar = "baz"; + /// + /// let d = c; + /// let e = c; + /// + /// class Foo { + /// a = 10; + /// b = 10; + /// } + /// + /// a = "quux"; + /// b = "quux"; + /// ``` + /// + /// ### Options + /// + /// This rule has an object option: + /// * `"ignoreNonDeclaration"`: When set to `true`, the rule allows chains that don't include initializing a variable in a declaration or initializing a class field. Default is `false`. + /// + /// #### ignoreNonDeclaration + /// + /// Examples of **correct** code for the `{ "ignoreNonDeclaration": true }` option: + /// ```js + /// let a; + /// let b; + /// a = b = "baz"; + /// + /// const x = {}; + /// const y = {}; + /// x.one = y.one = 1; + /// ``` + /// + /// Examples of **incorrect** code for the `{ "ignoreNonDeclaration": true }` option: + /// ```js + /// let a = b = "baz"; + /// + /// const foo = bar = 1; + /// + /// class Foo { + /// a = b = 10; + /// } + /// ``` + NoMultiAssign, + style, +); + +impl Rule for NoMultiAssign { + fn from_configuration(value: serde_json::Value) -> Self { + let ignore_non_declaration = value + .get(0) + .and_then(|config| config.get("ignoreNonDeclaration")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + Self { ignore_non_declaration } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + // e.g. `var a = b = c;` + if let AstKind::VariableDeclarator(declarator) = node.kind() { + let Some(Expression::AssignmentExpression(assign_expr)) = &declarator.init else { + return; + }; + ctx.diagnostic(no_multi_assign_diagnostic(assign_expr.span)); + } + + // e.g. `class A { a = b = 1; }` + if let AstKind::PropertyDefinition(prop_def) = node.kind() { + let Some(Expression::AssignmentExpression(assign_expr)) = &prop_def.value else { + return; + }; + ctx.diagnostic(no_multi_assign_diagnostic(assign_expr.span)); + } + + // e.g. `let a; let b; a = b = 1;` + if !self.ignore_non_declaration { + if let AstKind::AssignmentExpression(parent_expr) = node.kind() { + let Expression::AssignmentExpression(expr) = &parent_expr.right else { + return; + }; + ctx.diagnostic(no_multi_assign_diagnostic(expr.span)); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + "var a, b, c, + d = 0;", + None, + ), + ( + "var a = 1; var b = 2; var c = 3; + var d = 0;", + None, + ), + ("var a = 1 + (b === 10 ? 5 : 4);", None), + ("const a = 1, b = 2, c = 3;", None), // { "ecmaVersion": 6 }, + ( + "const a = 1; + const b = 2; + const c = 3;", + None, + ), // { "ecmaVersion": 6 }, + ("for(var a = 0, b = 0;;){}", None), + ("for(let a = 0, b = 0;;){}", None), // { "ecmaVersion": 6 }, + ("for(const a = 0, b = 0;;){}", None), // { "ecmaVersion": 6 }, + ("export let a, b;", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "export let a, + b = 0;", + None, + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "const x = {};const y = {};x.one = y.one = 1;", + Some(serde_json::json!([{ "ignoreNonDeclaration": true }])), + ), // { "ecmaVersion": 6 }, + ("let a, b;a = b = 1", Some(serde_json::json!([{ "ignoreNonDeclaration": true }]))), // { "ecmaVersion": 6 }, + ("class C { [foo = 0] = 0 }", None), // { "ecmaVersion": 2022 } + ]; + + let fail = vec![ + ("var a = b = c;", None), + ("var a = b = c = d;", None), + ("let foo = bar = cee = 100;", None), // { "ecmaVersion": 6 }, + ("a=b=c=d=e", None), + ("a=b=c", None), + ( + "a + =b + =c", + None, + ), + ("var a = (b) = (((c)))", None), + ("var a = ((b)) = (c)", None), + ("var a = b = ( (c * 12) + 2)", None), + ( + "var a = + ((b)) + = (c)", + None, + ), + ("a = b = '=' + c + 'foo';", None), + ("a = b = 7 * 12 + 5;", None), + ( + "const x = {}; + const y = x.one = 1;", + Some(serde_json::json!([{ "ignoreNonDeclaration": true }])), + ), // { "ecmaVersion": 6 }, + ("let a, b;a = b = 1", Some(serde_json::json!([{}]))), // { "ecmaVersion": 6 }, + ("let x, y;x = y = 'baz'", Some(serde_json::json!([{ "ignoreNonDeclaration": false }]))), // { "ecmaVersion": 6 }, + ("const a = b = 1", Some(serde_json::json!([{ "ignoreNonDeclaration": true }]))), // { "ecmaVersion": 6 }, + ("class C { field = foo = 0 }", None), // { "ecmaVersion": 2022 }, + ( + "class C { field = foo = 0 }", + Some(serde_json::json!([{ "ignoreNonDeclaration": true }])), + ), // { "ecmaVersion": 2022 } + ]; + + Tester::new(NoMultiAssign::NAME, NoMultiAssign::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/unicorn/no_negated_condition.rs b/crates/oxc_linter/src/rules/eslint/no_negated_condition.rs similarity index 85% rename from crates/oxc_linter/src/rules/unicorn/no_negated_condition.rs rename to crates/oxc_linter/src/rules/eslint/no_negated_condition.rs index 2786b86798af2..60ffa1770a169 100644 --- a/crates/oxc_linter/src/rules/unicorn/no_negated_condition.rs +++ b/crates/oxc_linter/src/rules/eslint/no_negated_condition.rs @@ -106,6 +106,20 @@ fn test() { use crate::tester::Tester; let pass = vec![ + "if (a) {}", + "if (a) {} else {}", + "if (!a) {}", + "if (!a) {} else if (b) {}", + "if (!a) {} else if (b) {} else {}", + "if (a == b) {}", + "if (a == b) {} else {}", + "if (a != b) {}", + "if (a != b) {} else if (b) {}", + "if (a != b) {} else if (b) {} else {}", + "if (a !== b) {}", + "if (a === b) {} else {}", + "a ? b : c", + // Test cases from eslint-plugin-unicorn r"if (a) {}", r"if (a) {} else {}", r"if (!a) {}", @@ -122,6 +136,13 @@ fn test() { ]; let fail = vec![ + "if (!a) {;} else {;}", + "if (a != b) {;} else {;}", + "if (a !== b) {;} else {;}", + "!a ? b : c", + "a != b ? c : d", + "a !== b ? c : d", + // Test cases from eslint-plugin-unicorn r"if (!a) {;} else {;}", r"if (a != b) {;} else {;}", r"if (a !== b) {;} else {;}", diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs index 6ae3bb022f932..9a5f605ca6ec7 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs @@ -214,12 +214,12 @@ impl Rule for NoUnusedVars { } fn should_run(&self, ctx: &ContextHost) -> bool { - // ignore .d.ts and vue files. + // ignore .d.ts and vue/svelte files. // 1. declarations have side effects (they get merged together) - // 2. vue scripts declare variables that get used in the template, which + // 2. vue/svelte scripts declare variables that get used in the template, which // we can't detect !ctx.source_type().is_typescript_definition() - && !ctx.file_path().extension().is_some_and(|ext| ext == "vue") + && !ctx.file_path().extension().is_some_and(|ext| ext == "vue" || ext == "svelte") } } diff --git a/crates/oxc_linter/src/rules/eslint/prefer_rest_params.rs b/crates/oxc_linter/src/rules/eslint/prefer_rest_params.rs new file mode 100644 index 0000000000000..f7807fc8fba51 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/prefer_rest_params.rs @@ -0,0 +1,126 @@ +use crate::{context::LintContext, rule::Rule, AstNode}; +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +fn prefer_rest_params_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Use the rest parameters instead of 'arguments'.").with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct PreferRestParams; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallows the use of the `arguments` object and instead enforces the use of rest parameters. + /// + /// ### Why is this bad? + /// + /// The `arguments` object does not have methods from `Array.prototype`, making it inconvenient for array-like operations. + /// Using rest parameters provides a more intuitive and efficient way to handle variadic arguments. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```javascript + /// function foo() { + /// console.log(arguments); + /// } + /// + /// function foo(action) { + /// var args = Array.prototype.slice.call(arguments, 1); + /// action.apply(null, args); + /// } + /// + /// function foo(action) { + /// var args = [].slice.call(arguments, 1); + /// action.apply(null, args); + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// function foo(...args) { + /// console.log(args); + /// } + /// + /// function foo(action, ...args) { + /// action.apply(null, args); // Or use `action(...args)` (related to `prefer-spread` rule). + /// } + /// + /// // Note: Implicit `arguments` can be shadowed. + /// function foo(arguments) { + /// console.log(arguments); // This refers to the first argument. + /// } + /// function foo() { + /// var arguments = 0; + /// console.log(arguments); // This is a local variable. + /// } + /// ``` + PreferRestParams, + style, +); + +impl Rule for PreferRestParams { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::IdentifierReference(identifier) = node.kind() { + if identifier.name != "arguments" + || !is_inside_of_function(node, ctx) + || is_not_normal_member_access(node, ctx) + { + return; + } + let binding = ctx.scopes().find_binding(node.scope_id(), "arguments"); + if binding.is_none() { + ctx.diagnostic(prefer_rest_params_diagnostic(node.span())); + } + } + } +} + +fn is_inside_of_function(node: &AstNode, ctx: &LintContext) -> bool { + let mut current = node; + while let Some(parent) = ctx.nodes().parent_node(current.id()) { + if matches!(parent.kind(), AstKind::Function(_)) { + return true; + } + current = parent; + } + false +} + +fn is_not_normal_member_access(identifier: &AstNode, ctx: &LintContext) -> bool { + let parent = ctx.nodes().parent_node(identifier.id()); + if let Some(parent) = parent { + if let AstKind::MemberExpression(member) = parent.kind() { + return member.object().span() == identifier.span() && !member.is_computed(); + } + } + false +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "arguments;", + "function foo(arguments) { arguments; }", + "function foo() { var arguments; arguments; }", + "var foo = () => arguments;", + "function foo(...args) { args; }", + "function foo() { arguments.length; }", + "function foo() { arguments.callee; }", + ]; + + let fail = vec![ + "function foo() { arguments; }", + "function foo() { arguments[0]; }", + "function foo() { arguments[1]; }", + "function foo() { arguments[Symbol.iterator]; }", + ]; + + Tester::new(PreferRestParams::NAME, PreferRestParams::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/eslint/vars_on_top.rs b/crates/oxc_linter/src/rules/eslint/vars_on_top.rs new file mode 100644 index 0000000000000..b2d4226262f69 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/vars_on_top.rs @@ -0,0 +1,544 @@ +use oxc_ast::ast::{Declaration, Expression, Program, Statement, VariableDeclarationKind}; +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn vars_on_top_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("All 'var' declarations must be at the top of the function scope.") + .with_help("Consider moving this to the top of the functions scope or using let or const to declare this variable.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct VarsOnTop; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces that all `var` declarations are placed at the top of their containing scope. + /// + /// ### Why is this bad? + /// + /// In JavaScript, `var` declarations are hoisted to the top of their containing scope. Placing `var` declarations at the top explicitly improves code readability and maintainability by making the scope of variables clear. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// function doSomething() { + /// if (true) { + /// var first = true; + /// } + /// var second; + /// } + /// + /// function doSomethingElse() { + /// for (var i = 0; i < 10; i++) {} + /// } + /// + /// f(); + /// var a; + /// + /// class C { + /// static { + /// if (something) { + /// var a = true; + /// } + /// } + /// static { + /// f(); + /// var a; + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// function doSomething() { + /// var first; + /// var second; + /// if (true) { + /// first = true; + /// } + /// } + /// + /// function doSomethingElse() { + /// var i; + /// for (i = 0; i < 10; i++) {} + /// } + /// + /// var a; + /// f(); + /// + /// class C { + /// static { + /// var a; + /// if (something) { + /// a = true; + /// } + /// } + /// static { + /// var a; + /// f(); + /// } + /// } + /// ``` + VarsOnTop, + style, +); + +impl Rule for VarsOnTop { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::VariableDeclaration(declaration) = node.kind() else { + return; + }; + if declaration.kind != VariableDeclarationKind::Var { + return; + } + let Some(parent) = ctx.nodes().parent_node(node.id()) else { + return; + }; + + match parent.kind() { + AstKind::ExportNamedDeclaration(_) => { + if let Some(grand_parent) = ctx.nodes().parent_node(parent.id()) { + if let AstKind::Program(grand_parent) = grand_parent.kind() { + global_var_check(parent, grand_parent, ctx); + } + } + } + AstKind::Program(parent) => { + global_var_check(node, parent, ctx); + } + _ => block_scope_var_check(node, ctx), + } + } +} + +fn looks_like_directive(node: &Statement) -> bool { + matches!( + node, + Statement::ExpressionStatement(expr_stmt) if matches!( + &expr_stmt.expression, + Expression::StringLiteral(_) + ) + ) +} + +fn looks_like_import(node: &Statement) -> bool { + matches!(node, Statement::ImportDeclaration(_)) +} + +fn is_variable_declaration(node: &Statement) -> bool { + if matches!(node, Statement::VariableDeclaration(_)) { + return true; + } + + if let Statement::ExportNamedDeclaration(export) = node { + return matches!(export.declaration, Some(Declaration::VariableDeclaration(_))); + } + + false +} + +fn is_var_on_top(node: &AstNode, statements: &[Statement], ctx: &LintContext) -> bool { + let mut i = 0; + let len = statements.len(); + let parent = ctx.nodes().parent_node(node.id()); + + if let Some(parent) = parent { + if !matches!(parent.kind(), AstKind::StaticBlock(_)) { + while i < len { + if !looks_like_directive(&statements[i]) && !looks_like_import(&statements[i]) { + break; + } + i += 1; + } + } + } + + let node_span = node.span(); + while i < len { + if !is_variable_declaration(&statements[i]) { + return false; + } + let stmt_span = statements[i].span(); + + if stmt_span == node_span { + return true; + } + i += 1; + } + + false +} + +fn global_var_check(node: &AstNode, parent: &Program, ctx: &LintContext) { + if !is_var_on_top(node, &parent.body, ctx) { + ctx.diagnostic(vars_on_top_diagnostic(node.span())); + } +} + +fn block_scope_var_check(node: &AstNode, ctx: &LintContext) { + if let Some(parent) = ctx.nodes().parent_node(node.id()) { + match parent.kind() { + AstKind::BlockStatement(block) => { + if check_var_on_top_in_function_scope(node, &block.body, parent, ctx) { + return; + } + } + AstKind::FunctionBody(block) => { + if check_var_on_top_in_function_scope(node, &block.statements, parent, ctx) { + return; + } + } + AstKind::StaticBlock(block) => { + if is_var_on_top(node, &block.body, ctx) { + return; + } + } + _ => {} + } + } + ctx.diagnostic(vars_on_top_diagnostic(node.span())); +} + +fn check_var_on_top_in_function_scope( + node: &AstNode, + statements: &[Statement], + parent: &AstNode, + ctx: &LintContext, +) -> bool { + if let Some(grandparent) = ctx.nodes().parent_node(parent.id()) { + if matches!( + grandparent.kind(), + AstKind::Function(_) | AstKind::FunctionBody(_) | AstKind::ArrowFunctionExpression(_) + ) && is_var_on_top(node, statements, ctx) + { + return true; + } + } + + false +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "var first = 0; + function foo() { + first = 2; + } + ", +"function foo() { + } + ", +"function foo() { + var first; + if (true) { + first = true; + } else { + first = 1; + } + } + ", +"function foo() { + var first; + var second = 1; + var third; + var fourth = 1, fifth, sixth = third; + var seventh; + if (true) { + third = true; + } + first = second; + } + ", +"function foo() { + var i; + for (i = 0; i < 10; i++) { + alert(i); + } + } + ", +"function foo() { + var outer; + function inner() { + var inner = 1; + var outer = inner; + } + outer = 1; + } + ", +"function foo() { + var first; + //Hello + var second = 1; + first = second; + } + ", +"function foo() { + var first; + /* + Hello Clarice + */ + var second = 1; + first = second; + } + ", +"function foo() { + var first; + var second = 1; + function bar(){ + var first; + first = 5; + } + first = second; + } + ", +"function foo() { + var first; + var second = 1; + function bar(){ + var third; + third = 5; + } + first = second; + } + ", +"function foo() { + var first; + var bar = function(){ + var third; + third = 5; + } + first = 5; + } + ", +"function foo() { + var first; + first.onclick(function(){ + var third; + third = 5; + }); + first = 5; + } + ", +"function foo() { + var i = 0; + for (let j = 0; j < 10; j++) { + alert(j); + } + i = i + 1; + }", // { "ecmaVersion": 6 }, +"'use strict'; var x; f();", +"'use strict'; 'directive'; var x; var y; f();", +"function f() { 'use strict'; var x; f(); }", +"function f() { 'use strict'; 'directive'; var x; var y; f(); }", +"import React from 'react'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"'use strict'; import React from 'react'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"import React from 'react'; 'use strict'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"import * as foo from 'mod.js'; 'use strict'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"import { square, diag } from 'lib'; 'use strict'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"import { default as foo } from 'lib'; 'use strict'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"import 'src/mylib'; 'use strict'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"import theDefault, { named1, named2 } from 'src/mylib'; 'use strict'; var y; function f() { 'use strict'; var x; var y; f(); }", // { "ecmaVersion": 6, "sourceType": "module" }, +"export var x; + var y; + var z;", // { "ecmaVersion": 6, "sourceType": "module" }, +"var x; + export var y; + var z;", // { "ecmaVersion": 6, "sourceType": "module" }, +"var x; + var y; + export var z;", // { "ecmaVersion": 6, "sourceType": "module" }, +"class C { + static { + var x; + } + }", // { "ecmaVersion": 2022 }, +"class C { + static { + var x; + foo(); + } + }", // { "ecmaVersion": 2022 }, +"class C { + static { + var x; + var y; + } + }", // { "ecmaVersion": 2022 }, +"class C { + static { + var x; + var y; + foo(); + } + }", // { "ecmaVersion": 2022 }, +"class C { + static { + let x; + var y; + } + }", // { "ecmaVersion": 2022 }, +"class C { + static { + foo(); + let x; + } + }", // { "ecmaVersion": 2022 } + ]; + + let fail = vec![ + "var first = 0; + function foo() { + first = 2; + second = 2; + } + var second = 0;", + "function foo() { + var first; + first = 1; + first = 2; + first = 3; + first = 4; + var second = 1; + second = 2; + first = second; + }", + "function foo() { + var first; + if (true) { + var second = true; + } + first = second; + }", + "function foo() { + for (var i = 0; i < 10; i++) { + alert(i); + } + }", + "function foo() { + var first = 10; + var i; + for (i = 0; i < first; i ++) { + var second = i; + } + }", + "function foo() { + var first = 10; + var i; + switch (first) { + case 10: + var hello = 1; + break; + } + }", + "function foo() { + var first = 10; + var i; + try { + var hello = 1; + } catch (e) { + alert('error'); + } + }", + "function foo() { + var first = 10; + var i; + try { + asdf; + } catch (e) { + var hello = 1; + } + }", + "function foo() { + var first = 10; + while (first) { + var hello = 1; + } + }", + "function foo() { + var first = 10; + do { + var hello = 1; + } while (first == 10); + }", + "function foo() { + var first = [1,2,3]; + for (var item in first) { + item++; + } + }", + "function foo() { + var first = [1,2,3]; + var item; + for (item in first) { + var hello = item; + } + }", + "var foo = () => { + var first = [1,2,3]; + var item; + for (item in first) { + var hello = item; + } + }", // { "ecmaVersion": 6 }, + "'use strict'; 0; var x; f();", + "'use strict'; var x; 'directive'; var y; f();", + "function f() { 'use strict'; 0; var x; f(); }", + "function f() { 'use strict'; var x; 'directive'; var y; f(); }", + "export function f() {} + var x;", // { "ecmaVersion": 6, "sourceType": "module" }, + "var x; + export function f() {} + var y;", // { "ecmaVersion": 6, "sourceType": "module" }, + "import {foo} from 'foo'; + export {foo}; + var test = 1;", // { "ecmaVersion": 6, "sourceType": "module" }, + "export {foo} from 'foo'; + var test = 1;", // { "ecmaVersion": 6, "sourceType": "module" }, + "export * from 'foo'; + var test = 1;", // { "ecmaVersion": 6, "sourceType": "module" }, + "class C { + static { + foo(); + var x; + } + }", // { "ecmaVersion": 2022 }, + "class C { + static { + 'use strict'; + var x; + } + }", // { "ecmaVersion": 2022 }, + "class C { + static { + var x; + foo(); + var y; + } + }", // { "ecmaVersion": 2022 }, + "class C { + static { + if (foo) { + var x; + } + } + }", // { "ecmaVersion": 2022 }, + "class C { + static { + if (foo) + var x; + } + }", // { "ecmaVersion": 2022 } + ]; + + Tester::new(VarsOnTop::NAME, VarsOnTop::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/import/no_namespace.rs b/crates/oxc_linter/src/rules/import/no_namespace.rs index 6e98a40f5b045..ddf15b2d15271 100644 --- a/crates/oxc_linter/src/rules/import/no_namespace.rs +++ b/crates/oxc_linter/src/rules/import/no_namespace.rs @@ -115,11 +115,9 @@ impl Rule for NoNamespace { return; } - if self - .ignore - .iter() - .any(|pattern| glob_match(pattern, source.trim_start_matches("./"))) - { + if self.ignore.iter().any(|pattern| { + glob_match(pattern.as_str(), source.trim_start_matches("./")) + }) { return; } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_tabindex.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_tabindex.rs new file mode 100644 index 0000000000000..2251324f9ee4d --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_tabindex.rs @@ -0,0 +1,217 @@ +use oxc_ast::{ + ast::{JSXAttributeItem, JSXAttributeValue}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; +use phf::phf_set; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{get_element_type, has_jsx_prop_ignore_case}, + AstNode, +}; + +fn no_noninteractive_tabindex_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("tabIndex should only be declared on interactive elements") + .with_help("tabIndex attribute should be removed") + .with_label(span) +} + +#[derive(Debug, Clone)] +pub struct NoNoninteractiveTabindex(Box); + +#[derive(Debug, Clone)] +struct NoNoninteractiveTabindexConfig { + tags: Vec, + roles: Vec, + allow_expression_values: bool, +} + +impl Default for NoNoninteractiveTabindex { + fn default() -> Self { + Self(Box::new(NoNoninteractiveTabindexConfig { + roles: vec![CompactStr::new("tabpanel")], + allow_expression_values: true, + tags: vec![], + })) + } +} + +declare_oxc_lint!( + /// ### What it does + /// This rule checks that non-interactive elements don't have a tabIndex which would make them interactive via keyboard navigation. + /// + /// ### Why is this bad? + /// + /// Tab key navigation should be limited to elements on the page that can be interacted with. + /// Thus it is not necessary to add a tabindex to items in an unordered list, for example, + /// to make them navigable through assistive technology. + /// + /// These applications already afford page traversal mechanisms based on the HTML of the page. + /// Generally, we should try to reduce the size of the page's tab ring rather than increasing it. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```jsx + ///
+ ///
+ ///
+ ///
+ /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```jsx + ///
+ /// + ///