diff --git a/.editorconfig b/.editorconfig index ade256ee1714f..98b0c57dbb1fa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,10 @@ +# https://EditorConfig.org +root = true + +[*] +charset = utf-8 trim_trailing_whitespace = true +end_of_line = lf insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000..f8edd38e325a3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# Default to project owner @Boshen + +@Boshen + +# Ignore lock files + +Cargo.lock +pnpm-lock.yaml + +# Crates + +/crates/oxc_transformer @overlookmotel @dunqing +/crates/oxc_semantic @Boshen @dunqing +/crates/oxc_data_structures @overlookmotel +/crates/oxc_traverse @overlookmotel +/crates/oxc_isolated_declarations @dunqing +/tasks/ast_tools diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2da9fa2b62c80..52566463185c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,7 @@ jobs: RUSTFLAGS: "--cfg tokio_unstable -C target-feature=+atomics,+bulk-memory,+mutable-globals,+simd128 -C link-args=--max-memory=67108864" CARGO_TARGET_WASM32_WASIP1_THREADS_RUNNER: "wasmtime run -W bulk-memory=y -W threads=y -S threads=y --dir=${{ github.workspace }}::${{ github.workspace }} --" # Insta is not able to run on wasmtime, omit the packages that depend on it - TEST_FLAGS: "-p oxc_sourcemap -p oxc_ast -p oxc_cfg -p oxc_index -p oxc_regular_expression -p oxc_module_lexer -- --nocapture" + TEST_FLAGS: "-p oxc_ast -p oxc_cfg -p oxc_index -p oxc_regular_expression -p oxc_module_lexer -- --nocapture" steps: - uses: taiki-e/checkout-action@v1 - uses: Boshen/setup-rust@main diff --git a/.github/workflows/ci_vscode.yml b/.github/workflows/ci_vscode.yml new file mode 100644 index 0000000000000..565360a690d72 --- /dev/null +++ b/.github/workflows/ci_vscode.yml @@ -0,0 +1,69 @@ +name: CI VSCode + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize] + paths: + - "pnpm-lock.yaml" + - "crates/oxc_language_server/**" + - "editors/vscode/**" + - ".github/worfkflows/ci_vscode.yml" + push: + branches: + - main + - "renovate/**" + paths: + - "pnpm-lock.yaml" + - "crates/oxc_language_server/**" + - "editors/vscode/**" + - ".github/worfkflows/ci_vscode.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +jobs: + compile: + name: Compile + runs-on: ubuntu-latest + steps: + - uses: taiki-e/checkout-action@v1 + - uses: ./.github/actions/pnpm + + - name: Compile VSCode + working-directory: editors/vscode + run: pnpm run compile + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: taiki-e/checkout-action@v1 + - uses: ./.github/actions/pnpm + + - name: Lint VSCode + working-directory: editors/vscode + run: pnpm run lint + + type-check: + name: Type-Check + runs-on: ubuntu-latest + steps: + - uses: taiki-e/checkout-action@v1 + - uses: ./.github/actions/pnpm + + - name: Type-Check VSCode + working-directory: editors/vscode + run: pnpm run type-check + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: taiki-e/checkout-action@v1 + - uses: ./.github/actions/pnpm + + - name: Test VSCode + working-directory: editors/vscode + run: xvfb-run -a pnpm run test diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index db92ce669dc1e..989ff44c76271 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -66,7 +66,7 @@ jobs: - name: Upload to codecov.io if: env.CODECOV_TOKEN - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index dfc41dfbf6d0c..770ac8bcd3af1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ target/ /tasks/compat_data/node_modules/ # vscode +/editors/vscode/.vscode-test/ /editors/vscode/node_modules/ /editors/vscode/icon.png /editors/vscode/out/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000..54f36615b26a3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", // official rust extension + "vadimcn.vscode-lldb", // for debugging + "tamasfe.even-better-toml" // toml syntax + ] +} diff --git a/Cargo.lock b/Cargo.lock index dff09b9f4c9d0..8819d68eba28b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1397,7 +1397,7 @@ checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "oxc" -version = "0.36.0" +version = "0.37.0" dependencies = [ "napi", "napi-derive", @@ -1464,7 +1464,7 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.36.0" +version = "0.37.0" dependencies = [ "allocator-api2", "bumpalo", @@ -1474,7 +1474,7 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.36.0" +version = "0.37.0" dependencies = [ "bitflags 2.6.0", "cow-utils", @@ -1492,7 +1492,7 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.36.0" +version = "0.37.0" dependencies = [ "proc-macro2", "quote", @@ -1541,7 +1541,7 @@ dependencies = [ [[package]] name = "oxc_cfg" -version = "0.36.0" +version = "0.37.0" dependencies = [ "bitflags 2.6.0", "itertools", @@ -1554,7 +1554,7 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.36.0" +version = "0.37.0" dependencies = [ "assert-unchecked", "base64", @@ -1615,14 +1615,14 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.36.0" +version = "0.37.0" dependencies = [ "assert-unchecked", ] [[package]] name = "oxc_diagnostics" -version = "0.36.0" +version = "0.37.0" dependencies = [ "oxc-miette", "rustc-hash", @@ -1630,7 +1630,7 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.36.0" +version = "0.37.0" dependencies = [ "num-bigint", "num-traits", @@ -1643,14 +1643,14 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.36.0" +version = "0.37.0" dependencies = [ "serde", ] [[package]] name = "oxc_index" -version = "0.36.0" +version = "0.37.0" dependencies = [ "rayon", "serde", @@ -1658,7 +1658,7 @@ dependencies = [ [[package]] name = "oxc_isolated_declarations" -version = "0.36.0" +version = "0.37.0" dependencies = [ "bitflags 2.6.0", "insta", @@ -1700,7 +1700,7 @@ dependencies = [ [[package]] name = "oxc_linter" -version = "0.11.1" +version = "0.13.0" dependencies = [ "aho-corasick", "bitflags 2.6.0", @@ -1759,7 +1759,7 @@ dependencies = [ [[package]] name = "oxc_mangler" -version = "0.36.0" +version = "0.37.0" dependencies = [ "itertools", "oxc_ast", @@ -1770,7 +1770,7 @@ dependencies = [ [[package]] name = "oxc_minifier" -version = "0.36.0" +version = "0.37.0" dependencies = [ "cow-utils", "insta", @@ -1818,7 +1818,7 @@ dependencies = [ [[package]] name = "oxc_module_lexer" -version = "0.36.0" +version = "0.37.0" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1829,7 +1829,7 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.36.0" +version = "0.37.0" dependencies = [ "assert-unchecked", "bitflags 2.6.0", @@ -1904,7 +1904,7 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.36.0" +version = "0.37.0" dependencies = [ "oxc_allocator", "oxc_ast_macros", @@ -1939,7 +1939,7 @@ dependencies = [ [[package]] name = "oxc_semantic" -version = "0.36.0" +version = "0.37.0" dependencies = [ "assert-unchecked", "indexmap", @@ -1964,11 +1964,12 @@ dependencies = [ [[package]] name = "oxc_sourcemap" -version = "0.36.0" +version = "0.37.0" dependencies = [ "base64-simd", "cfg-if", "cow-utils", + "insta", "rayon", "rustc-hash", "serde", @@ -1977,7 +1978,7 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.36.0" +version = "0.37.0" dependencies = [ "compact_str", "oxc-miette", @@ -1990,7 +1991,7 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.36.0" +version = "0.37.0" dependencies = [ "assert-unchecked", "bitflags 2.6.0", @@ -2049,7 +2050,7 @@ dependencies = [ [[package]] name = "oxc_transform_napi" -version = "0.36.0" +version = "0.37.0" dependencies = [ "napi", "napi-build", @@ -2059,7 +2060,7 @@ dependencies = [ [[package]] name = "oxc_transformer" -version = "0.36.0" +version = "0.37.0" dependencies = [ "base64", "compact_str", @@ -2091,7 +2092,7 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.36.0" +version = "0.37.0" dependencies = [ "compact_str", "itoa", @@ -2123,7 +2124,7 @@ dependencies = [ [[package]] name = "oxlint" -version = "0.11.1" +version = "0.13.0" dependencies = [ "bpaf", "glob", diff --git a/Cargo.toml b/Cargo.toml index 8d32f30ead571..803a73db4c4d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,30 +78,30 @@ doc_lazy_continuation = "allow" # FIXME [workspace.dependencies] # publish = true -oxc = { version = "0.36.0", path = "crates/oxc" } -oxc_allocator = { version = "0.36.0", path = "crates/oxc_allocator" } -oxc_ast = { version = "0.36.0", path = "crates/oxc_ast" } -oxc_ast_macros = { version = "0.36.0", path = "crates/oxc_ast_macros" } -oxc_cfg = { version = "0.36.0", path = "crates/oxc_cfg" } -oxc_codegen = { version = "0.36.0", path = "crates/oxc_codegen" } -oxc_data_structures = { version = "0.36.0", path = "crates/oxc_data_structures" } -oxc_diagnostics = { version = "0.36.0", path = "crates/oxc_diagnostics" } -oxc_ecmascript = { version = "0.36.0", path = "crates/oxc_ecmascript" } -oxc_estree = { version = "0.36.0", path = "crates/oxc_estree" } -oxc_index = { version = "0.36.0", path = "crates/oxc_index" } -oxc_isolated_declarations = { version = "0.36.0", path = "crates/oxc_isolated_declarations" } -oxc_mangler = { version = "0.36.0", path = "crates/oxc_mangler" } -oxc_minifier = { version = "0.36.0", path = "crates/oxc_minifier" } -oxc_module_lexer = { version = "0.36.0", path = "crates/oxc_module_lexer" } -oxc_parser = { version = "0.36.0", path = "crates/oxc_parser" } -oxc_regular_expression = { version = "0.36.0", path = "crates/oxc_regular_expression" } -oxc_semantic = { version = "0.36.0", path = "crates/oxc_semantic" } -oxc_sourcemap = { version = "0.36.0", path = "crates/oxc_sourcemap" } -oxc_span = { version = "0.36.0", path = "crates/oxc_span" } -oxc_syntax = { version = "0.36.0", path = "crates/oxc_syntax" } -oxc_transform_napi = { version = "0.36.0", path = "napi/transform" } -oxc_transformer = { version = "0.36.0", path = "crates/oxc_transformer" } -oxc_traverse = { version = "0.36.0", path = "crates/oxc_traverse" } +oxc = { version = "0.37.0", path = "crates/oxc" } +oxc_allocator = { version = "0.37.0", path = "crates/oxc_allocator" } +oxc_ast = { version = "0.37.0", path = "crates/oxc_ast" } +oxc_ast_macros = { version = "0.37.0", path = "crates/oxc_ast_macros" } +oxc_cfg = { version = "0.37.0", path = "crates/oxc_cfg" } +oxc_codegen = { version = "0.37.0", path = "crates/oxc_codegen" } +oxc_data_structures = { version = "0.37.0", path = "crates/oxc_data_structures" } +oxc_diagnostics = { version = "0.37.0", path = "crates/oxc_diagnostics" } +oxc_ecmascript = { version = "0.37.0", path = "crates/oxc_ecmascript" } +oxc_estree = { version = "0.37.0", path = "crates/oxc_estree" } +oxc_index = { version = "0.37.0", path = "crates/oxc_index" } +oxc_isolated_declarations = { version = "0.37.0", path = "crates/oxc_isolated_declarations" } +oxc_mangler = { version = "0.37.0", path = "crates/oxc_mangler" } +oxc_minifier = { version = "0.37.0", path = "crates/oxc_minifier" } +oxc_module_lexer = { version = "0.37.0", path = "crates/oxc_module_lexer" } +oxc_parser = { version = "0.37.0", path = "crates/oxc_parser" } +oxc_regular_expression = { version = "0.37.0", path = "crates/oxc_regular_expression" } +oxc_semantic = { version = "0.37.0", path = "crates/oxc_semantic" } +oxc_sourcemap = { version = "0.37.0", path = "crates/oxc_sourcemap" } +oxc_span = { version = "0.37.0", path = "crates/oxc_span" } +oxc_syntax = { version = "0.37.0", path = "crates/oxc_syntax" } +oxc_transform_napi = { version = "0.37.0", path = "napi/transform" } +oxc_transformer = { version = "0.37.0", path = "crates/oxc_transformer" } +oxc_traverse = { version = "0.37.0", path = "crates/oxc_traverse" } # publish = false oxc_linter = { path = "crates/oxc_linter" } diff --git a/apps/oxlint/CHANGELOG.md b/apps/oxlint/CHANGELOG.md index 5d3173eee354b..e2d8288b8f84f 100644 --- a/apps/oxlint/CHANGELOG.md +++ b/apps/oxlint/CHANGELOG.md @@ -4,6 +4,24 @@ 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.13.0] - 2024-11-21 + +- 878189c parser,linter: [**BREAKING**] Add `ParserReturn::is_flow_language`; linter ignore flow error (#7373) (Boshen) + +### Features + + +## [0.12.0] - 2024-11-20 + +### Features + +- 2268a0e linter: Support `overrides` config field (#6974) (DonIsaac) +- d3a0119 oxlint: Add `cwd` property to `LintRunner` (#7352) (Alexander S.) + +### Bug Fixes + +- df5c535 linter: Revert unmatched rule error (#7257) (Cameron A McHenry) + ## [0.11.0] - 2024-11-03 - 1f2a6c6 linter: [**BREAKING**] Report unmatched rules with error exit code (#7027) (camchenry) diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml index 40cd92790b83c..d4778c1c1d206 100644 --- a/apps/oxlint/Cargo.toml +++ b/apps/oxlint/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxlint" -version = "0.11.1" +version = "0.13.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/apps/oxlint/fixtures/flow/flow.js b/apps/oxlint/fixtures/flow/flow.js new file mode 100644 index 0000000000000..7b9f49cc147ac --- /dev/null +++ b/apps/oxlint/fixtures/flow/flow.js @@ -0,0 +1,2 @@ +// @flow +import { type Node, type ElementRef } from 'react'; diff --git a/apps/oxlint/fixtures/flow/index.mjs b/apps/oxlint/fixtures/flow/index.mjs new file mode 100644 index 0000000000000..307a5e3cdf37e --- /dev/null +++ b/apps/oxlint/fixtures/flow/index.mjs @@ -0,0 +1 @@ +import './flow.js' diff --git a/apps/oxlint/fixtures/overrides/directories-config.json b/apps/oxlint/fixtures/overrides/directories-config.json new file mode 100644 index 0000000000000..02c53eda8fcea --- /dev/null +++ b/apps/oxlint/fixtures/overrides/directories-config.json @@ -0,0 +1,19 @@ +{ + "rules": { + "no-debugger": "off" + }, + "overrides": [ + { + "files": ["lib/*.{js,ts}", "src/*"], + "rules": { + "no-debugger": "error" + } + }, + { + "files": ["**/tests/*"], + "rules": { + "no-debugger": "warn" + } + } + ] +} diff --git a/apps/oxlint/fixtures/overrides/lib/index.ts b/apps/oxlint/fixtures/overrides/lib/index.ts new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/fixtures/overrides/lib/index.ts @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/fixtures/overrides/lib/tests/index.js b/apps/oxlint/fixtures/overrides/lib/tests/index.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/fixtures/overrides/lib/tests/index.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/fixtures/overrides/src/oxlint.js b/apps/oxlint/fixtures/overrides/src/oxlint.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/fixtures/overrides/src/oxlint.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/fixtures/overrides/src/tests/index.js b/apps/oxlint/fixtures/overrides/src/tests/index.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/fixtures/overrides/src/tests/index.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 52049828c588b..9cd252be2eba5 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -1,4 +1,4 @@ -use std::{env, io::BufWriter, time::Instant}; +use std::{env, io::BufWriter, path::PathBuf, time::Instant}; use ignore::gitignore::Gitignore; use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler}; @@ -18,13 +18,14 @@ use crate::{ pub struct LintRunner { options: LintCommand, + cwd: PathBuf, } impl Runner for LintRunner { type Options = LintCommand; fn new(options: Self::Options) -> Self { - Self { options } + Self { options, cwd: env::current_dir().expect("Failed to get current working directory") } } fn run(self) -> CliRunResult { @@ -51,6 +52,16 @@ impl Runner for LintRunner { let provided_path_count = paths.len(); let now = Instant::now(); + // append cwd to all paths + paths = paths + .into_iter() + .map(|x| { + let mut path_with_cwd = self.cwd.clone(); + path_with_cwd.push(x); + path_with_cwd + }) + .collect(); + // The ignore crate whitelists explicit paths, but priority // should be given to the ignore file. Many users lint // automatically and pass a list of changed files explicitly. @@ -72,13 +83,7 @@ impl Runner for LintRunner { }); } - if let Ok(cwd) = env::current_dir() { - paths.push(cwd); - } else { - return CliRunResult::InvalidOptions { - message: "Failed to get current working directory.".to_string(), - }; - } + paths.push(self.cwd.clone()); } let filter = match Self::get_filters(filter) { @@ -97,8 +102,6 @@ impl Runner for LintRunner { let number_of_files = paths.len(); - let cwd = std::env::current_dir().unwrap(); - let mut oxlintrc = if let Some(config_path) = basic_options.config.as_ref() { match Oxlintrc::from_file(config_path) { Ok(config) => config, @@ -129,8 +132,9 @@ impl Runner for LintRunner { }; } - let mut options = - LintServiceOptions::new(cwd, paths).with_cross_module(builder.plugins().has_import()); + let mut options = LintServiceOptions::new(self.cwd, paths) + .with_cross_module(builder.plugins().has_import()); + let linter = builder.build(); let tsconfig = basic_options.tsconfig; @@ -175,6 +179,12 @@ impl Runner for LintRunner { } impl LintRunner { + #[must_use] + pub fn with_cwd(mut self, cwd: PathBuf) -> Self { + self.cwd = cwd; + self + } + fn get_diagnostic_service( warning_options: &WarningOptions, output_options: &OutputOptions, @@ -235,6 +245,8 @@ impl LintRunner { #[cfg(all(test, not(target_os = "windows")))] mod test { + use std::env; + use super::LintRunner; use crate::cli::{lint_command, CliRunResult, LintResult, Runner}; @@ -248,6 +260,20 @@ mod test { } } + fn test_with_cwd(cwd: &str, args: &[&str]) -> LintResult { + let mut new_args = vec!["--silent"]; + new_args.extend(args); + let options = lint_command().run_inner(new_args.as_slice()).unwrap(); + + let mut current_cwd = env::current_dir().unwrap(); + current_cwd.push(cwd); + + match LintRunner::new(options).with_cwd(current_cwd).run() { + CliRunResult::LintResult(lint_result) => lint_result, + other => panic!("{other:?}"), + } + } + fn test_invalid_options(args: &[&str]) -> String { let mut new_args = vec!["--quiet"]; new_args.extend(args); @@ -279,6 +305,16 @@ mod test { assert_eq!(result.number_of_errors, 0); } + #[test] + fn cwd() { + let args = &["debugger.js"]; + let result = test_with_cwd("fixtures/linter", args); + assert!(result.number_of_rules > 0); + assert_eq!(result.number_of_files, 1); + assert_eq!(result.number_of_warnings, 1); + assert_eq!(result.number_of_errors, 0); + } + #[test] fn file() { let args = &["fixtures/linter/debugger.js"]; @@ -342,6 +378,15 @@ mod test { assert_eq!(result.number_of_errors, 0); } + #[test] + fn ignore_flow() { + let args = &["--import-plugin", "fixtures/flow/index.mjs"]; + let result = test(args); + assert_eq!(result.number_of_files, 1); + assert_eq!(result.number_of_warnings, 0); + assert_eq!(result.number_of_errors, 0); + } + #[test] fn filter_allow_all() { let args = &["-A", "all", "fixtures/linter"]; @@ -650,4 +695,13 @@ mod test { assert_eq!(result.number_of_warnings, 0); assert_eq!(result.number_of_errors, 1); } + + #[test] + fn test_overrides_directories() { + let result = + test(&["-c", "fixtures/overrides/directories-config.json", "fixtures/overrides"]); + assert_eq!(result.number_of_files, 7); + assert_eq!(result.number_of_warnings, 2); + assert_eq!(result.number_of_errors, 2); + } } diff --git a/crates/oxc/Cargo.toml b/crates/oxc/Cargo.toml index d17ce2122d716..a458b18246e5c 100644 --- a/crates/oxc/Cargo.toml +++ b/crates/oxc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc/src/napi/transform.rs b/crates/oxc/src/napi/transform.rs index d37ae5db6588a..3d761b2ccee3b 100644 --- a/crates/oxc/src/napi/transform.rs +++ b/crates/oxc/src/napi/transform.rs @@ -89,9 +89,11 @@ pub struct TransformOptions { pub inject: Option>>>, } -impl From for oxc_transformer::TransformOptions { - fn from(options: TransformOptions) -> Self { - Self { +impl TryFrom for oxc_transformer::TransformOptions { + type Error = String; + + fn try_from(options: TransformOptions) -> Result { + Ok(Self { cwd: options.cwd.map(PathBuf::from).unwrap_or_default(), typescript: options .typescript @@ -99,7 +101,7 @@ impl From for oxc_transformer::TransformOptions { .unwrap_or_default(), jsx: options.jsx.map(Into::into).unwrap_or_default(), ..Self::default() - } + }) } } diff --git a/crates/oxc_allocator/CHANGELOG.md b/crates/oxc_allocator/CHANGELOG.md index abc4ecfb2b648..c68b355bdc526 100644 --- a/crates/oxc_allocator/CHANGELOG.md +++ b/crates/oxc_allocator/CHANGELOG.md @@ -4,6 +4,12 @@ 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.37.0] - 2024-11-21 + +### Features + +- 39afb48 allocator: Introduce `Vec::from_array_in` (#7331) (overlookmotel) + ## [0.34.0] - 2024-10-26 ### Features diff --git a/crates/oxc_allocator/Cargo.toml b/crates/oxc_allocator/Cargo.toml index e8848f57785ee..90268a3050103 100644 --- a/crates/oxc_allocator/Cargo.toml +++ b/crates/oxc_allocator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_allocator" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_ast/CHANGELOG.md b/crates/oxc_ast/CHANGELOG.md index f86f7ba477c05..b8f1c95e12936 100644 --- a/crates/oxc_ast/CHANGELOG.md +++ b/crates/oxc_ast/CHANGELOG.md @@ -4,6 +4,42 @@ 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.37.0] - 2024-11-21 + +- f059b0e ast: [**BREAKING**] Add missing `ChainExpression` from `TSNonNullExpression` (#7377) (Boshen) + +- 41a0e60 ast: [**BREAKING**] Remove `impl GetAddress for Function` (#7343) (overlookmotel) + +- 44375a5 ast: [**BREAKING**] Rename `TSEnumMemberName` enum variants (#7250) (overlookmotel) + +### Features + +- 39afb48 allocator: Introduce `Vec::from_array_in` (#7331) (overlookmotel) +- 897d3b1 ast: Serialize StringLiterals to ESTree without `raw` (#7263) (ottomated) +- 224775c transformer: Transform object rest spread (#7003) (Boshen) +- 885e37f transformer: Optional Chaining (#6990) (Boshen) + +### Bug Fixes + + +### Performance + +- c84e892 ast: `AstBuilder::vec1` use `Vec::from_array_in` (#7334) (overlookmotel) + +### Documentation + +- f0affa2 ast: Improve docs examples for `PropertyDefinition` (#7287) (overlookmotel) +- 740ba4b ast: Correct doc comment for `StringLiteral` (#7255) (overlookmotel) + +### Refactor + +- de472ca ast: Move `StringLiteral` definition higher up (#7270) (overlookmotel) +- d3d58f8 ast: Remove `inherit_variants!` from `TSEnumMemberName` (#7248) (overlookmotel) + +### Styling + +- 10cdce9 ast: Add line break (#7271) (overlookmotel) + ## [0.36.0] - 2024-11-09 - b11ed2c ast: [**BREAKING**] Remove useless `ObjectProperty::init` field (#7220) (Boshen) diff --git a/crates/oxc_ast/Cargo.toml b/crates/oxc_ast/Cargo.toml index f2b1a88301303..9b8b954bb0ead 100644 --- a/crates/oxc_ast/Cargo.toml +++ b/crates/oxc_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_ast" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 4d6d7bf766f8e..d8b18db5d4775 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -950,6 +950,8 @@ inherit_variants! { #[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ContentHash, ESTree)] pub enum ChainElement<'a> { CallExpression(Box<'a, CallExpression<'a>>) = 0, + /// `foo?.baz!` or `foo?.[bar]!` + TSNonNullExpression(Box<'a, TSNonNullExpression<'a>>) = 1, // `MemberExpression` variants added here by `inherit_variants!` macro @inherit MemberExpression } diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index 60d4f5ee9a522..d7a9eb4951478 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -427,6 +427,15 @@ impl<'a> MemberExpression<'a> { } } + #[allow(missing_docs)] + pub fn object_mut(&mut self) -> &mut Expression<'a> { + match self { + MemberExpression::ComputedMemberExpression(expr) => &mut expr.object, + MemberExpression::StaticMemberExpression(expr) => &mut expr.object, + MemberExpression::PrivateFieldExpression(expr) => &mut expr.object, + } + } + #[allow(missing_docs)] pub fn static_property_name(&self) -> Option<&'a str> { match self { @@ -462,10 +471,9 @@ impl<'a> MemberExpression<'a> { #[allow(missing_docs)] pub fn through_optional_is_specific_member_access(&self, object: &str, property: &str) -> bool { let object_matches = match self.object().without_parentheses() { - Expression::ChainExpression(x) => match &x.expression { - ChainElement::CallExpression(_) => false, - match_member_expression!(ChainElement) => { - let member_expr = x.expression.to_member_expression(); + Expression::ChainExpression(x) => match x.expression.member_expression() { + None => false, + Some(member_expr) => { member_expr.object().without_parentheses().is_specific_id(object) } }, @@ -523,6 +531,19 @@ impl<'a> StaticMemberExpression<'a> { } } +impl<'a> ChainElement<'a> { + /// Returns the member expression. + pub fn member_expression(&self) -> Option<&MemberExpression<'a>> { + match self { + ChainElement::TSNonNullExpression(e) => match &e.expression { + match_member_expression!(Expression) => e.expression.as_member_expression(), + _ => None, + }, + _ => self.as_member_expression(), + } + } +} + impl<'a> CallExpression<'a> { #[allow(missing_docs)] pub fn callee_name(&self) -> Option<&str> { @@ -931,6 +952,16 @@ impl<'a> BindingPatternKind<'a> { matches!(self, Self::BindingIdentifier(_)) } + #[allow(missing_docs)] + pub fn is_object_pattern(&self) -> bool { + matches!(self, Self::ObjectPattern(_)) + } + + #[allow(missing_docs)] + pub fn is_array_pattern(&self) -> bool { + matches!(self, Self::ArrayPattern(_)) + } + #[allow(missing_docs)] pub fn is_assignment_pattern(&self) -> bool { matches!(self, Self::AssignmentPattern(_)) @@ -1011,16 +1042,6 @@ impl<'a> Function<'a> { } } -// FIXME: This is a workaround for we can't get current address by `TraverseCtx`, -// we will remove this once we support `TraverseCtx::current_address`. -// See: -impl GetAddress for Function<'_> { - #[inline] - fn address(&self) -> Address { - Address::from_ptr(self) - } -} - impl<'a> FormalParameters<'a> { /// Number of parameters bound in this parameter list. pub fn parameters_count(&self) -> usize { diff --git a/crates/oxc_ast/src/generated/ast_builder.rs b/crates/oxc_ast/src/generated/ast_builder.rs index 8e203cbbb1d0d..193e190787496 100644 --- a/crates/oxc_ast/src/generated/ast_builder.rs +++ b/crates/oxc_ast/src/generated/ast_builder.rs @@ -3092,6 +3092,22 @@ impl<'a> AstBuilder<'a> { ))) } + /// Build a [`ChainElement::TSNonNullExpression`] + /// + /// This node contains a [`TSNonNullExpression`] that will be stored in the memory arena. + /// + /// ## Parameters + /// - span: The [`Span`] covering this node + /// - expression + #[inline] + pub fn chain_element_ts_non_null_expression( + self, + span: Span, + expression: Expression<'a>, + ) -> ChainElement<'a> { + ChainElement::TSNonNullExpression(self.alloc(self.ts_non_null_expression(span, expression))) + } + /// Build a [`ParenthesizedExpression`]. /// /// If you want the built node to be allocated in the memory arena, use [`AstBuilder::alloc_parenthesized_expression`] instead. diff --git a/crates/oxc_ast/src/generated/derive_clone_in.rs b/crates/oxc_ast/src/generated/derive_clone_in.rs index 620ac93adb19f..a1278e1fd6bd2 100644 --- a/crates/oxc_ast/src/generated/derive_clone_in.rs +++ b/crates/oxc_ast/src/generated/derive_clone_in.rs @@ -1210,6 +1210,9 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for ChainElement<'old_alloc> { Self::CallExpression(it) => { ChainElement::CallExpression(CloneIn::clone_in(it, allocator)) } + Self::TSNonNullExpression(it) => { + ChainElement::TSNonNullExpression(CloneIn::clone_in(it, allocator)) + } Self::ComputedMemberExpression(it) => { ChainElement::ComputedMemberExpression(CloneIn::clone_in(it, allocator)) } diff --git a/crates/oxc_ast/src/generated/derive_content_eq.rs b/crates/oxc_ast/src/generated/derive_content_eq.rs index 2c1113a453601..e86fa1d9fb798 100644 --- a/crates/oxc_ast/src/generated/derive_content_eq.rs +++ b/crates/oxc_ast/src/generated/derive_content_eq.rs @@ -1310,6 +1310,10 @@ impl<'a> ContentEq for ChainElement<'a> { Self::CallExpression(other) if ContentEq::content_eq(it, other) => true, _ => false, }, + Self::TSNonNullExpression(it) => match other { + Self::TSNonNullExpression(other) if ContentEq::content_eq(it, other) => true, + _ => false, + }, Self::ComputedMemberExpression(it) => match other { Self::ComputedMemberExpression(other) if ContentEq::content_eq(it, other) => true, _ => false, diff --git a/crates/oxc_ast/src/generated/derive_content_hash.rs b/crates/oxc_ast/src/generated/derive_content_hash.rs index 2c2756b150105..b3ad44c59f97e 100644 --- a/crates/oxc_ast/src/generated/derive_content_hash.rs +++ b/crates/oxc_ast/src/generated/derive_content_hash.rs @@ -641,6 +641,7 @@ impl<'a> ContentHash for ChainElement<'a> { ContentHash::content_hash(&discriminant(self), state); match self { Self::CallExpression(it) => ContentHash::content_hash(it, state), + Self::TSNonNullExpression(it) => ContentHash::content_hash(it, state), Self::ComputedMemberExpression(it) => ContentHash::content_hash(it, state), Self::StaticMemberExpression(it) => ContentHash::content_hash(it, state), Self::PrivateFieldExpression(it) => ContentHash::content_hash(it, state), diff --git a/crates/oxc_ast/src/generated/derive_estree.rs b/crates/oxc_ast/src/generated/derive_estree.rs index e4964b858642e..a1b369825816a 100644 --- a/crates/oxc_ast/src/generated/derive_estree.rs +++ b/crates/oxc_ast/src/generated/derive_estree.rs @@ -838,6 +838,7 @@ impl<'a> Serialize for ChainElement<'a> { fn serialize(&self, serializer: S) -> Result { match self { ChainElement::CallExpression(x) => Serialize::serialize(x, serializer), + ChainElement::TSNonNullExpression(x) => Serialize::serialize(x, serializer), ChainElement::ComputedMemberExpression(x) => Serialize::serialize(x, serializer), ChainElement::StaticMemberExpression(x) => Serialize::serialize(x, serializer), ChainElement::PrivateFieldExpression(x) => Serialize::serialize(x, serializer), diff --git a/crates/oxc_ast/src/generated/derive_get_span.rs b/crates/oxc_ast/src/generated/derive_get_span.rs index aab2a51ecb20a..d74eb2c546a49 100644 --- a/crates/oxc_ast/src/generated/derive_get_span.rs +++ b/crates/oxc_ast/src/generated/derive_get_span.rs @@ -609,6 +609,7 @@ impl<'a> GetSpan for ChainElement<'a> { fn span(&self) -> Span { match self { Self::CallExpression(it) => GetSpan::span(it.as_ref()), + Self::TSNonNullExpression(it) => GetSpan::span(it.as_ref()), Self::ComputedMemberExpression(it) => GetSpan::span(it.as_ref()), Self::StaticMemberExpression(it) => GetSpan::span(it.as_ref()), Self::PrivateFieldExpression(it) => GetSpan::span(it.as_ref()), diff --git a/crates/oxc_ast/src/generated/derive_get_span_mut.rs b/crates/oxc_ast/src/generated/derive_get_span_mut.rs index 46a1b7518b011..6b03140108034 100644 --- a/crates/oxc_ast/src/generated/derive_get_span_mut.rs +++ b/crates/oxc_ast/src/generated/derive_get_span_mut.rs @@ -609,6 +609,7 @@ impl<'a> GetSpanMut for ChainElement<'a> { fn span_mut(&mut self) -> &mut Span { match self { Self::CallExpression(it) => GetSpanMut::span_mut(&mut **it), + Self::TSNonNullExpression(it) => GetSpanMut::span_mut(&mut **it), Self::ComputedMemberExpression(it) => GetSpanMut::span_mut(&mut **it), Self::StaticMemberExpression(it) => GetSpanMut::span_mut(&mut **it), Self::PrivateFieldExpression(it) => GetSpanMut::span_mut(&mut **it), diff --git a/crates/oxc_ast/src/generated/visit.rs b/crates/oxc_ast/src/generated/visit.rs index a528d712d3737..f2293447ea194 100644 --- a/crates/oxc_ast/src/generated/visit.rs +++ b/crates/oxc_ast/src/generated/visit.rs @@ -2909,6 +2909,7 @@ pub mod walk { pub fn walk_chain_element<'a, V: Visit<'a>>(visitor: &mut V, it: &ChainElement<'a>) { match it { ChainElement::CallExpression(it) => visitor.visit_call_expression(it), + ChainElement::TSNonNullExpression(it) => visitor.visit_ts_non_null_expression(it), match_member_expression!(ChainElement) => { visitor.visit_member_expression(it.to_member_expression()) } diff --git a/crates/oxc_ast/src/generated/visit_mut.rs b/crates/oxc_ast/src/generated/visit_mut.rs index 47d62f6254762..6b0e840445d8b 100644 --- a/crates/oxc_ast/src/generated/visit_mut.rs +++ b/crates/oxc_ast/src/generated/visit_mut.rs @@ -3033,6 +3033,7 @@ pub mod walk_mut { pub fn walk_chain_element<'a, V: VisitMut<'a>>(visitor: &mut V, it: &mut ChainElement<'a>) { match it { ChainElement::CallExpression(it) => visitor.visit_call_expression(it), + ChainElement::TSNonNullExpression(it) => visitor.visit_ts_non_null_expression(it), match_member_expression!(ChainElement) => { visitor.visit_member_expression(it.to_member_expression_mut()) } diff --git a/crates/oxc_ast_macros/Cargo.toml b/crates/oxc_ast_macros/Cargo.toml index be43f079a4c08..7a4a99115864e 100644 --- a/crates/oxc_ast_macros/Cargo.toml +++ b/crates/oxc_ast_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_ast_macros" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_ast_macros/src/lib.rs b/crates/oxc_ast_macros/src/lib.rs index 1dee384057a01..57f6a9584eb71 100644 --- a/crates/oxc_ast_macros/src/lib.rs +++ b/crates/oxc_ast_macros/src/lib.rs @@ -4,74 +4,73 @@ use syn::{parse_macro_input, Item}; mod ast; /// This attribute serves two purposes. -/// First, it is a marker for our `ast_tools` to detect AST types. +/// First, it is a marker for `ast_tools`, to understand AST types. /// Secondly, it generates the following code: /// -/// * Prepend `#[repr(C)]` to structs -/// * Prepend `#[repr(C, u8)]` to fieldful enums e.g. `enum E { X: u32, Y: u8 }` -/// * Prepend `#[repr(u8)]` to unit (fieldless) enums e.g. `enum E { X, Y, Z, }` -/// * Prepend `#[derive(oxc_ast_macros::Ast)]` to all structs and enums +/// * Prepend `#[repr(C)]` to structs. +/// * Prepend `#[repr(C, u8)]` to fieldful enums e.g. `enum E { X: u32, Y: u8 }`. +/// * Prepend `#[repr(u8)]` to unit (fieldless) enums e.g. `enum E { X, Y, Z, }`. +/// * Prepend `#[derive(oxc_ast_macros::Ast)]` to all structs and enums. /// * Add assertions that traits used in `#[generate_derive(...)]` are in scope. /// -/// It also allows the usage of these helper attributes via deriving a "no-op" derive macro. +/// It allows the usage of these helper attributes via deriving the `Ast` no-op derive macro: /// -/// # Generator Attributes: +/// # Generator Attributes /// -/// ## `#[scope(...)]`: +/// ## `#[scope(...)]` /// /// This attribute can be used in 2 places: -/// ### On `struct`/`enum` items: +/// +/// ### On `struct`s / `enum`s /// When this attribute comes before an AST type definition it accepts 3 optional arguments. -/// 1. `flags(expr)`: It accepts an expression that would evaluate to `ScopeFlags`. It is used to annotate scope flags of the AST type. +/// 1. `flags(expr)`: It accepts an expression that would evaluate to `ScopeFlags`. +/// It is used to annotate scope flags of the AST type. /// 2. `if(expr)`: It accepts an expression that would evaluate to `bool` used for conditional scope creation. -/// 3. `strict_if(expr)`: It accepts an expression that would evaluate to `bool`, If this value is `true` the created scope would be `strict`. +/// 3. `strict_if(expr)`: It accepts an expression that would evaluate to `bool`. +/// If this value is `true` the created scope would be `strict`. /// -/// NOTE: All these expressions can use `self` to access the current node they are getting executed on via an immutable reference. +/// NOTE: All these expressions can use `self` to access the current node they are getting executed on +/// via an immutable reference. /// -/// ### On `struct` fields: -/// At this position this attribute can only have one shape: `#[scope(enter_before)]`. -/// It marks where `Visit::enter_scope` events should be fired for this AST type. +/// ### On `struct` fields +/// At this position this attribute can only have one shape: `#[scope(enter_before)]` / `#[scope(exit_before)]`. +/// It marks where `Visit::enter_scope` and `Visit::exit_scope` events should be fired for this AST type. /// -/// ## `#[visit(...)]`: +/// ## `#[visit(args(arg = expr))]` /// -/// This attribute can only occur on `struct` fields, Or `enum` attributes. -/// It accepts 4 optional arguments. -/// 1. `as(ident)`: It accepts an identifier, our generators would treat the type of this field/variant as if they were called as the given identifier. -/// 2. `args(arg = expr)`: It accepts an argument name and an expression. Currently it only -/// accepts one argument. -/// a. `args(flags = expr)`: `expr` is an expression that would evaluate to `ScopeFlags`, This argument can only be used at places where the AST type is `Function`. -/// 3. `enter_before`: It marks where this AST type should fire `Visit::enter_node` events. -/// 4. `ignore`: It would ignore this field/variant in visits. +/// This attribute can only occur on `struct` fields, or `enum` variants. +/// Accepts an argument name and an expression. +/// `expr` is an expression that would evaluate to `ScopeFlags`. +/// This argument can only be used at places where the AST type is `Function`. /// -/// ## `#[span]`: +/// ## `#[span]` /// -/// This attribute can be used to hint to the `ast_tools` which field should be used to obtain the span of this AST type. +/// This attribute can be used to hint to `ast_tools` which field should be used to obtain the span +/// of this AST type. /// /// ## `#[generate_derive(...)]` /// -/// This attribute has the same spirit as the `#[derive(...)]` macro, It is used to derive traits for the types. -/// However, Instead of expanding the derive at compile-time, We do this process on PR submits via `ast_tools` code generation. -/// These derived implementations would be output in the `crates/oxc_ast/src/generated` directory. +/// This attribute has the same purpose as Rust's `#[derive(...)]` macro. +/// It is used to derive traits for the types. +/// However, instead of expanding the derive at compile-time, we generate the derived code at build time +/// via `ast_tools` code generation. +/// These derived implementations are output as `src/generated/derive_*.rs` in the crate the type is +/// defined in. /// -/// ## `#[ts]`: +/// ## `#[ts]` /// /// Marks a struct field as only relevant for TypeScript ASTs. /// -/// # Derive Helper Attributes: +/// # Derive Helper Attributes /// -/// These are helper attributes that are only meaningful when their respective trait is derived via `generate_derive`. +/// These are helper attributes that are only meaningful when their respective trait is derived +/// via `generate_derive`. /// /// ## `#[clone_in(default)]` /// /// This attribute is only used by `CloneIn` derive. -/// `struct` fields marked with this attribute at cloning will use the `Default::default()` value instead of `CloneIn::clone_in` to initialize. -/// -/// # Mocked attributes: -/// -/// These are just here to remove the need for boilerplate `#[cfg_attr(...)]`. If their actual trait is derived they would consume these, Otherwise, Our mock attributes will prevent compile errors. -/// -/// 1. `serde` -/// 2. `tsify` +/// `struct` fields marked with this attribute at cloning will use the `Default::default()` value +/// instead of `CloneIn::clone_in` to initialize. #[proc_macro_attribute] pub fn ast(_args: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as Item); @@ -82,13 +81,10 @@ pub fn ast(_args: TokenStream, input: TokenStream) -> TokenStream { /// Dummy derive macro for a non-existent trait `Ast`. /// /// Does not generate any code. -/// The only purpose is to allow the occurrence of helper attributes used with the `tasks/ast_tools`. +/// Its only purpose is to allow the occurrence of helper attributes used in `tasks/ast_tools`. /// /// Read [`macro@ast`] for further details. -#[proc_macro_derive( - Ast, - attributes(scope, visit, span, generate_derive, clone_in, estree, tsify, ts) -)] +#[proc_macro_derive(Ast, attributes(scope, visit, span, generate_derive, clone_in, estree, ts))] pub fn ast_derive(_input: TokenStream) -> TokenStream { TokenStream::new() } diff --git a/crates/oxc_cfg/CHANGELOG.md b/crates/oxc_cfg/CHANGELOG.md index 6744c8367b678..b11cad62fea85 100644 --- a/crates/oxc_cfg/CHANGELOG.md +++ b/crates/oxc_cfg/CHANGELOG.md @@ -4,6 +4,12 @@ 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.37.0] - 2024-11-21 + +### Features + +- 8cfea3c oxc_cfg: Add implicit return instruction (#5568) (IWANABETHATGUY) + ## [0.31.0] - 2024-10-08 - 95ca01c cfg: [**BREAKING**] Make BasicBlock::unreachable private (#6321) (DonIsaac) diff --git a/crates/oxc_cfg/Cargo.toml b/crates/oxc_cfg/Cargo.toml index 174a12d3569d4..a572635a2c70e 100644 --- a/crates/oxc_cfg/Cargo.toml +++ b/crates/oxc_cfg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_cfg" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_cfg/src/block.rs b/crates/oxc_cfg/src/block.rs index 9d94e065c896d..b80a84304f82d 100644 --- a/crates/oxc_cfg/src/block.rs +++ b/crates/oxc_cfg/src/block.rs @@ -47,6 +47,7 @@ impl Instruction { pub enum InstructionKind { Unreachable, Statement, + ImplicitReturn, Return(ReturnInstructionKind), Break(LabeledInstruction), Continue(LabeledInstruction), diff --git a/crates/oxc_cfg/src/builder/mod.rs b/crates/oxc_cfg/src/builder/mod.rs index c325bbe264ea8..201edb4e30960 100644 --- a/crates/oxc_cfg/src/builder/mod.rs +++ b/crates/oxc_cfg/src/builder/mod.rs @@ -111,8 +111,12 @@ impl<'a> ControlFlowGraphBuilder<'a> { self.push_instruction(InstructionKind::Statement, Some(stmt)); } - pub fn push_return(&mut self, kind: ReturnInstructionKind, node: NodeId) { - self.push_instruction(InstructionKind::Return(kind), Some(node)); + pub fn push_return(&mut self, kind: ReturnInstructionKind, node: Option) { + self.push_instruction(InstructionKind::Return(kind), node); + } + + pub fn push_implicit_return(&mut self) { + self.push_instruction(InstructionKind::ImplicitReturn, None); } /// Creates and push a new `BasicBlockId` onto `self.error_path` stack. diff --git a/crates/oxc_cfg/src/dot.rs b/crates/oxc_cfg/src/dot.rs index 9076aab4d85b7..b868716a3df6e 100644 --- a/crates/oxc_cfg/src/dot.rs +++ b/crates/oxc_cfg/src/dot.rs @@ -78,6 +78,7 @@ impl DisplayDot for Instruction { InstructionKind::Return(ReturnInstructionKind::ImplicitUndefined) => { "return " } + InstructionKind::ImplicitReturn => "return", InstructionKind::Return(ReturnInstructionKind::NotImplicitUndefined) => { "return " } diff --git a/crates/oxc_codegen/CHANGELOG.md b/crates/oxc_codegen/CHANGELOG.md index 5ffa81a25dc8d..ac391e601fc68 100644 --- a/crates/oxc_codegen/CHANGELOG.md +++ b/crates/oxc_codegen/CHANGELOG.md @@ -4,6 +4,27 @@ 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.37.0] - 2024-11-21 + +- f059b0e ast: [**BREAKING**] Add missing `ChainExpression` from `TSNonNullExpression` (#7377) (Boshen) + +- 44375a5 ast: [**BREAKING**] Rename `TSEnumMemberName` enum variants (#7250) (overlookmotel) + +### Features + +- 82773cb codegen: Remove underscore from bigint (#7367) (Boshen) + +### Bug Fixes + +- c587dd3 codegen: Do not print parenthesis for `in` expression in ArrowFunctionExpression (#7360) (Dunqing) +- a0766e6 codegen: Fix arithmetic overflow printing unspanned nodes (#7292) (overlookmotel) +- 33ec4e6 codegen: Fix arithmetic overflow printing unspanned `NewExpression` (#7289) (overlookmotel) +- 1282221 codegen: Print comments when block is empty (#7236) (Boshen) + +### Refactor + +- 58db9ef codegen: Do not print unnecessary parentheses if both sides use the same logical operator (#7325) (Dunqing) + ## [0.36.0] - 2024-11-09 - 0e4adc1 ast: [**BREAKING**] Remove invalid expressions from `TSEnumMemberName` (#7219) (Boshen) diff --git a/crates/oxc_codegen/Cargo.toml b/crates/oxc_codegen/Cargo.toml index d9f721e221dfb..ad112030c3f13 100644 --- a/crates/oxc_codegen/Cargo.toml +++ b/crates/oxc_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_codegen" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_codegen/src/binary_expr_visitor.rs b/crates/oxc_codegen/src/binary_expr_visitor.rs index ca4b22527a37a..f5417ad4883c3 100644 --- a/crates/oxc_codegen/src/binary_expr_visitor.rs +++ b/crates/oxc_codegen/src/binary_expr_visitor.rs @@ -46,6 +46,12 @@ pub enum BinaryishOperator { Logical(LogicalOperator), } +impl BinaryishOperator { + fn is_binary(self) -> bool { + matches!(self, Self::Binary(_)) + } +} + fn print_binary_operator(op: BinaryOperator, p: &mut Codegen) { let operator = op.as_str(); if op.is_keyword() { @@ -148,9 +154,15 @@ impl<'a> BinaryExpressionVisitor<'a> { pub fn check_and_prepare(&mut self, p: &mut Codegen) -> bool { let e = self.e; - self.operator = e.operator(); - self.wrap = self.precedence >= self.operator.precedence() + // We don't need to print parentheses if both sides use the same logical operator + // For example: `(a && b) && c` should be printed as `a && b && c` + // ^^ e.operator() ^^ self.operator + let precedence_check = self.precedence >= e.operator().precedence() + && (self.operator.is_binary() || self.precedence != self.operator.precedence()); + + self.operator = e.operator(); + self.wrap = precedence_check || (self.operator == BinaryishOperator::Binary(BinaryOperator::In) && self.ctx.intersects(Context::FORBID_IN)); diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index a7f1cc4d9b8d7..56a88dbe0d4e7 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -1163,12 +1163,14 @@ impl<'a> GenExpr for NumericLiteral<'a> { impl<'a> Gen for BigIntLiteral<'a> { fn gen(&self, p: &mut Codegen, _ctx: Context) { - if self.raw.starts_with('-') { + let raw = self.raw.as_str().cow_replace('_', ""); + if raw.starts_with('-') { p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation)); } + p.print_space_before_identifier(); p.add_source_mapping(self.span.start); - p.print_str(self.raw.as_str()); + p.print_str(&raw); } } @@ -1623,7 +1625,7 @@ impl<'a> GenExpr for ArrowFunctionExpression<'a> { if self.expression { if let Some(Statement::ExpressionStatement(stmt)) = &self.body.statements.first() { p.start_of_arrow_expr = p.code_len(); - stmt.expression.print_expr(p, Precedence::Comma, ctx.and_forbid_in(true)); + stmt.expression.print_expr(p, Precedence::Comma, ctx); } } else { self.body.print(p, ctx); @@ -2047,6 +2049,7 @@ impl<'a> GenExpr for ChainExpression<'a> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { p.wrap(precedence >= Precedence::Postfix, |p| match &self.expression { ChainElement::CallExpression(expr) => expr.print_expr(p, precedence, ctx), + ChainElement::TSNonNullExpression(expr) => expr.print_expr(p, precedence, ctx), match_member_expression!(ChainElement) => { self.expression.to_member_expression().print_expr(p, precedence, ctx); } diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index 2dc4bd1e7cd9d..dae518dd5bcab 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -174,8 +174,8 @@ fn conditional() { fn coalesce() { test_minify("a ?? b", "a??b;"); test_minify("a ?? b ?? c ?? d", "a??b??c??d;"); - test_minify("a ?? (b ?? (c ?? d))", "a??(b??(c??d));"); - test_minify("(a ?? (b ?? (c ?? d)))", "a??(b??(c??d));"); + test_minify("a ?? (b ?? (c ?? d))", "a??b??c??d;"); + test_minify("(a ?? (b ?? (c ?? d)))", "a??b??c??d;"); test_minify("a, b ?? c", "a,b??c;"); test_minify("(a, b) ?? c", "(a,b)??c;"); test_minify("a, b ?? c, d", "a,b??c,d;"); @@ -188,8 +188,8 @@ fn coalesce() { #[test] fn logical_or() { test_minify("a || b || c", "a||b||c;"); - test_minify("(a || (b || c)) || d", "a||(b||c)||d;"); - test_minify("a || (b || (c || d))", "a||(b||(c||d));"); + test_minify("(a || (b || c)) || d", "a||b||c||d;"); + test_minify("a || (b || (c || d))", "a||b||c||d;"); test_minify("a || b && c", "a||b&&c;"); test_minify("(a || b) && c", "(a||b)&&c;"); test_minify("a, b || c, d", "a,b||c,d;"); @@ -201,7 +201,7 @@ fn logical_or() { #[test] fn logical_and() { test_minify("a && b && c", "a&&b&&c;"); - test_minify("a && ((b && c) && d)", "a&&(b&&c&&d);"); + test_minify("a && ((b && c) && d)", "a&&b&&c&&d;"); test_minify("((a && b) && c) && d", "a&&b&&c&&d;"); test_minify("(a || b) && (c || d)", "(a||b)&&(c||d);"); test_minify("a, b && c, d", "a,b&&c,d;"); @@ -294,3 +294,75 @@ fn in_expr_in_sequence_in_for_loop_init() { "for ((\"hidden\" in a) && (m = a.hidden), r = 0; s > r; r++) {}\n", ); } + +#[test] +fn in_expr_in_arrow_function_expression() { + test("() => ('foo' in bar)", "() => \"foo\" in bar;\n"); + test("() => 'foo' in bar", "() => \"foo\" in bar;\n"); + test("() => { ('foo' in bar) }", "() => {\n\t\"foo\" in bar;\n};\n"); +} + +#[test] +fn big_int() { + test("9007199254740991n;", "9007199254740991n;\n"); + test("-9007199254740991n;", "-9007199254740991n;\n"); + test("-90_0719_92547_40991n;", "-9007199254740991n;\n"); + test("+9007199254740991n;", "+9007199254740991n;\n"); + test("1000n", "1000n;\n"); + test("-15n", "-15n;\n"); + + test("100_000_000n;", "100000000n;\n"); + test("10000000000000000n;", "10000000000000000n;\n"); + test("0n;", "0n;\n"); + test("+0n;", "+0n;\n"); + test("-0n;", "-0n;\n"); + + test("0x1_0n;", "0x10n;\n"); + test("0x10n;", "0x10n;\n"); + + test("0b1_01n;", "0b101n;\n"); + test("0b101n;", "0b101n;\n"); + test("0b101_101n;", "0b101101n;\n"); + test("0b10_1n", "0b101n;\n"); + + test("0o13n;", "0o13n;\n"); + test("0o7n", "0o7n;\n"); + + test("0x2_0n", "0x20n;\n"); + test("0xfabn", "0xfabn;\n"); + test("0xaef_en;", "0xaefen;\n"); + test("0xaefen;", "0xaefen;\n"); +} + +#[test] +#[ignore = "Minify bigint is not implemented."] +fn big_int_minify() { + test_minify("9007199254740991n", "9007199254740991n;"); + test_minify("-9007199254740991n;", "-9007199254740991n;"); + test_minify("-90_0719_92547_40991n;", "-9007199254740991n;"); + test_minify("+9007199254740991n;", "+9007199254740991n;"); + test_minify("1000n", "1000n;"); + test_minify("-15n", "-15n;"); + + test_minify("100_000_000n;", "100000000n;"); + test_minify("10000000000000000n;", "0x2386f26fc10000n;"); + test_minify("0n;", "0n;"); + test_minify("+0n;", "+0n;"); + test_minify("-0n;", "-0n;"); + + test_minify("0x1_0n;", "16n;"); + test_minify("0x10n;", "16n;"); + + test_minify("0b1_01n;", "5n;"); + test_minify("0b101n;", "5n;"); + test_minify("0b101_101n;", "45n;"); + test_minify("0b10_1n", "5n;"); + + test_minify("0o13n;", "11n;"); + test_minify("0o7n", "7n;"); + + test_minify("0x2_0n", "32n;"); + test_minify("0xfabn", "4011n;"); + test_minify("0xaef_en;", "44798n;"); + test_minify("0xaefen;", "44798n;"); +} diff --git a/crates/oxc_data_structures/CHANGELOG.md b/crates/oxc_data_structures/CHANGELOG.md index 43444012588c3..2e8900d203fd2 100644 --- a/crates/oxc_data_structures/CHANGELOG.md +++ b/crates/oxc_data_structures/CHANGELOG.md @@ -4,6 +4,12 @@ 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.37.0] - 2024-11-21 + +### Features + +- d135d3e data_structures: Add methods to `SparseStack` (#7305) (overlookmotel) + ## [0.35.0] - 2024-11-04 ### Performance diff --git a/crates/oxc_data_structures/Cargo.toml b/crates/oxc_data_structures/Cargo.toml index a47119f8c2cf6..177742489b0d6 100644 --- a/crates/oxc_data_structures/Cargo.toml +++ b/crates/oxc_data_structures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_data_structures" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_diagnostics/CHANGELOG.md b/crates/oxc_diagnostics/CHANGELOG.md index e924881de05f9..6a352da1234c2 100644 --- a/crates/oxc_diagnostics/CHANGELOG.md +++ b/crates/oxc_diagnostics/CHANGELOG.md @@ -4,6 +4,12 @@ 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.37.0] - 2024-11-21 + +### Features + +- 2268a0e linter: Support `overrides` config field (#6974) (DonIsaac) + ## [0.36.0] - 2024-11-09 ### Features diff --git a/crates/oxc_diagnostics/Cargo.toml b/crates/oxc_diagnostics/Cargo.toml index b9271a535559a..c89a01a820440 100644 --- a/crates/oxc_diagnostics/Cargo.toml +++ b/crates/oxc_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_diagnostics" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_ecmascript/Cargo.toml b/crates/oxc_ecmascript/Cargo.toml index d9ec84cfb598a..4eb97ab4e3779 100644 --- a/crates/oxc_ecmascript/Cargo.toml +++ b/crates/oxc_ecmascript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_ecmascript" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_estree/Cargo.toml b/crates/oxc_estree/Cargo.toml index 23327e8146741..d9c4f5edc2efa 100644 --- a/crates/oxc_estree/Cargo.toml +++ b/crates/oxc_estree/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_estree" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_index/Cargo.toml b/crates/oxc_index/Cargo.toml index 3cc359a0c3550..6272bd6839ad4 100644 --- a/crates/oxc_index/Cargo.toml +++ b/crates/oxc_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_index" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_isolated_declarations/CHANGELOG.md b/crates/oxc_isolated_declarations/CHANGELOG.md index 5b3d7bcf886bc..e27d37cfa82c0 100644 --- a/crates/oxc_isolated_declarations/CHANGELOG.md +++ b/crates/oxc_isolated_declarations/CHANGELOG.md @@ -4,6 +4,19 @@ 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.37.0] - 2024-11-21 + +- 44375a5 ast: [**BREAKING**] Rename `TSEnumMemberName` enum variants (#7250) (overlookmotel) + +### Features + +- 39afb48 allocator: Introduce `Vec::from_array_in` (#7331) (overlookmotel) +- 82773cb codegen: Remove underscore from bigint (#7367) (Boshen) + +### Refactor + +- 1938a1d isolated_declarations: Do not copy `Vec` unnecessarily (#7332) (overlookmotel) + ## [0.36.0] - 2024-11-09 - 0e4adc1 ast: [**BREAKING**] Remove invalid expressions from `TSEnumMemberName` (#7219) (Boshen) diff --git a/crates/oxc_isolated_declarations/Cargo.toml b/crates/oxc_isolated_declarations/Cargo.toml index 84e38f2cb8eb5..c00b693f96ef4 100644 --- a/crates/oxc_isolated_declarations/Cargo.toml +++ b/crates/oxc_isolated_declarations/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_isolated_declarations" -version = "0.36.0" +version = "0.37.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_isolated_declarations/tests/snapshots/as-const.snap b/crates/oxc_isolated_declarations/tests/snapshots/as-const.snap index edb2066d8a11e..db05afc0f607d 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/as-const.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/as-const.snap @@ -1,6 +1,7 @@ --- source: crates/oxc_isolated_declarations/tests/mod.rs input_file: crates/oxc_isolated_declarations/tests/fixtures/as-const.ts +snapshot_kind: text --- ``` ==================== .D.TS ==================== @@ -9,7 +10,7 @@ declare const F: { readonly string: "string"; readonly templateLiteral: "templateLiteral"; readonly number: 1.23; - readonly bigint: -1_2_3n; + readonly bigint: -123n; readonly boolean: true; readonly null: null; readonly undefined: undefined; diff --git a/crates/oxc_isolated_declarations/tests/snapshots/infer-expression.snap b/crates/oxc_isolated_declarations/tests/snapshots/infer-expression.snap index 6a49c344a90bf..3ce34e4905d03 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/infer-expression.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/infer-expression.snap @@ -1,6 +1,7 @@ --- source: crates/oxc_isolated_declarations/tests/mod.rs input_file: crates/oxc_isolated_declarations/tests/fixtures/infer-expression.ts +snapshot_kind: text --- ``` ==================== .D.TS ==================== @@ -10,7 +11,7 @@ declare const s: string; declare const t: string; declare const b: boolean; declare let unaryA: number; -declare const unaryB = -1_2n; +declare const unaryB = -12n; declare const unaryC: unknown; declare const unaryD: unknown; declare const unaryE: {}; diff --git a/crates/oxc_linter/CHANGELOG.md b/crates/oxc_linter/CHANGELOG.md index 50248a5f74369..cb98251d2976e 100644 --- a/crates/oxc_linter/CHANGELOG.md +++ b/crates/oxc_linter/CHANGELOG.md @@ -4,6 +4,69 @@ 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.13.0] - 2024-11-21 + +- f059b0e ast: [**BREAKING**] Add missing `ChainExpression` from `TSNonNullExpression` (#7377) (Boshen) + +- 878189c parser,linter: [**BREAKING**] Add `ParserReturn::is_flow_language`; linter ignore flow error (#7373) (Boshen) + +- 7bf970a linter: [**BREAKING**] Remove tree_shaking plugin (#7372) (Boshen) + +### Features + +- 7f8747d linter: Implement `react/no-array-index-key` (#6960) (BitterGourd) +- be152c0 linter: Add `typescript/no-require-imports` rule (#7315) (Dmitry Zakharov) +- 849489e linter: Add suggestion for no-console (#4312) (DonIsaac) +- 8cebdc8 linter: Allow appending plugins in override (#7379) (camchenry) +- 8cfea3c oxc_cfg: Add implicit return instruction (#5568) (IWANABETHATGUY) +- e6922df parser: Fix incorrect AST for `x?.f()` (#7387) (Boshen) + +### Bug Fixes + +- e91c287 linter: Fix panic in react/no-array-index-key (#7395) (Boshen) +- a32f5a7 linter/no-array-index-key: Compile error due to it uses a renamed API (#7391) (Dunqing) +- 666b6c1 parser: Add missing `ChainExpression` in optional `TSInstantiationExpression` (#7371) (Boshen) + +### Documentation + +- df143ca linter: Add docs for config settings (#4827) (DonIsaac) +- ad44cfa linter: Import/first options (#7381) (Zak) + +### Refactor + +- c34d649 linter: Use `scope_id` etc methods (#7394) (overlookmotel) + +## [0.12.0] - 2024-11-20 + +- 20d9080 linter: [**BREAKING**] Override plugins array when passed in config file (#7303) (camchenry) + +- 44375a5 ast: [**BREAKING**] Rename `TSEnumMemberName` enum variants (#7250) (overlookmotel) + +### Features + +- 1d9f528 linter: Implement `unicorn/prefer-string-raw` lint rule (#7335) (Ryan Walker) +- d445e0f linter: Implement `unicorn/consistent-existence-index-check` (#7262) (Ryan Walker) +- 01ddf37 linter: Add `allowReject` option to `no-useless-promise-resolve-reject` (#7274) (no-yan) +- 755a31b linter: Support bind function case for compatibility with `promise/no-return-wrap` (#7232) (no-yan) +- 428770e linter: Add `import/no-namespace` rule (#7229) (Dmitry Zakharov) +- 9c91151 linter: Implement typescript/no-empty-object-type (#6977) (Orenbek) +- 2268a0e linter: Support `overrides` config field (#6974) (DonIsaac) +- 3dcac1a linter: React/exhaustive-deps (#7151) (camc314) + +### Bug Fixes + +- bc0e72c linter: Handle user variables correctly for import/no_commonjs (#7316) (Dmitry Zakharov) +- bf839c1 linter: False positive in `jest/expect-expect` (#7341) (dalaoshu) +- ff2a1d4 linter: Move `exhaustive-deps` to `react` (#7251) (camc314) +- df5c535 linter: Revert unmatched rule error (#7257) (Cameron A McHenry) +- c4ed230 linter: Fix false positive in eslint/no-cond-assign (#7241) (camc314) +- ef847da linter: False positive in `jsx-a11y/iframe-has-title` (#7253) (dalaoshu) +- 62b6327 linter: React/exhaustive-deps update span for unknown deps diagnostic (#7249) (camc314) + +### Refactor + +- c6a4868 linter: Temporarily remove unknown rules checking (#7260) (camchenry) + ## [0.11.1] - 2024-11-09 - 0e4adc1 ast: [**BREAKING**] Remove invalid expressions from `TSEnumMemberName` (#7219) (Boshen) diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 50a9006ba030a..346be12d5b095 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_linter" -version = "0.11.1" +version = "0.13.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_linter/src/ast_util.rs b/crates/oxc_linter/src/ast_util.rs index be05b2ba1bcad..983b921711568 100644 --- a/crates/oxc_linter/src/ast_util.rs +++ b/crates/oxc_linter/src/ast_util.rs @@ -295,10 +295,7 @@ pub fn get_symbol_id_of_variable( ident: &IdentifierReference, semantic: &Semantic<'_>, ) -> Option { - let symbol_table = semantic.symbols(); - let reference_id = ident.reference_id.get()?; - let reference = symbol_table.get_reference(reference_id); - reference.symbol_id() + semantic.symbols().get_reference(ident.reference_id()).symbol_id() } pub fn extract_regex_flags<'a>( @@ -344,9 +341,9 @@ pub fn is_method_call<'a>( let callee_without_parentheses = call_expr.callee.without_parentheses(); let member_expr = match callee_without_parentheses { match_member_expression!(Expression) => callee_without_parentheses.to_member_expression(), - Expression::ChainExpression(chain) => match chain.expression { - match_member_expression!(ChainElement) => chain.expression.to_member_expression(), - ChainElement::CallExpression(_) => return false, + Expression::ChainExpression(chain) => match chain.expression.member_expression() { + Some(e) => e, + None => return false, }, _ => return false, }; diff --git a/crates/oxc_linter/src/builder.rs b/crates/oxc_linter/src/builder.rs index 873c4179dc692..dfbe827274dd2 100644 --- a/crates/oxc_linter/src/builder.rs +++ b/crates/oxc_linter/src/builder.rs @@ -94,9 +94,10 @@ impl LinterBuilder { categories, rules: oxlintrc_rules, overrides, + path, } = oxlintrc; - let config = LintConfig { plugins, settings, env, globals }; + let config = LintConfig { plugins, settings, env, globals, path: Some(path) }; let options = LintOptions::default(); let rules = if start_empty { FxHashSet::default() } else { Self::warn_correctness(plugins) }; diff --git a/crates/oxc_linter/src/config/flat.rs b/crates/oxc_linter/src/config/flat.rs index f80020883391d..dea2b9129a6ac 100644 --- a/crates/oxc_linter/src/config/flat.rs +++ b/crates/oxc_linter/src/config/flat.rs @@ -92,8 +92,24 @@ impl ConfigStore { let mut overrides_to_apply: Vec = Vec::new(); let mut hasher = FxBuildHasher.build_hasher(); + // Compute the path of the file relative to the configuration file for glob matching. Globs should match + // relative to the location of the configuration file. + // - path: /some/path/like/this/to/file.js + // - config_path: /some/path/like/.oxlintrc.json + // => relative_path: this/to/file.js + // TODO: Handle nested configuration file paths. + let relative_path = if let Some(config_path) = &self.base.config.path { + if let Some(parent) = config_path.parent() { + path.strip_prefix(parent).unwrap_or(path) + } else { + path + } + } else { + path + }; + for (id, override_config) in self.overrides.iter_enumerated() { - if override_config.files.is_match(path) { + if override_config.files.is_match(relative_path) { overrides_to_apply.push(id); id.hash(&mut hasher); } @@ -113,12 +129,7 @@ impl ConfigStore { /// NOTE: this function must not borrow any entries from `self.cache` or DashMap will deadlock. fn apply_overrides(&self, override_ids: &[OverrideId]) -> ResolvedLinterState { - let plugins = self - .overrides - .iter() - .rev() - .find_map(|cfg| cfg.plugins) - .unwrap_or(self.base.config.plugins); + let mut plugins = self.base.config.plugins; let all_rules = RULES .iter() @@ -135,10 +146,14 @@ impl ConfigStore { let overrides = override_ids.iter().map(|id| &self.overrides[*id]); for override_config in overrides { - if override_config.rules.is_empty() { - continue; + if !override_config.rules.is_empty() { + override_config.rules.override_rules(&mut rules, &all_rules); + } + + // Append the override's plugins to the base list of enabled plugins. + if let Some(override_plugins) = override_config.plugins { + plugins |= override_plugins; } - override_config.rules.override_rules(&mut rules, &all_rules); } let rules = rules.into_iter().collect::>(); @@ -158,7 +173,10 @@ impl ConfigStore { #[cfg(test)] mod test { use super::{ConfigStore, OxlintOverrides}; - use crate::{config::LintConfig, AllowWarnDeny, LintPlugins, RuleEnum, RuleWithSeverity}; + use crate::{ + config::{LintConfig, OxlintEnv, OxlintGlobals, OxlintSettings}, + AllowWarnDeny, LintPlugins, RuleEnum, RuleWithSeverity, + }; macro_rules! from_json { ($json:tt) => { @@ -171,11 +189,6 @@ mod test { RuleWithSeverity::new(RuleEnum::NoExplicitAny(Default::default()), AllowWarnDeny::Warn) } - #[allow(clippy::default_trait_access)] - fn no_cycle() -> RuleWithSeverity { - RuleWithSeverity::new(RuleEnum::NoCycle(Default::default()), AllowWarnDeny::Warn) - } - /// an empty ruleset is a no-op #[test] fn test_no_rules() { @@ -219,26 +232,6 @@ mod test { ); } - /// removing plugins strips rules from those plugins, even if no rules are - /// added/removed explicitly - #[test] - fn test_no_rules_and_remove_plugins() { - let base_rules = vec![no_cycle()]; - let overrides = from_json!([{ - "files": ["*.test.{ts,tsx}"], - "plugins": ["jest"], - "rules": {} - }]); - let config = LintConfig { - plugins: LintPlugins::default() | LintPlugins::IMPORT, - ..LintConfig::default() - }; - let store = ConfigStore::new(base_rules, config, overrides); - - assert_eq!(store.resolve("App.tsx".as_ref()).rules.len(), 1); - assert_eq!(store.resolve("App.test.tsx".as_ref()).rules.len(), 0); - } - #[test] fn test_remove_rule() { let base_rules = vec![no_explicit_any()]; @@ -300,4 +293,37 @@ mod test { assert_eq!(src_app.len(), 1); assert_eq!(src_app[0].severity, AllowWarnDeny::Deny); } + + #[test] + fn test_add_plugins() { + let base_config = LintConfig { + plugins: LintPlugins::IMPORT, + env: OxlintEnv::default(), + settings: OxlintSettings::default(), + globals: OxlintGlobals::default(), + path: None, + }; + let overrides = from_json!([{ + "files": ["*.jsx", "*.tsx"], + "plugins": ["react"], + }, { + "files": ["*.ts", "*.tsx"], + "plugins": ["typescript"], + }]); + + let store = ConfigStore::new(vec![], base_config, overrides); + assert_eq!(store.base.config.plugins, LintPlugins::IMPORT); + + let app = store.resolve("other.mjs".as_ref()).config; + assert_eq!(app.plugins, LintPlugins::IMPORT); + + let app = store.resolve("App.jsx".as_ref()).config; + assert_eq!(app.plugins, LintPlugins::IMPORT | LintPlugins::REACT); + + let app = store.resolve("App.ts".as_ref()).config; + assert_eq!(app.plugins, LintPlugins::IMPORT | LintPlugins::TYPESCRIPT); + + let app = store.resolve("App.tsx".as_ref()).config; + assert_eq!(app.plugins, LintPlugins::IMPORT | LintPlugins::REACT | LintPlugins::TYPESCRIPT); + } } diff --git a/crates/oxc_linter/src/config/mod.rs b/crates/oxc_linter/src/config/mod.rs index cad1a00c72df1..54f012be7b769 100644 --- a/crates/oxc_linter/src/config/mod.rs +++ b/crates/oxc_linter/src/config/mod.rs @@ -8,6 +8,8 @@ mod plugins; mod rules; mod settings; +use std::path::PathBuf; + pub(crate) use self::flat::ResolvedLinterState; pub use self::{ env::OxlintEnv, @@ -29,6 +31,8 @@ pub(crate) struct LintConfig { pub(crate) env: OxlintEnv, /// Enabled or disabled specific global variables. pub(crate) globals: OxlintGlobals, + /// Absolute path to the configuration file (may be `None` if there is no file). + pub(crate) path: Option, } impl From for LintConfig { @@ -38,6 +42,7 @@ impl From for LintConfig { settings: config.settings, env: config.env, globals: config.globals, + path: Some(config.path), } } } @@ -58,6 +63,7 @@ mod test { let fixture_path = env::current_dir().unwrap().join("fixtures/eslint_config.json"); let config = Oxlintrc::from_file(&fixture_path).unwrap(); assert!(!config.rules.is_empty()); + assert!(config.path.ends_with("fixtures/eslint_config.json")); } #[test] diff --git a/crates/oxc_linter/src/config/overrides.rs b/crates/oxc_linter/src/config/overrides.rs index 576466cd30fae..6a64222584aa0 100644 --- a/crates/oxc_linter/src/config/overrides.rs +++ b/crates/oxc_linter/src/config/overrides.rs @@ -151,3 +151,51 @@ impl JsonSchema for GlobSet { gen.subschema_for::>() } } + +mod test { + #[test] + fn test_globset() { + use super::*; + use serde_json::{from_value, json}; + + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx",], + })) + .unwrap(); + assert!(config.files.globs.is_match("/some/path/foo.tsx")); + assert!(!config.files.globs.is_match("/some/path/foo.ts")); + + let config: OxlintOverride = from_value(json!({ + "files": ["lib/*.ts",], + })) + .unwrap(); + assert!(config.files.globs.is_match("lib/foo.ts")); + assert!(!config.files.globs.is_match("src/foo.ts")); + } + + #[test] + fn test_parsing_plugins() { + use super::*; + use serde_json::{from_value, json}; + + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + })) + .unwrap(); + assert_eq!(config.plugins, None); + + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + "plugins": [], + })) + .unwrap(); + assert_eq!(config.plugins, Some(LintPlugins::empty())); + + let config: OxlintOverride = from_value(json!({ + "files": ["*.tsx"], + "plugins": ["typescript", "react"], + })) + .unwrap(); + assert_eq!(config.plugins, Some(LintPlugins::REACT | LintPlugins::TYPESCRIPT)); + } +} diff --git a/crates/oxc_linter/src/config/oxlintrc.rs b/crates/oxc_linter/src/config/oxlintrc.rs index fc96ea1b2d41b..1fc3f64e32055 100644 --- a/crates/oxc_linter/src/config/oxlintrc.rs +++ b/crates/oxc_linter/src/config/oxlintrc.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use oxc_diagnostics::OxcDiagnostic; use schemars::JsonSchema; @@ -85,6 +85,9 @@ pub struct Oxlintrc { /// Add, remove, or otherwise reconfigure rules for specific files or groups of files. #[serde(skip_serializing_if = "OxlintOverrides::is_empty")] pub overrides: OxlintOverrides, + /// Absolute path to the configuration file. + #[serde(skip)] + pub path: PathBuf, } impl Oxlintrc { @@ -116,10 +119,15 @@ impl Oxlintrc { OxcDiagnostic::error(format!("Failed to parse eslint config {path:?}.\n{err}")) })?; - let config = Self::deserialize(&json).map_err(|err| { + let mut config = Self::deserialize(&json).map_err(|err| { OxcDiagnostic::error(format!("Failed to parse config with error {err:?}")) })?; + // Get absolute path from `path` + let absolute_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + + config.path = absolute_path; + Ok(config) } } @@ -137,6 +145,7 @@ mod test { assert!(config.rules.is_empty()); assert_eq!(config.settings, OxlintSettings::default()); assert_eq!(config.env, OxlintEnv::default()); + assert_eq!(config.path, PathBuf::default()); } #[test] diff --git a/crates/oxc_linter/src/config/plugins.rs b/crates/oxc_linter/src/config/plugins.rs index b507b32a351d2..85cedb9e43306 100644 --- a/crates/oxc_linter/src/config/plugins.rs +++ b/crates/oxc_linter/src/config/plugins.rs @@ -149,7 +149,7 @@ impl> FromIterator for LintPlugins { fn from_iter>(iter: T) -> Self { iter.into_iter() .map(|plugin| plugin.as_ref().into()) - .fold(LintPlugins::default(), LintPlugins::union) + .fold(LintPlugins::empty(), LintPlugins::union) } } @@ -184,18 +184,17 @@ impl<'de> Deserialize<'de> for LintPlugins { // serde_json::from_value provides a String. The former is // used in almost all cases, but the latter is more // convenient for test cases. - match seq.next_element::<&str>() { + match seq.next_element::() { Ok(Some(next)) => { - plugins |= next.into(); + plugins |= next.as_str().into(); } Ok(None) => break, - Err(_) => { - if let Some(next) = seq.next_element::()? { - plugins |= next.as_str().into(); - } else { - break; + Err(_) => match seq.next_element::<&str>() { + Ok(Some(next)) => { + plugins |= next.into(); } - } + Ok(None) | Err(_) => break, + }, }; } @@ -316,7 +315,7 @@ impl LintPluginOptions { impl> FromIterator<(S, bool)> for LintPluginOptions { fn from_iter>(iter: I) -> Self { - let mut options = Self::default(); + let mut options = Self::none(); for (s, enabled) in iter { let flags = LintPlugins::from(s.as_ref()); match flags { @@ -381,11 +380,11 @@ mod test { fn test_collect_empty() { let empty: &[&str] = &[]; let plugins: LintPluginOptions = empty.iter().copied().collect(); - assert_eq!(plugins, LintPluginOptions::default()); + assert_eq!(plugins, LintPluginOptions::none()); let empty: Vec<(String, bool)> = vec![]; let plugins: LintPluginOptions = empty.into_iter().collect(); - assert_eq!(plugins, LintPluginOptions::default()); + assert_eq!(plugins, LintPluginOptions::none()); } #[test] @@ -394,9 +393,9 @@ mod test { let plugins: LintPluginOptions = enabled.into_iter().collect(); let expected = LintPluginOptions { react: true, - unicorn: true, + unicorn: false, typescript: true, - oxc: true, + oxc: false, import: false, jsdoc: false, jest: true, diff --git a/crates/oxc_linter/src/config/settings/jsx_a11y.rs b/crates/oxc_linter/src/config/settings/jsx_a11y.rs index be81c6d17896d..e2744ee5ba8c3 100644 --- a/crates/oxc_linter/src/config/settings/jsx_a11y.rs +++ b/crates/oxc_linter/src/config/settings/jsx_a11y.rs @@ -3,12 +3,46 @@ use rustc_hash::FxHashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -// +/// Configure JSX A11y plugin rules. +/// +/// See +/// [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#configurations)'s +/// configuration for a full reference. #[derive(Debug, Clone, Deserialize, Default, Serialize, JsonSchema)] #[cfg_attr(test, derive(PartialEq))] pub struct JSXA11yPluginSettings { + /// An optional setting that define the prop your code uses to create polymorphic components. + /// This setting will be used to determine the element type in rules that + /// require semantic context. + /// + /// For example, if you set the `polymorphicPropName` to `as`, then this element: + /// + /// ```jsx + /// Hello + /// ``` + /// + /// Will be treated as an `h3`. If not set, this component will be treated + /// as a `Box`. #[serde(rename = "polymorphicPropName")] pub polymorphic_prop_name: Option, + + /// To have your custom components be checked as DOM elements, you can + /// provide a mapping of your component names to the DOM element name. + /// + /// ## Example + /// + /// ```json + /// { + /// "settings": { + /// "jsx-a11y": { + /// "components": { + /// "Link": "a", + /// "IconButton": "button" + /// } + /// } + /// } + /// } + /// ``` #[serde(default)] pub components: FxHashMap, } diff --git a/crates/oxc_linter/src/config/settings/mod.rs b/crates/oxc_linter/src/config/settings/mod.rs index 6cc38c0a83ed4..0aed87547a34c 100644 --- a/crates/oxc_linter/src/config/settings/mod.rs +++ b/crates/oxc_linter/src/config/settings/mod.rs @@ -11,7 +11,34 @@ use self::{ react::ReactPluginSettings, }; -/// Shared settings for plugins +/// # Oxlint Plugin Settings +/// +/// Configure the behavior of linter plugins. +/// +/// ## Example +/// +/// Here's an example if you're using Next.js in a monorepo: +/// +/// ```json +/// { +/// "settings": { +/// "next": { +/// "rootDir": "apps/dashboard/" +/// }, +/// "react": { +/// "linkComponents": [ +/// { "name": "Link", "linkAttribute": "to" } +/// ] +/// }, +/// "jsx-a11y": { +/// "components": { +/// "Link": "a", +/// "Button": "button" +/// } +/// } +/// } +/// } +/// ``` #[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema)] #[cfg_attr(test, derive(PartialEq))] pub struct OxlintSettings { diff --git a/crates/oxc_linter/src/config/settings/next.rs b/crates/oxc_linter/src/config/settings/next.rs index e9bde3d9dcf9b..7e0f1b31e9193 100644 --- a/crates/oxc_linter/src/config/settings/next.rs +++ b/crates/oxc_linter/src/config/settings/next.rs @@ -3,9 +3,26 @@ use std::borrow::Cow; use schemars::JsonSchema; use serde::{Deserialize, Serialize, Serializer}; +/// Configure Next.js plugin rules. #[derive(Debug, Clone, Deserialize, Default, Serialize, JsonSchema)] #[cfg_attr(test, derive(PartialEq))] pub struct NextPluginSettings { + /// The root directory of the Next.js project. + /// + /// This is particularly useful when you have a monorepo and your Next.js + /// project is in a subfolder. + /// + /// ## Example + /// + /// ```json + /// { + /// "settings": { + /// "next": { + /// "rootDir": "apps/dashboard/" + /// } + /// } + /// } + /// ``` #[serde(default)] #[serde(rename = "rootDir")] root_dir: OneOrMany, diff --git a/crates/oxc_linter/src/config/settings/react.rs b/crates/oxc_linter/src/config/settings/react.rs index 770b71a15add9..0432189c7d2d1 100644 --- a/crates/oxc_linter/src/config/settings/react.rs +++ b/crates/oxc_linter/src/config/settings/react.rs @@ -4,14 +4,55 @@ use oxc_span::CompactStr; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -// +/// Configure React plugin rules. +/// +/// Derived from [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react#configuration-legacy-eslintrc-) #[derive(Debug, Clone, Deserialize, Default, Serialize, JsonSchema)] #[cfg_attr(test, derive(PartialEq))] pub struct ReactPluginSettings { + /// Components used as alternatives to `
` for forms, such as ``. + /// + /// ## Example + /// + /// ```jsonc + /// { + /// "settings": { + /// "react": { + /// "formComponents": [ + /// "CustomForm", + /// // OtherForm is considered a form component and has an endpoint attribute + /// { "name": "OtherForm", "formAttribute": "endpoint" }, + /// // allows specifying multiple properties if necessary + /// { "name": "Form", "formAttribute": ["registerEndpoint", "loginEndpoint"] } + /// ] + /// } + /// } + /// } + /// ``` #[serde(default)] #[serde(rename = "formComponents")] form_components: Vec, + /// Components used as alternatives to `` for linking, such as ``. + /// + /// ## Example + /// + /// ```jsonc + /// { + /// "settings": { + /// "react": { + /// "linkComponents": [ + /// "HyperLink", + /// // Use `linkAttribute` for components that use a different prop name + /// // than `href`. + /// { "name": "MyLink", "linkAttribute": "to" }, + /// // allows specifying multiple properties if necessary + /// { "name": "Link", "linkAttribute": ["to", "href"] } + /// ] + /// } + /// } + /// } + /// ``` #[serde(default)] #[serde(rename = "linkComponents")] link_components: Vec, diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index 3f57ce094773d..6076b8767dea7 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -341,7 +341,6 @@ const PLUGIN_PREFIXES: phf::Map<&'static str, &'static str> = phf::phf_map! { "promise" => "eslint-plugin-promise", "react_perf" => "eslint-plugin-react-perf", "react" => "eslint-plugin-react", - "tree_shaking" => "eslint-plugin-tree-shaking", "typescript" => "typescript-eslint", "unicorn" => "eslint-plugin-unicorn", "vitest" => "eslint-plugin-vitest", diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index e2cddafc36c91..bf0a1f8a312ab 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -171,6 +171,7 @@ mod typescript { pub mod no_non_null_asserted_nullish_coalescing; pub mod no_non_null_asserted_optional_chain; pub mod no_non_null_assertion; + pub mod no_require_imports; pub mod no_this_alias; pub mod no_unnecessary_type_constraint; pub mod no_unsafe_declaration_merging; @@ -253,6 +254,7 @@ mod react { pub mod jsx_no_undef; pub mod jsx_no_useless_fragment; pub mod jsx_props_no_spread_multi; + pub mod no_array_index_key; pub mod no_children_prop; pub mod no_danger; pub mod no_danger_with_children; @@ -359,6 +361,7 @@ mod unicorn { pub mod prefer_regexp_test; pub mod prefer_set_size; pub mod prefer_spread; + pub mod prefer_string_raw; pub mod prefer_string_replace_all; pub mod prefer_string_slice; pub mod prefer_string_starts_ends_with; @@ -478,15 +481,12 @@ mod jsdoc { pub mod require_yields; } -mod tree_shaking { - pub mod no_side_effects_in_initialization; -} - mod promise { pub mod avoid_new; pub mod catch_or_return; pub mod no_callback_in_promise; pub mod no_new_statics; + pub mod no_promise_in_callback; pub mod no_return_in_finally; pub mod param_names; pub mod prefer_await_to_callbacks; @@ -787,6 +787,7 @@ oxc_macros::declare_all_lint_rules! { oxc::uninvoked_array_callback, promise::avoid_new, promise::catch_or_return, + promise::no_promise_in_callback, promise::no_callback_in_promise, promise::no_new_statics, promise::no_return_in_finally, @@ -808,6 +809,7 @@ oxc_macros::declare_all_lint_rules! { react::jsx_no_undef, react::jsx_no_useless_fragment, react::jsx_props_no_spread_multi, + react::no_array_index_key, react::no_children_prop, react::no_danger_with_children, react::no_danger, @@ -831,7 +833,6 @@ oxc_macros::declare_all_lint_rules! { react_perf::jsx_no_new_function_as_prop, react_perf::jsx_no_new_object_as_prop, security::api_keys, - tree_shaking::no_side_effects_in_initialization, typescript::adjacent_overload_signatures, typescript::array_type, typescript::ban_ts_comment, @@ -855,6 +856,7 @@ oxc_macros::declare_all_lint_rules! { typescript::no_non_null_asserted_nullish_coalescing, typescript::no_non_null_asserted_optional_chain, typescript::no_non_null_assertion, + typescript::no_require_imports, typescript::no_this_alias, typescript::no_unnecessary_type_constraint, typescript::no_unsafe_declaration_merging, @@ -948,6 +950,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::prefer_regexp_test, unicorn::prefer_set_size, unicorn::prefer_spread, + unicorn::prefer_string_raw, unicorn::prefer_string_replace_all, unicorn::prefer_string_slice, unicorn::prefer_string_starts_ends_with, diff --git a/crates/oxc_linter/src/rules/eslint/func_names.rs b/crates/oxc_linter/src/rules/eslint/func_names.rs index 2cea0af50069b..cea5c330b2b1e 100644 --- a/crates/oxc_linter/src/rules/eslint/func_names.rs +++ b/crates/oxc_linter/src/rules/eslint/func_names.rs @@ -449,10 +449,9 @@ impl Rule for FuncNames { |name| { // if this name shadows a variable in the outer scope **and** that name is referenced // inside the function body, it is unsafe to add a name to this function - if ctx - .scopes() - .find_binding(func.scope_id.get().unwrap(), &name) - .map_or(false, |shadowed_var| { + if ctx.scopes().find_binding(func.scope_id(), &name).map_or( + false, + |shadowed_var| { ctx.semantic().symbol_references(shadowed_var).any( |reference| { func.span.contains_inclusive( @@ -463,8 +462,8 @@ impl Rule for FuncNames { ) }, ) - }) - { + }, + ) { return fixer.noop(); } diff --git a/crates/oxc_linter/src/rules/eslint/getter_return.rs b/crates/oxc_linter/src/rules/eslint/getter_return.rs index 4952d38dfd711..a3b38c1c740d7 100644 --- a/crates/oxc_linter/src/rules/eslint/getter_return.rs +++ b/crates/oxc_linter/src/rules/eslint/getter_return.rs @@ -129,7 +129,7 @@ impl GetterReturn { match_member_expression!(ChainElement) => { Self::handle_member_expression(ce.expression.to_member_expression()) } - ChainElement::CallExpression(_) => { + ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => { false // todo: make a test for this } }, @@ -257,8 +257,10 @@ impl GetterReturn { match it.kind { // Throws are classified as returning. InstructionKind::Return(_) | InstructionKind::Throw => true, + // Ignore irrelevant elements. - InstructionKind::Break(_) + InstructionKind::ImplicitReturn + | InstructionKind::Break(_) | InstructionKind::Continue(_) | InstructionKind::Iteration(_) | InstructionKind::Unreachable diff --git a/crates/oxc_linter/src/rules/eslint/no_console.rs b/crates/oxc_linter/src/rules/eslint/no_console.rs index 4a757fdcccba5..07a0336e6a4e0 100644 --- a/crates/oxc_linter/src/rules/eslint/no_console.rs +++ b/crates/oxc_linter/src/rules/eslint/no_console.rs @@ -1,12 +1,19 @@ use oxc_ast::{ast::Expression, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{CompactStr, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::{ + context::LintContext, + fixer::{RuleFix, RuleFixer}, + rule::Rule, + AstNode, +}; fn no_console_diagnostic(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Unexpected console statement.").with_label(span) + OxcDiagnostic::warn("eslint(no-console): Unexpected console statement.") + .with_label(span) + .with_help("Delete this console statement.") } #[derive(Debug, Default, Clone)] @@ -47,7 +54,8 @@ declare_oxc_lint!( /// console.log('here'); /// ``` NoConsole, - restriction + restriction, + suggestion ); impl Rule for NoConsole { @@ -79,13 +87,57 @@ impl Rule for NoConsole { && ident.name == "console" && !self.allow.iter().any(|s| mem.static_property_name().is_some_and(|f| f == s)) { - if let Some(mem) = mem.static_property_info() { - ctx.diagnostic(no_console_diagnostic(mem.0)); + if let Some((mem_span, _)) = mem.static_property_info() { + let diagnostic_span = ident.span().merge(&mem_span); + ctx.diagnostic_with_suggestion(no_console_diagnostic(diagnostic_span), |fixer| { + remove_console(fixer, ctx, node) + }); } } } } +fn remove_console<'c, 'a: 'c>( + fixer: RuleFixer<'c, 'a>, + ctx: &'c LintContext<'a>, + node: &AstNode<'a>, +) -> RuleFix<'a> { + let mut node_to_delete = node; + for parent in ctx.nodes().ancestors(node.id()).skip(1) { + match parent.kind() { + AstKind::ParenthesizedExpression(_) + | AstKind::ExpressionStatement(_) + => node_to_delete = parent, + AstKind::IfStatement(_) + | AstKind::WhileStatement(_) + | AstKind::ForStatement(_) + | AstKind::ForInStatement(_) + | AstKind::ForOfStatement(_) + | AstKind::ArrowFunctionExpression(_) => { + return fixer.replace(node_to_delete.span(), "{}") + } + // Arrow function AST nodes do not say whether they have brackets or + // not, so we need to check manually. + // e.g: const x = () => { console.log(foo) } + // vs: const x = () => console.log(foo) + | AstKind::FunctionBody(body) if !fixer.source_range(body.span).starts_with('{') => { + return fixer.replace(node_to_delete.span(), "{}") + } + // Marked as dangerous until we're sure this is safe + AstKind::ConditionalExpression(_) + // from: const x = (console.log("foo"), 5); + // to: const x = (undefined, 5); + | AstKind::SequenceExpression(_) + | AstKind::ObjectProperty(_) + => { + return fixer.replace(node_to_delete.span(), "undefined").dangerously() + } + _ => break, + } + } + fixer.delete(node_to_delete) +} + #[test] fn test() { use crate::tester::Tester; @@ -120,5 +172,20 @@ fn test() { ("console.warn(foo)", Some(serde_json::json!([{ "allow": ["info", "log"] }]))), ]; - Tester::new(NoConsole::NAME, pass, fail).test_and_snapshot(); + let fix = vec![ + ("function foo() { console.log(bar); }", "function foo() { }", None), + ("function foo() { console.log(bar) }", "function foo() { }", None), + ("const x = () => console.log(foo)", "const x = () => {}", None), + ("const x = () => { console.log(foo) }", "const x = () => { }", None), + ("const x = () => { console.log(foo); }", "const x = () => { }", None), + ("const x = () => { ((console.log(foo))); }", "const x = () => { }", None), + ("const x = () => { console.log(foo); return 5 }", "const x = () => { return 5 }", None), + ("if (foo) { console.log(foo) }", "if (foo) { }", None), + ("foo ? console.log(foo) : 5", "foo ? undefined : 5", None), + ("(console.log(foo), 5)", "(undefined, 5)", None), + ("(5, console.log(foo))", "(5, undefined)", None), + ("const x = { foo: console.log(bar) }", "const x = { foo: undefined }", None), + ]; + + Tester::new(NoConsole::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/eslint/no_const_assign.rs b/crates/oxc_linter/src/rules/eslint/no_const_assign.rs index 36537160b1214..f863487c6c4d9 100644 --- a/crates/oxc_linter/src/rules/eslint/no_const_assign.rs +++ b/crates/oxc_linter/src/rules/eslint/no_const_assign.rs @@ -82,6 +82,7 @@ fn test() { ("try {} catch (x) { x = 1; }", None), ("const a = 1; { let a = 2; { a += 1; } }", None), ("const foo = 1;let bar;bar[foo ?? foo] = 42;", None), + ("const FOO = 1; ({ files = FOO } = arg1); ", None), ]; let fail = vec![ diff --git a/crates/oxc_linter/src/rules/eslint/no_else_return.rs b/crates/oxc_linter/src/rules/eslint/no_else_return.rs index 84d7bcd4c4568..8ea9b310ec8a2 100644 --- a/crates/oxc_linter/src/rules/eslint/no_else_return.rs +++ b/crates/oxc_linter/src/rules/eslint/no_else_return.rs @@ -183,7 +183,7 @@ fn is_safe_from_name_collisions( match stmt { Statement::BlockStatement(block) => { - let block_scope_id = block.scope_id.get().unwrap(); + let block_scope_id = block.scope_id(); let bindings = scopes.get_bindings(block_scope_id); let parent_bindings = scopes.get_bindings(parent_scope_id); diff --git a/crates/oxc_linter/src/rules/eslint/no_import_assign.rs b/crates/oxc_linter/src/rules/eslint/no_import_assign.rs index b3964fb277711..2f0aba6eafb39 100644 --- a/crates/oxc_linter/src/rules/eslint/no_import_assign.rs +++ b/crates/oxc_linter/src/rules/eslint/no_import_assign.rs @@ -126,7 +126,7 @@ fn is_argument_of_well_known_mutation_function(node_id: NodeId, ctx: &LintContex if ((ident.name == "Object" && OBJECT_MUTATION_METHODS.contains(property_name)) || (ident.name == "Reflect" && REFLECT_MUTATION_METHODS.contains(property_name))) - && ident.reference_id.get().is_some_and(|id| !ctx.symbols().has_binding(id)) + && !ctx.symbols().has_binding(ident.reference_id()) { return expr .arguments diff --git a/crates/oxc_linter/src/rules/eslint/no_unsafe_optional_chaining.rs b/crates/oxc_linter/src/rules/eslint/no_unsafe_optional_chaining.rs index 5f9be9e87c962..d4ae93e9218ab 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unsafe_optional_chaining.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unsafe_optional_chaining.rs @@ -261,6 +261,10 @@ fn test() { "disallowArithmeticOperators": false }])), ), + ("x?.f();", None), + ("x?.f?.();", None), + ("f?.();", None), + ("a?.c?.b", None), ]; let fail = vec![ diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs index fcaa75ead7395..fdc3574413070 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs @@ -2,84 +2,35 @@ //! consider variables ignored by name pattern, but by where they are declared. use oxc_ast::{ast::*, AstKind}; use oxc_semantic::{NodeId, Semantic}; -use oxc_span::GetSpan; use super::{options::ArgsOption, NoUnusedVars, Symbol}; use crate::rules::eslint::no_unused_vars::binding_pattern::{BindingContext, HasAnyUsedBinding}; impl<'s, 'a> Symbol<'s, 'a> { - /// Returns `true` if this function is use. + /// Check if the declaration of this [`Symbol`] is use. /// - /// Checks for these cases - /// 1. passed as a callback to another [`CallExpression`] or [`NewExpression`] - /// 2. invoked as an IIFE - /// 3. Returned from another function - /// 4. Used as an attribute in a JSX element + /// If it's an expression, then it's always passed in as an argument + /// or assigned to a variable, so it's always used. + /// + /// ```js + /// // True: + /// const a = class Name{} + /// export default (function Name() {}) + /// console.log(function Name() {}) + /// + /// // False + /// function foo() {} + /// { + /// class Foo {} + /// } + /// ``` #[inline] - pub fn is_function_or_class_declaration_used(&self) -> bool { - #[cfg(debug_assertions)] - { - let kind = self.declaration().kind(); - assert!(kind.is_function_like() || matches!(kind, AstKind::Class(_))); - } - - for parent in self.iter_relevant_parents() { - match parent.kind() { - AstKind::MemberExpression(_) | AstKind::ParenthesizedExpression(_) - // e.g. `const x = [function foo() {}]` - // Only considered used if the array containing the symbol is used. - | AstKind::ArrayExpressionElement(_) - | AstKind::ArrayExpression(_) - // a ? b : function foo() {} - // Only considered used if the function is the test or the selected branch, - // but we can't determine that here. - | AstKind::ConditionalExpression(_) - => { - continue; - } - // Returned from another function. Definitely won't be the same - // function because we're walking up from its declaration - AstKind::ReturnStatement(_) - // - | AstKind::JSXExpressionContainer(_) - // Function declaration is passed as an argument to another function. - | AstKind::CallExpression(_) | AstKind::Argument(_) - // e.g. `const x = { foo: function foo() {} }` - // Allowed off-the-bat since objects being the only child of an - // ExpressionStatement is rare, since you would need to wrap the - // object in parentheses to avoid creating a block statement. - | AstKind::ObjectProperty(_) - // e.g. var foo = function bar() { } - // we don't want to check for violations on `bar`, just `foo` - | AstKind::VariableDeclarator(_) - // new (class CustomRenderer{}) - // new (function() {}) - | AstKind::NewExpression(_) - => { - return true; - } - // !function() {}; is an IIFE - AstKind::UnaryExpression(expr) => return expr.operator.is_not(), - // function is used as a value for an assignment - // e.g. Array.prototype.sort ||= function sort(a, b) { } - AstKind::AssignmentExpression(assignment) if assignment.right.span().contains_inclusive(self.span()) => { - return self != &assignment.left; - } - AstKind::ExpressionStatement(_) => { - // implicit return in arrow function expression - let Some(AstKind::FunctionBody(body)) = self.nodes().parent_kind(parent.id()) else { - return false; - }; - return body.span.contains_inclusive(self.span()) && body.statements.len() == 1 && !self.get_snippet(body.span).starts_with('{') - } - _ => { - parent.kind().debug_name(); - return false; - } - } + pub(crate) fn is_function_or_class_declaration_used(&self) -> bool { + match self.declaration().kind() { + AstKind::Class(class) => class.is_expression(), + AstKind::Function(func) => func.is_expression(), + _ => false, } - - false } fn is_declared_in_for_of_loop(&self) -> bool { @@ -124,12 +75,6 @@ fn is_ambient_namespace(namespace: &TSModuleDeclaration) -> bool { } impl NoUnusedVars { - #[allow(clippy::unused_self)] - pub(super) fn is_allowed_class_or_function(&self, symbol: &Symbol<'_, '_>) -> bool { - symbol.is_function_or_class_declaration_used() - // || symbol.is_function_or_class_assigned_to_same_name_variable() - } - #[allow(clippy::unused_self)] pub(super) fn is_allowed_ts_namespace<'a>( &self, diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs index 3f9157eb8bdd5..eb11b4f179708 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs @@ -44,7 +44,7 @@ impl<'a> HasAnyUsedBinding<'a> for BindingPatternKind<'a> { impl<'a> HasAnyUsedBinding<'a> for BindingIdentifier<'a> { fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool { - self.symbol_id.get().is_some_and(|symbol_id| ctx.has_usages(symbol_id)) + ctx.has_usages(self.symbol_id()) } } impl<'a> HasAnyUsedBinding<'a> for ObjectPattern<'a> { 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 551dd5d4cea5c..9f4dcdc1a4393 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 @@ -232,8 +232,7 @@ impl NoUnusedVars { } // Order matters. We want to call cheap/high "yield" functions first. - let is_exported = symbol.is_exported(); - let is_used = is_exported || symbol.has_usages(self); + let is_used = symbol.is_exported() || symbol.has_usages(self); match (is_used, is_ignored) { (true, true) => { @@ -306,12 +305,6 @@ impl NoUnusedVars { } ctx.diagnostic(diagnostic::declared(symbol, &self.vars_ignore_pattern)); } - AstKind::Class(_) | AstKind::Function(_) => { - if self.is_allowed_class_or_function(symbol) { - return; - } - ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None)); - } AstKind::TSModuleDeclaration(namespace) => { if self.is_allowed_ts_namespace(symbol, namespace) { return; diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs index 1537a96248873..5b76775d2673b 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs @@ -45,7 +45,7 @@ impl<'s, 'a> Symbol<'s, 'a> { } #[inline] - pub const fn flags(&self) -> SymbolFlags { + pub fn flags(&self) -> SymbolFlags { self.flags } @@ -105,11 +105,6 @@ impl<'s, 'a> Symbol<'s, 'a> { self.nodes().ancestors(self.declaration_id()) } - #[inline] - pub fn iter_relevant_parents(&self) -> impl Iterator> + Clone + '_ { - self.iter_relevant_parents_of(self.declaration_id()) - } - pub fn iter_relevant_parents_of( &self, node_id: NodeId, @@ -176,10 +171,9 @@ impl<'s, 'a> Symbol<'s, 'a> { /// NOTE: does not support CJS right now. pub fn is_exported(&self) -> bool { let is_in_exportable_scope = self.is_root() || self.is_in_ts_namespace(); - (is_in_exportable_scope - && (self.flags.contains(SymbolFlags::Export) - || self.semantic.module_record().exported_bindings.contains_key(self.name()))) - || self.in_export_node() + is_in_exportable_scope + && (self.semantic.module_record().exported_bindings.contains_key(self.name()) + || self.in_export_node()) } #[inline] @@ -194,7 +188,7 @@ impl<'s, 'a> Symbol<'s, 'a> { AstKind::ModuleDeclaration(module) => { return module.is_export(); } - AstKind::ExportDefaultDeclaration(_) => { + AstKind::ExportNamedDeclaration(_) | AstKind::ExportDefaultDeclaration(_) => { return true; } AstKind::VariableDeclaration(_) @@ -244,17 +238,14 @@ impl GetSpan for Symbol<'_, '_> { impl<'a> PartialEq> for Symbol<'_, 'a> { fn eq(&self, other: &IdentifierReference<'a>) -> bool { // cheap: no resolved reference means its a global reference - let Some(reference_id) = other.reference_id.get() else { - return false; - }; - let reference = self.symbols().get_reference(reference_id); + let reference = self.symbols().get_reference(other.reference_id()); reference.symbol_id().is_some_and(|symbol_id| self.id == symbol_id) } } impl<'a> PartialEq> for Symbol<'_, 'a> { fn eq(&self, id: &BindingIdentifier<'a>) -> bool { - id.symbol_id.get().is_some_and(|id| self.id == id) + self.id == id.symbol_id() } } diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs index 9631943a01d1f..99b4b3150f96b 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs @@ -34,7 +34,7 @@ fn test() { let pass = vec![ ( "var foo = 5; - + label: while (true) { console.log(foo); break label; @@ -43,7 +43,7 @@ fn test() { ), ( "var foo = 5; - + while (true) { console.log(foo); break; diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs index 5c21298d8a680..5235c6e32e18c 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -270,7 +270,6 @@ fn test_vars_reassignment() { } ", "let a = 0; let b = a++; f(b);", - "let a = 0, b = 1; let c = b = a = 1; f(c+b);", // implicit returns " let i = 0; @@ -332,6 +331,7 @@ fn test_vars_reassignment() { "let a = 0; let b = (0, (a++, 0)); f(b);", "let a = 0; let b = ((0, a++), 0); f(b);", "let a = 0; let b = (a, 0) + 1; f(b);", + "let a = 0, b = 1; let c = b = a = 1; f(c+b);", ]; Tester::new(NoUnusedVars::NAME, pass, fail) @@ -819,11 +819,7 @@ fn test_used_declarations() { "export const Foo = class Bar {}", "export const Foo = @SomeDecorator() class Foo {}", ]; - let fail = vec![ - // array is not used, so the function is not used - ";[function foo() {}]", - ";[class Foo {}]", - ]; + let fail = vec![]; Tester::new(NoUnusedVars::NAME, pass, fail) .intentionally_allow_no_fix_tests() diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs index 3a10d42429f05..634105a9616df 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs @@ -23,7 +23,7 @@ impl<'s, 'a> Symbol<'s, 'a> { /// 2. Catch variables are always parameter-like and will therefore never have /// a function declaration. #[inline] - const fn is_maybe_callable(&self) -> bool { + fn is_maybe_callable(&self) -> bool { // NOTE: imports are technically callable, but that call will never // occur within its own declaration since it's declared in another // module. @@ -47,8 +47,8 @@ impl<'s, 'a> Symbol<'s, 'a> { /// eslint's original rule requires it. Const reassignments are not a syntax /// error in JavaScript, only TypeScript. #[inline] - const fn is_possibly_reassignable(&self) -> bool { - self.flags().intersects(SymbolFlags::Variable) + fn is_possibly_reassignable(&self) -> bool { + self.flags().is_variable() } /// Check if this [`Symbol`] is definitely reassignable. @@ -65,10 +65,9 @@ impl<'s, 'a> Symbol<'s, 'a> { /// - `var` and `let` variable declarations /// - function parameters #[inline] - const fn is_definitely_reassignable_variable(&self) -> bool { + fn is_definitely_reassignable_variable(&self) -> bool { let f = self.flags(); - f.intersects(SymbolFlags::Variable) - && !f.contains(SymbolFlags::ConstVariable.union(SymbolFlags::Function)) + f.is_variable() && !f.contains(SymbolFlags::ConstVariable.union(SymbolFlags::Function)) } /// Checks if this [`Symbol`] could be used as a type reference within its @@ -77,7 +76,7 @@ impl<'s, 'a> Symbol<'s, 'a> { /// This does _not_ imply this symbol is a type (negative cases include type /// imports, type parameters, etc). #[inline] - const fn could_have_type_reference_within_own_decl(&self) -> bool { + fn could_have_type_reference_within_own_decl(&self) -> bool { #[rustfmt::skip] const TYPE_DECLS: SymbolFlags = SymbolFlags::TypeAlias .union(SymbolFlags::Interface) @@ -92,6 +91,10 @@ impl<'s, 'a> Symbol<'s, 'a> { /// Check if this [`Symbol`] has any [`Reference`]s that are considered a usage. pub fn has_usages(&self, options: &NoUnusedVars) -> bool { + if self.is_function_or_class_declaration_used() { + return true; + } + // Use symbol flags to skip the usage checks we are certain don't need // to be run. let do_reassignment_checks = self.is_possibly_reassignable(); @@ -731,21 +734,20 @@ impl<'s, 'a> Symbol<'s, 'a> { for parent in self.iter_relevant_parents_of(node_id) { match parent.kind() { AstKind::Function(f) => { - return f.id.as_ref().and_then(|id| id.symbol_id.get()); + return f.id.as_ref().map(BindingIdentifier::symbol_id); } AstKind::ArrowFunctionExpression(_) => { needs_variable_identifier = true; continue; } AstKind::VariableDeclarator(decl) if needs_variable_identifier => { - return decl.id.get_binding_identifier().and_then(|id| id.symbol_id.get()); + return decl.id.get_binding_identifier().map(BindingIdentifier::symbol_id); } AstKind::AssignmentTarget(target) if needs_variable_identifier => { return match target { - AssignmentTarget::AssignmentTargetIdentifier(id) => id - .reference_id - .get() - .and_then(|rid| self.symbols().get_reference(rid).symbol_id()), + AssignmentTarget::AssignmentTargetIdentifier(id) => { + self.symbols().get_reference(id.reference_id()).symbol_id() + } _ => None, }; } diff --git a/crates/oxc_linter/src/rules/eslint/no_var.rs b/crates/oxc_linter/src/rules/eslint/no_var.rs index a30e6d49373f1..15ef7205a71d6 100644 --- a/crates/oxc_linter/src/rules/eslint/no_var.rs +++ b/crates/oxc_linter/src/rules/eslint/no_var.rs @@ -71,7 +71,7 @@ fn is_written_to(binding_pat: &BindingPattern, ctx: &LintContext) -> bool { match &binding_pat.kind { BindingPatternKind::BindingIdentifier(binding_ident) => ctx .semantic() - .symbol_references(binding_ident.symbol_id.get().expect("symbol id should be set")) + .symbol_references(binding_ident.symbol_id()) .any(oxc_semantic::Reference::is_write), BindingPatternKind::ObjectPattern(object_pat) => { if object_pat.properties.iter().any(|prop| is_written_to(&prop.value, ctx)) { diff --git a/crates/oxc_linter/src/rules/import/first.rs b/crates/oxc_linter/src/rules/import/first.rs index 57ff1d6329a6d..69fc06c2be20e 100644 --- a/crates/oxc_linter/src/rules/import/first.rs +++ b/crates/oxc_linter/src/rules/import/first.rs @@ -72,7 +72,7 @@ declare_oxc_lint!( /// /// ### Options /// - /// with `"absolute-import"`: + /// with `"absolute-first"`: /// /// Examples of **incorrect** code for this rule: /// ```js diff --git a/crates/oxc_linter/src/rules/import/no_commonjs.rs b/crates/oxc_linter/src/rules/import/no_commonjs.rs index 0dc3151028d40..8dc2f16c5fac1 100644 --- a/crates/oxc_linter/src/rules/import/no_commonjs.rs +++ b/crates/oxc_linter/src/rules/import/no_commonjs.rs @@ -10,7 +10,6 @@ use oxc_ast::{ use crate::{context::LintContext, rule::Rule, AstNode}; fn no_commonjs_diagnostic(span: Span, name: &str, actual: &str) -> OxcDiagnostic { - // See for details OxcDiagnostic::warn(format!("Expected {name} instead of {actual}")) .with_help("Do not use CommonJS `require` calls and `module.exports` or `exports.*`") .with_label(span) @@ -214,6 +213,10 @@ impl Rule for NoCommonjs { return; } + if ctx.scopes().find_binding(ctx.scopes().root_scope_id(), "require").is_some() { + return; + } + if let Argument::TemplateLiteral(template_literal) = &call_expr.arguments[0] { if template_literal.expressions.len() != 0 { return; @@ -299,6 +302,15 @@ fn test() { Some(json!([{ "allowRequire": false }])), ), (r#"try { require("x") } catch (error) {}"#, None), + // covers user variables + ( + " + import { createRequire } from 'module'; + const require = createRequire(); + require('remark-preset-prettier'); + ", + None, + ), ]; let fail = vec![ diff --git a/crates/oxc_linter/src/rules/import/no_named_as_default_member.rs b/crates/oxc_linter/src/rules/import/no_named_as_default_member.rs index 65e5b2ece28d5..9c16062e9754b 100644 --- a/crates/oxc_linter/src/rules/import/no_named_as_default_member.rs +++ b/crates/oxc_linter/src/rules/import/no_named_as_default_member.rs @@ -64,13 +64,12 @@ declare_oxc_lint!( NoNamedAsDefaultMember, suspicious ); + fn get_symbol_id_from_ident( ctx: &LintContext<'_>, ident: &IdentifierReference, ) -> Option { - let reference_id = ident.reference_id.get().unwrap(); - let reference = &ctx.symbols().references[reference_id]; - reference.symbol_id() + ctx.symbols().get_reference(ident.reference_id()).symbol_id() } impl Rule for NoNamedAsDefaultMember { diff --git a/crates/oxc_linter/src/rules/jest/no_conditional_expect.rs b/crates/oxc_linter/src/rules/jest/no_conditional_expect.rs index 27c08a317154a..09f9a664469ad 100644 --- a/crates/oxc_linter/src/rules/jest/no_conditional_expect.rs +++ b/crates/oxc_linter/src/rules/jest/no_conditional_expect.rs @@ -145,9 +145,7 @@ fn check_parents<'a>( return InConditional(false); }; let symbol_table = ctx.semantic().symbols(); - let Some(symbol_id) = ident.symbol_id.get() else { - return InConditional(false); - }; + let symbol_id = ident.symbol_id(); // Consider cases like: // ```javascript diff --git a/crates/oxc_linter/src/rules/jsdoc/require_returns.rs b/crates/oxc_linter/src/rules/jsdoc/require_returns.rs index 2decaedcc4ffe..6b71c500531b2 100644 --- a/crates/oxc_linter/src/rules/jsdoc/require_returns.rs +++ b/crates/oxc_linter/src/rules/jsdoc/require_returns.rs @@ -304,8 +304,7 @@ fn is_promise_resolve_with_value(expr: &Expression, ctx: &LintContext) -> Option // }) // ``` // IMO: This is a fault of the original rule design... - for resolve_ref in ctx.symbols().get_resolved_references(ident.symbol_id.get()?) - { + for resolve_ref in ctx.symbols().get_resolved_references(ident.symbol_id()) { // Check if `resolve` is called with value match ctx.nodes().parent_node(resolve_ref.node_id())?.kind() { // `resolve(foo)` diff --git a/crates/oxc_linter/src/rules/nextjs/inline_script_id.rs b/crates/oxc_linter/src/rules/nextjs/inline_script_id.rs index 868d855b913d3..939929d06d34c 100644 --- a/crates/oxc_linter/src/rules/nextjs/inline_script_id.rs +++ b/crates/oxc_linter/src/rules/nextjs/inline_script_id.rs @@ -49,7 +49,7 @@ impl Rule for InlineScriptId { } 'references_loop: for reference in - ctx.semantic().symbol_references(specifier.local.symbol_id.get().unwrap()) + ctx.semantic().symbol_references(specifier.local.symbol_id()) { let Some(node) = ctx.nodes().parent_node(reference.node_id()) else { return; diff --git a/crates/oxc_linter/src/rules/nextjs/no_script_component_in_head.rs b/crates/oxc_linter/src/rules/nextjs/no_script_component_in_head.rs index aee71c512d3ab..37df397b1cb74 100644 --- a/crates/oxc_linter/src/rules/nextjs/no_script_component_in_head.rs +++ b/crates/oxc_linter/src/rules/nextjs/no_script_component_in_head.rs @@ -59,9 +59,7 @@ impl Rule for NoScriptComponentInHead { return; }; - for reference in - ctx.semantic().symbol_references(default_import.local.symbol_id.get().unwrap()) - { + for reference in ctx.semantic().symbol_references(default_import.local.symbol_id()) { let Some(node) = ctx.nodes().parent_node(reference.node_id()) else { return; }; diff --git a/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs b/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs index c540d3a986968..417ffe80020d3 100644 --- a/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs +++ b/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs @@ -59,9 +59,7 @@ impl Rule for NoTitleInDocumentHead { return; }; - for reference in - ctx.semantic().symbol_references(default_import.local.symbol_id.get().unwrap()) - { + for reference in ctx.semantic().symbol_references(default_import.local.symbol_id()) { let Some(node) = ctx.nodes().parent_node(reference.node_id()) else { return; }; diff --git a/crates/oxc_linter/src/rules/oxc/no_accumulating_spread.rs b/crates/oxc_linter/src/rules/oxc/no_accumulating_spread.rs index d0a6fa0fbcb72..fa81707daacf1 100644 --- a/crates/oxc_linter/src/rules/oxc/no_accumulating_spread.rs +++ b/crates/oxc_linter/src/rules/oxc/no_accumulating_spread.rs @@ -135,10 +135,7 @@ impl Rule for NoAccumulatingSpread { let symbols = ctx.semantic().symbols(); // get the AST node + symbol id of the declaration of the identifier - let Some(reference_id) = ident.reference_id.get() else { - return; - }; - let reference = symbols.get_reference(reference_id); + let reference = symbols.get_reference(ident.reference_id()); let Some(referenced_symbol_id) = reference.symbol_id() else { return; }; @@ -305,7 +302,7 @@ fn get_reduce_diagnostic<'a>( fn get_identifier_symbol_id(ident: &BindingPatternKind<'_>) -> Option { match ident { - BindingPatternKind::BindingIdentifier(ident) => ident.symbol_id.get(), + BindingPatternKind::BindingIdentifier(ident) => Some(ident.symbol_id()), BindingPatternKind::AssignmentPattern(ident) => get_identifier_symbol_id(&ident.left.kind), _ => None, } diff --git a/crates/oxc_linter/src/rules/oxc/no_map_spread.rs b/crates/oxc_linter/src/rules/oxc/no_map_spread.rs index c95ae3c14661c..072d70695279f 100644 --- a/crates/oxc_linter/src/rules/oxc/no_map_spread.rs +++ b/crates/oxc_linter/src/rules/oxc/no_map_spread.rs @@ -598,7 +598,7 @@ where let v = Self { ctx, cb, - cb_scope_id: f.scope_id.get().unwrap(), + cb_scope_id: f.scope_id(), is_in_return: f.expression, return_span: f.expression.then(|| f.body.span()), }; @@ -608,7 +608,7 @@ where let v = Self { ctx, cb, - cb_scope_id: f.scope_id.get().unwrap(), + cb_scope_id: f.scope_id(), is_in_return: false, return_span: None, }; diff --git a/crates/oxc_linter/src/rules/oxc/no_optional_chaining.rs b/crates/oxc_linter/src/rules/oxc/no_optional_chaining.rs index f859b4c657524..dd8e5c4792f11 100644 --- a/crates/oxc_linter/src/rules/oxc/no_optional_chaining.rs +++ b/crates/oxc_linter/src/rules/oxc/no_optional_chaining.rs @@ -98,6 +98,12 @@ fn test() { ("var x = a/*?.*/?.b", None), ("var x = '?.'?.['?.']", None), ("var x = '?.'?.['?.']", None), + ("a?.c?.b", None), + ("foo?.bar!", None), + ("foo?.[bar]!", None), + ("x?.f();", None), + ("x?.f?.();", None), + ("f?.();", None), ( "var x = a?.b", Some(serde_json::json!([{ diff --git a/crates/oxc_linter/src/rules/oxc/only_used_in_recursion.rs b/crates/oxc_linter/src/rules/oxc/only_used_in_recursion.rs index cbee4ffbb467e..8a105ca33516e 100644 --- a/crates/oxc_linter/src/rules/oxc/only_used_in_recursion.rs +++ b/crates/oxc_linter/src/rules/oxc/only_used_in_recursion.rs @@ -64,6 +64,12 @@ declare_oxc_lint!( dangerous_fix ); +fn is_exported(id: &BindingIdentifier<'_>, ctx: &LintContext<'_>) -> bool { + let module_record = ctx.module_record(); + module_record.exported_bindings.contains_key(id.name.as_str()) + || module_record.export_default.is_some_and(|default| default == id.span) +} + impl Rule for OnlyUsedInRecursion { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { let (function_id, function_parameters, function_span) = match node.kind() { @@ -137,13 +143,8 @@ fn create_diagnostic( function_span: Span, ) { let is_last_arg = arg_index == function_parameters.items.len() - 1; - let is_exported = ctx - .semantic() - .symbols() - .get_flags(function_id.symbol_id.get().expect("`symbol_id` should be set")) - .is_export(); - let is_diagnostic_only = !is_last_arg || is_exported; + let is_diagnostic_only = !is_last_arg || is_exported(function_id, ctx); if is_diagnostic_only { return ctx.diagnostic(only_used_in_recursion_diagnostic(arg.span, arg.name.as_str())); @@ -153,26 +154,17 @@ fn create_diagnostic( only_used_in_recursion_diagnostic(arg.span, arg.name.as_str()), |fixer| { let mut fix = fixer.new_fix_with_capacity( - ctx.semantic() - .symbol_references(arg.symbol_id.get().expect("`symbol_id` should be set")) - .count() - + 1, + ctx.semantic().symbol_references(arg.symbol_id()).count() + 1, ); fix.push(Fix::delete(arg.span())); - for reference in ctx - .semantic() - .symbol_references(arg.symbol_id.get().expect("`symbol_id` should be set")) - { + for reference in ctx.semantic().symbol_references(arg.symbol_id()) { let node = ctx.nodes().get_node(reference.node_id()); fix.push(Fix::delete(node.span())); } // search for references to the function and remove the argument - for reference in ctx - .semantic() - .symbol_references(function_id.symbol_id.get().expect("`symbol_id` should be set")) - { + for reference in ctx.semantic().symbol_references(function_id.symbol_id()) { let node = ctx.nodes().get_node(reference.node_id()); if let Some(AstKind::CallExpression(call_expr)) = ctx.nodes().parent_kind(node.id()) @@ -206,16 +198,13 @@ fn create_diagnostic_jsx( function_id: &BindingIdentifier, property: &BindingProperty, ) { - let Some(function_symbol_id) = function_id.symbol_id.get() else { return }; - let is_exported = ctx.semantic().symbols().get_flags(function_symbol_id).is_export(); - let Some(property_name) = &property.key.static_name() else { return }; - if is_exported { + if is_exported(function_id, ctx) { return ctx.diagnostic(only_used_in_recursion_diagnostic(property.span(), property_name)); } let Some(property_ident) = property.value.get_binding_identifier() else { return }; - let Some(property_symbol_id) = property_ident.symbol_id.get() else { return }; + let property_symbol_id = property_ident.symbol_id(); let mut references = ctx.semantic().symbol_references(property_symbol_id); let has_spread_attribute = references.any(|x| used_with_spread_attribute(x.node_id(), ctx)); @@ -281,17 +270,14 @@ fn is_argument_only_used_in_recursion<'a>( arg_index: usize, ctx: &'a LintContext<'_>, ) -> bool { - let mut references = ctx - .semantic() - .symbol_references(arg.symbol_id.get().expect("`symbol_id` should be set")) - .peekable(); + let mut references = ctx.semantic().symbol_references(arg.symbol_id()).peekable(); // Avoid returning true for an empty iterator if references.peek().is_none() { return false; } - let function_symbol_id = function_id.symbol_id.get().unwrap(); + let function_symbol_id = function_id.symbol_id(); for reference in references { let Some(AstKind::Argument(argument)) = ctx.nodes().parent_kind(reference.node_id()) else { @@ -325,15 +311,12 @@ fn is_property_only_used_in_recursion_jsx( function_ident: &BindingIdentifier, ctx: &LintContext, ) -> bool { - let Some(ident_symbol_id) = ident.symbol_id.get() else { return false }; - let mut references = ctx.semantic().symbol_references(ident_symbol_id).peekable(); - + let mut references = ctx.semantic().symbol_references(ident.symbol_id()).peekable(); if references.peek().is_none() { return false; } - let Some(function_symbol_id) = function_ident.symbol_id.get() else { return false }; - + let function_symbol_id = function_ident.symbol_id(); for reference in references { // Conditions: // 1. The reference is inside a JSXExpressionContainer. @@ -412,14 +395,12 @@ fn is_function_maybe_reassigned<'a>( function_id: &'a BindingIdentifier, ctx: &'a LintContext<'_>, ) -> bool { - ctx.semantic() - .symbol_references(function_id.symbol_id.get().expect("`symbol_id` should be set")) - .any(|reference| { - matches!( - ctx.nodes().parent_kind(reference.node_id()), - Some(AstKind::SimpleAssignmentTarget(_)) - ) - }) + ctx.semantic().symbol_references(function_id.symbol_id()).any(|reference| { + matches!( + ctx.nodes().parent_kind(reference.node_id()), + Some(AstKind::SimpleAssignmentTarget(_)) + ) + }) } fn get_jsx_element_symbol_id<'a>( @@ -805,8 +786,8 @@ function writeChunks(a,callac){writeChunks(m,callac)}writeChunks(i,{})", } export default test; ", - r"function test(a) { - test(a) + r"function test() { + test() } export default test; ", diff --git a/crates/oxc_linter/src/rules/promise/no_promise_in_callback.rs b/crates/oxc_linter/src/rules/promise/no_promise_in_callback.rs new file mode 100644 index 0000000000000..b6938cf085fd1 --- /dev/null +++ b/crates/oxc_linter/src/rules/promise/no_promise_in_callback.rs @@ -0,0 +1,168 @@ +use oxc_ast::ast::FormalParameters; +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::utils::is_promise; +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_promise_in_callback_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Avoid using promises inside of callbacks.") + .with_help("Use either promises or callbacks exclusively for handling asynchronous code.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoPromiseInCallback; + +declare_oxc_lint!( + /// ### What it does + /// Disallows the use of Promises within error-first callback functions. + /// + /// ### Why is this bad? + /// Mixing Promises and callbacks can lead to unclear and inconsistent code. + /// Promises and callbacks are different patterns for handling asynchronous code. + /// Mixing them makes the logic flow harder to follow and complicates error handling, + /// as callbacks rely on an error-first pattern, while Promises use `catch`. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// doSomething((err, val) => { + /// if (err) console.error(err) + /// else doSomethingElse(val).then(console.log) + /// }) + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// promisify(doSomething)() + /// .then(doSomethingElse) + /// .then(console.log) + /// .catch(console.error) + /// ``` + NoPromiseInCallback, + suspicious, +); + +impl Rule for NoPromiseInCallback { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + + if is_promise(call_expr).is_none() { + return; + } + + // When a Promise is returned in a ReturnStatement, the function is most likely + // being used as part of a Promise chain rather than as a callback function. + // To avoid false positives, this case is intentionally excluded from the scope of this rule. + if let Some(AstKind::ReturnStatement(_)) = ctx.nodes().parent_kind(node.id()) { + return; + }; + + let mut ancestors = ctx.nodes().ancestors(node.id()); + if ancestors.any(|node| is_callback_function(node, ctx)) { + ctx.diagnostic(no_promise_in_callback_diagnostic(call_expr.callee.span())); + } + } +} + +fn is_callback_function<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool { + if !node.kind().is_function_like() { + return false; + } + + if is_within_promise_handler(node, ctx) { + return false; + } + + match node.kind() { + AstKind::Function(function) => is_error_first_callback(&function.params), + AstKind::ArrowFunctionExpression(arrow_function) => { + is_error_first_callback(&arrow_function.params) + } + _ => false, + } +} + +fn is_error_first_callback(params: &FormalParameters) -> bool { + let Some(first_parameter) = params.items.first() else { + return false; + }; + + let Some(ident) = first_parameter.pattern.get_binding_identifier() else { + return false; + }; + + matches!(ident.name.as_str(), "err" | "error") +} + +fn is_within_promise_handler<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool { + if !matches!(node.kind(), AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)) { + return false; + } + + let Some(parent) = ctx.nodes().parent_node(node.id()) else { + return false; + }; + if !matches!(ctx.nodes().kind(parent.id()), AstKind::Argument(_)) { + return false; + }; + + let Some(AstKind::CallExpression(call_expr)) = ctx.nodes().parent_kind(parent.id()) else { + return false; + }; + + matches!(call_expr.callee_name(), Some("then" | "catch")) +} + +#[test] +fn test() { + use crate::tester::Tester; + + // The following test cases are based on the original + // implementation from eslint-plugin-promise and are licensed under the ISC License. + // + // Copyright (c) 2020, Jamund Ferguson + // https://github.com/eslint-community/eslint-plugin-promise/blob/266ddbb03076c05c362a6daecb9382b80cdd7108/__tests__/no-promise-in-callback.js + let pass = vec![ + "go(function() { return Promise.resolve(4) })", + "go(function() { return a.then(b) })", + "go(function() { b.catch(c) })", + "go(function() { b.then(c, d) })", + "go(() => Promise.resolve(4))", + "go((errrr) => a.then(b))", + "go((helpers) => { b.catch(c) })", + "go((e) => { b.then(c, d) })", + "a.catch((err) => { b.then(c, d) })", + "var x = function() { return Promise.resolve(4) }", + "function y() { return Promise.resolve(4) }", + "function then() { return Promise.reject() }", + "doThing(function(x) { return Promise.reject(x) })", + "doThing().then(function() { return Promise.all([a,b,c]) })", + "doThing().then(function() { return Promise.resolve(4) })", + "doThing().then(() => Promise.resolve(4))", + "doThing().then(() => Promise.all([a]))", + "a(function(err) { return doThing().then(a) })", + ]; + + let fail = vec![ + "a(function(err) { doThing().then(a) })", + "a(function(error, zup, supa) { doThing().then(a) })", + "a(function(error) { doThing().then(a) })", + "a((error) => { doThing().then(a) })", + "a((error) => doThing().then(a))", + "a((err, data) => { doThing().then(a) })", + "a((err, data) => doThing().then(a))", + "function x(err) { Promise.all() }", + "function x(err) { Promise.allSettled() }", + "function x(err) { Promise.any() }", + "let x = (err) => doThingWith(err).then(a)", + ]; + + Tester::new(NoPromiseInCallback::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/react/no_array_index_key.rs b/crates/oxc_linter/src/rules/react/no_array_index_key.rs new file mode 100644 index 0000000000000..1a48790911fb6 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_array_index_key.rs @@ -0,0 +1,297 @@ +use oxc_ast::{ + ast::{ + Argument, CallExpression, Expression, JSXAttributeItem, JSXAttributeName, + JSXAttributeValue, JSXElement, JSXExpression, ObjectPropertyKind, PropertyKey, + }, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ast_util::is_method_call, context::LintContext, rule::Rule, AstNode}; + +fn no_array_index_key_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Usage of Array index in keys is not allowed") + .with_help("Use a unique data-dependent key to avoid unnecessary rerenders") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoArrayIndexKey; + +declare_oxc_lint!( + /// ### What it does + /// Warn if an element uses an Array index in its key. + /// + /// ### Why is this bad? + /// It's a bad idea to use the array index since it doesn't uniquely identify your elements. + /// In cases where the array is sorted or an element is added to the beginning of the array, + /// the index will be changed even though the element representing that index may be the same. + /// This results in unnecessary renders. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```jsx + /// things.map((thing, index) => ( + /// + /// )); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```jsx + /// things.map((thing, index) => ( + /// + /// )); + /// ``` + NoArrayIndexKey, + perf, +); + +fn check_jsx_element<'a>( + jsx: &'a JSXElement, + node: &'a AstNode, + ctx: &'a LintContext, + prop_name: &'static str, +) { + let Some(index_param_name) = find_index_param_name(node, ctx) else { + return; + }; + + for attr in &jsx.opening_element.attributes { + let JSXAttributeItem::Attribute(attr) = attr else { + return; + }; + + let JSXAttributeName::Identifier(ident) = &attr.name else { + return; + }; + + if ident.name.as_str() != prop_name { + return; + } + + let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value else { + return; + }; + + let JSXExpression::Identifier(expr) = &container.expression else { + return; + }; + + if expr.name.as_str() == index_param_name { + ctx.diagnostic(no_array_index_key_diagnostic(attr.span)); + } + } +} + +fn check_react_clone_element<'a>( + call_expr: &'a CallExpression, + node: &'a AstNode, + ctx: &'a LintContext, +) { + let Some(index_param_name) = find_index_param_name(node, ctx) else { + return; + }; + + if is_method_call(call_expr, Some(&["React"]), Some(&["cloneElement"]), Some(2), Some(3)) { + let Some(Argument::ObjectExpression(obj_expr)) = call_expr.arguments.get(1) else { + return; + }; + + for prop_kind in &obj_expr.properties { + let ObjectPropertyKind::ObjectProperty(prop) = prop_kind else { + continue; + }; + + let PropertyKey::StaticIdentifier(key_ident) = &prop.key else { + continue; + }; + + let Expression::Identifier(value_ident) = &prop.value else { + continue; + }; + + if key_ident.name.as_str() == "key" && value_ident.name.as_str() == index_param_name { + ctx.diagnostic(no_array_index_key_diagnostic(obj_expr.span)); + } + } + } +} + +fn find_index_param_name<'a>(node: &'a AstNode, ctx: &'a LintContext) -> Option<&'a str> { + for ancestor in ctx.nodes().ancestors(node.id()).skip(1) { + if let AstKind::CallExpression(call_expr) = ancestor.kind() { + let Expression::StaticMemberExpression(expr) = &call_expr.callee else { + return None; + }; + + if SECOND_INDEX_METHODS.contains(expr.property.name.as_str()) { + return find_index_param_name_by_position(call_expr, 1); + } + + if THIRD_INDEX_METHODS.contains(expr.property.name.as_str()) { + return find_index_param_name_by_position(call_expr, 2); + } + } + } + + None +} + +fn find_index_param_name_by_position<'a>( + call_expr: &'a CallExpression, + position: usize, +) -> Option<&'a str> { + call_expr.arguments.first().and_then(|argument| match argument { + Argument::ArrowFunctionExpression(arrow_fn_expr) => { + Some(arrow_fn_expr.params.items.get(position)?.pattern.get_identifier()?.as_str()) + } + Argument::FunctionExpression(regular_fn_expr) => { + Some(regular_fn_expr.params.items.get(position)?.pattern.get_identifier()?.as_str()) + } + _ => None, + }) +} + +const SECOND_INDEX_METHODS: phf::Set<&'static str> = phf::phf_set! { + // things.map((thing, index) => ()); + "map", + // things.forEach((thing, index) => {otherThings.push();}); + "forEach", + // things.filter((thing, index) => {otherThings.push();}); + "filter", + // things.some((thing, index) => {otherThings.push();}); + "some", + // things.every((thing, index) => {otherThings.push();}); + "every", + // things.find((thing, index) => {otherThings.push();}); + "find", + // things.findIndex((thing, index) => {otherThings.push();}); + "findIndex", + // things.flatMap((thing, index) => ()); + "flatMap", +}; + +const THIRD_INDEX_METHODS: phf::Set<&'static str> = phf::phf_set! { + // things.reduce((collection, thing, index) => (collection.concat()), []); + "reduce", + // things.reduceRight((collection, thing, index) => (collection.concat()), []); + "reduceRight", +}; + +impl Rule for NoArrayIndexKey { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::JSXElement(jsx) => { + check_jsx_element(jsx, node, ctx, "key"); + } + AstKind::CallExpression(call_expr) => { + check_react_clone_element(call_expr, node, ctx); + } + _ => (), + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r"things.map((thing) => ( + + )); + ", + r"things.map((thing, index) => ( + React.cloneElement(thing, { key: thing.id }) + )); + ", + r"things.forEach((thing, index) => { + otherThings.push(); + }); + ", + r"things.filter((thing, index) => { + otherThings.push(); + }); + ", + r"things.some((thing, index) => { + otherThings.push(); + }); + ", + r"things.every((thing, index) => { + otherThings.push(); + }); + ", + r"things.find((thing, index) => { + otherThings.push(); + }); + ", + r"things.findIndex((thing, index) => { + otherThings.push(); + }); + ", + r"things.flatMap((thing, index) => ( + + )); + ", + r"things.reduce((collection, thing, index) => ( + collection.concat() + ), []); + ", + r"things.reduceRight((collection, thing, index) => ( + collection.concat() + ), []); + ", + ]; + + let fail = vec![ + r"things.map((thing, index) => ( + + )); + ", + r"things.map((thing, index) => ( + React.cloneElement(thing, { key: index }) + )); + ", + r"things.forEach((thing, index) => { + otherThings.push(); + }); + ", + r"things.filter((thing, index) => { + otherThings.push(); + }); + ", + r"things.some((thing, index) => { + otherThings.push(); + }); + ", + r"things.every((thing, index) => { + otherThings.push(); + }); + ", + r"things.find((thing, index) => { + otherThings.push(); + }); + ", + r"things.findIndex((thing, index) => { + otherThings.push(); + }); + ", + r"things.flatMap((thing, index) => ( + + )); + ", + r"things.reduce((collection, thing, index) => ( + collection.concat() + ), []); + ", + r"things.reduceRight((collection, thing, index) => ( + collection.concat() + ), []); + ", + ]; + + Tester::new(NoArrayIndexKey::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/react/require_render_return.rs b/crates/oxc_linter/src/rules/react/require_render_return.rs index 5be0cdf30ed79..aeeda788a1054 100644 --- a/crates/oxc_linter/src/rules/react/require_render_return.rs +++ b/crates/oxc_linter/src/rules/react/require_render_return.rs @@ -127,6 +127,7 @@ fn contains_return_statement(node: &AstNode, ctx: &LintContext) -> bool { return (FoundReturn::No, STOP_WALKING_ON_THIS_PATH); } InstructionKind::Return(ReturnInstructionKind::ImplicitUndefined) + | InstructionKind::ImplicitReturn | InstructionKind::Break(_) | InstructionKind::Continue(_) | InstructionKind::Iteration(_) diff --git a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs deleted file mode 100644 index 0e898d1fa1065..0000000000000 --- a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs +++ /dev/null @@ -1,1354 +0,0 @@ -use oxc_ast::{ - ast::{ - match_declaration, match_expression, match_member_expression, - match_simple_assignment_target, Argument, ArrayExpressionElement, ArrowFunctionExpression, - AssignmentTarget, BinaryExpression, BindingIdentifier, BindingPattern, BindingPatternKind, - CallExpression, Class, ClassBody, ClassElement, ComputedMemberExpression, - ConditionalExpression, Declaration, ExportSpecifier, Expression, ForStatementInit, - FormalParameter, Function, IdentifierReference, JSXAttribute, JSXAttributeItem, - JSXAttributeValue, JSXChild, JSXElement, JSXElementName, JSXExpression, - JSXExpressionContainer, JSXFragment, JSXMemberExpression, JSXOpeningElement, - LogicalExpression, MemberExpression, ModuleExportName, NewExpression, ObjectExpression, - ObjectPropertyKind, ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey, - SequenceExpression, SimpleAssignmentTarget, Statement, StaticMemberExpression, SwitchCase, - ThisExpression, UnaryExpression, VariableDeclarator, - }, - AstKind, -}; -use oxc_semantic::{AstNode, NodeId}; -use oxc_span::{GetSpan, Span}; -use oxc_syntax::operator::{LogicalOperator, UnaryOperator}; - -use crate::{ - ast_util::{get_declaration_of_variable, get_symbol_id_of_variable}, - utils::{ - calculate_binary_operation, calculate_logical_operation, calculate_unary_operation, - get_write_expr, has_comment_about_side_effect_check, has_pure_notation, - is_function_side_effect_free, is_local_variable_a_whitelisted_module, is_pure_function, - no_effects, FunctionName, NodeListenerOptions, Value, - }, -}; - -pub trait ListenerMap<'a> { - fn report_effects(&self, _options: &NodeListenerOptions<'a, '_>) {} - fn report_effects_when_assigned(&self, _options: &NodeListenerOptions<'a, '_>) {} - fn report_effects_when_called(&self, _options: &NodeListenerOptions<'a, '_>) {} - fn report_effects_when_mutated(&self, _options: &NodeListenerOptions<'a, '_>) {} - fn get_value_and_report_effects(&self, _options: &NodeListenerOptions<'a, '_>) -> Value { - Value::Unknown - } -} - -impl<'a> ListenerMap<'a> for Program<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.body.iter().for_each(|stmt| stmt.report_effects(options)); - } -} - -impl<'a> ListenerMap<'a> for Statement<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::ExpressionStatement(expr_stmt) => { - expr_stmt.expression.report_effects(options); - } - #[allow(clippy::match_same_arms)] - Self::BreakStatement(_) | Self::ContinueStatement(_) | Self::EmptyStatement(_) => { - no_effects(); - } - match_declaration!(Self) => self.to_declaration().report_effects(options), - Self::ReturnStatement(stmt) => { - if let Some(arg) = &stmt.argument { - arg.report_effects(options); - } - } - Self::ExportAllDeclaration(_) | Self::ImportDeclaration(_) => { - no_effects(); - } - Self::ExportDefaultDeclaration(stmt) => { - if let Some(expr) = &stmt.declaration.as_expression() { - if has_comment_about_side_effect_check(expr.span(), options.ctx) { - expr.report_effects_when_called(options); - } - expr.report_effects(options); - } - } - Self::ExportNamedDeclaration(stmt) => { - stmt.specifiers.iter().for_each(|specifier| { - specifier.report_effects(options); - }); - - if let Some(decl) = &stmt.declaration { - decl.report_effects(options); - } - } - Self::TryStatement(stmt) => { - stmt.block.body.iter().for_each(|stmt| stmt.report_effects(options)); - stmt.handler.iter().for_each(|handler| { - handler.body.body.iter().for_each(|stmt| stmt.report_effects(options)); - }); - stmt.finalizer.iter().for_each(|finalizer| { - finalizer.body.iter().for_each(|stmt| stmt.report_effects(options)); - }); - } - Self::ThrowStatement(stmt) => { - options.ctx.diagnostic(super::throw(stmt.span)); - } - Self::BlockStatement(stmt) => { - stmt.body.iter().for_each(|stmt| stmt.report_effects(options)); - } - Self::IfStatement(stmt) => { - let test_result = stmt.test.get_value_and_report_effects(options); - - if let Some(is_falsy) = test_result.get_falsy_value() { - if is_falsy { - if let Some(alternate) = &stmt.alternate { - alternate.report_effects(options); - } - } else { - stmt.consequent.report_effects(options); - } - } else { - stmt.consequent.report_effects(options); - if let Some(alternate) = &stmt.alternate { - alternate.report_effects(options); - } - } - } - Self::DoWhileStatement(stmt) => { - if stmt - .test - .get_value_and_report_effects(options) - .get_falsy_value() - .is_some_and(|is_falsy| is_falsy) - { - return; - } - stmt.body.report_effects(options); - } - Self::DebuggerStatement(stmt) => { - options.ctx.diagnostic(super::debugger(stmt.span)); - } - Self::ForStatement(stmt) => { - if let Some(init) = &stmt.init { - init.report_effects(options); - } - if let Some(test) = &stmt.test { - test.report_effects(options); - } - if let Some(update) = &stmt.update { - update.report_effects(options); - } - stmt.body.report_effects(options); - } - Self::ForInStatement(stmt) => { - if let Some(assign) = stmt.left.as_assignment_target() { - assign.report_effects_when_assigned(options); - } - stmt.right.report_effects(options); - stmt.body.report_effects(options); - } - Self::ForOfStatement(stmt) => { - if let Some(assign) = stmt.left.as_assignment_target() { - assign.report_effects_when_assigned(options); - } - stmt.right.report_effects(options); - stmt.body.report_effects(options); - } - Self::LabeledStatement(stmt) => { - stmt.body.report_effects(options); - } - Self::WhileStatement(stmt) => { - if stmt - .test - .get_value_and_report_effects(options) - .get_falsy_value() - .is_some_and(|is_falsy| is_falsy) - { - return; - } - stmt.body.report_effects(options); - } - Self::SwitchStatement(stmt) => { - stmt.discriminant.report_effects(options); - stmt.cases.iter().for_each(|case| { - case.report_effects(options); - }); - } - _ => {} - } - } -} - -impl<'a> ListenerMap<'a> for ForStatementInit<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - match_expression!(Self) => self.to_expression().report_effects(options), - Self::VariableDeclaration(decl) => { - decl.declarations.iter().for_each(|decl| decl.report_effects(options)); - } - } - } -} - -impl<'a> ListenerMap<'a> for ExportSpecifier<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - let ctx = options.ctx; - let symbol_table = ctx.symbols(); - if has_comment_about_side_effect_check(self.exported.span(), ctx) { - let ModuleExportName::IdentifierReference(ident) = &self.local else { - return; - }; - let Some(symbol_id) = get_symbol_id_of_variable(ident, ctx) else { - return; - }; - - for reference in symbol_table.get_resolved_references(symbol_id) { - if reference.is_write() { - let node_id = reference.node_id(); - if let Some(expr) = get_write_expr(node_id, ctx) { - expr.report_effects_when_called(options); - } - } - } - let symbol_table = ctx.semantic().symbols(); - let node = ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)); - node.report_effects_when_called(options); - } - } -} - -// we don't need implement all AstNode -// it's same as `reportSideEffectsInDefinitionWhenCalled` in eslint-plugin-tree-shaking -// -impl<'a> ListenerMap<'a> for AstNode<'a> { - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - match self.kind() { - AstKind::VariableDeclarator(decl) => { - if let Some(init) = &decl.init { - init.report_effects_when_called(options); - } - } - AstKind::FormalParameter(param) => { - options.ctx.diagnostic(super::call_parameter(param.span)); - } - AstKind::BindingRestElement(rest) => { - let start = rest.span.start + 3; - let end = rest.span.end; - options.ctx.diagnostic(super::call_parameter(Span::new(start, end))); - } - AstKind::Function(function) => { - let old_val = options.has_valid_this.get(); - options.has_valid_this.set(options.called_with_new.get()); - function.report_effects_when_called(options); - options.has_valid_this.set(old_val); - } - AstKind::Class(class) => { - class.report_effects_when_called(options); - } - AstKind::ImportDefaultSpecifier(specifier) => { - report_on_imported_call( - specifier.local.span, - &specifier.local.name, - self.id(), - options, - ); - } - AstKind::ImportSpecifier(specifier) => { - report_on_imported_call( - specifier.local.span, - &specifier.local.name, - self.id(), - options, - ); - } - AstKind::ImportNamespaceSpecifier(specifier) => { - report_on_imported_call( - specifier.local.span, - &specifier.local.name, - self.id(), - options, - ); - } - _ => {} - } - } - - fn report_effects_when_mutated(&self, options: &NodeListenerOptions<'a, '_>) { - match self.kind() { - AstKind::VariableDeclarator(decl) => { - if let Some(init) = &decl.init { - init.report_effects_when_mutated(options); - } - } - AstKind::FormalParameter(param) => { - options.ctx.diagnostic(super::mutate_parameter(param.span)); - } - AstKind::BindingRestElement(rest) => { - let start = rest.span.start + 3; - let end = rest.span.end; - options.ctx.diagnostic(super::mutate_parameter(Span::new(start, end))); - } - AstKind::ImportDefaultSpecifier(specifier) => { - options.ctx.diagnostic(super::mutate_import(specifier.span)); - } - AstKind::ImportSpecifier(specifier) => { - options.ctx.diagnostic(super::mutate_import(specifier.local.span)); - } - AstKind::ImportNamespaceSpecifier(specifier) => { - options.ctx.diagnostic(super::mutate_import(specifier.local.span)); - } - _ => {} - } - } -} - -fn report_on_imported_call(span: Span, name: &str, node_id: NodeId, options: &NodeListenerOptions) { - if has_comment_about_side_effect_check(span, options.ctx) { - return; - } - let Some(AstKind::ImportDeclaration(decl)) = options.ctx.nodes().parent_kind(node_id) else { - return; - }; - if is_function_side_effect_free(name, &decl.source.value, options) { - return; - } - options.ctx.diagnostic(super::call_import(span)); -} - -impl<'a> ListenerMap<'a> for Declaration<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::VariableDeclaration(decl) => { - decl.declarations.iter().for_each(|decl| decl.report_effects(options)); - } - Self::ClassDeclaration(decl) => { - decl.report_effects(options); - } - Self::FunctionDeclaration(function) => { - if let Some(id) = &function.id { - if has_comment_about_side_effect_check(id.span, options.ctx) { - id.report_effects_when_called(options); - } - } - } - _ => {} - } - } -} - -impl<'a> ListenerMap<'a> for Class<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - if let Some(super_class) = &self.super_class { - super_class.report_effects(options); - } - self.body.report_effects(options); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - if let Some(super_class) = &self.super_class { - super_class.report_effects_when_called(options); - } - self.body.report_effects_when_called(options); - } -} - -impl<'a> ListenerMap<'a> for ClassBody<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.body.iter().for_each(|class_element| { - class_element.report_effects(options); - }); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - let constructor = self.body.iter().find(|class_element| { - if let ClassElement::MethodDefinition(definition) = class_element { - return definition.kind.is_constructor(); - } - false - }); - - if let Some(constructor) = constructor { - let old_val = options.has_valid_this.get(); - options.has_valid_this.set(options.called_with_new.get()); - constructor.report_effects_when_called(options); - options.has_valid_this.set(old_val); - } - - self.body - .iter() - .filter(|class_element| matches!(class_element, ClassElement::PropertyDefinition(_))) - .for_each(|property_definition| { - property_definition.report_effects_when_called(options); - }); - } -} - -impl<'a> ListenerMap<'a> for ClassElement<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::MethodDefinition(method) => { - method.key.report_effects(options); - } - Self::PropertyDefinition(prop) => { - prop.key.report_effects(options); - } - _ => {} - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::MethodDefinition(method) => { - method.value.report_effects_when_called(options); - } - Self::PropertyDefinition(prop) => { - if let Some(value) = &prop.value { - value.report_effects_when_called(options); - } - } - _ => {} - } - } -} - -impl<'a> ListenerMap<'a> for PropertyKey<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self.as_expression() { - Some(expr) => expr.report_effects(options), - None => no_effects(), - } - } -} - -impl<'a> ListenerMap<'a> for VariableDeclarator<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.id.report_effects(options); - if has_comment_about_side_effect_check(self.id.span(), options.ctx) { - self.id.report_effects_when_called(options); - } - - if let Some(init) = &self.init { - init.report_effects(options); - } - } -} - -impl<'a> ListenerMap<'a> for BindingPattern<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match &self.kind { - BindingPatternKind::BindingIdentifier(_) => {} - BindingPatternKind::ArrayPattern(array) => { - array.elements.iter().for_each(|el| { - if let Some(el) = el { - el.report_effects(options); - } - }); - } - BindingPatternKind::ObjectPattern(object) => { - object.properties.iter().for_each(|prop| { - prop.key.report_effects(options); - prop.value.report_effects(options); - }); - } - BindingPatternKind::AssignmentPattern(assign_p) => { - assign_p.left.report_effects(options); - assign_p.right.report_effects(options); - } - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - if let BindingPatternKind::BindingIdentifier(ident) = &self.kind { - ident.report_effects_when_called(options); - } - } -} - -impl<'a> ListenerMap<'a> for BindingIdentifier<'a> { - fn report_effects(&self, _options: &NodeListenerOptions<'a, '_>) { - no_effects(); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - let ctx = options.ctx; - if let Some(symbol_id) = self.symbol_id.get() { - let symbol_table = ctx.semantic().symbols(); - for reference in symbol_table.get_resolved_references(symbol_id) { - if reference.is_write() { - let node_id = reference.node_id(); - if let Some(expr) = get_write_expr(node_id, ctx) { - expr.report_effects_when_called(options); - } - } - } - let node = ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)); - node.report_effects_when_called(options); - } - } -} - -impl<'a> ListenerMap<'a> for Expression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::ArrayExpression(array_expr) => { - array_expr.elements.iter().for_each(|el| el.report_effects(options)); - } - Self::AssignmentExpression(assign_expr) => { - assign_expr.left.report_effects_when_assigned(options); - assign_expr.right.report_effects(options); - } - Self::CallExpression(call_expr) => { - call_expr.report_effects(options); - } - Self::ParenthesizedExpression(expr) => { - expr.report_effects(options); - } - Self::NewExpression(expr) => { - expr.report_effects(options); - } - Self::AwaitExpression(expr) => { - expr.argument.report_effects(options); - } - Self::BinaryExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::ClassExpression(expr) => { - expr.report_effects(options); - } - Self::ConditionalExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::JSXElement(expr) => { - expr.report_effects(options); - } - Self::ObjectExpression(expr) => { - expr.report_effects(options); - } - Self::LogicalExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::StaticMemberExpression(expr) => { - expr.report_effects(options); - } - Self::ComputedMemberExpression(expr) => { - expr.report_effects(options); - } - Self::PrivateFieldExpression(expr) => { - expr.report_effects(options); - } - Self::UnaryExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::UpdateExpression(expr) => { - expr.argument.report_effects_when_assigned(options); - } - Self::SequenceExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::YieldExpression(expr) => { - expr.argument.iter().for_each(|arg| arg.report_effects(options)); - } - Self::TaggedTemplateExpression(expr) => { - expr.tag.report_effects_when_called(options); - expr.quasi.expressions.iter().for_each(|expr| { - expr.report_effects(options); - }); - } - Self::TemplateLiteral(expr) => { - expr.expressions.iter().for_each(|expr| { - expr.report_effects(options); - }); - } - Self::ArrowFunctionExpression(_) - | Self::FunctionExpression(_) - | Self::Identifier(_) - | Self::MetaProperty(_) - | Self::Super(_) - | Self::ThisExpression(_) => no_effects(), - _ => {} - } - } - - fn report_effects_when_mutated(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::Identifier(ident) => { - ident.report_effects_when_mutated(options); - } - Self::ArrowFunctionExpression(_) | Self::ObjectExpression(_) => no_effects(), - Self::ParenthesizedExpression(expr) => { - expr.report_effects_when_mutated(options); - } - Self::CallExpression(expr) => { - expr.report_effects_when_mutated(options); - } - Self::ThisExpression(expr) => { - expr.report_effects_when_mutated(options); - } - _ => { - // Default behavior - options.ctx.diagnostic(super::mutate(self.span())); - } - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::CallExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::Identifier(expr) => { - expr.report_effects_when_called(options); - } - Self::FunctionExpression(expr) => { - let old_val = options.has_valid_this.get(); - options.has_valid_this.set(options.called_with_new.get()); - expr.report_effects_when_called(options); - options.has_valid_this.set(old_val); - } - Self::ArrowFunctionExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::ParenthesizedExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::ClassExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::ConditionalExpression(expr) => expr.report_effects_when_called(options), - Self::StaticMemberExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::ComputedMemberExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::PrivateFieldExpression(expr) => { - expr.report_effects_when_called(options); - } - _ => { - // Default behavior - options.ctx.diagnostic(super::call(self.span())); - } - } - } - - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - match self { - Self::BooleanLiteral(_) - | Self::StringLiteral(_) - | Self::NumericLiteral(_) - | Self::TemplateLiteral(_) => Value::new(self), - Self::BinaryExpression(expr) => expr.get_value_and_report_effects(options), - Self::ConditionalExpression(expr) => expr.get_value_and_report_effects(options), - Self::LogicalExpression(expr) => expr.get_value_and_report_effects(options), - Self::SequenceExpression(expr) => expr.get_value_and_report_effects(options), - _ => { - self.report_effects(options); - Value::Unknown - } - } - } -} - -// which kind of Expression defines `report_effects_when_called` method. -fn defined_custom_report_effects_when_called(expr: &Expression) -> bool { - matches!( - expr.get_inner_expression(), - Expression::ArrowFunctionExpression(_) - | Expression::CallExpression(_) - | Expression::ClassExpression(_) - | Expression::ConditionalExpression(_) - | Expression::FunctionExpression(_) - | Expression::Identifier(_) - | Expression::ComputedMemberExpression(_) - | Expression::StaticMemberExpression(_) - | Expression::PrivateFieldExpression(_) - ) -} - -impl<'a> ListenerMap<'a> for SwitchCase<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - if let Some(test) = &self.test { - test.report_effects(options); - } - self.consequent.iter().for_each(|stmt| { - stmt.report_effects(options); - }); - } -} - -impl<'a> ListenerMap<'a> for SequenceExpression<'a> { - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - let mut val = Value::Unknown; - for expr in &self.expressions { - val = expr.get_value_and_report_effects(options); - } - val - } -} - -impl<'a> ListenerMap<'a> for UnaryExpression<'a> { - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - if self.operator == UnaryOperator::Delete { - match &self.argument { - Expression::StaticMemberExpression(expr) => { - expr.object.report_effects_when_mutated(options); - } - Expression::ComputedMemberExpression(expr) => { - expr.object.report_effects_when_mutated(options); - } - Expression::PrivateFieldExpression(expr) => { - expr.object.report_effects_when_mutated(options); - } - _ => options.ctx.diagnostic(super::delete(self.argument.span())), - } - return Value::Unknown; - } - - let value = self.argument.get_value_and_report_effects(options); - calculate_unary_operation(self.operator, value) - } -} - -impl<'a> ListenerMap<'a> for LogicalExpression<'a> { - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - let left = self.left.get_value_and_report_effects(options); - // `false && foo` - if self.operator == LogicalOperator::And - && left.get_falsy_value().is_some_and(|is_falsy| is_falsy) - { - return left; - } - // `true || foo` - if self.operator == LogicalOperator::Or - && left.get_falsy_value().is_some_and(|is_falsy| !is_falsy) - { - return left; - } - let right = self.right.get_value_and_report_effects(options); - calculate_logical_operation(self.operator, left, right) - } -} - -impl<'a> ListenerMap<'a> for ObjectExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.properties.iter().for_each(|property| match property { - ObjectPropertyKind::ObjectProperty(p) => { - p.key.report_effects(options); - p.value.report_effects(options); - } - ObjectPropertyKind::SpreadProperty(spreed) => { - spreed.argument.report_effects(options); - } - }); - } -} - -impl<'a> ListenerMap<'a> for JSXElement<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.opening_element.report_effects(options); - self.children.iter().for_each(|child| { - child.report_effects(options); - }); - } -} - -impl<'a> ListenerMap<'a> for JSXChild<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - JSXChild::Element(element) => { - element.report_effects(options); - } - JSXChild::Spread(spread) => { - spread.expression.report_effects(options); - } - JSXChild::Fragment(fragment) => { - fragment.report_effects(options); - } - JSXChild::ExpressionContainer(container) => { - container.report_effects(options); - } - JSXChild::Text(_) => { - no_effects(); - } - } - } -} - -impl<'a> ListenerMap<'a> for JSXOpeningElement<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.name.report_effects_when_called(options); - self.attributes.iter().for_each(|attr| attr.report_effects(options)); - } -} - -impl<'a> ListenerMap<'a> for JSXElementName<'a> { - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::Identifier(_) | Self::NamespacedName(_) => {} - Self::IdentifierReference(ident) => ident.report_effects_when_called(options), - Self::MemberExpression(member) => member.report_effects_when_called(options), - Self::ThisExpression(expr) => expr.report_effects_when_called(options), - } - } -} - -impl<'a> ListenerMap<'a> for JSXMemberExpression<'a> { - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - options.ctx.diagnostic(super::call_member(self.property.span())); - } -} - -impl<'a> ListenerMap<'a> for JSXAttributeItem<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::Attribute(attribute) => { - attribute.report_effects(options); - } - Self::SpreadAttribute(attribute) => { - attribute.argument.report_effects(options); - } - } - } -} - -impl<'a> ListenerMap<'a> for JSXAttribute<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - if let Some(value) = &self.value { - match value { - JSXAttributeValue::ExpressionContainer(container) => { - container.report_effects(options); - } - JSXAttributeValue::Element(element) => { - element.report_effects(options); - } - JSXAttributeValue::Fragment(fragment) => { - fragment.report_effects(options); - } - JSXAttributeValue::StringLiteral(_) => { - no_effects(); - } - } - } - } -} - -impl<'a> ListenerMap<'a> for JSXExpressionContainer<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.expression.report_effects(options); - } -} - -impl<'a> ListenerMap<'a> for JSXExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::ArrayExpression(array_expr) => { - array_expr.elements.iter().for_each(|el| el.report_effects(options)); - } - Self::AssignmentExpression(assign_expr) => { - assign_expr.left.report_effects_when_assigned(options); - assign_expr.right.report_effects(options); - } - Self::CallExpression(call_expr) => { - call_expr.report_effects(options); - } - Self::ParenthesizedExpression(expr) => { - expr.report_effects(options); - } - Self::NewExpression(expr) => { - expr.report_effects(options); - } - Self::AwaitExpression(expr) => { - expr.argument.report_effects(options); - } - Self::BinaryExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::ClassExpression(expr) => { - expr.report_effects(options); - } - Self::ConditionalExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::JSXElement(expr) => { - expr.report_effects(options); - } - Self::ObjectExpression(expr) => { - expr.report_effects(options); - } - Self::StaticMemberExpression(expr) => { - expr.report_effects(options); - } - Self::ComputedMemberExpression(expr) => { - expr.report_effects(options); - } - Self::PrivateFieldExpression(expr) => { - expr.report_effects(options); - } - Self::UnaryExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::SequenceExpression(expr) => { - expr.get_value_and_report_effects(options); - } - Self::ArrowFunctionExpression(_) - | Self::EmptyExpression(_) - | Self::FunctionExpression(_) - | Self::Identifier(_) - | Self::MetaProperty(_) - | Self::Super(_) - | Self::ThisExpression(_) => no_effects(), - _ => {} - } - } -} - -impl<'a> ListenerMap<'a> for JSXFragment<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.children.iter().for_each(|child| child.report_effects(options)); - } -} - -impl<'a> ListenerMap<'a> for ConditionalExpression<'a> { - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - let test_result = self.test.get_value_and_report_effects(options); - - if let Some(is_falsy) = test_result.get_falsy_value() { - if is_falsy { - self.alternate.get_value_and_report_effects(options) - } else { - self.consequent.get_value_and_report_effects(options) - } - } else { - self.consequent.report_effects(options); - self.alternate.report_effects(options); - test_result - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - let test_result = self.test.get_value_and_report_effects(options); - - if let Some(falsy) = test_result.get_falsy_value() { - if falsy { - self.alternate.report_effects_when_called(options); - } else { - self.consequent.report_effects_when_called(options); - } - } else { - self.consequent.report_effects_when_called(options); - self.alternate.report_effects_when_called(options); - } - } -} - -impl<'a> ListenerMap<'a> for BinaryExpression<'a> { - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - let left = self.left.get_value_and_report_effects(options); - let right = self.right.get_value_and_report_effects(options); - calculate_binary_operation(self.operator, left, right) - } -} - -impl<'a> ListenerMap<'a> for ThisExpression { - fn report_effects_when_mutated(&self, options: &NodeListenerOptions<'a, '_>) { - if !options.has_valid_this.get() { - options.ctx.diagnostic(super::mutate_of_this(self.span)); - } - } -} - -impl<'a> ListenerMap<'a> for NewExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - if has_pure_notation(self.span, options.ctx) { - return; - } - self.arguments.iter().for_each(|arg| arg.report_effects(options)); - let old_val = options.called_with_new.get(); - options.called_with_new.set(true); - self.callee.report_effects_when_called(options); - options.called_with_new.set(old_val); - } -} - -impl<'a> ListenerMap<'a> for ParenthesizedExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.expression.report_effects(options); - } - - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - self.expression.report_effects_when_assigned(options); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - self.expression.report_effects_when_called(options); - } - - fn report_effects_when_mutated(&self, options: &NodeListenerOptions<'a, '_>) { - self.expression.report_effects_when_mutated(options); - } - - fn get_value_and_report_effects(&self, options: &NodeListenerOptions<'a, '_>) -> Value { - self.expression.get_value_and_report_effects(options) - } -} - -impl<'a> ListenerMap<'a> for ArrowFunctionExpression<'a> { - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - self.params.items.iter().for_each(|param| param.report_effects(options)); - self.body.statements.iter().for_each(|stmt| stmt.report_effects(options)); - } -} - -impl<'a> ListenerMap<'a> for Function<'a> { - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - self.params.items.iter().for_each(|param| param.report_effects(options)); - - if let Some(body) = &self.body { - body.statements.iter().for_each(|stmt| stmt.report_effects(options)); - } - } -} - -impl<'a> ListenerMap<'a> for FormalParameter<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.pattern.report_effects(options); - } -} - -impl<'a> ListenerMap<'a> for CallExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.arguments.iter().for_each(|arg| arg.report_effects(options)); - if defined_custom_report_effects_when_called(&self.callee) { - let old_value = options.called_with_new.get(); - options.called_with_new.set(false); - self.callee.report_effects_when_called(options); - options.called_with_new.set(old_value); - } else { - options.ctx.diagnostic(super::call(self.callee.span())); - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - let ctx = options.ctx; - if let Expression::Identifier(ident) = &self.callee { - if let Some(node) = get_declaration_of_variable(ident, ctx) { - if is_local_variable_a_whitelisted_module(node, ident.name.as_str(), options) { - return; - } - options.ctx.diagnostic(super::call_return_value(self.span)); - } else { - options.ctx.diagnostic(super::call_return_value(self.span)); - } - } - } - - fn report_effects_when_mutated(&self, options: &NodeListenerOptions<'a, '_>) { - options.ctx.diagnostic(super::mutate_function_return_value(self.span)); - } -} - -impl<'a> ListenerMap<'a> for Argument<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - match_expression!(Self) => self.to_expression().report_effects(options), - Self::SpreadElement(spread) => { - spread.argument.report_effects(options); - } - } - } -} - -impl<'a> ListenerMap<'a> for AssignmentTarget<'a> { - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - match_simple_assignment_target!(Self) => { - self.to_simple_assignment_target().report_effects_when_assigned(options); - } - Self::ArrayAssignmentTarget(_) | Self::ObjectAssignmentTarget(_) => {} - } - } -} - -impl<'a> ListenerMap<'a> for SimpleAssignmentTarget<'a> { - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::AssignmentTargetIdentifier(ident) => { - ident.report_effects_when_assigned(options); - } - match_member_expression!(Self) => { - self.to_member_expression().report_effects_when_assigned(options); - } - _ => { - // For remain TypeScript AST, just visit its expression - if let Some(expr) = self.get_expression() { - expr.report_effects_when_assigned(options); - } - } - } - } -} - -impl<'a> ListenerMap<'a> for IdentifierReference<'a> { - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - if get_symbol_id_of_variable(self, options.ctx).is_none() { - options.ctx.diagnostic(super::assignment(self.name.as_str(), self.span)); - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - if is_pure_function(&FunctionName::Identifier(self), options) { - return; - } - - let ctx = options.ctx; - - if let Some(symbol_id) = get_symbol_id_of_variable(self, ctx) { - let is_used_in_jsx = matches!( - ctx.nodes().parent_kind( - ctx.symbols().get_reference(self.reference_id.get().unwrap()).node_id() - ), - Some(AstKind::JSXElementName(_) | AstKind::JSXMemberExpressionObject(_)) - ); - - if is_used_in_jsx { - for reference in options.ctx.symbols().get_resolved_references(symbol_id) { - if reference.is_write() { - let node_id = reference.node_id(); - if let Some(expr) = get_write_expr(node_id, options.ctx) { - let old_val = options.called_with_new.get(); - options.called_with_new.set(true); - expr.report_effects_when_called(options); - options.called_with_new.set(old_val); - } - } - } - let symbol_table = options.ctx.semantic().symbols(); - let node = options.ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)); - let old_val = options.called_with_new.get(); - options.called_with_new.set(true); - node.report_effects_when_called(options); - options.called_with_new.set(old_val); - return; - } - - if options.insert_called_node(symbol_id) { - let symbol_table = ctx.semantic().symbols(); - for reference in symbol_table.get_resolved_references(symbol_id) { - if reference.is_write() { - let node_id = reference.node_id(); - if let Some(expr) = get_write_expr(node_id, ctx) { - expr.report_effects_when_called(options); - } - } - } - let symbol_table = ctx.semantic().symbols(); - let node = ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)); - node.report_effects_when_called(options); - } - } else { - ctx.diagnostic(super::call_global(self.name.as_str(), self.span)); - } - } - - fn report_effects_when_mutated(&self, options: &NodeListenerOptions<'a, '_>) { - let ctx = options.ctx; - if let Some(symbol_id) = get_symbol_id_of_variable(self, ctx) { - if options.insert_mutated_node(symbol_id) { - for reference in ctx.symbols().get_resolved_references(symbol_id) { - if reference.is_write() { - let node_id = reference.node_id(); - if let Some(expr) = get_write_expr(node_id, ctx) { - expr.report_effects_when_mutated(options); - } - } - } - - let symbol_table = ctx.semantic().symbols(); - let node = ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)); - node.report_effects_when_mutated(options); - } - } else { - ctx.diagnostic(super::mutate_with_name(self.name.as_str(), self.span)); - } - } -} - -impl<'a> ListenerMap<'a> for MemberExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::ComputedMemberExpression(expr) => { - expr.report_effects(options); - } - Self::StaticMemberExpression(expr) => { - expr.report_effects(options); - } - Self::PrivateFieldExpression(expr) => { - expr.report_effects(options); - } - } - } - - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::ComputedMemberExpression(expr) => { - expr.report_effects_when_assigned(options); - } - Self::StaticMemberExpression(expr) => { - expr.report_effects_when_assigned(options); - } - Self::PrivateFieldExpression(expr) => { - expr.report_effects_when_assigned(options); - } - } - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - Self::ComputedMemberExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::StaticMemberExpression(expr) => { - expr.report_effects_when_called(options); - } - Self::PrivateFieldExpression(expr) => { - expr.report_effects_when_called(options); - } - } - } -} - -impl<'a> ListenerMap<'a> for ComputedMemberExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.expression.report_effects(options); - self.object.report_effects(options); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - self.report_effects(options); - - let mut node = &self.object; - loop { - match node { - Expression::ComputedMemberExpression(expr) => { - node = &expr.object; - } - Expression::StaticMemberExpression(expr) => node = &expr.object, - Expression::PrivateInExpression(expr) => node = &expr.right, - _ => { - break; - } - } - } - - if let Expression::Identifier(ident) = node { - ident.report_effects_when_called(options); - } else { - options.ctx.diagnostic(super::call_member(node.span())); - } - } - - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - self.report_effects(options); - self.object.report_effects_when_mutated(options); - } -} - -impl<'a> ListenerMap<'a> for StaticMemberExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.object.report_effects(options); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - self.report_effects(options); - - let mut root_member_expr = &self.object; - loop { - match root_member_expr { - Expression::ComputedMemberExpression(expr) => { - root_member_expr = &expr.object; - } - Expression::StaticMemberExpression(expr) => root_member_expr = &expr.object, - Expression::PrivateInExpression(expr) => root_member_expr = &expr.right, - _ => { - break; - } - } - } - - let Expression::Identifier(ident) = root_member_expr else { - options.ctx.diagnostic(super::call_member(root_member_expr.span())); - return; - }; - - let Some(node) = get_declaration_of_variable(ident, options.ctx) else { - // If the variable is not declared, it is a global variable. - // `ext.x()` - if !is_pure_function(&FunctionName::StaticMemberExpr(self), options) { - options.ctx.diagnostic(super::call_member(self.span)); - } - return; - }; - - if is_local_variable_a_whitelisted_module(node, &ident.name, options) { - return; - }; - - if has_pure_notation(self.span, options.ctx) { - return; - } - - options.ctx.diagnostic(super::call_member(self.span)); - } - - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - self.report_effects(options); - self.object.report_effects_when_mutated(options); - } -} - -impl<'a> ListenerMap<'a> for PrivateFieldExpression<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - self.object.report_effects(options); - } - - fn report_effects_when_called(&self, options: &NodeListenerOptions<'a, '_>) { - self.report_effects(options); - - let mut node = &self.object; - loop { - match node { - Expression::ComputedMemberExpression(expr) => { - node = &expr.object; - } - Expression::StaticMemberExpression(expr) => node = &expr.object, - Expression::PrivateInExpression(expr) => node = &expr.right, - _ => { - break; - } - } - } - - if let Expression::Identifier(ident) = node { - ident.report_effects_when_called(options); - } else { - options.ctx.diagnostic(super::call_member(node.span())); - } - } - - fn report_effects_when_assigned(&self, options: &NodeListenerOptions<'a, '_>) { - self.report_effects(options); - self.object.report_effects_when_mutated(options); - } -} - -impl<'a> ListenerMap<'a> for ArrayExpressionElement<'a> { - fn report_effects(&self, options: &NodeListenerOptions<'a, '_>) { - match self { - match_expression!(Self) => self.to_expression().report_effects(options), - Self::SpreadElement(spreed) => { - spreed.argument.report_effects(options); - } - Self::Elision(_) => {} - } - } -} diff --git a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs deleted file mode 100644 index be2dab30c97f2..0000000000000 --- a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs +++ /dev/null @@ -1,896 +0,0 @@ -use oxc_ast::AstKind; -use oxc_diagnostics::OxcDiagnostic; -use oxc_macros::declare_oxc_lint; -use oxc_span::Span; -use serde_json::Value; - -use self::listener_map::ListenerMap; -use crate::{ - context::LintContext, - rule::Rule, - utils::{ModuleFunctions, NodeListenerOptions, WhitelistModule}, -}; - -mod listener_map; - -fn assignment(x0: &str, span1: Span) -> OxcDiagnostic { - OxcDiagnostic::warn(format!("Cannot determine side-effects of assignment to `{x0}`")) - .with_label(span1) -} - -fn mutate(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of mutating").with_label(span) -} - -fn mutate_with_name(x0: &str, span1: Span) -> OxcDiagnostic { - OxcDiagnostic::warn(format!("Cannot determine side-effects of mutating `{x0}`")) - .with_label(span1) -} - -fn mutate_function_return_value(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of mutating function return value") - .with_label(span) -} - -fn mutate_parameter(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of mutating function parameter") - .with_label(span) -} - -fn mutate_of_this(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of mutating unknown this value") - .with_label(span) -} - -fn mutate_import(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of mutating imported variable") - .with_label(span) -} - -fn call(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of calling").with_label(span) -} - -fn call_return_value(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of calling function return value") - .with_label(span) -} - -fn call_global(x0: &str, span1: Span) -> OxcDiagnostic { - OxcDiagnostic::warn(format!("Cannot determine side-effects of calling global function `{x0}`")) - .with_label(span1) -} - -fn call_parameter(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of calling function parameter") - .with_label(span) -} - -fn call_import(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of calling imported function") - .with_label(span) -} - -fn call_member(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of calling member function").with_label(span) -} - -fn debugger(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Debugger statements are side-effects").with_label(span) -} - -fn delete(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Cannot determine side-effects of deleting anything but a MemberExpression") - .with_label(span) -} - -fn throw(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Throwing an error is a side-effect").with_label(span) -} - -/// -#[derive(Debug, Default, Clone)] -pub struct NoSideEffectsInInitialization(Box); - -impl std::ops::Deref for NoSideEffectsInInitialization { - type Target = NoSideEffectsInInitiallizationOptions; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Debug, Default, Clone)] -pub struct NoSideEffectsInInitiallizationOptions { - functions: Vec, - modules: Vec, -} - -declare_oxc_lint!( - /// ### What it does - /// - /// Marks all side-effects in module initialization that will interfere with tree-shaking. - /// - /// This plugin is intended as a means for library developers to identify patterns that will - /// interfere with the tree-shaking algorithm of their module bundler (i.e. rollup or webpack). - /// - /// ### Why is this bad? - /// - /// Side-effects in module initialization can hinder tree-shaking, which aims to remove - /// unused code. If side-effects exist, it's harder for the bundler to safely eliminate - /// code, leading to larger bundles and potentially unexpected behavior. Ensuring minimal - /// side-effects allows bundlers to optimize code effectively. - /// - /// ### Examples - /// - /// Examples of **incorrect** code for this rule: - /// ```javascript - /// myGlobal = 17; // Cannot determine side-effects of assignment to global variable - /// const x = { [globalFunction()]: "myString" }; // Cannot determine side-effects of calling global function - /// ``` - /// - /// Examples of **correct** code for this rule: - /// ```javascript - /// const localVar = 17; // Local variable assignment, no global side-effects - /// export default 42; // Pure export with no side-effects - /// ``` - /// - /// ### Options - /// - /// ```json - /// { - /// "rules": { - /// "tree-shaking/no-side-effects-in-initialization": [ - /// 2, - /// { - /// "noSideEffectsWhenCalled": [ - /// // If you want to mark a function call as side-effect free - /// { "function": "Object.freeze" }, - /// { - /// "module": "react", - /// "functions": ["createContext", "createRef"] - /// }, - /// { - /// "module": "zod", - /// "functions": ["array", "string", "nativeEnum", "number", "object", "optional"] - /// }, - /// { - /// "module": "my/local/module", - /// "functions": ["foo", "bar", "baz"] - /// }, - /// // If you want to whitelist all functions of a module - /// { - /// "module": "lodash", - /// "functions": "*" - /// } - /// ] - /// } - /// ] - /// } - /// } - /// ``` - /// - /// ### Magic Comments - /// - /// Besides the configuration, you can also use magic comments to mark a function call as side effect free. - /// - /// By default, imported functions are assumed to have side-effects, unless they are marked with a magic comment: - /// - /// ```js - /// import { /* tree-shaking no-side-effects-when-called */ x } from "./some-file"; - /// x(); - /// ``` - /// - /// `@__PURE__` is also supported: - /// - /// ```js - /// import {x} from "./some-file"; - /// /*@__PURE__*/ x(); - /// ``` - NoSideEffectsInInitialization, - nursery -); - -impl Rule for NoSideEffectsInInitialization { - fn from_configuration(value: serde_json::Value) -> Self { - let mut functions = vec![]; - let mut modules = vec![]; - - if let Value::Array(arr) = value { - for obj in arr { - let Value::Object(obj) = obj else { - continue; - }; - - // { "function": "Object.freeze" } - if let Some(name) = obj.get("function").and_then(Value::as_str) { - functions.push(name.to_string()); - continue; - } - - // { "module": "react", "functions": ["createContext", "createRef"] } - // { "module": "react", "functions": "*" } - if let Some(name) = obj.get("module").and_then(Value::as_str) { - let functions = match obj.get("functions") { - Some(Value::Array(arr)) => { - let val = arr - .iter() - .filter_map(Value::as_str) - .map(String::from) - .collect::>(); - Some(ModuleFunctions::Specific(val)) - } - Some(Value::String(str)) => { - if str == "*" { - Some(ModuleFunctions::All) - } else { - None - } - } - _ => None, - }; - if let Some(functions) = functions { - modules.push(WhitelistModule { name: name.to_string(), functions }); - } - } - } - } - - Self(Box::new(NoSideEffectsInInitiallizationOptions { functions, modules })) - } - - fn run_once(&self, ctx: &LintContext) { - let Some(root) = ctx.nodes().root_node() else { - return; - }; - let AstKind::Program(program) = root.kind() else { unreachable!() }; - let node_listener_options = NodeListenerOptions::new(ctx) - .with_whitelist_functions(&self.functions) - .with_whitelist_modules(&self.modules); - program.report_effects(&node_listener_options); - } -} - -#[test] -fn test() { - use crate::tester::Tester; - - let pass = vec![ - // ArrayExpression - "[]", - "const x = []", - "const x = [ext,ext]", - "const x = [1,,2,]", - // ArrayPattern - "const [x] = []", - "const [,x,] = []", - // ArrowFunctionExpression - "const x = a=>{a(); ext()}", - // ArrowFunctionExpression when called - "(()=>{})()", - "(a=>{})()", - "((...a)=>{})()", - "(({a})=>{})()", - // ArrowFunctionExpression when mutated - "const x = ()=>{}; x.y = 1", - // AssignmentExpression - "var x;x = {}", - "var x;x += 1", - "const x = {}; x.y = 1", - r#"const x = {}; x["y"] = 1"#, - "function x(){this.y = 1}; const z = new x()", - "let x = 1; x = 2 + 3", - "let x; x = 2 + 3", - // AssignmentPattern - "const {x = ext} = {}", - "const {x: y = ext} = {}", - "const {[ext]: x = ext} = {}", - "const x = ()=>{}, {y = x()} = {}", - // BinaryExpression - "const x = 1 + 2", - "if (1-1) ext()", - // BlockStatement - "{}", - "const x = ()=>{};{const x = ext}x()", - "const x = ext;{const x = ()=>{}; x()}", - // BreakStatement - "while(true){break}", - // CallExpression - "(a=>{const y = a})(ext, ext)", - "const x = ()=>{}, y = ()=>{}; x(y())", - // CatchClause - "try {} catch (error) {}", - "const x = ()=>{}; try {} catch (error) {const x = ext}; x()", - "const x = ext; try {} catch (error) {const x = ()=>{}; x()}", - // ClassBody - "class x {a(){ext()}}", - // ClassBody when called - "class x {a(){ext()}}; const y = new x()", - "class x {constructor(){}}; const y = new x()", - "class y{}; class x extends y{}; const z = new x()", - // ClassDeclaration - "class x extends ext {}", - // ClassDeclaration when called - "class x {}; const y = new x()", - // ClassExpression - "const x = class extends ext {}", - // ClassExpression when called - "const x = new (class {})()", - // ClassProperty - "class x {y}", - "class x {y = 1}", - "class x {y = ext()}", - // ConditionalExpression - "const x = ext ? 1 : 2", - "const x = true ? 1 : ext()", - "const x = false ? ext() : 2", - "if (true ? false : true) ext()", - "ext ? 1 : ext.x", - "ext ? ext.x : 1", - // // ConditionalExpression when called - "const x = ()=>{}, y = ()=>{};(ext ? x : y)()", - "const x = ()=>{}; (true ? x : ext)()", - "const x = ()=>{}; (false ? ext : x)()", - // ContinueStatement - "while(true){continue}", - // DoWhileStatement - "do {} while(true)", - "do {} while(ext > 0)", - "const x = ()=>{}; do x(); while(true)", - // EmptyStatement - ";", - // ExportAllDeclaration - r#"export * from "import""#, - // ExportDefaultDeclaration - "export default ext", - "const x = ext; export default x", - "export default function(){}", - "export default (function(){})", - "const x = function(){}; export default /* tree-shaking no-side-effects-when-called */ x", - "export default /* tree-shaking no-side-effects-when-called */ function(){}", - // ExportNamedDeclaration - "export const x = ext", - "export function x(){ext()}", - "const x = ext; export {x}", - r#"export {x} from "import""#, - r#"export {x as y} from "import""#, - r#"export {x as default} from "import""#, - "export const /* tree-shaking no-side-effects-when-called */ x = function(){}", - "export function /* tree-shaking no-side-effects-when-called */ x(){}", - " - { let x = ext; } - let x = () => {} - export {/* tree-shaking no-side-effects-when-called */ x} - ", - "const x = function(){}; export {/* tree-shaking no-side-effects-when-called */ x}", - // ExpressionStatement - "const x = 1", - // ForInStatement - "for(const x in ext){x = 1}", - "let x; for(x in ext){}", - // ForStatement - "for(let i = 0; i < 3; i++){i++}", - "for(;;){}", - // FunctionDeclaration - "function x(a){a(); ext()}", - // FunctionDeclaration when called - "function x(){}; x()", - "function x(a){}; x()", - "function x(...a){}; x()", - "function x({a}){}; x()", - // FunctionDeclaration when mutated - "function x(){}; x.y = 1", - // FunctionExpression - "const x = function (a){a(); ext()}", - // FunctionExpression when called - "(function (){}())", - "(function (a){}())", - "(function (...a){}())", - "(function ({a}){}())", - // Identifier - "var x;x = 1", - // Identifier when called - "const x = ()=>{};x(ext)", - "function x(){};x(ext)", - "var x = ()=>{};x(ext)", - "const x = ()=>{}, y = ()=>{x()}; y()", - "const x = ext, y = ()=>{const x = ()=>{}; x()}; y()", - // Identifier when mutated - "const x = {}; x.y = ext", - // IfStatement - "let y;if (ext > 0) {y = 1} else {y = 2}", - "if (false) {ext()}", - "if (true) {} else {ext()}", - // ImportDeclaration - r#"import "import""#, - r#"import x from "import-default""#, - r#"import {x} from "import""#, - r#"import {x as y} from "import""#, - r#"import * as x from "import""#, - r#"import /* tree-shaking no-side-effects-when-called */ x from "import-default-no-effects"; x()"#, - r#"import /* test */ /*tree-shaking no-side-effects-when-called */ x from "import-default-no-effects"; x()"#, - // TODO: Current only support the comment next to code. - // r#"import /* tree-shaking no-side-effects-when-called*/ /* test */ x from "import-default-no-effects"; x()"#, - r#"import {/* tree-shaking no-side-effects-when-called */ x} from "import-no-effects"; x()"#, - r#"import {x as /* tree-shaking no-side-effects-when-called */ y} from "import-no-effects"; y()"#, - r#"import {x} from "import"; /*@__PURE__*/ x()"#, - r#"import {x} from "import"; /* @__PURE__ */ x()"#, - // JSXAttribute - r#"class X {}; const x = "#, - "class X {}; const x = ", - "class X {}; const x = />", - // JSXElement - "class X {}; const x = ", - "class X {}; const x = Text", - // JSXEmptyExpression - "class X {}; const x = {}", - // JSXExpressionContainer - "class X {}; const x = {3}", - // JSXIdentifier - "class X {}; const x = ", - "const X = class {constructor() {this.x = 1}}; const x = ", - // JSXOpeningElement - "class X {}; const x = ", - "class X {}; const x = ", - r#"class X {}; const x = "#, - // JSXSpreadAttribute - "class X {}; const x = ", - // LabeledStatement - "loop: for(;true;){continue loop}", - // Literal - "const x = 3", - "if (false) ext()", - r#""use strict""#, - // LogicalExpression - "const x = 3 || 4", - "true || ext()", - "false && ext()", - "if (false && false) ext()", - "if (true && false) ext()", - "if (false && true) ext()", - "if (false || false) ext()", - // MemberExpression - "const x = ext.y", - r#"const x = ext["y"]"#, - "let x = ()=>{}; x.y = 1", - // MemberExpression when called - "const x = Object.keys({})", - // MemberExpression when mutated - "const x = {};x.y = ext", - "const x = {y: 1};delete x.y", - // MetaProperty - "function x(){const y = new.target}; x()", - // MethodDefinition - "class x {a(){}}", - "class x {static a(){}}", - // NewExpression - "const x = new (function (){this.x = 1})()", - "function x(){this.y = 1}; const z = new x()", - "/*@__PURE__*/ new ext()", - // ObjectExpression - "const x = {y: ext}", - r#"const x = {["y"]: ext}"#, - "const x = {};x.y = ext", - // ObjectPattern - "const {x} = {}", - "const {[ext]: x} = {}", - // RestElement - "const [...x] = []", - // ReturnStatement - "(()=>{return})()", - "(()=>{return 1})()", - // SequenceExpression - "let x = 1; x++, x++", - "if (ext, false) ext()", - // SwitchCase - "switch(ext){case ext:const x = 1;break;default:}", - // SwitchStatement - "switch(ext){}", - "const x = ()=>{}; switch(ext){case 1:const x = ext}; x()", - "const x = ext; switch(ext){case 1:const x = ()=>{}; x()}", - // TaggedTemplateExpression - "const x = ()=>{}; const y = x``", - // TemplateLiteral - "const x = ``", - "const x = `Literal`", - "const x = `Literal ${ext}`", - r#"const x = ()=>"a"; const y = `Literal ${x()}`"#, - // ThisExpression - "const y = this.x", - // ThisExpression when mutated - "const y = new (function (){this.x = 1})()", - "const y = new (function (){{this.x = 1}})()", - "const y = new (function (){(()=>{this.x = 1})()})()", - "function x(){this.y = 1}; const y = new x()", - // TryStatement - "try {} catch (error) {}", - "try {} finally {}", - "try {} catch (error) {} finally {}", - // UnaryExpression - "!ext", - "const x = {};delete x.y", - r#"const x = {};delete x["y"]"#, - // UpdateExpression - "let x=1;x++", - "const x = {};x.y++", - // VariableDeclaration - "const x = 1", - // VariableDeclarator - "var x, y", - "var x = 1, y = 2", - "const x = 1, y = 2", - "let x = 1, y = 2", - "const {x} = {}", - // WhileStatement - "while(true){}", - "while(ext > 0){}", - "const x = ()=>{}; while(true)x()", - // YieldExpression - "function* x(){const a = yield}; x()", - "function* x(){yield ext}; x()", - // Supports TypeScript nodes - "interface Blub {}", - " - function a() { - a - } - function b() { - b - } - export { - a, - b - } - ", - " - const Comp = () => { -
- -
- } - ", - ]; - - let fail = vec![ - // ArrayExpression - "const x = [ext()]", - "const x = [,,ext(),]", - // ArrayPattern - "const [x = ext()] = []", - "const [,x = ext(),] = []", - // ArrowFunctionExpression when called - "(()=>{ext()})()", - "(({a = ext()})=>{})()", - "(a=>{a()})(ext)", - "((...a)=>{a()})(ext)", - "(({a})=>{a()})(ext)", - "(a=>{a.x = 1})(ext)", - "(a=>{const b = a;b.x = 1})(ext)", - "((...a)=>{a.x = 1})(ext)", - "(({a})=>{a.x = 1})(ext)", - // AssignmentExpression - "ext = 1", - "ext += 1", - "ext.x = 1", - "const x = {};x[ext()] = 1", - "this.x = 1", - // AssignmentPattern - "const {x = ext()} = {}", - "const {y: {x = ext()} = {}} = {}", - // AwaitExpression - "const x = async ()=>{await ext()}; x()", - // // BinaryExpression - "const x = 1 + ext()", - "const x = ext() + 1", - // BlockStatement - "{ext()}", - // "var x=()=>{};{var x=ext}x()", - "var x=ext;{x(); var x=()=>{}}", - // CallExpression - "(()=>{})(ext(), 1)", - "(()=>{})(1, ext())", - // CallExpression when called - "const x = ()=>ext; const y = x(); y()", - // CallExpression when mutated - "const x = ()=>ext; const y = x(); y.z = 1", - // CatchClause - "try {} catch (error) {ext()}", - // TODO: check global function `ext` call when called `x()` in no strict mode - // "var x=()=>{}; try {} catch (error) {var x=ext}; x()", - // ClassBody - "class x {[ext()](){}}", - // ClassBody when called - "class x {constructor(){ext()}}; new x()", - "class x {constructor(){ext()}}; const y = new x()", - "class x extends ext {}; const y = new x()", - "class y {constructor(){ext()}}; class x extends y {}; const z = new x()", - "class y {constructor(){ext()}}; class x extends y {constructor(){super()}}; const z = new x()", - "class y{}; class x extends y{constructor(){super()}}; const z = new x()", - // ClassDeclaration - "class x extends ext() {}", - "class x {[ext()](){}}", - // ClassDeclaration when called - "class x {constructor(){ext()}}; new x()", - "class x {constructor(){ext()}}; const y = new x()", - "class x extends ext {}; const y = new x()", - // ClassExpression - "const x = class extends ext() {}", - "const x = class {[ext()](){}}", - // ClassExpression when called - "new (class {constructor(){ext()}})()", - "const x = new (class {constructor(){ext()}})()", - "const x = new (class extends ext {})()", - // ClassProperty - "class x {[ext()] = 1}", - // ClassProperty when called - "class x {y = ext()}; new x()", - // ConditionalExpression - "const x = ext() ? 1 : 2", - "const x = ext ? ext() : 2", - "const x = ext ? 1 : ext()", - "if (false ? false : true) ext()", - // ConditionalExpression when called - "const x = ()=>{}; (true ? ext : x)()", - "const x = ()=>{}; (false ? x : ext)()", - "const x = ()=>{}; (ext ? x : ext)()", - // DebuggerStatement - "debugger", - // DoWhileStatement - "do {} while(ext())", - "do ext(); while(true)", - "do {ext()} while(true)", - // ExportDefaultDeclaration - "export default ext()", - "export default /* tree-shaking no-side-effects-when-called */ ext", - "const x = ext; export default /* tree-shaking no-side-effects-when-called */ x", - // ExportNamedDeclaration - "export const x = ext()", - "export const /* tree-shaking no-side-effects-when-called */ x = ext", - "export function /* tree-shaking no-side-effects-when-called */ x(){ext()}", - "const x = ext; export {/* tree-shaking no-side-effects-when-called */ x}", - " - { let x = () => {}; } - let x = ext - export {/* tree-shaking no-side-effects-when-called */ x} - ", - // ExpressionStatement - "ext()", - // ForInStatement - "for(ext in {a: 1}){}", - "for(const x in ext()){}", - "for(const x in {a: 1}){ext()}", - "for(const x in {a: 1}) ext()", - // ForOfStatement - "for(ext of {a: 1}){}", - "for(const x of ext()){}", - "for(const x of {a: 1}){ext()}", - "for(const x of {a: 1}) ext()", - // ForStatement - "for(ext();;){}", - "for(;ext();){}", - "for(;true;ext()){}", - "for(;true;) ext()", - "for(;true;){ext()}", - // FunctionDeclaration when called - "function x(){ext()}; x()", - "function x(){ext()}; const y = new x()", - "function x(){ext()}; new x()", - "function x(a = ext()){}; x()", - "function x(a){a()}; x(ext)", - "function x(...a){a()}; x(ext)", - "function x({a}){a()}; x(ext)", - "function x(a){a(); a(); a()}; x(ext)", - "function x(a){a.y = 1}; x(ext)", - "function x(...a){a.y = 1}; x(ext)", - "function x({a}){a.y = 1}; x(ext)", - "function x(a){a.y = 1; a.y = 2; a.y = 3}; x(ext)", - "function x(){ext = 1}; x(); x(); x()", - "function x(){ext = 1}; const y = new x(); y = new x(); y = new x()", - // FunctionExpression when called - "(function (){ext()}())", - "const x = new (function (){ext()})()", - "new (function (){ext()})()", - "(function ({a = ext()}){}())", - "(function (a){a()}(ext))", - "(function (...a){a()}(ext))", - "(function ({a}){a()}(ext))", - "(function (a){a.x = 1}(ext))", - "(function (a){const b = a;b.x = 1}(ext))", - "(function (...a){a.x = 1}(ext))", - "(function ({a}){a.x = 1}(ext))", - // Identifier when called - "ext()", - "const x = ext; x()", - "let x = ()=>{}; x = ext; x()", - // "var x = ()=>{}; var x = ext; x()", - "const x = ()=>{ext()}; x()", - "const x = ()=>{ext = 1}; x(); x(); x()", - // "let x = ()=>{}; const y = ()=>{x()}; x = ext; y()", - // "var x = ()=>{}; const y = ()=>{x()}; var x = ext; y()", - "const x = ()=>{}; const {y} = x(); y()", - "const x = ()=>{}; const [y] = x(); y()", - // // Identifier when mutated - "var x = ext; x.y = 1", - // "var x = {}; x = ext; x.y = 1", - // "var x = {}; var x = ext; x.y = 1", - // "var x = {}; x = ext; x.y = 1; x.y = 1; x.y = 1", - // "const x = {y:ext}; const {y} = x; y.z = 1", - // IfStatement - "if (ext()>0){}", - "if (1>0){ext()}", - "if (1<0){} else {ext()}", - "if (ext>0){ext()} else {ext()}", - // ImportDeclaration - r#"import x from "import-default"; x()"#, - r#"import x from "import-default"; x.z = 1"#, - r#"import {x} from "import"; x()"#, - r#"import {x} from "import"; x.z = 1"#, - r#"import {x as y} from "import"; y()"#, - r#"import {x as y} from "import"; y.a = 1"#, - r#"import * as y from "import"; y.x()"#, - r#"import * as y from "import"; y.x = 1"#, - // JSXAttribute - "class X {}; const x = ", - "class X {}; class Y {constructor(){ext()}}; const x = />", - // JSXElement - "class X {constructor(){ext()}}; const x = ", - "class X {}; const x = {ext()}", - // JSXExpressionContainer - "class X {}; const x = {ext()}", - // JSXIdentifier - "class X {constructor(){ext()}}; const x = ", - "const X = class {constructor(){ext()}}; const x = ", - "const x = ", - // JSXMemberExpression - "const X = {Y: ext}; const x = ", - // JSXOpeningElement - "class X {}; const x = ", - // JSXSpreadAttribute - "class X {}; const x = ", - // LabeledStatement - "loop: for(;true;){ext()}", - // Literal - "if (true) ext()", - // LogicalExpression - "ext() && true", - "true && ext()", - "false || ext()", - "if (true && true) ext()", - "if (false || true) ext()", - "if (true || false) ext()", - "if (true || true) ext()", - // MemberExpression - "const x = {};const y = x[ext()]", - // MemberExpression when called - "ext.x()", - "const x = {}; x.y()", - "const x = ()=>{}; x().y()", - "const Object = {}; const x = Object.keys({})", - "const x = {}; x[ext()]()", - // MemberExpression when mutated - "const x = {y: ext};x.y.z = 1", - "const x = {y:ext};const y = x.y; y.z = 1", - "const x = {y: ext};delete x.y.z", - // MethodDefinition - "class x {static [ext()](){}}", - // NewExpression - "const x = new ext()", - "new ext()", - // ObjectExpression - "const x = {y: ext()}", - r#"const x = {["y"]: ext()}"#, - "const x = {[ext()]: 1}", - // ObjectPattern - "const {[ext()]: x} = {}", - // ReturnStatement - "(()=>{return ext()})()", - // SequenceExpression - "ext(), 1", - "1, ext()", - "if (1, true) ext()", - "if (1, ext) ext()", - // Super when called - "class y {constructor(){ext()}}; class x extends y {constructor(){super()}}; const z = new x()", - "class y{}; class x extends y{constructor(){super(); super.test()}}; const z = new x()", - "class y{}; class x extends y{constructor(){super()}}; const z = new x()", - // SwitchCase - "switch(ext){case ext():}", - "switch(ext){case 1:ext()}", - // SwitchStatement - "switch(ext()){}", - // "var x=()=>{}; switch(ext){case 1:var x=ext}; x()", - // TaggedTemplateExpression - "const x = ext``", - "ext``", - "const x = ()=>{}; const y = x`${ext()}`", - // TemplateLiteral - "const x = `Literal ${ext()}`", - // ThisExpression when mutated - "this.x = 1", - "(()=>{this.x = 1})()", - "(function(){this.x = 1}())", - "const y = new (function (){(function(){this.x = 1}())})()", - "function x(){this.y = 1}; x()", - // ThrowStatement - r#"throw new Error("Hello Error")"#, - // TryStatement - "try {ext()} catch (error) {}", - "try {} finally {ext()}", - // UnaryExpression - "!ext()", - "delete ext.x", - r#"delete ext["x"]"#, - "const x = ()=>{};delete x()", - // UpdateExpression - "ext++", - "const x = {};x[ext()]++", - // VariableDeclaration - "const x = ext()", - // VariableDeclarator - "var x = ext(),y = ext()", - "const x = ext(),y = ext()", - "let x = ext(),y = ext()", - "const {x = ext()} = {}", - // WhileStatement - "while(ext()){}", - "while(true)ext()", - "while(true){ext()}", - // YieldExpression - "function* x(){yield ext()}; x()", - // YieldExpression when called - "function* x(){yield ext()}; x()", - " - function f() { - try { - f(); - } catch(e) { - a.map(v => v + 1); - } - } - f(); - ", - ]; - - // test options - let pass_with_options = vec![ - ( - "Object.freeze({})", - Some(serde_json::json!([ - { "function": "Object.freeze" }, - ])), - ), - ( - "import {createContext, createRef} from 'react'; createContext(); createRef();", - Some(serde_json::json!([ - { "module": "react", "functions": ["createContext", "createRef"] }, - ])), - ), - ( - "import _ from 'lodash'; _.cloneDeep({});", - Some(serde_json::json!([ - { "module": "lodash", "functions": "*" }, - ])), - ), - ( - "import * as React from 'react'; React.createRef();", - Some(serde_json::json!([ - { "module": "react", "functions": "*" }, - ])), - ), - ]; - - let fail_with_options = vec![ - ("Object.freeze({})", None), - ("import {createContext, createRef} from 'react'; createContext(); createRef();", None), - ("import _ from 'lodash'; _.cloneDeep({});", None), - ("import * as React from 'react'; React.createRef();", None), - ]; - - let pass = - pass.into_iter().map(|case| (case, None)).chain(pass_with_options).collect::>(); - - let fail = - fail.into_iter().map(|case| (case, None)).chain(fail_with_options).collect::>(); - - Tester::new(NoSideEffectsInInitialization::NAME, pass, fail).test_and_snapshot(); -} diff --git a/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs b/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs index 13f1603b5fbbb..3874434fec05f 100644 --- a/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs +++ b/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs @@ -203,9 +203,7 @@ impl Rule for ConsistentTypeImports { if let Some(specifiers) = &import_decl.specifiers { for specifier in specifiers { - let Some(symbol_id) = specifier.local().symbol_id.get() else { - continue; - }; + let symbol_id = specifier.local().symbol_id(); let no_type_qualifier = match specifier { ImportDeclarationSpecifier::ImportSpecifier(specifier) => { specifier.import_kind.is_value() diff --git a/crates/oxc_linter/src/rules/typescript/no_require_imports.rs b/crates/oxc_linter/src/rules/typescript/no_require_imports.rs new file mode 100644 index 0000000000000..d8b46bfcb9f19 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_require_imports.rs @@ -0,0 +1,405 @@ +use oxc_ast::{ + ast::{Argument, TSModuleReference}, + AstKind, +}; +use oxc_semantic::IsGlobalReference; +use regex::Regex; + +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_require_imports_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Expected \"import\" statement instead of \"require\" call") + .with_help("Do not use CommonJS `require` calls") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoRequireImports(Box); + +#[derive(Debug, Default, Clone)] +pub struct NoRequireImportsConfig { + allow: Vec, + allow_as_import: bool, +} + +impl std::ops::Deref for NoRequireImports { + type Target = NoRequireImportsConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Forbids the use of CommonJS `require` calls. + /// + /// ### Why is this bad? + /// + /// `require` imports, while functional in Node.js and older JavaScript environments, are generally + /// considered less desirable than ES modules (`import`) for several key reasons in modern JavaScript development: + /// + /// 1. **Static vs. Dynamic**: `require` is a __runtime__ function. It executes when the code runs, which means errors related to missing modules or incorrect paths are only discovered during runtime. ES modules (`import`) are static imports. Their resolution and potential errors are checked during the compilation or bundling process, making them easier to catch during development. + /// + /// 2. **Code Organization and Readability**: `require` statements are scattered throughout the code, potentially making it harder to quickly identify the dependencies of a given module. `import` statements are typically grouped at the top of a file, improving code organization and readability. + /// + /// 3. **Tree Shaking and Optimization**: Modern bundlers like Webpack and Rollup use tree-shaking to remove unused code from the final bundle. Tree-shaking works significantly better with ES modules because their dependencies are declared statically and explicitly. `require` makes it harder for bundlers to accurately identify and remove unused code, resulting in larger bundle sizes and slower load times. + /// + /// 4. **Cyclic Dependencies**: Handling cyclic dependencies (where module A imports B, and B imports A) is significantly more challenging with `require`. ES modules, through their declarative nature and the use of dynamic imports (`import()`), provide better mechanisms to handle cyclic imports and manage asynchronous loading. + /// + /// 5. **Maintainability and Refactoring**: Changing module names or paths is simpler with ES modules because the changes are declared directly and the compiler or bundler catches any errors. With `require`, you might have to track down all instances of a specific `require` statement for a particular module, making refactoring more error-prone. + /// + /// 6. Modern JavaScript Standards: import is the standard way to import modules in modern JavaScript, aligned with current best practices and language specifications. Using require necessitates additional build steps or tools to translate it to a format that the browser or modern JavaScript environments can understand. + /// + /// 7. Error Handling: ES modules provide a more structured way to handle errors during module loading using `try...catch` blocks with dynamic imports, enhancing error management. `require` errors can be less predictable. + /// + /// In summary, while `require` works, the benefits of ES modules in terms of static analysis, better bundling, improved code organization, and easier maintainability make it the preferred method for importing modules in modern JavaScript projects. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// const lib1 = require('lib1'); + /// const { lib2 } = require('lib2'); + /// import lib3 = require('lib3'); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// import * as lib1 from 'lib1'; + /// import { lib2 } from 'lib2'; + /// import * as lib3 from 'lib3'; + /// ``` + /// + /// ### Options + /// + /// #### `allow` + /// + /// array of strings + /// + /// These strings will be compiled into regular expressions with the u flag and be used to test against the imported path. + /// A common use case is to allow importing `package.json`. This is because `package.json` commonly lives outside of the TS root directory, + /// so statically importing it would lead to root directory conflicts, especially with `resolveJsonModule` enabled. + /// You can also use it to allow importing any JSON if your environment doesn't support JSON modules, or use it for other cases where `import` statements cannot work. + /// + /// With { allow: ['/package\\.json$'] }: + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// console.log(require('../package.json').version); + /// ``` + /// + /// #### `allowAsImport` + /// + /// When set to `true`, `import ... = require(...)` declarations won't be reported. + /// This is useful if you use certain module options that require strict CommonJS interop semantics. + /// + /// With `{ allowAsImport: true }`: + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// var foo = require('foo'); + /// const foo = require('foo'); + /// let foo = require('foo'); + /// ``` + /// Examples of **correct** code for this rule: + /// ```ts + /// import foo = require('foo'); + /// import foo from 'foo'; + /// ``` + /// + NoRequireImports, + restriction, + pending // TODO: fixer (change require to import) +); + +fn match_argument_value_with_regex(allow: &[CompactStr], argument_value: &str) -> bool { + return allow + .iter() + .map(|pattern| Regex::new(pattern).unwrap()) + .any(|regex| regex.is_match(argument_value)); +} + +impl Rule for NoRequireImports { + fn from_configuration(value: serde_json::Value) -> Self { + let obj = value.get(0); + Self(Box::new(NoRequireImportsConfig { + allow: obj + .and_then(|v| v.get("allow")) + .and_then(serde_json::Value::as_array) + .map(|v| { + v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect() + }) + .unwrap_or_default(), + allow_as_import: obj + .and_then(|v| v.get("allowAsImport")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + })) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::CallExpression(call_expr) => { + if node.scope_id() != ctx.scopes().root_scope_id() { + if let Some(id) = call_expr.callee.get_identifier_reference() { + if !id.is_global_reference_name("require", ctx.symbols()) { + return; + } + } + } + + if !call_expr.is_require_call() { + return; + } + + if !self.allow.is_empty() { + let Some(argument) = call_expr.arguments.first() else { + return; + }; + + match argument { + Argument::TemplateLiteral(template_literal) => { + let Some(quasi) = template_literal.quasis.first() else { + return; + }; + + if match_argument_value_with_regex(&self.allow, &quasi.value.raw) { + return; + } + + ctx.diagnostic(no_require_imports_diagnostic(quasi.span)); + } + Argument::StringLiteral(string_literal) => { + if match_argument_value_with_regex(&self.allow, &string_literal.value) { + return; + } + + ctx.diagnostic(no_require_imports_diagnostic(string_literal.span)); + } + _ => {} + } + } + + if ctx.scopes().find_binding(ctx.scopes().root_scope_id(), "require").is_some() { + return; + } + + ctx.diagnostic(no_require_imports_diagnostic(call_expr.span)); + } + AstKind::TSImportEqualsDeclaration(decl) => match decl.module_reference { + TSModuleReference::ExternalModuleReference(ref mod_ref) => { + if self.allow_as_import { + return; + } + + if !self.allow.is_empty() { + if match_argument_value_with_regex(&self.allow, &mod_ref.expression.value) { + return; + } + + ctx.diagnostic(no_require_imports_diagnostic(mod_ref.span)); + } + + ctx.diagnostic(no_require_imports_diagnostic(decl.span)); + } + TSModuleReference::IdentifierReference(_) | TSModuleReference::QualifiedName(_) => { + } + }, + _ => {} + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("import { l } from 'lib';", None), + ("var lib3 = load('not_an_import');", None), + ("var lib4 = lib2.subImport;", None), + ("var lib7 = 700;", None), + ("import lib9 = lib2.anotherSubImport;", None), + ("import lib10 from 'lib10';", None), + ("var lib3 = load?.('not_an_import');", None), + ( + " + import { createRequire } from 'module'; + const require = createRequire(); + require('remark-preset-prettier'); + ", + None, + ), + ( + "const pkg = require('./package.json');", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "const pkg = require('../package.json');", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "const pkg = require(`./package.json`);", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "const pkg = require('../packages/package.json');", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "import pkg = require('../packages/package.json');", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "import pkg = require('data.json');", + Some(serde_json::json!([{ "allow": ["\\.json$"] }])), + ), + ( + "import pkg = require('some-package');", + Some(serde_json::json!([{ "allow": ["^some-package$"] }])), + ), + ("import foo = require('foo');", Some(serde_json::json!([{ "allowAsImport": true }]))), + ( + " + let require = bazz; + trick(require('foo')); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + let require = bazz; + const foo = require('./foo.json') as Foo; + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + let require = bazz; + const foo: Foo = require('./foo.json').default; + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + let require = bazz; + const foo = require('./foo.json'); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + let require = bazz; + const configValidator = new Validator(require('./a.json')); + configValidator.addSchema(require('./a.json')); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + let require = bazz; + require('foo'); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + let require = bazz; + require?.('foo'); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + import { createRequire } from 'module'; + const require = createRequire(); + require('remark-preset-prettier'); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ]; + + let fail = vec![ + ("var lib = require('lib');", None), + ("let lib2 = require('lib2');", None), + ( + " + var lib5 = require('lib5'), + lib6 = require('lib6'); + ", + None, + ), + ("import lib8 = require('lib8');", None), + ("var lib = require?.('lib');", None), + ("let lib2 = require?.('lib2');", None), + ( + " + var lib5 = require?.('lib5'), + lib6 = require?.('lib6'); + ", + None, + ), + ("const pkg = require('./package.json');", None), + ( + "const pkg = require('./package.jsonc');", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "const pkg = require(`./package.jsonc`);", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ("import pkg = require('./package.json');", None), + ( + "import pkg = require('./package.jsonc');", + Some(serde_json::json!([{ "allow": ["/package\\.json$"] }])), + ), + ( + "import pkg = require('./package.json');", + Some(serde_json::json!([{ "allow": ["^some-package$"] }])), + ), + ("var foo = require?.('foo');", Some(serde_json::json!([{ "allowAsImport": true }]))), + ( + "let foo = trick(require?.('foo'));", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ("trick(require('foo'));", Some(serde_json::json!([{ "allowAsImport": true }]))), + ( + "const foo = require('./foo.json') as Foo;", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + "const foo: Foo = require('./foo.json').default;", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + "const foo = require('./foo.json');", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ( + " + const configValidator = new Validator(require('./a.json')); + configValidator.addSchema(require('./a.json')); + ", + Some(serde_json::json!([{ "allowAsImport": true }])), + ), + ("require('foo');", Some(serde_json::json!([{ "allowAsImport": true }]))), + ("require?.('foo');", Some(serde_json::json!([{ "allowAsImport": true }]))), + // covers global require in scope + ( + r"function foo() { + require('foo') + }", + None, + ), + ]; + + Tester::new(NoRequireImports::NAME, pass, fail) + .change_rule_path_extension("ts") + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/typescript/prefer_for_of.rs b/crates/oxc_linter/src/rules/typescript/prefer_for_of.rs index 0f198025d155b..911ed418fcbd4 100644 --- a/crates/oxc_linter/src/rules/typescript/prefer_for_of.rs +++ b/crates/oxc_linter/src/rules/typescript/prefer_for_of.rs @@ -132,7 +132,7 @@ impl Rule for PreferForOf { let decl = &for_stmt_init.declarations[0]; let (var_name, var_symbol_id) = match &decl.id.kind { - BindingPatternKind::BindingIdentifier(id) => (&id.name, id.symbol_id.get()), + BindingPatternKind::BindingIdentifier(id) => (&id.name, id.symbol_id()), _ => return, }; @@ -183,9 +183,6 @@ impl Rule for PreferForOf { let nodes = ctx.nodes(); let body_span = for_stmt.body.span(); - let Some(var_symbol_id) = var_symbol_id else { - return; - }; if ctx.semantic().symbol_references(var_symbol_id).any(|reference| { let ref_id = reference.node_id(); diff --git a/crates/oxc_linter/src/rules/unicorn/catch_error_name.rs b/crates/oxc_linter/src/rules/unicorn/catch_error_name.rs index 6f6703fd7ab2b..b18ef402f45ce 100644 --- a/crates/oxc_linter/src/rules/unicorn/catch_error_name.rs +++ b/crates/oxc_linter/src/rules/unicorn/catch_error_name.rs @@ -100,7 +100,7 @@ impl Rule for CatchErrorName { } if binding_ident.name.starts_with('_') { - if symbol_has_references(binding_ident.symbol_id.get(), ctx) { + if symbol_has_references(binding_ident.symbol_id(), ctx) { ctx.diagnostic(catch_error_name_diagnostic( binding_ident.name.as_str(), &self.name, @@ -161,7 +161,7 @@ impl CatchErrorName { } if v.name.starts_with('_') { - if symbol_has_references(v.symbol_id.get(), ctx) { + if symbol_has_references(v.symbol_id(), ctx) { ctx.diagnostic(catch_error_name_diagnostic( v.name.as_str(), &self.name, @@ -185,7 +185,7 @@ impl CatchErrorName { } if binding_ident.name.starts_with('_') { - if symbol_has_references(binding_ident.symbol_id.get(), ctx) { + if symbol_has_references(binding_ident.symbol_id(), ctx) { ctx.diagnostic(catch_error_name_diagnostic( binding_ident.name.as_str(), &self.name, @@ -209,11 +209,8 @@ impl CatchErrorName { } } -fn symbol_has_references(symbol_id: Option, ctx: &LintContext) -> bool { - if let Some(symbol_id) = symbol_id { - return ctx.semantic().symbol_references(symbol_id).next().is_some(); - } - false +fn symbol_has_references(symbol_id: SymbolId, ctx: &LintContext) -> bool { + ctx.semantic().symbol_references(symbol_id).next().is_some() } #[test] diff --git a/crates/oxc_linter/src/rules/unicorn/consistent_existence_index_check.rs b/crates/oxc_linter/src/rules/unicorn/consistent_existence_index_check.rs index f2bb4dbbde6da..684cfc87292eb 100644 --- a/crates/oxc_linter/src/rules/unicorn/consistent_existence_index_check.rs +++ b/crates/oxc_linter/src/rules/unicorn/consistent_existence_index_check.rs @@ -87,17 +87,13 @@ impl Rule for ConsistentExistenceIndexCheck { return; }; - let Some(reference_id) = identifier.reference_id.get() else { - return; - }; - - let Some(reference_symbol_id) = ctx.symbols().get_reference(reference_id).symbol_id() + let Some(symbol_id) = ctx.symbols().get_reference(identifier.reference_id()).symbol_id() else { return; }; - let declaration = ctx.symbols().get_declaration(reference_symbol_id); - let node = ctx.nodes().get_node(declaration); + let declaration_node_id = ctx.symbols().get_declaration(symbol_id); + let node = ctx.nodes().get_node(declaration_node_id); if let AstKind::VariableDeclarator(variables_declarator) = node.kind() { if variables_declarator.kind != VariableDeclarationKind::Const { diff --git a/crates/oxc_linter/src/rules/unicorn/consistent_function_scoping.rs b/crates/oxc_linter/src/rules/unicorn/consistent_function_scoping.rs index bf02be8017a2b..4c25e4eb8bdf1 100644 --- a/crates/oxc_linter/src/rules/unicorn/consistent_function_scoping.rs +++ b/crates/oxc_linter/src/rules/unicorn/consistent_function_scoping.rs @@ -166,15 +166,14 @@ impl Rule for ConsistentFunctionScoping { return; } - if let Some(func_scope_id) = function.scope_id.get() { - if let Some(parent_scope_id) = ctx.scopes().get_parent_id(func_scope_id) { - // Example: const foo = function bar() {}; - // The bar function scope id is 1. In order to ignore this rule, - // its parent's scope id (in this case `foo`'s scope id is 0 and is equal to root scope id) - // should be considered. - if parent_scope_id == ctx.scopes().root_scope_id() { - return; - } + let func_scope_id = function.scope_id(); + if let Some(parent_scope_id) = ctx.scopes().get_parent_id(func_scope_id) { + // Example: const foo = function bar() {}; + // The bar function scope id is 1. In order to ignore this rule, + // its parent's scope id (in this case `foo`'s scope id is 0 and is equal to root scope id) + // should be considered. + if parent_scope_id == ctx.scopes().root_scope_id() { + return; } } @@ -184,7 +183,7 @@ impl Rule for ConsistentFunctionScoping { if let Some(binding_ident) = get_function_like_declaration(node, ctx) { ( - binding_ident.symbol_id.get().unwrap(), + binding_ident.symbol_id(), function_body, function.id.as_ref().map_or( Span::sized(function.span.start, 8), @@ -192,10 +191,7 @@ impl Rule for ConsistentFunctionScoping { ), ) } else if let Some(function_id) = &function.id { - let Some(symbol_id) = function_id.symbol_id.get() else { - return; - }; - (symbol_id, function_body, function_id.span()) + (function_id.symbol_id(), function_body, function_id.span()) } else { return; } @@ -204,11 +200,8 @@ impl Rule for ConsistentFunctionScoping { let Some(binding_ident) = get_function_like_declaration(node, ctx) else { return; }; - let Some(symbol_id) = binding_ident.symbol_id.get() else { - return; - }; - (symbol_id, &arrow_function.body, binding_ident.span()) + (binding_ident.symbol_id(), &arrow_function.body, binding_ident.span()) } _ => return, }; diff --git a/crates/oxc_linter/src/rules/unicorn/no_thenable.rs b/crates/oxc_linter/src/rules/unicorn/no_thenable.rs index 3da15aaa2636f..daefb2318b6fd 100644 --- a/crates/oxc_linter/src/rules/unicorn/no_thenable.rs +++ b/crates/oxc_linter/src/rules/unicorn/no_thenable.rs @@ -235,23 +235,22 @@ fn check_expression(expr: &Expression, ctx: &LintContext<'_>) -> Option { - let tab = ctx.semantic().symbols(); - ident.reference_id.get().and_then(|ref_id| { - tab.get_reference(ref_id).symbol_id().and_then(|symbol_id| { - let decl = ctx.semantic().nodes().get_node(tab.get_declaration(symbol_id)); - let var_decl = decl.kind().as_variable_declarator()?; + let symbols = ctx.semantic().symbols(); + let reference_id = ident.reference_id(); + symbols.get_reference(reference_id).symbol_id().and_then(|symbol_id| { + let decl = ctx.semantic().nodes().get_node(symbols.get_declaration(symbol_id)); + let var_decl = decl.kind().as_variable_declarator()?; - match var_decl.init { - Some(Expression::StringLiteral(ref lit)) => { - if lit.value == "then" { - Some(lit.span) - } else { - None - } + match var_decl.init { + Some(Expression::StringLiteral(ref lit)) => { + if lit.value == "then" { + Some(lit.span) + } else { + None } - _ => None, } - }) + _ => None, + } }) } _ => None, diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_array_some.rs b/crates/oxc_linter/src/rules/unicorn/prefer_array_some.rs index d52097d4b8f2a..e5eb4bd362c0f 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_array_some.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_array_some.rs @@ -1,5 +1,5 @@ use oxc_ast::{ - ast::{Argument, CallExpression, Expression}, + ast::{Argument, CallExpression, Expression, UnaryOperator}, AstKind, }; use oxc_diagnostics::OxcDiagnostic; @@ -16,7 +16,7 @@ use crate::{ }; fn over_method(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Prefer `.some(…)` over `.find(…)`or `.findLast(…)`.").with_label(span) + OxcDiagnostic::warn("Prefer `.some(…)` over `.find(…)` or `.findLast(…)`.").with_label(span) } fn non_zero_filter(span: Span) -> OxcDiagnostic { @@ -24,13 +24,20 @@ fn non_zero_filter(span: Span) -> OxcDiagnostic { .with_label(span) } +fn negative_one_or_zero_filter(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Prefer `.some(…)` over `.findIndex(…)` or `.findLastIndex(…)`.") + .with_label(span) +} + #[derive(Debug, Default, Clone)] pub struct PreferArraySome; declare_oxc_lint!( /// ### What it does /// - /// Prefers using [`Array#some`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) over [`Array#find()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find), [`Array#findLast()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) and a non-zero length check on the result of [`Array#filter()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) + /// Prefers using [`Array#some()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) over [`Array#find()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find), [`Array#findLast()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) with comparing to undefined, + /// or [`Array#findIndex()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex), [`Array#findLastIndex()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex) + /// and a non-zero length check on the result of [`Array#filter()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) /// /// ### Why is this bad? /// @@ -41,26 +48,37 @@ declare_oxc_lint!( /// Examples of **incorrect** code for this rule: /// ```javascript /// const foo = array.find(fn) ? bar : baz; + /// const foo = array.findLast(elem => hasRole(elem)) !== null; + /// foo.findIndex(bar) < 0; + /// foo.findIndex(element => element.bar === 1) !== -1; + /// foo.findLastIndex(element => element.bar === 1) !== -1; + /// array.filter(fn).length === 0; /// ``` /// /// Examples of **correct** code for this rule: /// ```javascript /// const foo = array.some(fn) ? bar : baz; + /// foo.some(element => element.bar === 1); + /// !array.some(fn); /// ``` PreferArraySome, pedantic, fix ); +/// impl Rule for PreferArraySome { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { match node.kind() { + // `.find(…)` + // `.findLast(…)` AstKind::CallExpression(call_expr) => { if !is_method_call(call_expr, None, Some(&["find", "findLast"]), Some(1), Some(2)) { return; } let is_compare = is_checking_undefined(node, call_expr, ctx); + if !is_compare && !is_boolean_node(node, ctx) { return; } @@ -87,6 +105,97 @@ impl Rule for PreferArraySome { ); } AstKind::BinaryExpression(bin_expr) => { + // `.{findIndex,findLastIndex}(…) !== -1` + // `.{findIndex,findLastIndex}(…) != -1` + // `.{findIndex,findLastIndex}(…) > -1` + // `.{findIndex,findLastIndex}(…) === -1` + // `.{findIndex,findLastIndex}(…) == -1` + // `.{findIndex,findLastIndex}(…) >= 0` + // `.{findIndex,findLastIndex}(…) < 0` + let with_negative_one = matches!( + bin_expr.operator, + BinaryOperator::StrictInequality + | BinaryOperator::Inequality + | BinaryOperator::GreaterThan + | BinaryOperator::StrictEquality + | BinaryOperator::Equality + ) && matches!( + bin_expr.right.without_parentheses(), + Expression::UnaryExpression(_) + ); + + let matches_against_zero = matches!( + bin_expr.operator, + BinaryOperator::GreaterEqualThan | BinaryOperator::LessThan + ); + + if with_negative_one { + if let Expression::UnaryExpression(right_unary_expr) = + &bin_expr.right.without_parentheses() + { + if matches!(right_unary_expr.operator, UnaryOperator::UnaryNegation) + && right_unary_expr.argument.is_number_literal() + && right_unary_expr.argument.is_number_value(1_f64) + { + let Expression::CallExpression(left_call_expr) = + &bin_expr.left.without_parentheses() + else { + return; + }; + + let Some(argument) = left_call_expr.arguments.first() else { + return; + }; + + if matches!(argument, Argument::SpreadElement(_)) { + return; + } + + if is_method_call( + left_call_expr, + None, + Some(&["findIndex", "findLastIndex"]), + None, + Some(1), + ) { + // TODO: fixer + ctx.diagnostic(negative_one_or_zero_filter( + call_expr_method_callee_info(left_call_expr).unwrap().0, + )); + } + } + } + } + + if matches_against_zero { + let Expression::NumericLiteral(right_num_lit) = &bin_expr.right else { + return; + }; + + let Expression::CallExpression(left_call_expr) = + &bin_expr.left.without_parentheses() + else { + return; + }; + + if right_num_lit.raw == "0" + && is_method_call( + left_call_expr, + None, + Some(&["findIndex", "findLastIndex"]), + None, + Some(1), + ) + { + // TODO: fixer + ctx.diagnostic(negative_one_or_zero_filter( + call_expr_method_callee_info(left_call_expr).unwrap().0, + )); + } + } + + // `.filter(…).length > 0` + // `.filter(…).length !== 0` if !matches!( bin_expr.operator, BinaryOperator::GreaterThan | BinaryOperator::StrictInequality @@ -281,6 +390,17 @@ fn test() { r"foo.find(fn) >= undefined", r"foo.find(fn) instanceof undefined", r#"typeof foo.find(fn) === "undefined""#, + // findIndex: negative one + r"foo.notMatchedMethod(bar) !== -1", + r"new foo.findIndex(bar) !== -1", + r"foo.findIndex(bar, extraArgument) !== -1", + r"foo.findIndex(bar) instanceof -1", + r"foo.findIndex(...bar) !== -1", + // findLastIndex: negative one + r"new foo.findLastIndex(bar) !== -1", + r"foo.findLastIndex(bar, extraArgument) !== -1", + r"foo.findLastIndex(bar) instanceof -1", + r"foo.findLastIndex(...bar) !== -1", ]; let fail = vec![ @@ -297,6 +417,26 @@ fn test() { r"foo.find(fn) != undefined", r"foo.find(fn) !== undefined", r#"a = (( ((foo.find(fn))) == ((null)) )) ? "no" : "yes";"#, + // findIndex: negative one || ( >= || < ) 0 + r"foo.findIndex(bar) !== -1", + r"foo.findIndex(bar) != -1", + r"foo.findIndex(bar) > - 1", + r"foo.findIndex(bar) === -1", + r"foo.findIndex(bar) == - 1", + r"foo.findIndex(bar) >= 0", + r"foo.findIndex(bar) < 0", + r"foo.findIndex(bar) !== (( - 1 ))", + r"foo.findIndex(element => element.bar === 1) !== (( - 1 ))", + // findLastIndex: negative one || ( >= || < ) 0 + r"foo.findLastIndex(bar) !== -1", + r"foo.findLastIndex(bar) != -1", + r"foo.findLastIndex(bar) > - 1", + r"foo.findLastIndex(bar) === -1", + r"foo.findLastIndex(bar) == - 1", + r"foo.findLastIndex(bar) >= 0", + r"foo.findLastIndex(bar) < 0", + r"foo.findLastIndex(bar) !== (( - 1 ))", + r"foo.findLastIndex(element => element.bar === 1) !== (( - 1 ))", ]; let fix = vec![ diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_optional_catch_binding.rs b/crates/oxc_linter/src/rules/unicorn/prefer_optional_catch_binding.rs index acd33fe0424f1..50bf636f00640 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_optional_catch_binding.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_optional_catch_binding.rs @@ -85,7 +85,7 @@ impl Rule for PreferOptionalCatchBinding { fn get_param_references_count(binding_pat: &BindingPattern, ctx: &LintContext) -> usize { match &binding_pat.kind { BindingPatternKind::BindingIdentifier(binding_ident) => { - ctx.semantic().symbol_references(binding_ident.symbol_id.get().unwrap()).count() + ctx.semantic().symbol_references(binding_ident.symbol_id()).count() } BindingPatternKind::ObjectPattern(object_pat) => { let mut count = 0; diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_string_raw.rs b/crates/oxc_linter/src/rules/unicorn/prefer_string_raw.rs new file mode 100644 index 0000000000000..8e67479592db2 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_string_raw.rs @@ -0,0 +1,283 @@ +use oxc_ast::{ + ast::{JSXAttributeValue, PropertyKey, TSEnumMemberName}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_ecmascript::StringCharAt; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use oxc_syntax::keyword::RESERVED_KEYWORDS; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn prefer_string_raw(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(r"`String.raw` should be used to avoid escaping `\`.").with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct PreferStringRaw; + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefers use of String.raw to avoid escaping \. + /// + /// ### Why is this bad? + /// + /// Excessive backslashes can make string values less readable which can be avoided by using `String.raw`. + /// + /// ### Example + /// + /// Examples of **incorrect** code for this rule: + /// ```javascript + /// const file = "C:\\windows\\style\\path\\to\\file.js"; + /// const regexp = new RegExp('foo\\.bar'); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// const file = String.raw`C:\windows\style\path\to\file.js`; + /// const regexp = new RegExp(String.raw`foo\.bar`); + /// ``` + PreferStringRaw, + style, + fix, +); + +fn unescape_backslash(input: &str, quote: char) -> String { + let mut result = String::with_capacity(input.len()); + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\\' { + if let Some(next) = chars.peek() { + if *next == '\\' || *next == quote { + result.push(*next); + chars.next(); + continue; + } + } + } + + result.push(c); + } + + result +} + +impl Rule for PreferStringRaw { + #[allow(clippy::cast_precision_loss)] + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::StringLiteral(string_literal) = node.kind() else { + return; + }; + + let parent_node = ctx.nodes().parent_node(node.id()); + + if let Some(parent_node) = parent_node { + match parent_node.kind() { + AstKind::Directive(_) => { + return; + } + AstKind::ImportDeclaration(decl) => { + if string_literal.span == decl.source.span { + return; + } + } + AstKind::ExportNamedDeclaration(decl) => { + if let Some(source) = &decl.source { + if string_literal.span == source.span { + return; + } + } + } + AstKind::ExportAllDeclaration(decl) => { + if string_literal.span == decl.source.span { + return; + } + } + AstKind::ObjectProperty(prop) => { + let PropertyKey::StringLiteral(key) = &prop.key else { + return; + }; + + if !prop.computed && string_literal.span == key.span { + return; + } + } + AstKind::PropertyKey(_) => { + if let Some(AstKind::ObjectProperty(prop)) = + ctx.nodes().parent_node(parent_node.id()).map(AstNode::kind) + { + let PropertyKey::StringLiteral(key) = &prop.key else { + return; + }; + + if !prop.computed && key.span == string_literal.span { + return; + } + } + } + AstKind::JSXAttributeItem(attr) => { + let Some(attr) = attr.as_attribute() else { + return; + }; + + let Some(JSXAttributeValue::StringLiteral(value)) = &attr.value else { + return; + }; + + if value.span == string_literal.span { + return; + } + } + AstKind::TSEnumMember(member) => { + if member.span == string_literal.span { + return; + }; + + let TSEnumMemberName::String(id) = &member.id else { + return; + }; + + if id.span == string_literal.span { + return; + } + } + _ => {} + } + } + + let raw = ctx.source_range(string_literal.span); + + let last_char_index = raw.len() - 2; + if raw.char_at(Some(last_char_index as f64)) == Some('\\') { + return; + } + + if !raw.contains(r"\\") || raw.contains('`') || raw.contains("${") { + return; + } + + let Some(quote) = raw.char_at(Some(0.0)) else { + return; + }; + + let trimmed = ctx.source_range(string_literal.span.shrink(1)); + + let unescaped = unescape_backslash(trimmed, quote); + + if unescaped != string_literal.value.as_ref() { + return; + } + + ctx.diagnostic_with_fix(prefer_string_raw(string_literal.span), |fixer| { + let end = string_literal.span.start; + let before = ctx.source_range(oxc_span::Span::new(0, end)); + + let mut fix = format!("String.raw`{unescaped}`"); + + if ends_with_keyword(before) { + fix = format!(" {fix}"); + } + + fixer.replace(string_literal.span, fix) + }); + } +} + +fn ends_with_keyword(source: &str) -> bool { + for keyword in &RESERVED_KEYWORDS { + if source.ends_with(keyword) { + return true; + } + } + + if source.ends_with("of") { + return true; + } + + false +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass: Vec<&str> = vec![ + r"const file = String.raw`C:\windows\style\path\to\file.js`;", + r"const regexp = new RegExp(String.raw`foo\.bar`);", + r"a = '\''", + r"'a\\b'", + r#"import foo from "./foo\\bar.js";"#, + r#"export {foo} from "./foo\\bar.js";"#, + r#"export * from "./foo\\bar.js";"#, + r"a = {'a\\b': 1}", + " + a = '\\\\a \\ + b' + ", + r"a = 'a\\b\u{51}c'", + "a = 'a\\\\b`'", + "a = 'a\\\\b${foo}'", + r#""#, + r#" + enum Files { + Foo = "C:\\\\path\\\\to\\\\foo.js", + } + "#, + r#" + enum Foo { + "\\\\a\\\\b" = "baz", + } + "#, + r"const a = 'a\\';", + ]; + + let fail = vec![ + r#"const file = "C:\\windows\\style\\path\\to\\file.js";"#, + r"const regexp = new RegExp('foo\\.bar');", + r"a = 'a\\b'", + r"a = {['a\\b']: b}", + r"function a() {return'a\\b'}", + r"function* a() {yield'a\\b'}", + r"function a() {throw'a\\b'}", + r"if (typeof'a\\b' === 'string') {}", + r"const a = () => void'a\\b';", + r"const foo = 'foo \\x46';", + r"for (const f of'a\\b') {}", + ]; + + let fix = vec![ + ( + r#"const file = "C:\\windows\\style\\path\\to\\file.js";"#, + r"const file = String.raw`C:\windows\style\path\to\file.js`;", + None, + ), + ( + r"const regexp = new RegExp('foo\\.bar');", + r"const regexp = new RegExp(String.raw`foo\.bar`);", + None, + ), + (r"a = 'a\\b'", r"a = String.raw`a\b`", None), + (r"a = {['a\\b']: b}", r"a = {[String.raw`a\b`]: b}", None), + (r"function a() {return'a\\b'}", r"function a() {return String.raw`a\b`}", None), + (r"const foo = 'foo \\x46';", r"const foo = String.raw`foo \x46`;", None), + (r"for (const f of'a\\b') {}", r"for (const f of String.raw`a\b`) {}", None), + (r"a = 'a\\b'", r"a = String.raw`a\b`", None), + (r"a = {['a\\b']: b}", r"a = {[String.raw`a\b`]: b}", None), + (r"function a() {return'a\\b'}", r"function a() {return String.raw`a\b`}", None), + (r"function* a() {yield'a\\b'}", r"function* a() {yield String.raw`a\b`}", None), + (r"function a() {throw'a\\b'}", r"function a() {throw String.raw`a\b`}", None), + ( + r"if (typeof'a\\b' === 'string') {}", + r"if (typeof String.raw`a\b` === 'string') {}", + None, + ), + (r"const a = () => void'a\\b';", r"const a = () => void String.raw`a\b`;", None), + (r"const foo = 'foo \\x46';", r"const foo = String.raw`foo \x46`;", None), + (r"for (const f of'a\\b') {}", r"for (const f of String.raw`a\b`) {}", None), + ]; + + Tester::new(PreferStringRaw::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/service/runtime.rs b/crates/oxc_linter/src/service/runtime.rs index 5d54da9063828..2e012b455cfe2 100644 --- a/crates/oxc_linter/src/service/runtime.rs +++ b/crates/oxc_linter/src/service/runtime.rs @@ -199,7 +199,11 @@ impl Runtime { .parse(); if !ret.errors.is_empty() { - return ret.errors.into_iter().map(|err| Message::new(err, None)).collect(); + return if ret.is_flow_language { + vec![] + } else { + ret.errors.into_iter().map(|err| Message::new(err, None)).collect() + }; }; // Build the module record to unblock other threads from waiting for too long. diff --git a/crates/oxc_linter/src/snapshots/adjacent_overload_signatures.snap b/crates/oxc_linter/src/snapshots/adjacent_overload_signatures.snap index e479260d484b3..1e43b227075dc 100644 --- a/crates/oxc_linter/src/snapshots/adjacent_overload_signatures.snap +++ b/crates/oxc_linter/src/snapshots/adjacent_overload_signatures.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(adjacent-overload-signatures): All "foo" signatures should be adjacent. ╭─[adjacent_overload_signatures.tsx:3:18] diff --git a/crates/oxc_linter/src/snapshots/alt_text.snap b/crates/oxc_linter/src/snapshots/alt_text.snap index 8abd61a0da3b7..e577f78693640 100644 --- a/crates/oxc_linter/src/snapshots/alt_text.snap +++ b/crates/oxc_linter/src/snapshots/alt_text.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(alt-text): Missing `alt` attribute. ╭─[alt_text.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/anchor_has_content.snap b/crates/oxc_linter/src/snapshots/anchor_has_content.snap index 1b95f380f53cf..11e218fa7169b 100644 --- a/crates/oxc_linter/src/snapshots/anchor_has_content.snap +++ b/crates/oxc_linter/src/snapshots/anchor_has_content.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements. ╭─[anchor_has_content.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/anchor_is_valid.snap b/crates/oxc_linter/src/snapshots/anchor_is_valid.snap index 2eb831732ff80..360e07ff3961d 100644 --- a/crates/oxc_linter/src/snapshots/anchor_is_valid.snap +++ b/crates/oxc_linter/src/snapshots/anchor_is_valid.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(anchor-is-valid): Missing `href` attribute for the `a` element. ╭─[anchor_is_valid.tsx:1:2] diff --git a/crates/oxc_linter/src/snapshots/api_keys@aws_access_token.snap b/crates/oxc_linter/src/snapshots/api_keys@aws_access_token.snap index 099f8cfbc8353..65c9da5091aa7 100644 --- a/crates/oxc_linter/src/snapshots/api_keys@aws_access_token.snap +++ b/crates/oxc_linter/src/snapshots/api_keys@aws_access_token.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc-security(api-keys/aws-access-token): Detected an AWS Access Key ID, which may lead to unauthorized access to AWS resources. ╭─[api_keys.tsx:1:11] diff --git a/crates/oxc_linter/src/snapshots/approx_constant.snap b/crates/oxc_linter/src/snapshots/approx_constant.snap index a445efde94f51..9b6675d836b81 100644 --- a/crates/oxc_linter/src/snapshots/approx_constant.snap +++ b/crates/oxc_linter/src/snapshots/approx_constant.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(approx-constant): Approximate value of `PI` found. ╭─[approx_constant.tsx:1:29] diff --git a/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap b/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap index 2a2e9f693ee82..8663b2625a4be 100644 --- a/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap +++ b/crates/oxc_linter/src/snapshots/aria_activedescendant_has_tabindex.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(aria-activedescendant-has-tabindex): Elements with `aria-activedescendant` must be tabbable. ╭─[aria_activedescendant_has_tabindex.tsx:1:2] diff --git a/crates/oxc_linter/src/snapshots/aria_props.snap b/crates/oxc_linter/src/snapshots/aria_props.snap index 2b7a97035c20f..9d0317bcd35a3 100644 --- a/crates/oxc_linter/src/snapshots/aria_props.snap +++ b/crates/oxc_linter/src/snapshots/aria_props.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(aria-props): 'aria-' is not a valid ARIA attribute. ╭─[aria_props.tsx:1:6] diff --git a/crates/oxc_linter/src/snapshots/aria_role.snap b/crates/oxc_linter/src/snapshots/aria_role.snap index d107ef9f5990e..c1c33d5dcea50 100644 --- a/crates/oxc_linter/src/snapshots/aria_role.snap +++ b/crates/oxc_linter/src/snapshots/aria_role.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(aria-role): Elements with ARIA roles must use a valid, non-abstract ARIA role. ╭─[aria_role.tsx:1:11] diff --git a/crates/oxc_linter/src/snapshots/aria_unsupported_elements.snap b/crates/oxc_linter/src/snapshots/aria_unsupported_elements.snap index 9fd930a8a4061..97c79c7a285e8 100644 --- a/crates/oxc_linter/src/snapshots/aria_unsupported_elements.snap +++ b/crates/oxc_linter/src/snapshots/aria_unsupported_elements.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(aria-unsupported-elements): This element does not support ARIA roles, states and properties. ╭─[aria_unsupported_elements.tsx:1:7] diff --git a/crates/oxc_linter/src/snapshots/array_callback_return.snap b/crates/oxc_linter/src/snapshots/array_callback_return.snap index 6d2c209de3e9f..9c70c9abb1408 100644 --- a/crates/oxc_linter/src/snapshots/array_callback_return.snap +++ b/crates/oxc_linter/src/snapshots/array_callback_return.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(array-callback-return): Missing return on some path for array method "Array.from" ╭─[array_callback_return.tsx:1:26] diff --git a/crates/oxc_linter/src/snapshots/array_type.snap b/crates/oxc_linter/src/snapshots/array_type.snap index 13be26fb6c1f6..c4fec3865f49d 100644 --- a/crates/oxc_linter/src/snapshots/array_type.snap +++ b/crates/oxc_linter/src/snapshots/array_type.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(array-type): Array type using 'Array' is forbidden. Use 'number[]' instead. ╭─[array_type.tsx:1:8] diff --git a/crates/oxc_linter/src/snapshots/autocomplete_valid.snap b/crates/oxc_linter/src/snapshots/autocomplete_valid.snap index 57c5204304a46..66cd850e8a002 100644 --- a/crates/oxc_linter/src/snapshots/autocomplete_valid.snap +++ b/crates/oxc_linter/src/snapshots/autocomplete_valid.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `foo` is not a valid value for autocomplete. ╭─[autocomplete_valid.tsx:1:20] diff --git a/crates/oxc_linter/src/snapshots/avoid_new.snap b/crates/oxc_linter/src/snapshots/avoid_new.snap index d023f7b19141d..a84e8cd3ec6bf 100644 --- a/crates/oxc_linter/src/snapshots/avoid_new.snap +++ b/crates/oxc_linter/src/snapshots/avoid_new.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-promise(avoid-new): Avoid creating new promises ╭─[avoid_new.tsx:1:9] diff --git a/crates/oxc_linter/src/snapshots/bad_array_method_on_arguments.snap b/crates/oxc_linter/src/snapshots/bad_array_method_on_arguments.snap index e028d76fc91ef..df0f80c6bcf38 100644 --- a/crates/oxc_linter/src/snapshots/bad_array_method_on_arguments.snap +++ b/crates/oxc_linter/src/snapshots/bad_array_method_on_arguments.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-array-method-on-arguments): Bad array method on arguments ╭─[bad_array_method_on_arguments.tsx:1:16] diff --git a/crates/oxc_linter/src/snapshots/bad_bitwise_operator.snap b/crates/oxc_linter/src/snapshots/bad_bitwise_operator.snap index e7f5ec0fd2306..c1dc5522cf656 100644 --- a/crates/oxc_linter/src/snapshots/bad_bitwise_operator.snap +++ b/crates/oxc_linter/src/snapshots/bad_bitwise_operator.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-bitwise-operator): Bad bitwise operator ╭─[bad_bitwise_operator.tsx:1:9] diff --git a/crates/oxc_linter/src/snapshots/bad_char_at_comparison.snap b/crates/oxc_linter/src/snapshots/bad_char_at_comparison.snap index 50b0e2e794bea..8d747338313dc 100644 --- a/crates/oxc_linter/src/snapshots/bad_char_at_comparison.snap +++ b/crates/oxc_linter/src/snapshots/bad_char_at_comparison.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-char-at-comparison): Invalid comparison with `charAt` method ╭─[bad_char_at_comparison.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/bad_comparison_sequence.snap b/crates/oxc_linter/src/snapshots/bad_comparison_sequence.snap index 7b1097b4d4ad7..b08afd5b1b1aa 100644 --- a/crates/oxc_linter/src/snapshots/bad_comparison_sequence.snap +++ b/crates/oxc_linter/src/snapshots/bad_comparison_sequence.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-comparison-sequence): Bad comparison sequence ╭─[bad_comparison_sequence.tsx:1:5] diff --git a/crates/oxc_linter/src/snapshots/bad_min_max_func.snap b/crates/oxc_linter/src/snapshots/bad_min_max_func.snap index 2ac4bf34cdeb8..94fc63ff1356f 100644 --- a/crates/oxc_linter/src/snapshots/bad_min_max_func.snap +++ b/crates/oxc_linter/src/snapshots/bad_min_max_func.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-min-max-func): Math.min and Math.max combination leads to constant result ╭─[bad_min_max_func.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/bad_object_literal_comparison.snap b/crates/oxc_linter/src/snapshots/bad_object_literal_comparison.snap index 0a97b36c0523c..32fb8def65849 100644 --- a/crates/oxc_linter/src/snapshots/bad_object_literal_comparison.snap +++ b/crates/oxc_linter/src/snapshots/bad_object_literal_comparison.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-object-literal-comparison): Unexpected object literal comparison. ╭─[bad_object_literal_comparison.tsx:1:5] diff --git a/crates/oxc_linter/src/snapshots/bad_replace_all_arg.snap b/crates/oxc_linter/src/snapshots/bad_replace_all_arg.snap index acd18e36c06ed..255783a83e350 100644 --- a/crates/oxc_linter/src/snapshots/bad_replace_all_arg.snap +++ b/crates/oxc_linter/src/snapshots/bad_replace_all_arg.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(bad-replace-all-arg): Global flag (g) is missing in the regular expression supplied to the `replaceAll` method. ╭─[bad_replace_all_arg.tsx:1:12] diff --git a/crates/oxc_linter/src/snapshots/ban_ts_comment.snap b/crates/oxc_linter/src/snapshots/ban_ts_comment.snap index db4d8272c03e6..03269d05e8e18 100644 --- a/crates/oxc_linter/src/snapshots/ban_ts_comment.snap +++ b/crates/oxc_linter/src/snapshots/ban_ts_comment.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(ban-ts-comment): Do not use @ts-expect-error because it alters compilation errors. ╭─[ban_ts_comment.tsx:1:3] diff --git a/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap b/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap index a0f87008fe826..e2b56bbd6d84d 100644 --- a/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap +++ b/crates/oxc_linter/src/snapshots/ban_tslint_comment.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(ban-tslint-comment): tslint comment detected: "tslint:disable" ╭─[ban_tslint_comment.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/ban_types.snap b/crates/oxc_linter/src/snapshots/ban_types.snap index 241168cb006a6..04124ce7af496 100644 --- a/crates/oxc_linter/src/snapshots/ban_types.snap +++ b/crates/oxc_linter/src/snapshots/ban_types.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(ban-types): Do not use "String" as a type. Use "string" instead ╭─[ban_types.tsx:1:8] diff --git a/crates/oxc_linter/src/snapshots/button_has_type.snap b/crates/oxc_linter/src/snapshots/button_has_type.snap index a42f06e32ab1a..84e35b62a4aca 100644 --- a/crates/oxc_linter/src/snapshots/button_has_type.snap +++ b/crates/oxc_linter/src/snapshots/button_has_type.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(button-has-type): `button` elements must have an explicit `type` attribute. ╭─[button_has_type.tsx:1:2] diff --git a/crates/oxc_linter/src/snapshots/catch_error_name.snap b/crates/oxc_linter/src/snapshots/catch_error_name.snap index 8d20046cae372..8ae65a0df4920 100644 --- a/crates/oxc_linter/src/snapshots/catch_error_name.snap +++ b/crates/oxc_linter/src/snapshots/catch_error_name.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(catch-error-name): The catch parameter "descriptiveError" should be named "exception" ╭─[catch_error_name.tsx:1:16] diff --git a/crates/oxc_linter/src/snapshots/catch_or_return.snap b/crates/oxc_linter/src/snapshots/catch_or_return.snap index 279b77ebe3813..99fb2191895b7 100644 --- a/crates/oxc_linter/src/snapshots/catch_or_return.snap +++ b/crates/oxc_linter/src/snapshots/catch_or_return.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-promise(catch-or-return): eslint-plugin-promise(catch-or-return): Expected catch or return ╭─[catch_or_return.tsx:1:37] diff --git a/crates/oxc_linter/src/snapshots/check_access.snap b/crates/oxc_linter/src/snapshots/check_access.snap index 3dfb116c11ed0..eee127a65aac6 100644 --- a/crates/oxc_linter/src/snapshots/check_access.snap +++ b/crates/oxc_linter/src/snapshots/check_access.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsdoc(check-access): Invalid access level is specified or missing. ╭─[check_access.tsx:3:25] diff --git a/crates/oxc_linter/src/snapshots/check_property_names.snap b/crates/oxc_linter/src/snapshots/check_property_names.snap index 3feab86da4fd3..ff2b2043374a3 100644 --- a/crates/oxc_linter/src/snapshots/check_property_names.snap +++ b/crates/oxc_linter/src/snapshots/check_property_names.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsdoc(check-property-names): No root defined for @property path. ╭─[check_property_names.tsx:4:27] diff --git a/crates/oxc_linter/src/snapshots/check_tag_names.snap b/crates/oxc_linter/src/snapshots/check_tag_names.snap index 230be91090cbb..c0a668ca18a00 100644 --- a/crates/oxc_linter/src/snapshots/check_tag_names.snap +++ b/crates/oxc_linter/src/snapshots/check_tag_names.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsdoc(check-tag-names): Invalid tag name found. ╭─[check_tag_names.tsx:2:24] diff --git a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap index 9e4284fd0d950..2205f0abaf707 100644 --- a/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap +++ b/crates/oxc_linter/src/snapshots/checked_requires_onchange_or_readonly.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(checked-requires-onchange-or-readonly): `checked` should be used with either `onChange` or `readOnly`. ╭─[checked_requires_onchange_or_readonly.tsx:1:21] diff --git a/crates/oxc_linter/src/snapshots/click_events_have_key_events.snap b/crates/oxc_linter/src/snapshots/click_events_have_key_events.snap index cafa8776febf4..9d02284a18bcc 100644 --- a/crates/oxc_linter/src/snapshots/click_events_have_key_events.snap +++ b/crates/oxc_linter/src/snapshots/click_events_have_key_events.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener. ╭─[click_events_have_key_events.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/consistent_empty_array_spread.snap b/crates/oxc_linter/src/snapshots/consistent_empty_array_spread.snap index c9895f9863cc4..664933dec4b51 100644 --- a/crates/oxc_linter/src/snapshots/consistent_empty_array_spread.snap +++ b/crates/oxc_linter/src/snapshots/consistent_empty_array_spread.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(consistent-empty-array-spread): Prefer consistent types when spreading a ternary in an array literal. ╭─[consistent_empty_array_spread.tsx:1:24] diff --git a/crates/oxc_linter/src/snapshots/consistent_function_scoping.snap b/crates/oxc_linter/src/snapshots/consistent_function_scoping.snap index c449ccf26bb06..50b5406e32937 100644 --- a/crates/oxc_linter/src/snapshots/consistent_function_scoping.snap +++ b/crates/oxc_linter/src/snapshots/consistent_function_scoping.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(consistent-function-scoping): Function does not capture any variables from the outer scope. ╭─[consistent_function_scoping.tsx:3:30] diff --git a/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap b/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap index ff5ce7b7ae11e..f33ef8da85354 100644 --- a/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap +++ b/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(consistent-indexed-object-style): A record is preferred over an index signature. ╭─[consistent_indexed_object_style.tsx:3:12] diff --git a/crates/oxc_linter/src/snapshots/consistent_test_it.snap b/crates/oxc_linter/src/snapshots/consistent_test_it.snap index 1a71f34cdbe6d..8b05ea5c841b9 100644 --- a/crates/oxc_linter/src/snapshots/consistent_test_it.snap +++ b/crates/oxc_linter/src/snapshots/consistent_test_it.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions ╭─[consistent_test_it.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/consistent_type_definitions.snap b/crates/oxc_linter/src/snapshots/consistent_type_definitions.snap index caf9a49362022..5f6cf6ab08e97 100644 --- a/crates/oxc_linter/src/snapshots/consistent_type_definitions.snap +++ b/crates/oxc_linter/src/snapshots/consistent_type_definitions.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(consistent-type-definitions): Use an `interface` instead of a `type` ╭─[consistent_type_definitions.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/consistent_type_imports.snap b/crates/oxc_linter/src/snapshots/consistent_type_imports.snap index d3333cd4f9d8b..bed9424ce5ba9 100644 --- a/crates/oxc_linter/src/snapshots/consistent_type_imports.snap +++ b/crates/oxc_linter/src/snapshots/consistent_type_imports.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(consistent-type-imports): All imports in the declaration are only used as types. Use `import type`. ╭─[consistent_type_imports.tsx:2:15] diff --git a/crates/oxc_linter/src/snapshots/const_comparisons.snap b/crates/oxc_linter/src/snapshots/const_comparisons.snap index c392521e2a700..0a2c19fcd1fc9 100644 --- a/crates/oxc_linter/src/snapshots/const_comparisons.snap +++ b/crates/oxc_linter/src/snapshots/const_comparisons.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(const-comparisons): Unexpected constant comparison ╭─[const_comparisons.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/constructor_super.snap b/crates/oxc_linter/src/snapshots/constructor_super.snap index 2adf1447d2f84..03f218bf6f1cb 100644 --- a/crates/oxc_linter/src/snapshots/constructor_super.snap +++ b/crates/oxc_linter/src/snapshots/constructor_super.snap @@ -1,4 +1,5 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- diff --git a/crates/oxc_linter/src/snapshots/default.snap b/crates/oxc_linter/src/snapshots/default.snap index b85cc3452c597..ee9154a502a84 100644 --- a/crates/oxc_linter/src/snapshots/default.snap +++ b/crates/oxc_linter/src/snapshots/default.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-import(default): No default export found in imported module "./named-exports" ╭─[index.js:1:8] diff --git a/crates/oxc_linter/src/snapshots/default_case.snap b/crates/oxc_linter/src/snapshots/default_case.snap index 6144661844a54..0f3c5143449e1 100644 --- a/crates/oxc_linter/src/snapshots/default_case.snap +++ b/crates/oxc_linter/src/snapshots/default_case.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(default-case): Require default cases in switch statements. ╭─[default_case.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/default_case_last.snap b/crates/oxc_linter/src/snapshots/default_case_last.snap index cb2665ea19f62..1e991b83dc88f 100644 --- a/crates/oxc_linter/src/snapshots/default_case_last.snap +++ b/crates/oxc_linter/src/snapshots/default_case_last.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last ╭─[default_case_last.tsx:1:16] diff --git a/crates/oxc_linter/src/snapshots/default_param_last.snap b/crates/oxc_linter/src/snapshots/default_param_last.snap index 238a8dbec65d1..c0cd80f8183dd 100644 --- a/crates/oxc_linter/src/snapshots/default_param_last.snap +++ b/crates/oxc_linter/src/snapshots/default_param_last.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(default-param-last): Default parameters should be last ╭─[default_param_last.tsx:1:12] diff --git a/crates/oxc_linter/src/snapshots/double_comparisons.snap b/crates/oxc_linter/src/snapshots/double_comparisons.snap index 237b72f92c3f5..061094be4d11d 100644 --- a/crates/oxc_linter/src/snapshots/double_comparisons.snap +++ b/crates/oxc_linter/src/snapshots/double_comparisons.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(double-comparisons): Unexpected double comparisons. ╭─[double_comparisons.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap b/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap index c17528fcbfc00..3717b95fa29f6 100644 --- a/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap +++ b/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed ╭─[empty_brace_spaces.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/empty_tags.snap b/crates/oxc_linter/src/snapshots/empty_tags.snap index 95966dd47223c..f573002336a8e 100644 --- a/crates/oxc_linter/src/snapshots/empty_tags.snap +++ b/crates/oxc_linter/src/snapshots/empty_tags.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsdoc(empty-tags): Expects the void tags to be empty of any content. ╭─[empty_tags.tsx:3:27] diff --git a/crates/oxc_linter/src/snapshots/eqeqeq.snap b/crates/oxc_linter/src/snapshots/eqeqeq.snap index 5c9da273e6251..7bd4eb8cf98d3 100644 --- a/crates/oxc_linter/src/snapshots/eqeqeq.snap +++ b/crates/oxc_linter/src/snapshots/eqeqeq.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(eqeqeq): Expected > and instead saw >= ╭─[eqeqeq.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/erasing_op.snap b/crates/oxc_linter/src/snapshots/erasing_op.snap index 553cff506667b..daa994246381c 100644 --- a/crates/oxc_linter/src/snapshots/erasing_op.snap +++ b/crates/oxc_linter/src/snapshots/erasing_op.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ oxc(erasing-op): Unexpected erasing operation. This expression will always evaluate to zero. ╭─[erasing_op.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/error_message.snap b/crates/oxc_linter/src/snapshots/error_message.snap index abed373adc43b..a73a63e911562 100644 --- a/crates/oxc_linter/src/snapshots/error_message.snap +++ b/crates/oxc_linter/src/snapshots/error_message.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(error-message): Pass a message to the Error constructor. ╭─[error_message.tsx:1:7] diff --git a/crates/oxc_linter/src/snapshots/escape_case.snap b/crates/oxc_linter/src/snapshots/escape_case.snap index 2d57ac5467eb4..34a4aae059ee2 100644 --- a/crates/oxc_linter/src/snapshots/escape_case.snap +++ b/crates/oxc_linter/src/snapshots/escape_case.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(escape-case): Use uppercase characters for the value of the escape sequence. ╭─[escape_case.tsx:1:13] diff --git a/crates/oxc_linter/src/snapshots/exhaustive_deps.snap b/crates/oxc_linter/src/snapshots/exhaustive_deps.snap index 12bc5b2b3644d..6be9889e18f36 100644 --- a/crates/oxc_linter/src/snapshots/exhaustive_deps.snap +++ b/crates/oxc_linter/src/snapshots/exhaustive_deps.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react-hooks(exhaustive-deps): React Hook useCallback has a missing dependency: 'props.foo.toString' ╭─[exhaustive_deps.tsx:4:14] diff --git a/crates/oxc_linter/src/snapshots/expect_expect.snap b/crates/oxc_linter/src/snapshots/expect_expect.snap index 9546945ba3e3a..0d55db374536d 100644 --- a/crates/oxc_linter/src/snapshots/expect_expect.snap +++ b/crates/oxc_linter/src/snapshots/expect_expect.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-vitest(expect-expect): Test has no assertions ╭─[expect_expect.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/explicit_function_return_type.snap b/crates/oxc_linter/src/snapshots/explicit_function_return_type.snap index 8db941e370cc4..ed17312b109dc 100644 --- a/crates/oxc_linter/src/snapshots/explicit_function_return_type.snap +++ b/crates/oxc_linter/src/snapshots/explicit_function_return_type.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ typescript-eslint(explicit-function-return-type): Missing return type on function. ╭─[explicit_function_return_type.tsx:2:10] diff --git a/crates/oxc_linter/src/snapshots/explicit_length_check.snap b/crates/oxc_linter/src/snapshots/explicit_length_check.snap index e3fb1306b7791..47636f49ed6e6 100644 --- a/crates/oxc_linter/src/snapshots/explicit_length_check.snap +++ b/crates/oxc_linter/src/snapshots/explicit_length_check.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(explicit-length-check): Use `.length > 0` when checking length is not zero. ╭─[explicit_length_check.tsx:1:11] diff --git a/crates/oxc_linter/src/snapshots/export.snap b/crates/oxc_linter/src/snapshots/export.snap index ab121c635b966..6df1bdc54aa42 100644 --- a/crates/oxc_linter/src/snapshots/export.snap +++ b/crates/oxc_linter/src/snapshots/export.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-import(export): Multiple exports of name 'foo'. ╭─[index.ts:1:19] diff --git a/crates/oxc_linter/src/snapshots/filename_case.snap b/crates/oxc_linter/src/snapshots/filename_case.snap index 283b383a9096b..f878e421f06c9 100644 --- a/crates/oxc_linter/src/snapshots/filename_case.snap +++ b/crates/oxc_linter/src/snapshots/filename_case.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-unicorn(filename-case): Filename should be in kebab case ╭─[filename_case.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/first.snap b/crates/oxc_linter/src/snapshots/first.snap index 3b0cc6fed1a6b..3c53ebe2ff302 100644 --- a/crates/oxc_linter/src/snapshots/first.snap +++ b/crates/oxc_linter/src/snapshots/first.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-import(first): Import statements must come first ╭─[index.ts:3:15] diff --git a/crates/oxc_linter/src/snapshots/for_direction.snap b/crates/oxc_linter/src/snapshots/for_direction.snap index 3daf0e7b89930..ae6a3a2a100b0 100644 --- a/crates/oxc_linter/src/snapshots/for_direction.snap +++ b/crates/oxc_linter/src/snapshots/for_direction.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(for-direction): The update clause in this loop moves the variable in the wrong direction ╭─[for_direction.tsx:1:17] diff --git a/crates/oxc_linter/src/snapshots/func_names.snap b/crates/oxc_linter/src/snapshots/func_names.snap index 7431077940895..346c0e52aab57 100644 --- a/crates/oxc_linter/src/snapshots/func_names.snap +++ b/crates/oxc_linter/src/snapshots/func_names.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(func-names): Unexpected unnamed function. ╭─[func_names.tsx:1:21] diff --git a/crates/oxc_linter/src/snapshots/getter_return.snap b/crates/oxc_linter/src/snapshots/getter_return.snap index 088647bfad4a6..b290f39a4ae25 100644 --- a/crates/oxc_linter/src/snapshots/getter_return.snap +++ b/crates/oxc_linter/src/snapshots/getter_return.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(getter-return): Expected to always return a value in getter. ╭─[getter_return.js:1:20] diff --git a/crates/oxc_linter/src/snapshots/google_font_display.snap b/crates/oxc_linter/src/snapshots/google_font_display.snap index 503e15679202c..8014f7b968292 100644 --- a/crates/oxc_linter/src/snapshots/google_font_display.snap +++ b/crates/oxc_linter/src/snapshots/google_font_display.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-next(google-font-display): A font-display parameter is missing (adding `&display=optional` is recommended). ╭─[google_font_display.tsx:6:7] diff --git a/crates/oxc_linter/src/snapshots/google_font_preconnect.snap b/crates/oxc_linter/src/snapshots/google_font_preconnect.snap index 4d2179090b4dd..de31bb1b3f35d 100644 --- a/crates/oxc_linter/src/snapshots/google_font_preconnect.snap +++ b/crates/oxc_linter/src/snapshots/google_font_preconnect.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-next(google-font-preconnect): `rel="preconnect"` is missing from Google Font. ╭─[google_font_preconnect.tsx:4:15] diff --git a/crates/oxc_linter/src/snapshots/guard_for_in.snap b/crates/oxc_linter/src/snapshots/guard_for_in.snap index e8d1a0a92fbb5..7180608298e4a 100644 --- a/crates/oxc_linter/src/snapshots/guard_for_in.snap +++ b/crates/oxc_linter/src/snapshots/guard_for_in.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(guard-for-in): Require `for-in` loops to include an `if` statement ╭─[guard_for_in.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/heading_has_content.snap b/crates/oxc_linter/src/snapshots/heading_has_content.snap index b963e03a91066..d27ecd7d878e9 100644 --- a/crates/oxc_linter/src/snapshots/heading_has_content.snap +++ b/crates/oxc_linter/src/snapshots/heading_has_content.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(heading-has-content): Headings must have content and the content must be accessible by a screen reader. ╭─[heading_has_content.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/html_has_lang.snap b/crates/oxc_linter/src/snapshots/html_has_lang.snap index 2337ea66fe8e1..c5a60e5f83ab7 100644 --- a/crates/oxc_linter/src/snapshots/html_has_lang.snap +++ b/crates/oxc_linter/src/snapshots/html_has_lang.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(html-has-lang): Missing lang attribute. ╭─[html_has_lang.tsx:1:2] diff --git a/crates/oxc_linter/src/snapshots/iframe_has_title.snap b/crates/oxc_linter/src/snapshots/iframe_has_title.snap index 652503ce7b7da..487208f9e1d82 100644 --- a/crates/oxc_linter/src/snapshots/iframe_has_title.snap +++ b/crates/oxc_linter/src/snapshots/iframe_has_title.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(iframe-has-title): Missing `title` attribute for the `iframe` element. ╭─[iframe_has_title.tsx:1:2] diff --git a/crates/oxc_linter/src/snapshots/iframe_missing_sandbox.snap b/crates/oxc_linter/src/snapshots/iframe_missing_sandbox.snap index 5020fd10abae0..f414c95a88a8e 100644 --- a/crates/oxc_linter/src/snapshots/iframe_missing_sandbox.snap +++ b/crates/oxc_linter/src/snapshots/iframe_missing_sandbox.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute ╭─[iframe_missing_sandbox.tsx:1:2] diff --git a/crates/oxc_linter/src/snapshots/img_redundant_alt.snap b/crates/oxc_linter/src/snapshots/img_redundant_alt.snap index bdbc7736b21a2..a58f65cdd8327 100644 --- a/crates/oxc_linter/src/snapshots/img_redundant_alt.snap +++ b/crates/oxc_linter/src/snapshots/img_redundant_alt.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(img-redundant-alt): Redundant alt attribute. ╭─[img_redundant_alt.tsx:1:6] diff --git a/crates/oxc_linter/src/snapshots/implements_on_classes.snap b/crates/oxc_linter/src/snapshots/implements_on_classes.snap index 77a2cbf0a1345..147c1f099f89b 100644 --- a/crates/oxc_linter/src/snapshots/implements_on_classes.snap +++ b/crates/oxc_linter/src/snapshots/implements_on_classes.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsdoc(implements-on-classes): `@implements` used on a non-constructor function ╭─[implements_on_classes.tsx:3:13] diff --git a/crates/oxc_linter/src/snapshots/import_no_namespace.snap b/crates/oxc_linter/src/snapshots/import_no_namespace.snap index af3286c677717..e14e1a57c37e4 100644 --- a/crates/oxc_linter/src/snapshots/import_no_namespace.snap +++ b/crates/oxc_linter/src/snapshots/import_no_namespace.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-import(import-no-namespace): Usage of namespaced aka wildcard "*" imports prohibited ╭─[index.js:1:13] diff --git a/crates/oxc_linter/src/snapshots/inline_script_id.snap b/crates/oxc_linter/src/snapshots/inline_script_id.snap index 357b786421f82..e4b6e3a044836 100644 --- a/crates/oxc_linter/src/snapshots/inline_script_id.snap +++ b/crates/oxc_linter/src/snapshots/inline_script_id.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-next(inline-script-id): `next/script` components with inline content must specify an `id` attribute. ╭─[inline_script_id.tsx:5:17] diff --git a/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap b/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap index 46bf714d8bfea..bf8a2dfc79769 100644 --- a/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap +++ b/crates/oxc_linter/src/snapshots/jsx_boolean_value.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-boolean-value): Value must be omitted for boolean attribute "foo" ╭─[jsx_boolean_value.tsx:1:9] diff --git a/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap b/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap index 9d38b5b3e2856..b940ef90a1cd3 100644 --- a/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap +++ b/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. ╭─[jsx_curly_brace_presence.tsx:1:12] diff --git a/crates/oxc_linter/src/snapshots/jsx_key.snap b/crates/oxc_linter/src/snapshots/jsx_key.snap index 62f40cb899dca..e991b8c1c8a95 100644 --- a/crates/oxc_linter/src/snapshots/jsx_key.snap +++ b/crates/oxc_linter/src/snapshots/jsx_key.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-key): Missing "key" prop for element in array. ╭─[jsx_key.tsx:1:3] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_comment_textnodes.snap b/crates/oxc_linter/src/snapshots/jsx_no_comment_textnodes.snap index 6ffab88506555..8eb2797c87478 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_comment_textnodes.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_comment_textnodes.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-no-comment-textnodes): Comments inside children section of tag should be placed inside braces ╭─[jsx_no_comment_textnodes.tsx:4:29] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_duplicate_props.snap b/crates/oxc_linter/src/snapshots/jsx_no_duplicate_props.snap index a4e1417ac8098..a6ccdc1294d84 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_duplicate_props.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_duplicate_props.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-no-duplicate-props): No duplicate props allowed. The prop "a" is duplicated. ╭─[jsx_no_duplicate_props.tsx:1:6] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap index 7f25083157cea..7398e3a45e004 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react-perf(jsx-no-jsx-as-prop): JSX attribute values should not contain other JSX. ╭─[jsx_no_jsx_as_prop.tsx:1:31] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap index 3841422ccfb52..08d263bf240d2 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope. ╭─[jsx_no_new_array_as_prop.tsx:1:32] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap index 7ad4ce7042703..8ca86c7fa9aa4 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react-perf(jsx-no-new-function-as-prop): JSX attribute values should not contain functions created in the same scope. ╭─[jsx_no_new_function_as_prop.tsx:1:32] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap index cbfe2e3f935b6..81b73ce70284b 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope. ╭─[jsx_no_new_object_as_prop.tsx:1:33] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_target_blank.snap b/crates/oxc_linter/src/snapshots/jsx_no_target_blank.snap index 1ddef52dc8024..fe76d9cedc35b 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_target_blank.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_target_blank.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-no-target-blank): Using target=`_blank` without rel=`noreferrer` (which implies rel=`noopener`) is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations ╭─[jsx_no_target_blank.tsx:1:11] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_undef.snap b/crates/oxc_linter/src/snapshots/jsx_no_undef.snap index 35bff1978aac6..5ac8fd34a17f0 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_undef.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_undef.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-no-undef): 'App' is not defined. ╭─[jsx_no_undef.tsx:1:26] diff --git a/crates/oxc_linter/src/snapshots/jsx_no_useless_fragment.snap b/crates/oxc_linter/src/snapshots/jsx_no_useless_fragment.snap index e3c2166139d0f..e2cc4df814c25 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_useless_fragment.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_useless_fragment.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-no-useless-fragment): Fragments should contain more than one child. ╭─[jsx_no_useless_fragment.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap b/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap index 8d3c78bec993a..020a53299a725 100644 --- a/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap +++ b/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-react(jsx-props-no-spread-multi): Prop 'props' is spread multiple times. ╭─[jsx_props_no_spread_multi.tsx:3:16] diff --git a/crates/oxc_linter/src/snapshots/label_has_associated_control.snap b/crates/oxc_linter/src/snapshots/label_has_associated_control.snap index cb40285f0a682..d1d6c78b40ff8 100644 --- a/crates/oxc_linter/src/snapshots/label_has_associated_control.snap +++ b/crates/oxc_linter/src/snapshots/label_has_associated_control.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(label-has-associated-control): A form label must be associated with a control. ╭─[label_has_associated_control.tsx:1:1] diff --git a/crates/oxc_linter/src/snapshots/lang.snap b/crates/oxc_linter/src/snapshots/lang.snap index 9bc271ccb1310..d55ecb18bf2e3 100644 --- a/crates/oxc_linter/src/snapshots/lang.snap +++ b/crates/oxc_linter/src/snapshots/lang.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(lang): Lang attribute must have a valid value. ╭─[lang.tsx:1:7] diff --git a/crates/oxc_linter/src/snapshots/max_classes_per_file.snap b/crates/oxc_linter/src/snapshots/max_classes_per_file.snap index 64347a20cfdf9..4cbd4d776bca7 100644 --- a/crates/oxc_linter/src/snapshots/max_classes_per_file.snap +++ b/crates/oxc_linter/src/snapshots/max_classes_per_file.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1 ╭─[max_classes_per_file.tsx:2:4] diff --git a/crates/oxc_linter/src/snapshots/max_dependencies.snap b/crates/oxc_linter/src/snapshots/max_dependencies.snap index b545e447e5cfe..096a7d4edaee2 100644 --- a/crates/oxc_linter/src/snapshots/max_dependencies.snap +++ b/crates/oxc_linter/src/snapshots/max_dependencies.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-import(max-dependencies): File has too many dependencies (3). Maximum allowed is 1. ╭─[index.ts:3:31] diff --git a/crates/oxc_linter/src/snapshots/max_expects.snap b/crates/oxc_linter/src/snapshots/max_expects.snap index 1ff98d0a38c55..5f94658280d39 100644 --- a/crates/oxc_linter/src/snapshots/max_expects.snap +++ b/crates/oxc_linter/src/snapshots/max_expects.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. ╭─[max_expects.tsx:8:21] diff --git a/crates/oxc_linter/src/snapshots/max_lines.snap b/crates/oxc_linter/src/snapshots/max_lines.snap index 4c5d3d57a59f2..0464fdeb4c4db 100644 --- a/crates/oxc_linter/src/snapshots/max_lines.snap +++ b/crates/oxc_linter/src/snapshots/max_lines.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(max-lines): File has too many lines (3). ╭─[max_lines.tsx:3:8] diff --git a/crates/oxc_linter/src/snapshots/max_nested_describe.snap b/crates/oxc_linter/src/snapshots/max_nested_describe.snap index a5c9581c2f29d..47444b8b9d09b 100644 --- a/crates/oxc_linter/src/snapshots/max_nested_describe.snap +++ b/crates/oxc_linter/src/snapshots/max_nested_describe.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls. ╭─[max_nested_describe.tsx:7:37] diff --git a/crates/oxc_linter/src/snapshots/max_params.snap b/crates/oxc_linter/src/snapshots/max_params.snap index b228f543ea5b4..d8290c6074e8f 100644 --- a/crates/oxc_linter/src/snapshots/max_params.snap +++ b/crates/oxc_linter/src/snapshots/max_params.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint(max-params): Function 'test' has too many parameters (3). Maximum allowed is 2. ╭─[max_params.tsx:1:14] diff --git a/crates/oxc_linter/src/snapshots/media_has_caption.snap b/crates/oxc_linter/src/snapshots/media_has_caption.snap index a5016a69be74e..39abc45d6fd8c 100644 --- a/crates/oxc_linter/src/snapshots/media_has_caption.snap +++ b/crates/oxc_linter/src/snapshots/media_has_caption.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +snapshot_kind: text --- ⚠ eslint-plugin-jsx-a11y(media-has-caption): Missing element with captions inside