diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000..18705aeafa0fd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust +{ + "name": "Rust", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/rust:latest", + "features": { + // add `just` for running scripts + "ghcr.io/guiyomh/features/just:0": { + "version": "latest" + }, + // add `cargo-binstall` for installing other tools. + "ghcr.io/lee-orr/rusty-dev-containers/cargo-binstall:0": { + "version": "latest" + } + }, + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer" + ] + } + }, + + // Use 'mounts' to make the cargo cache persistent in a Docker Volume. + // "mounts": [ + // { + // "source": "devcontainer-cargo-cache-${devcontainerId}", + // "target": "/usr/local/cargo", + // "type": "volume" + // } + // ] + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "just init" + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/.generated_ast_watch_list.yml b/.github/.generated_ast_watch_list.yml index 818c9ee29d83c..b801c6d9f9d89 100644 --- a/.github/.generated_ast_watch_list.yml +++ b/.github/.generated_ast_watch_list.yml @@ -6,6 +6,7 @@ src: - 'crates/oxc_ast/src/ast/js.rs' - 'crates/oxc_ast/src/ast/ts.rs' - 'crates/oxc_ast/src/ast/jsx.rs' + - 'crates/oxc_ast/src/ast/comment.rs' - 'crates/oxc_syntax/src/number.rs' - 'crates/oxc_syntax/src/operator.rs' - 'crates/oxc_span/src/span/types.rs' diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e43f208a798b0..eeb7b54a4d9af 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,3 +4,9 @@ about: Create a report to help us improve. title: '' labels: C-bug --- + + diff --git a/.github/labeler.yml b/.github/labeler.yml index 7d3d7490356c2..c0b932cafa2b8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -14,6 +14,10 @@ A-cli: - changed-files: - any-glob-to-any-file: ["apps/oxlint/**"] +A-editor: + - changed-files: + - any-glob-to-any-file: ["crates/oxc_language_server/**", "editors/**"] + A-prettier: - changed-files: - any-glob-to-any-file: ["crates/oxc_prettier/**", "tasks/prettier_conformance/**"] diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 57b26b32b3ca0..e0a0fdc6e686b 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -21,7 +21,19 @@ jobs: restore-cache: false tools: just,cargo-shear@1,dprint components: rustfmt + - name: Restore dprint plugin cache + id: cache-restore + uses: actions/cache/restore@v4 + with: + key: dprint-autofix-ci-${{ runner.os }}-${{ hashFiles('dprint.json') }} + path: ~/.cache/dprint - run: just fmt - uses: autofix-ci/action@v1.3.1 with: fail-fast: false + - name: Save dprint plugin cache + id: cache-save + uses: actions/cache/save@v4 + with: + key: ${{ steps.cache-restore.outputs.cache-primary-key }} + path: ~/.cache/dprint diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 0f22817fafbd0..12913c215aaf0 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -123,9 +123,6 @@ jobs: cache-key: benchmark-linter save-cache: ${{ github.ref_name == 'main' }} - - uses: mozilla-actions/sccache-action@v0.0.5 - if: ${{ vars.USE_SCCACHE == 'true' }} - - name: Build benchmark env: RUSTFLAGS: "-C debuginfo=1 -C strip=none -g --cfg codspeed" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8c716495f4bb..0c7dbdee783cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: - run: git diff --exit-code # Must commit everything test-windows: + if: false name: Test (windows-latest) runs-on: windows-latest steps: @@ -142,7 +143,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: taiki-e/checkout-action@v1 - - uses: crate-ci/typos@v1.24.5 + - uses: crate-ci/typos@v1.26.0 with: files: . @@ -177,7 +178,16 @@ jobs: with: cache-key: warm components: clippy + tools: ast-grep - run: cargo lint -- -D warnings + - name: Check Char and Byte Offset + run: | + output=$(sg -p '$A.chars().enumerate()' -r '$A.char_indices()' -l rs) + echo "Output: $output" + if [ -n "$output" ]; then + echo "Error: Unexpected output detected" + exit 1 + fi doc: name: Doc @@ -273,13 +283,7 @@ jobs: cache-key: warm - uses: ./.github/actions/pnpm if: steps.filter.outputs.src == 'true' - - name: Test napi/parser - run: pnpm build && pnpm test - if: steps.filter.outputs.src == 'true' - working-directory: napi/parser - - name: Test napi/transform + - run: pnpm run build if: steps.filter.outputs.src == 'true' - working-directory: napi/transform - run: pnpm build && pnpm test - - run: git diff --exit-code + - run: pnpm run test if: steps.filter.outputs.src == 'true' diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 66d57c6bbd66c..ad4fb2d08c6c2 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -5,15 +5,19 @@ on: pull_request: types: [opened, synchronize] paths: - - "crates/oxc_parser/**" - "crates/oxc_allocator/**" + - "crates/oxc_data_structures/**" + - "crates/oxc_parser/**" + - "crates/oxc_traverse/**" - ".github/workflows/miri.yml" push: branches: - main paths: - - "crates/oxc_parser/**" - "crates/oxc_allocator/**" + - "crates/oxc_data_structures/**" + - "crates/oxc_parser/**" + - "crates/oxc_traverse/**" - ".github/workflows/miri.yml" concurrency: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fb0bc9f61433e..6b909aef0bf55 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,6 +21,7 @@ jobs: - uses: actions/labeler@v5 - name: Validate PR title + id: pr-title uses: amannn/action-semantic-pull-request@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -45,3 +46,39 @@ jobs: revert style test + + - name: Get category label from PR title + id: get-category + env: + PR_TYPE: ${{ steps.pr-title.outputs.type }} + run: | + case "$PR_TYPE" in + "feat") + CATEGORY="C-enhancement" + ;; + "fix") + CATEGORY="C-bug" + ;; + "test") + CATEGORY="C-test" + ;; + "refactor" | "chore" | "style") + CATEGORY="C-cleanup" + ;; + "docs") + CATEGORY="C-docs" + ;; + "perf") + CATEGORY="C-performance" + ;; + *) + CATEGORY="" + ;; + esac + echo "CATEGORY=$CATEGORY" >> $GITHUB_OUTPUT + + - name: Add category label + uses: actions-ecosystem/action-add-labels@v1 + if: ${{ steps.get-category.outputs.CATEGORY != '' }} + with: + labels: ${{ steps.get-category.outputs.CATEGORY }} diff --git a/.github/workflows/prepare_release_crates.yml b/.github/workflows/prepare_release_crates.yml index 66fb0857e9876..44ba9d487dea5 100644 --- a/.github/workflows/prepare_release_crates.yml +++ b/.github/workflows/prepare_release_crates.yml @@ -28,3 +28,25 @@ jobs: name: crates secrets: OXC_BOT_PAT: ${{ secrets.OXC_BOT_PAT }} + + ecosystem-ci: + needs: prepare + name: Trigger Monitor Oxc + runs-on: ubuntu-latest + steps: + - uses: taiki-e/checkout-action@v1 + + - uses: peter-evans/create-or-update-comment@v4 + id: comment + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ needs.prepare.outputs.pull-request-number }} + body: Triggering Monitor Oxc https://github.com/oxc-project/monitor-oxc/actions/workflows/ci.yml + + - uses: benc-uk/workflow-dispatch@v1 + with: + repo: oxc-project/monitor-oxc + workflow: ci.yml + token: ${{ secrets.OXC_BOT_PAT }} + ref: main + inputs: '{ "issue-number": "${{ needs.prepare.outputs.pull-request-number }}", "comment-id": "${{ steps.comment.outputs.comment-id }}" }' diff --git a/.github/workflows/release_napi_parser.yml b/.github/workflows/release_napi_parser.yml index fe119aeca7b0e..5a9cb9e357c27 100644 --- a/.github/workflows/release_napi_parser.yml +++ b/.github/workflows/release_napi_parser.yml @@ -130,12 +130,13 @@ jobs: mv target/${{ matrix.target }}/release/*.@(so|dll|dylib) napi/parser/parser.${{ matrix.code-target }}.node ls napi/parser + - uses: ./.github/actions/pnpm + if: ${{ contains(matrix.target, 'x86') && !contains(matrix.target, 'musl') }} # Need docker for aarch64 + - name: Test working-directory: napi/parser if: ${{ contains(matrix.target, 'x86') && !contains(matrix.target, 'musl') }} # Need docker for aarch64 - run: | - ls - node test.mjs + run: pnpm run test # The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss - name: Archive Binary diff --git a/.github/workflows/release_napi_transform.yml b/.github/workflows/release_napi_transform.yml index 82dbc3f403924..232f5db0ba3c3 100644 --- a/.github/workflows/release_napi_transform.yml +++ b/.github/workflows/release_napi_transform.yml @@ -130,12 +130,13 @@ jobs: mv target/${{ matrix.target }}/release/*.@(so|dll|dylib) napi/transform/transform.${{ matrix.code-target }}.node ls napi/transform + - uses: ./.github/actions/pnpm + if: ${{ contains(matrix.target, 'x86') && !contains(matrix.target, 'musl') }} # Need docker for aarch64 + - name: Test working-directory: napi/transform if: ${{ contains(matrix.target, 'x86') && !contains(matrix.target, 'musl') }} # Need docker for aarch64 - run: | - ls - node test.mjs + run: pnpm run test # The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss - name: Archive Binary diff --git a/.typos.toml b/.typos.toml index c83946cc636f4..c4002ac839070 100644 --- a/.typos.toml +++ b/.typos.toml @@ -19,7 +19,11 @@ extend-exclude = [ "tasks/coverage/babel", "tasks/coverage/test262", "tasks/coverage/typescript", + "tasks/coverage/snapshots", "tasks/prettier_conformance/prettier", + "tasks/prettier_conformance/snapshots", + "tasks/transform_conformance/tests/**/output.js", + "tasks/transform_conformance/snapshots", ] [default] diff --git a/Cargo.lock b/Cargo.lock index 6fb4373841743..8214921227c8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,19 +4,13 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -71,9 +65,9 @@ checksum = "7330592adf847ee2e3513587b4db2db410a0d751378654e7e993d9adcbe5c795" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -93,23 +87,23 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -154,9 +148,9 @@ dependencies = [ [[package]] name = "bpaf" -version = "0.9.12" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3280efcf6d66bc77c2cf9b67dc8acee47a217d9be67dd590b3230dffe663724d" +checksum = "50fd5174866dc2fa2ddc96e8fb800852d37f064f32a45c7b7c2f8fa2c64c77fa" dependencies = [ "bpaf_derive", "owo-colors", @@ -165,9 +159,9 @@ dependencies = [ [[package]] name = "bpaf_derive" -version = "0.5.10" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8d5b11f7fa1068e5bbac8ab6c8c2c6940047f69185987446b60c995d4bf89c" +checksum = "cf95d9c7e6aba67f8fc07761091e93254677f4db9e27197adecebc7039a58722" dependencies = [ "proc-macro2", "quote", @@ -201,9 +195,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cast" @@ -222,9 +216,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.10" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -261,9 +258,9 @@ dependencies = [ [[package]] name = "codspeed" -version = "2.6.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a104ac948e0188b921eb3fcbdd55dcf62e542df4c7ab7e660623f6288302089" +checksum = "450a0e9df9df1c154156f4344f99d8f6f6e69d0fc4de96ef6e2e68b2ec3bce97" dependencies = [ "colored", "libc", @@ -304,7 +301,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.14", "windows-sys 0.52.0", ] @@ -335,9 +332,9 @@ checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -353,9 +350,9 @@ dependencies = [ [[package]] name = "criterion2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d8111cea0da58d7bf5c6192202ff6b44bf6d712e45a376755708db425029f" +checksum = "d6c2be2af5816b76f45745ccfb9959a9efd1322ae4c88fc3c9b2d6bcdabe5554" dependencies = [ "anes", "bpaf", @@ -457,7 +454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -465,13 +462,13 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -536,9 +533,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ego-tree" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" +checksum = "12a0bb14ac04a9fcf170d0bbbef949b44cc492f4452bd20c095636956f653642" [[package]] name = "either" @@ -608,9 +605,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fixedbitset" @@ -620,12 +617,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -649,9 +646,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -664,9 +661,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -674,15 +671,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -691,15 +688,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -708,21 +705,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -761,7 +758,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -777,9 +774,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -789,9 +786,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", @@ -825,9 +822,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.0.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5226a0e122dc74917f3a701484482bed3ee86d016c7356836abbaa033133a157" +checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" dependencies = [ "log", "pest", @@ -847,13 +844,19 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -878,9 +881,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "humansize" @@ -909,9 +912,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ "crossbeam-deque", "globset", @@ -925,20 +928,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", "serde", ] [[package]] name = "insta" -version = "1.39.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60" dependencies = [ "console", "globset", @@ -1002,9 +1005,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1032,9 +1035,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libloading" @@ -1111,9 +1114,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markdown" -version = "1.0.0-alpha.20" +version = "1.0.0-alpha.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911a8325e6fb87b89890cd4529a2ab34c2669c026279e61c26b7140a3d821ccb" +checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81" dependencies = [ "unicode-id", ] @@ -1158,7 +1161,7 @@ dependencies = [ "owo-colors", "textwrap", "thiserror", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1203,15 +1206,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1223,9 +1217,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", @@ -1235,15 +1229,14 @@ dependencies = [ [[package]] name = "napi" -version = "3.0.0-alpha.8" +version = "3.0.0-alpha.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743b5a7769f54c95e20a26d9e66d1b43d5622b7dc8ec8f97b51ed8c58633841f" +checksum = "4b27505341e98aa6126eb9f56a6ea5d8608959f19f124b9d1f1a694633641e5a" dependencies = [ "bitflags 2.6.0", "ctor", "napi-build", "napi-sys", - "once_cell", "tokio", ] @@ -1255,11 +1248,10 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "3.0.0-alpha.7" +version = "3.0.0-alpha.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7619cfcc3985e1ed73d147d6950caabaedabcf5c98133502f9d18c3d0061320" +checksum = "2c98bc3e1aef12e58d9ec48325790838a736f8428ae562716c4df1893b65be22" dependencies = [ - "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", @@ -1269,12 +1261,11 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "2.0.0-alpha.7" +version = "2.0.0-alpha.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584f6a91c05e8c6bf80622fcc2675c7d27934754d4f1141cfd422d531a3f51fb" +checksum = "e2e7fbd5478c5ac30408e4599af0b532726477e347e45fc8b20d95ab1b589057" dependencies = [ "convert_case", - "once_cell", "proc-macro2", "quote", "regex", @@ -1359,18 +1350,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" @@ -1392,14 +1383,16 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "oxc" -version = "0.30.5" +version = "0.31.0" dependencies = [ + "napi", + "napi-derive", "oxc_allocator", "oxc_ast", "oxc_cfg", @@ -1416,6 +1409,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_transformer", + "rustc-hash", ] [[package]] @@ -1437,7 +1431,7 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.30.5" +version = "0.31.0" dependencies = [ "allocator-api2", "bumpalo", @@ -1447,7 +1441,7 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.30.5" +version = "0.31.0" dependencies = [ "bitflags 2.6.0", "num-bigint", @@ -1464,7 +1458,7 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.30.5" +version = "0.31.0" dependencies = [ "proc-macro2", "quote", @@ -1512,10 +1506,12 @@ dependencies = [ [[package]] name = "oxc_cfg" -version = "0.30.5" +version = "0.31.0" dependencies = [ "bitflags 2.6.0", "itertools", + "nonmax", + "oxc_index", "oxc_syntax", "petgraph", "rustc-hash", @@ -1523,8 +1519,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.30.5" +version = "0.31.0" dependencies = [ + "assert-unchecked", "base64", "bitflags 2.6.0", "cow-utils", @@ -1556,6 +1553,7 @@ dependencies = [ "oxc", "oxc_prettier", "oxc_tasks_common", + "oxc_tasks_transform_checker", "phf 0.11.2", "pico-args", "rayon", @@ -1569,20 +1567,38 @@ dependencies = [ "walkdir", ] +[[package]] +name = "oxc_data_structures" +version = "0.31.0" +dependencies = [ + "assert-unchecked", +] + [[package]] name = "oxc_diagnostics" -version = "0.30.5" +version = "0.31.0" dependencies = [ "miette", "owo-colors", "rustc-hash", "textwrap", - "unicode-width", + "unicode-width 0.2.0", +] + +[[package]] +name = "oxc_ecmascript" +version = "0.31.0" +dependencies = [ + "num-bigint", + "num-traits", + "oxc_ast", + "oxc_span", + "oxc_syntax", ] [[package]] name = "oxc_index" -version = "0.30.5" +version = "0.31.0" dependencies = [ "rayon", "serde", @@ -1590,7 +1606,7 @@ dependencies = [ [[package]] name = "oxc_isolated_declarations" -version = "0.30.5" +version = "0.31.0" dependencies = [ "bitflags 2.6.0", "insta", @@ -1598,6 +1614,7 @@ dependencies = [ "oxc_ast", "oxc_codegen", "oxc_diagnostics", + "oxc_ecmascript", "oxc_parser", "oxc_span", "oxc_syntax", @@ -1608,7 +1625,7 @@ dependencies = [ name = "oxc_language_server" version = "0.0.1" dependencies = [ - "dashmap 6.0.1", + "dashmap 6.1.0", "env_logger", "futures", "globset", @@ -1630,13 +1647,13 @@ dependencies = [ [[package]] name = "oxc_linter" -version = "0.9.9" +version = "0.9.10" dependencies = [ "aho-corasick", "bitflags 2.6.0", "convert_case", "cow-utils", - "dashmap 6.0.1", + "dashmap 6.1.0", "globset", "insta", "itertools", @@ -1652,6 +1669,7 @@ dependencies = [ "oxc_cfg", "oxc_codegen", "oxc_diagnostics", + "oxc_ecmascript", "oxc_index", "oxc_macros", "oxc_parser", @@ -1686,7 +1704,7 @@ dependencies = [ [[package]] name = "oxc_mangler" -version = "0.30.5" +version = "0.31.0" dependencies = [ "itertools", "oxc_ast", @@ -1697,7 +1715,7 @@ dependencies = [ [[package]] name = "oxc_minifier" -version = "0.30.5" +version = "0.31.0" dependencies = [ "cow-utils", "insta", @@ -1706,7 +1724,7 @@ dependencies = [ "oxc_allocator", "oxc_ast", "oxc_codegen", - "oxc_diagnostics", + "oxc_ecmascript", "oxc_mangler", "oxc_parser", "oxc_semantic", @@ -1747,17 +1765,18 @@ dependencies = [ [[package]] name = "oxc_module_lexer" -version = "0.30.5" +version = "0.31.0" dependencies = [ "oxc_allocator", "oxc_ast", + "oxc_ecmascript", "oxc_parser", "oxc_span", ] [[package]] name = "oxc_parser" -version = "0.30.5" +version = "0.31.0" dependencies = [ "assert-unchecked", "bitflags 2.6.0", @@ -1768,6 +1787,7 @@ dependencies = [ "oxc_allocator", "oxc_ast", "oxc_diagnostics", + "oxc_ecmascript", "oxc_regular_expression", "oxc_span", "oxc_syntax", @@ -1784,12 +1804,8 @@ dependencies = [ "napi", "napi-build", "napi-derive", - "oxc_allocator", - "oxc_ast", - "oxc_diagnostics", + "oxc", "oxc_module_lexer", - "oxc_parser", - "oxc_span", "serde_json", ] @@ -1836,7 +1852,7 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.30.5" +version = "0.31.0" dependencies = [ "oxc_allocator", "oxc_ast_macros", @@ -1852,11 +1868,12 @@ dependencies = [ [[package]] name = "oxc_resolver" -version = "1.10.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5f862ee8e1ba728378ac7e007de195ae00fbb21337ef152380c052cc07e2ee2" +checksum = "6c20bb345f290c46058ba650fef7ca2b579612cf2786b927ebad7b8bec0845a7" dependencies = [ - "dashmap 6.0.1", + "cfg-if", + "dashmap 6.1.0", "dunce", "indexmap", "json-strip-comments", @@ -1864,13 +1881,14 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "simdutf8", "thiserror", "tracing", ] [[package]] name = "oxc_semantic" -version = "0.30.5" +version = "0.31.0" dependencies = [ "assert-unchecked", "indexmap", @@ -1880,6 +1898,7 @@ dependencies = [ "oxc_ast", "oxc_cfg", "oxc_diagnostics", + "oxc_ecmascript", "oxc_index", "oxc_parser", "oxc_span", @@ -1894,7 +1913,7 @@ dependencies = [ [[package]] name = "oxc_sourcemap" -version = "0.30.5" +version = "0.31.0" dependencies = [ "base64-simd", "cfg-if", @@ -1907,7 +1926,7 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.30.5" +version = "0.31.0" dependencies = [ "compact_str", "miette", @@ -1921,11 +1940,11 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.30.5" +version = "0.31.0" dependencies = [ "assert-unchecked", "bitflags 2.6.0", - "dashmap 6.0.1", + "dashmap 6.1.0", "nonmax", "oxc_allocator", "oxc_ast_macros", @@ -1951,6 +1970,20 @@ dependencies = [ "url", ] +[[package]] +name = "oxc_tasks_transform_checker" +version = "0.0.0" +dependencies = [ + "indexmap", + "oxc_allocator", + "oxc_ast", + "oxc_diagnostics", + "oxc_semantic", + "oxc_span", + "oxc_syntax", + "rustc-hash", +] + [[package]] name = "oxc_transform_conformance" version = "0.0.0" @@ -1959,42 +1992,36 @@ dependencies = [ "indexmap", "oxc", "oxc_tasks_common", + "oxc_tasks_transform_checker", "pico-args", "walkdir", ] [[package]] name = "oxc_transform_napi" -version = "0.30.5" +version = "0.31.0" dependencies = [ "napi", "napi-build", "napi-derive", - "oxc_allocator", - "oxc_ast", - "oxc_codegen", - "oxc_diagnostics", - "oxc_isolated_declarations", - "oxc_parser", - "oxc_semantic", - "oxc_sourcemap", - "oxc_span", - "oxc_transformer", + "oxc", ] [[package]] name = "oxc_transformer" -version = "0.30.5" +version = "0.31.0" dependencies = [ - "assert-unchecked", "base64", - "dashmap 6.0.1", + "cow-utils", + "dashmap 6.1.0", "indexmap", "oxc-browserslist", "oxc_allocator", "oxc_ast", "oxc_codegen", + "oxc_data_structures", "oxc_diagnostics", + "oxc_ecmascript", "oxc_parser", "oxc_regular_expression", "oxc_semantic", @@ -2011,13 +2038,15 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.30.5" +version = "0.31.0" dependencies = [ "compact_str", "itoa", "memoffset", "oxc_allocator", "oxc_ast", + "oxc_data_structures", + "oxc_ecmascript", "oxc_semantic", "oxc_span", "oxc_syntax", @@ -2041,7 +2070,7 @@ dependencies = [ [[package]] name = "oxlint" -version = "0.9.9" +version = "0.9.10" dependencies = [ "bpaf", "glob", @@ -2088,9 +2117,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" dependencies = [ "memchr", "thiserror", @@ -2099,9 +2128,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" dependencies = [ "pest", "pest_generator", @@ -2109,9 +2138,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" dependencies = [ "pest", "pest_meta", @@ -2122,9 +2151,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" dependencies = [ "once_cell", "pest", @@ -2239,18 +2268,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" dependencies = [ "proc-macro2", "quote", @@ -2302,9 +2331,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -2376,18 +2405,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -2397,9 +2426,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -2408,9 +2437,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" @@ -2478,9 +2507,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -2491,9 +2520,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "log", "once_cell", @@ -2506,15 +2535,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -2650,9 +2679,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -2670,9 +2699,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -2703,9 +2732,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "indexmap", "itoa", @@ -2765,6 +2794,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2776,9 +2811,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" @@ -2881,18 +2916,18 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "supports-color" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" +checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77" dependencies = [ "is_ci", ] [[package]] name = "syn" -version = "2.0.76" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -2901,9 +2936,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -2931,23 +2966,23 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -3001,9 +3036,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -3030,9 +3065,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -3057,9 +3092,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-lsp" @@ -3097,9 +3132,9 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3191,9 +3226,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" @@ -3206,27 +3241,27 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-id" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" [[package]] name = "unicode-id-start" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3882f69607a2ac8cc4de3ee7993d8f68bb06f2974271195065b3bd07f2edea" +checksum = "97e2a3c5fc9de285c0e805d98eba666adb4b2d9e1049ce44821ff7707cc34e91" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" @@ -3236,24 +3271,30 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" @@ -3332,9 +3373,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -3343,9 +3384,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -3358,9 +3399,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3368,9 +3409,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -3381,15 +3422,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -3397,9 +3438,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 8c23232272c89..27c34a29cce6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,66 +76,77 @@ doc_lazy_continuation = "allow" # FIXME [workspace.dependencies] # publish = true -oxc = { version = "0.30.5", path = "crates/oxc" } -oxc_allocator = { version = "0.30.5", path = "crates/oxc_allocator" } -oxc_ast = { version = "0.30.5", path = "crates/oxc_ast" } -oxc_ast_macros = { version = "0.30.5", path = "crates/oxc_ast_macros" } -oxc_cfg = { version = "0.30.5", path = "crates/oxc_cfg" } -oxc_codegen = { version = "0.30.5", path = "crates/oxc_codegen" } -oxc_diagnostics = { version = "0.30.5", path = "crates/oxc_diagnostics" } -oxc_index = { version = "0.30.5", path = "crates/oxc_index" } -oxc_isolated_declarations = { version = "0.30.5", path = "crates/oxc_isolated_declarations" } -oxc_mangler = { version = "0.30.5", path = "crates/oxc_mangler" } -oxc_minifier = { version = "0.30.5", path = "crates/oxc_minifier" } -oxc_module_lexer = { version = "0.30.5", path = "crates/oxc_module_lexer" } -oxc_parser = { version = "0.30.5", path = "crates/oxc_parser" } -oxc_regular_expression = { version = "0.30.5", path = "crates/oxc_regular_expression" } -oxc_semantic = { version = "0.30.5", path = "crates/oxc_semantic" } -oxc_sourcemap = { version = "0.30.5", path = "crates/oxc_sourcemap" } -oxc_span = { version = "0.30.5", path = "crates/oxc_span" } -oxc_syntax = { version = "0.30.5", path = "crates/oxc_syntax" } -oxc_transform_napi = { version = "0.30.5", path = "napi/transform" } -oxc_transformer = { version = "0.30.5", path = "crates/oxc_transformer" } -oxc_traverse = { version = "0.30.5", path = "crates/oxc_traverse" } +oxc = { version = "0.31.0", path = "crates/oxc" } +oxc_allocator = { version = "0.31.0", path = "crates/oxc_allocator" } +oxc_ast = { version = "0.31.0", path = "crates/oxc_ast" } +oxc_ast_macros = { version = "0.31.0", path = "crates/oxc_ast_macros" } +oxc_cfg = { version = "0.31.0", path = "crates/oxc_cfg" } +oxc_codegen = { version = "0.31.0", path = "crates/oxc_codegen" } +oxc_data_structures = { version = "0.31.0", path = "crates/oxc_data_structures" } +oxc_diagnostics = { version = "0.31.0", path = "crates/oxc_diagnostics" } +oxc_ecmascript = { version = "0.31.0", path = "crates/oxc_ecmascript" } +oxc_index = { version = "0.31.0", path = "crates/oxc_index" } +oxc_isolated_declarations = { version = "0.31.0", path = "crates/oxc_isolated_declarations" } +oxc_mangler = { version = "0.31.0", path = "crates/oxc_mangler" } +oxc_minifier = { version = "0.31.0", path = "crates/oxc_minifier" } +oxc_module_lexer = { version = "0.31.0", path = "crates/oxc_module_lexer" } +oxc_parser = { version = "0.31.0", path = "crates/oxc_parser" } +oxc_regular_expression = { version = "0.31.0", path = "crates/oxc_regular_expression" } +oxc_semantic = { version = "0.31.0", path = "crates/oxc_semantic" } +oxc_sourcemap = { version = "0.31.0", path = "crates/oxc_sourcemap" } +oxc_span = { version = "0.31.0", path = "crates/oxc_span" } +oxc_syntax = { version = "0.31.0", path = "crates/oxc_syntax" } +oxc_transform_napi = { version = "0.31.0", path = "napi/transform" } +oxc_transformer = { version = "0.31.0", path = "crates/oxc_transformer" } +oxc_traverse = { version = "0.31.0", path = "crates/oxc_traverse" } # publish = false oxc_linter = { path = "crates/oxc_linter" } oxc_macros = { path = "crates/oxc_macros" } oxc_prettier = { path = "crates/oxc_prettier" } oxc_tasks_common = { path = "tasks/common" } +oxc_tasks_transform_checker = { path = "tasks/transform_checker" } +# Relaxed version so the user can decide which version to use. napi = "3.0.0-alpha" napi-build = "2.1.3" napi-derive = "3.0.0-alpha" +# Relaxed version so the user can decide which version to use. +proc-macro2 = "1" +quote = "1" +syn = { version = "2", default-features = false } +unicode-id-start = "1" + aho-corasick = "1.1.3" allocator-api2 = "0.2.18" assert-unchecked = "0.1.2" base64 = "0.22.1" base64-simd = "0.8" bitflags = "2.6.0" -bpaf = "0.9.12" +bpaf = "0.9.15" bumpalo = "3.16.0" cfg-if = "1.0.0" compact_str = "0.8.0" console = "0.15.8" +console_error_panic_hook = "0.1.7" convert_case = "0.6.0" cow-utils = "0.1.3" -criterion2 = { version = "1.1.0", default-features = false } +criterion2 = { version = "1.1.1", default-features = false } daachorse = { version = "1.0.0" } -dashmap = "6.0.1" +dashmap = "6.1.0" encoding_rs = "0.8.34" encoding_rs_io = "0.1.7" env_logger = { version = "0.11.5", default-features = false } -flate2 = "1.0.31" -futures = "0.3.30" +flate2 = "1.0.34" +futures = "0.3.31" glob = "0.3.1" -globset = "0.4.14" -handlebars = "6.0.0" +globset = "0.4.15" +handlebars = "6.1.0" humansize = "2.1.3" -ignore = "0.4.22" -indexmap = "2.3.0" -insta = "1.39.0" +ignore = "0.4.23" +indexmap = "2.6.0" +insta = "1.40.0" itertools = "0.13.0" itoa = "1.0.11" jemallocator = "0.5.4" @@ -143,6 +154,7 @@ json-strip-comments = "1.0.4" language-tags = "0.3.2" lazy_static = "1.5.0" log = "0.4.22" +markdown = "1.0.0-alpha.21" memchr = "2.7.4" memoffset = "0.9.1" miette = { version = "7.2.0", features = ["fancy-no-syscall"] } @@ -151,44 +163,42 @@ mime_guess = "2.0.5" nonmax = "0.5.5" num-bigint = "0.4.6" num-traits = "0.2.19" -once_cell = "1.19.0" -owo-colors = "4.0.0" +once_cell = "1.20.2" +owo-colors = "4.1.0" oxc-browserslist = "1.0.3" -oxc_resolver = "1.10.2" +oxc_resolver = "1.12.0" petgraph = "0.6.5" phf = "0.11.2" pico-args = "0.5.0" -prettyplease = "0.2.20" -proc-macro2 = "1.0.86" +prettyplease = "0.2.22" project-root = "0.2.2" -quote = "1.0.36" rayon = "1.10.0" -regex = "1.10.6" +regex = "1.11.0" ropey = "1.6.1" rust-lapper = "1.1.0" rustc-hash = "2.*" ryu-js = "1.0.1" saphyr = "0.0.1" schemars = "0.8.21" +scraper = "0.20.0" seq-macro = "0.3.5" -serde = "1.0.206" +serde = "1.0.210" serde-wasm-bindgen = "0.6.5" -serde_json = "1.0.124" -simdutf8 = { version = "0.1.4", features = ["aarch64_neon"] } +serde_json = "1.0.128" +sha1 = "0.10.6" +simdutf8 = { version = "0.1.5", features = ["aarch64_neon"] } similar = "2.6.0" -syn = { version = "2.0.74", default-features = false } -tempfile = "3.12.0" +tempfile = "3.13.0" textwrap = "0.16.1" -tokio = "1.39.2" +tokio = "1.40.0" tower-lsp = "0.20.0" tracing-subscriber = "0.3.18" tsify = "0.4.5" -unicode-id-start = "1" # Relaxed version so the user can decide which unicode version to use. -unicode-width = "0.1.13" +unicode-width = "0.2.0" ureq = { version = "2.10.1", default-features = false } url = "2.5.2" walkdir = "2.5.0" -wasm-bindgen = "0.2.92" +wasm-bindgen = "0.2.95" [workspace.metadata.cargo-shear] ignored = ["napi", "oxc_transform_napi", "prettyplease"] @@ -220,6 +230,13 @@ strip = "symbols" # Set to `false` for debug information debug = false # Set to `true` for debug information panic = "abort" # Let it crash and force ourselves to write safe Rust +# Profile used for release mode, but with debugging information for profiling +# and debugging. Use `cargo build --profile=release-with-debug` to build with this profile. +[profile.release-with-debug] +inherits = "release" +strip = false # Keep debug information in binary +debug = true # Include maximum amount of debug information + # Profile for `cargo coverage` [profile.coverage] inherits = "release" diff --git a/LICENSE b/LICENSE index e8308cad6bca6..444bffc00e519 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2023-present Boshen +Copyright (c) 2024-present VoidZero Inc. & Contributors +Copyright (c) 2023 Boshen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 96a41d9d02fe3..14ea36ca48ce3 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ Oxc is building a parser, linter, formatter, transformer, minifier, resolver ... See more at [oxc.rs](https://oxc.rs)! +## VoidZero Inc. + +Oxc is a project of [VoidZero](https://voidzero.dev/), see our announcement [Announcing VoidZero - Next Generation Toolchain for JavaScript](https://voidzero.dev/blog). + +If you have requirements for JavaScript tools at scale, please [get in touch](https://forms.gle/WQgjyzYJpwurpxWKA)! + ## 🙋Who's using Oxc? - [Rolldown] uses the [oxc][docs-oxc-url] crate for parsing and transformation. @@ -34,7 +40,7 @@ See more at [oxc.rs](https://oxc.rs)! ## ⚡️ Linter Quick Start -The linter is ready to catch mistakes for you. It comes with 93 rules turned on by default (out of 400+ in total) and no configuration is required. +The linter is ready to catch mistakes for you. It comes with 93 rules turned on by default (out of 430+ in total) and no configuration is required. To get started, run [oxlint][npm-oxlint] or via `npx`: @@ -79,7 +85,7 @@ where warm runs complete in 3 minutes. ### Node.js -- via napi: [oxc-parser][npm-napi], [oxc-transform][npm-napi-transform] +- via napi: [oxc-parser][npm-napi-parser], [oxc-transform][npm-napi-transform] ### Wasm diff --git a/apps/oxlint/CHANGELOG.md b/apps/oxlint/CHANGELOG.md index a4f471a7898a0..ca7dca7ed32dc 100644 --- a/apps/oxlint/CHANGELOG.md +++ b/apps/oxlint/CHANGELOG.md @@ -4,6 +4,16 @@ 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.9.10] - 2024-10-07 + +### Bug Fixes + +- 9e9808b linter: Fix regression when parsing ts in vue files (#6336) (Boshen) + +### Refactor + +- ea908f7 linter: Consolidate file loading logic (#6130) (DonIsaac) + ## [0.9.7] - 2024-09-23 ### Features diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml index 60aadfe79b387..51e39c05c5612 100644 --- a/apps/oxlint/Cargo.toml +++ b/apps/oxlint/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxlint" -version = "0.9.9" +version = "0.9.10" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/apps/oxlint/fixtures/import/.oxlintrc.json b/apps/oxlint/fixtures/import/.oxlintrc.json new file mode 100644 index 0000000000000..fc645f90991be --- /dev/null +++ b/apps/oxlint/fixtures/import/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "plugins": ["import"], + "rules": { + "import/no-default-export": "error", + "import/namespace": "allow" + } +} diff --git a/apps/oxlint/fixtures/import/test.js b/apps/oxlint/fixtures/import/test.js new file mode 100644 index 0000000000000..68f0e20a25e7e --- /dev/null +++ b/apps/oxlint/fixtures/import/test.js @@ -0,0 +1,9 @@ +import * as foo from './foo'; +// ^ import/namespace + +console.log(foo); + +// import/no-default-export +export default function foo() {} + + diff --git a/apps/oxlint/fixtures/typescript_eslint/eslintrc.json b/apps/oxlint/fixtures/typescript_eslint/eslintrc.json index c7773313f7cd0..2920935a9110c 100644 --- a/apps/oxlint/fixtures/typescript_eslint/eslintrc.json +++ b/apps/oxlint/fixtures/typescript_eslint/eslintrc.json @@ -1,4 +1,6 @@ { + // NOTE: enabled by default + "plugins": ["@typescript-eslint"], "rules": { "no-loss-of-precision": "off", "@typescript-eslint/no-loss-of-precision": "error", diff --git a/apps/oxlint/fixtures/vue/debugger.vue b/apps/oxlint/fixtures/vue/debugger.vue index 637ac2bb7ef52..35f2446ed0f7d 100644 --- a/apps/oxlint/fixtures/vue/debugger.vue +++ b/apps/oxlint/fixtures/vue/debugger.vue @@ -7,5 +7,6 @@ diff --git a/apps/oxlint/src/command/lint.rs b/apps/oxlint/src/command/lint.rs index b4e7bfcf4981b..f404714428505 100644 --- a/apps/oxlint/src/command/lint.rs +++ b/apps/oxlint/src/command/lint.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, str::FromStr}; use bpaf::Bpaf; -use oxc_linter::{AllowWarnDeny, FixKind}; +use oxc_linter::{AllowWarnDeny, FixKind, LintPlugins}; use super::{ expand_glob, @@ -211,64 +211,203 @@ impl FromStr for OutputFormat { /// Enable Plugins #[allow(clippy::struct_field_names)] -#[derive(Debug, Clone, Bpaf)] +#[derive(Debug, Default, Clone, Bpaf)] pub struct EnablePlugins { /// Disable react plugin, which is turned on by default - #[bpaf(long("disable-react-plugin"), flag(false, true), hide_usage)] - pub react_plugin: bool, + #[bpaf( + long("disable-react-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub react_plugin: OverrideToggle, /// Disable unicorn plugin, which is turned on by default - #[bpaf(long("disable-unicorn-plugin"), flag(false, true), hide_usage)] - pub unicorn_plugin: bool, + #[bpaf( + long("disable-unicorn-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub unicorn_plugin: OverrideToggle, /// Disable oxc unique rules, which is turned on by default - #[bpaf(long("disable-oxc-plugin"), flag(false, true), hide_usage)] - pub oxc_plugin: bool, + #[bpaf( + long("disable-oxc-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub oxc_plugin: OverrideToggle, /// Disable TypeScript plugin, which is turned on by default - #[bpaf(long("disable-typescript-plugin"), flag(false, true), hide_usage)] - pub typescript_plugin: bool, + #[bpaf( + long("disable-typescript-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub typescript_plugin: OverrideToggle, /// Enable the experimental import plugin and detect ESM problems. /// It is recommended to use along side with the `--tsconfig` option. - #[bpaf(switch, hide_usage)] - pub import_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub import_plugin: OverrideToggle, /// Enable the experimental jsdoc plugin and detect JSDoc problems - #[bpaf(switch, hide_usage)] - pub jsdoc_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub jsdoc_plugin: OverrideToggle, /// Enable the Jest plugin and detect test problems - #[bpaf(switch, hide_usage)] - pub jest_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub jest_plugin: OverrideToggle, /// Enable the Vitest plugin and detect test problems - #[bpaf(switch, hide_usage)] - pub vitest_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub vitest_plugin: OverrideToggle, /// Enable the JSX-a11y plugin and detect accessibility problems - #[bpaf(switch, hide_usage)] - pub jsx_a11y_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub jsx_a11y_plugin: OverrideToggle, /// Enable the Next.js plugin and detect Next.js problems - #[bpaf(switch, hide_usage)] - pub nextjs_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub nextjs_plugin: OverrideToggle, /// Enable the React performance plugin and detect rendering performance problems - #[bpaf(switch, hide_usage)] - pub react_perf_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub react_perf_plugin: OverrideToggle, /// Enable the promise plugin and detect promise usage problems - #[bpaf(switch, hide_usage)] - pub promise_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub promise_plugin: OverrideToggle, /// Enable the node plugin and detect node usage problems - #[bpaf(switch, hide_usage)] - pub node_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub node_plugin: OverrideToggle, /// Enable the security plugin and detect security problems - #[bpaf(switch, hide_usage)] - pub security_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub security_plugin: OverrideToggle, +} + +/// Enables or disables a boolean option, or leaves it unset. +/// +/// We want CLI flags to modify whatever's set in the user's config file, but we don't want them +/// changing default behavior if they're not explicitly passed by the user. This scheme is a bit +/// convoluted, but needed due to architectural constraints imposed by `bpaf`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[allow(clippy::enum_variant_names)] +pub enum OverrideToggle { + /// Override the option to enabled + Enable, + /// Override the option to disabled + Disable, + /// Do not override. + #[default] + NotSet, +} + +impl From> for OverrideToggle { + fn from(value: Option) -> Self { + match value { + Some(true) => Self::Enable, + Some(false) => Self::Disable, + None => Self::NotSet, + } + } +} + +impl From for Option { + fn from(value: OverrideToggle) -> Self { + match value { + OverrideToggle::Enable => Some(true), + OverrideToggle::Disable => Some(false), + OverrideToggle::NotSet => None, + } + } +} + +impl OverrideToggle { + #[inline] + pub fn is_enabled(self) -> bool { + matches!(self, Self::Enable) + } + + #[inline] + pub fn is_not_set(self) -> bool { + matches!(self, Self::NotSet) + } + + pub fn inspect(self, f: F) + where + F: FnOnce(bool), + { + if let Some(v) = self.into() { + f(v); + } + } +} + +impl EnablePlugins { + pub fn apply_overrides(&self, plugins: &mut LintPlugins) { + self.react_plugin.inspect(|yes| plugins.set(LintPlugins::REACT, yes)); + self.unicorn_plugin.inspect(|yes| plugins.set(LintPlugins::UNICORN, yes)); + self.oxc_plugin.inspect(|yes| plugins.set(LintPlugins::OXC, yes)); + self.typescript_plugin.inspect(|yes| plugins.set(LintPlugins::TYPESCRIPT, yes)); + self.import_plugin.inspect(|yes| plugins.set(LintPlugins::IMPORT, yes)); + self.jsdoc_plugin.inspect(|yes| plugins.set(LintPlugins::JSDOC, yes)); + self.jest_plugin.inspect(|yes| plugins.set(LintPlugins::JEST, yes)); + self.vitest_plugin.inspect(|yes| plugins.set(LintPlugins::VITEST, yes)); + self.jsx_a11y_plugin.inspect(|yes| plugins.set(LintPlugins::JSX_A11Y, yes)); + self.nextjs_plugin.inspect(|yes| plugins.set(LintPlugins::NEXTJS, yes)); + self.react_perf_plugin.inspect(|yes| plugins.set(LintPlugins::REACT_PERF, yes)); + self.promise_plugin.inspect(|yes| plugins.set(LintPlugins::PROMISE, yes)); + self.node_plugin.inspect(|yes| plugins.set(LintPlugins::NODE, yes)); + self.security_plugin.inspect(|yes| plugins.set(LintPlugins::SECURITY, yes)); + + // Without this, jest plugins adapted to vitest will not be enabled. + if self.vitest_plugin.is_enabled() && self.jest_plugin.is_not_set() { + plugins.set(LintPlugins::JEST, true); + } + } +} + +#[cfg(test)] +mod plugins { + use super::{EnablePlugins, OverrideToggle}; + use oxc_linter::LintPlugins; + + #[test] + fn test_override_default() { + let mut plugins = LintPlugins::default(); + let enable = EnablePlugins::default(); + + enable.apply_overrides(&mut plugins); + assert_eq!(plugins, LintPlugins::default()); + } + + #[test] + fn test_overrides() { + let mut plugins = LintPlugins::default(); + let enable = EnablePlugins { + react_plugin: OverrideToggle::Enable, + unicorn_plugin: OverrideToggle::Disable, + ..EnablePlugins::default() + }; + let expected = + LintPlugins::default().union(LintPlugins::REACT).difference(LintPlugins::UNICORN); + + enable.apply_overrides(&mut plugins); + assert_eq!(plugins, expected); + } + + #[test] + fn test_override_vitest() { + let mut plugins = LintPlugins::default(); + let enable = + EnablePlugins { vitest_plugin: OverrideToggle::Enable, ..EnablePlugins::default() }; + let expected = LintPlugins::default() | LintPlugins::VITEST | LintPlugins::JEST; + + enable.apply_overrides(&mut plugins); + assert_eq!(plugins, expected); + } } #[cfg(test)] diff --git a/apps/oxlint/src/lint/mod.rs b/apps/oxlint/src/lint/mod.rs index abbd0ed9a73ad..918b6c806f22a 100644 --- a/apps/oxlint/src/lint/mod.rs +++ b/apps/oxlint/src/lint/mod.rs @@ -4,7 +4,7 @@ use ignore::gitignore::Gitignore; use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler}; use oxc_linter::{ loader::LINT_PARTIAL_LOADER_EXT, AllowWarnDeny, InvalidFilterKind, LintFilter, LintService, - LintServiceOptions, Linter, OxlintOptions, + LintServiceOptions, Linter, LinterBuilder, Oxlintrc, }; use oxc_span::VALID_EXTENSIONS; @@ -98,39 +98,32 @@ impl Runner for LintRunner { let number_of_files = paths.len(); let cwd = std::env::current_dir().unwrap(); - let mut options = - LintServiceOptions::new(cwd, paths).with_cross_module(enable_plugins.import_plugin); - let lint_options = OxlintOptions::default() - .with_filter(filter) - .with_config_path(basic_options.config) - .with_fix(fix_options.fix_kind()) - .with_react_plugin(enable_plugins.react_plugin) - .with_unicorn_plugin(enable_plugins.unicorn_plugin) - .with_typescript_plugin(enable_plugins.typescript_plugin) - .with_oxc_plugin(enable_plugins.oxc_plugin) - .with_import_plugin(enable_plugins.import_plugin) - .with_jsdoc_plugin(enable_plugins.jsdoc_plugin) - .with_jest_plugin(enable_plugins.jest_plugin) - .with_vitest_plugin(enable_plugins.vitest_plugin) - .with_jsx_a11y_plugin(enable_plugins.jsx_a11y_plugin) - .with_nextjs_plugin(enable_plugins.nextjs_plugin) - .with_react_perf_plugin(enable_plugins.react_perf_plugin) - .with_promise_plugin(enable_plugins.promise_plugin) - .with_node_plugin(enable_plugins.node_plugin) - .with_security_plugin(enable_plugins.security_plugin); - - let linter = match Linter::from_options(lint_options) { - Ok(lint_service) => lint_service, - Err(diagnostic) => { - let handler = GraphicalReportHandler::new(); - let mut err = String::new(); - handler.render_report(&mut err, diagnostic.as_ref()).unwrap(); - return CliRunResult::InvalidOptions { - message: format!("Failed to parse configuration file.\n{err}"), - }; + + let mut oxlintrc = if let Some(config_path) = basic_options.config.as_ref() { + match Oxlintrc::from_file(config_path) { + Ok(config) => config, + Err(diagnostic) => { + let handler = GraphicalReportHandler::new(); + let mut err = String::new(); + handler.render_report(&mut err, &diagnostic).unwrap(); + return CliRunResult::InvalidOptions { + message: format!("Failed to parse configuration file.\n{err}"), + }; + } } + } else { + Oxlintrc::default() }; + enable_plugins.apply_overrides(&mut oxlintrc.plugins); + let builder = LinterBuilder::from_oxlintrc(false, oxlintrc) + .with_filters(filter) + .with_fix(fix_options.fix_kind()); + + let mut options = + LintServiceOptions::new(cwd, paths).with_cross_module(builder.plugins().has_import()); + let linter = builder.build(); + let tsconfig = basic_options.tsconfig; if let Some(path) = tsconfig.as_ref() { if path.is_file() { @@ -562,4 +555,13 @@ mod test { assert_eq!(result.number_of_files, 1); assert_eq!(result.number_of_errors, 1); } + + #[test] + fn test_import_plugin_enabled_in_config() { + let args = &["-c", "fixtures/import/.oxlintrc.json", "fixtures/import/test.js"]; + let result = test(args); + assert_eq!(result.number_of_files, 1); + assert_eq!(result.number_of_warnings, 0); + assert_eq!(result.number_of_errors, 1); + } } diff --git a/crates/oxc/CHANGELOG.md b/crates/oxc/CHANGELOG.md index 346b58f1090c4..814969382827a 100644 --- a/crates/oxc/CHANGELOG.md +++ b/crates/oxc/CHANGELOG.md @@ -4,6 +4,23 @@ 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.31.0] - 2024-10-08 + +- 020bb80 codegen: [**BREAKING**] Change to `CodegenReturn::code` and `CodegenReturn::map` (#6310) (Boshen) + +- 4f6bc79 transformer: [**BREAKING**] Remove `source_type` param from `Transformer::new` (#6251) (overlookmotel) + +### Features + +- abd3a9f napi/transform: Perform dce after define plugin (#6312) (Boshen) +- a0ccc26 napi/transform: Add `lang` option to change source type (#6309) (Boshen) +- 2f888ed oxc: Add napi transform options (#6268) (Boshen) +- 8729755 oxc,napi/transform: Napi/transform use oxc compiler pipeline (#6298) (Boshen) + +### Refactor + +- aa0dbb6 oxc: Add `napi` feature, change napi parser to use `oxc` crate (#6265) (Boshen) + ## [0.30.2] - 2024-09-27 ### Documentation diff --git a/crates/oxc/Cargo.toml b/crates/oxc/Cargo.toml index 5c542b9878939..7cc5ec89a4c3e 100644 --- a/crates/oxc/Cargo.toml +++ b/crates/oxc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc" -version = "0.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true @@ -44,6 +44,11 @@ oxc_span = { workspace = true } oxc_syntax = { workspace = true } oxc_transformer = { workspace = true, optional = true } +napi = { workspace = true, optional = true, features = ["async"] } +napi-derive = { workspace = true, optional = true } + +rustc-hash = { workspace = true, optional = true } + [features] full = [ "codegen", @@ -56,12 +61,14 @@ full = [ "cfg", ] +parser = [] # for napi semantic = ["oxc_semantic"] transformer = ["oxc_transformer"] minifier = ["oxc_mangler", "oxc_minifier"] codegen = ["oxc_codegen"] mangler = ["oxc_mangler"] cfg = ["oxc_cfg"] +isolated_declarations = ["oxc_isolated_declarations"] serialize = [ "oxc_ast/serialize", @@ -73,9 +80,8 @@ serialize = [ sourcemap = ["oxc_sourcemap"] sourcemap_concurrent = ["oxc_sourcemap/concurrent", "sourcemap"] -isolated_declarations = ["oxc_isolated_declarations"] - wasm = ["oxc_transformer/wasm"] +napi = ["dep:napi", "dep:napi-derive", "dep:rustc-hash"] [package.metadata.docs.rs] all-features = true diff --git a/crates/oxc/README.md b/crates/oxc/README.md index e5fb1399df72b..39a894cbb41cd 100644 --- a/crates/oxc/README.md +++ b/crates/oxc/README.md @@ -99,9 +99,8 @@ if panicked { let SemanticBuilderReturn { semantic, errors: semantic_errors, -} = SemanticBuilder::new(source_text) +} = SemanticBuilder::new() .with_check_syntax_error(true) // Enable extra syntax error checking - .with_trivias(trivias) // Pass comments for JSDoc parsing .with_build_jsdoc(true) // Enable JSDoc parsing .with_cfg(true) // Build a Control Flow Graph .build(&program); // Produce the `Semantic` diff --git a/crates/oxc/src/compiler.rs b/crates/oxc/src/compiler.rs index 532d2af4b7786..8c95baf044340 100644 --- a/crates/oxc/src/compiler.rs +++ b/crates/oxc/src/compiler.rs @@ -1,15 +1,19 @@ use std::{mem, ops::ControlFlow, path::Path}; use oxc_allocator::Allocator; -use oxc_ast::{ast::Program, Trivias}; -use oxc_codegen::{CodeGenerator, CodegenOptions, CommentOptions}; +use oxc_ast::ast::Program; +use oxc_codegen::{CodeGenerator, CodegenOptions, CodegenReturn}; use oxc_diagnostics::OxcDiagnostic; +use oxc_isolated_declarations::{IsolatedDeclarations, IsolatedDeclarationsOptions}; use oxc_mangler::{MangleOptions, Mangler}; use oxc_minifier::{CompressOptions, Compressor}; use oxc_parser::{ParseOptions, Parser, ParserReturn}; use oxc_semantic::{ScopeTree, SemanticBuilder, SemanticBuilderReturn, SymbolTable}; use oxc_span::SourceType; -use oxc_transformer::{TransformOptions, Transformer, TransformerReturn}; +use oxc_transformer::{ + InjectGlobalVariables, InjectGlobalVariablesConfig, ReplaceGlobalDefines, + ReplaceGlobalDefinesConfig, TransformOptions, Transformer, TransformerReturn, +}; #[derive(Default)] pub struct Compiler { @@ -22,8 +26,8 @@ impl CompilerInterface for Compiler { self.errors.extend(errors); } - fn after_codegen(&mut self, printed: String) { - self.printed = printed; + fn after_codegen(&mut self, ret: CodegenReturn) { + self.printed = ret.code; } } @@ -49,14 +53,30 @@ impl Compiler { pub trait CompilerInterface { fn handle_errors(&mut self, _errors: Vec) {} + fn enable_sourcemap(&self) -> bool { + false + } + fn parse_options(&self) -> ParseOptions { ParseOptions::default() } + fn isolated_declaration_options(&self) -> Option { + None + } + fn transform_options(&self) -> Option { Some(TransformOptions::default()) } + fn define_options(&self) -> Option { + None + } + + fn inject_options(&self) -> Option { + None + } + fn compress_options(&self) -> Option { None } @@ -89,6 +109,8 @@ pub trait CompilerInterface { ControlFlow::Continue(()) } + fn after_isolated_declarations(&mut self, _ret: CodegenReturn) {} + fn after_transform( &mut self, _program: &mut Program<'_>, @@ -97,7 +119,7 @@ pub trait CompilerInterface { ControlFlow::Continue(()) } - fn after_codegen(&mut self, _printed: String) {} + fn after_codegen(&mut self, _ret: CodegenReturn) {} fn compile(&mut self, source_text: &str, source_type: SourceType, source_path: &Path) { let allocator = Allocator::default(); @@ -112,12 +134,16 @@ pub trait CompilerInterface { self.handle_errors(parser_return.errors); } - /* Semantic */ - let mut program = parser_return.program; - let trivias = parser_return.trivias; - let mut semantic_return = self.semantic(&program, source_text, source_path); + /* Isolated Declarations */ + if let Some(options) = self.isolated_declaration_options() { + self.isolated_declaration(options, &allocator, &program, source_path); + } + + /* Semantic */ + + let mut semantic_return = self.semantic(&program, source_path); if !semantic_return.errors.is_empty() { self.handle_errors(semantic_return.errors); return; @@ -126,22 +152,13 @@ pub trait CompilerInterface { return; } - let (symbols, scopes) = semantic_return.semantic.into_symbol_table_and_scope_tree(); + let (mut symbols, mut scopes) = semantic_return.semantic.into_symbol_table_and_scope_tree(); /* Transform */ if let Some(options) = self.transform_options() { - let mut transformer_return = self.transform( - options, - &allocator, - &mut program, - source_path, - source_text, - source_type, - &trivias, - symbols, - scopes, - ); + let mut transformer_return = + self.transform(options, &allocator, &mut program, source_path, symbols, scopes); if !transformer_return.errors.is_empty() { self.handle_errors(transformer_return.errors); @@ -151,6 +168,25 @@ pub trait CompilerInterface { if self.after_transform(&mut program, &mut transformer_return).is_break() { return; } + + symbols = transformer_return.symbols; + scopes = transformer_return.scopes; + } + + if let Some(config) = self.inject_options() { + let ret = + InjectGlobalVariables::new(&allocator, config).build(symbols, scopes, &mut program); + symbols = ret.symbols; + scopes = ret.scopes; + } + + if let Some(config) = self.define_options() { + let ret = + ReplaceGlobalDefines::new(&allocator, config).build(symbols, scopes, &mut program); + Compressor::new(&allocator, CompressOptions::dead_code_elimination()) + .build_with_symbols_and_scopes(ret.symbols, ret.scopes, &mut program); + // symbols = ret.symbols; + // scopes = ret.scopes; } /* Compress */ @@ -166,8 +202,8 @@ pub trait CompilerInterface { /* Codegen */ if let Some(options) = self.codegen_options() { - let printed = self.codegen(&program, source_text, &trivias, mangler, options); - self.after_codegen(printed); + let ret = self.codegen(&program, source_path, mangler, options); + self.after_codegen(ret); } } @@ -180,13 +216,8 @@ pub trait CompilerInterface { Parser::new(allocator, source_text, source_type).with_options(self.parse_options()).parse() } - fn semantic<'a>( - &self, - program: &Program<'a>, - source_text: &'a str, - source_path: &Path, - ) -> SemanticBuilderReturn<'a> { - let mut builder = SemanticBuilder::new(source_text); + fn semantic<'a>(&self, program: &Program<'a>, source_path: &Path) -> SemanticBuilderReturn<'a> { + let mut builder = SemanticBuilder::new(); if self.transform_options().is_some() { // Estimate transformer will triple scopes, symbols, references @@ -200,6 +231,24 @@ pub trait CompilerInterface { .build(program) } + fn isolated_declaration<'a>( + &mut self, + options: IsolatedDeclarationsOptions, + allocator: &'a Allocator, + program: &Program<'a>, + source_path: &Path, + ) { + let ret = IsolatedDeclarations::new(allocator, options).build(program); + self.handle_errors(ret.errors); + let ret = self.codegen( + &ret.program, + source_path, + None, + self.codegen_options().unwrap_or_default(), + ); + self.after_isolated_declarations(ret); + } + #[allow(clippy::too_many_arguments)] fn transform<'a>( &self, @@ -207,13 +256,10 @@ pub trait CompilerInterface { allocator: &'a Allocator, program: &mut Program<'a>, source_path: &Path, - source_text: &'a str, - source_type: SourceType, - trivias: &Trivias, symbols: SymbolTable, scopes: ScopeTree, ) -> TransformerReturn { - Transformer::new(allocator, source_path, source_type, source_text, trivias.clone(), options) + Transformer::new(allocator, source_path, options) .build_with_symbols_and_scopes(symbols, scopes, program) } @@ -230,20 +276,17 @@ pub trait CompilerInterface { Mangler::new().with_options(options).build(program) } - fn codegen<'a>( + fn codegen( &self, - program: &Program<'a>, - source_text: &'a str, - trivias: &Trivias, + program: &Program<'_>, + source_path: &Path, mangler: Option, options: CodegenOptions, - ) -> String { - let comment_options = CommentOptions { preserve_annotate_comments: true }; - CodeGenerator::new() - .with_options(options) - .with_mangler(mangler) - .enable_comment(source_text, trivias.clone(), comment_options) - .build(program) - .source_text + ) -> CodegenReturn { + let mut options = options; + if self.enable_sourcemap() { + options.source_map_path = Some(source_path.to_path_buf()); + } + CodeGenerator::new().with_options(options).with_mangler(mangler).build(program) } } diff --git a/crates/oxc/src/lib.rs b/crates/oxc/src/lib.rs index b7cab5a3a756f..74f2aaffc247d 100644 --- a/crates/oxc/src/lib.rs +++ b/crates/oxc/src/lib.rs @@ -3,6 +3,9 @@ #[cfg(feature = "full")] mod compiler; +#[cfg(feature = "napi")] +pub mod napi; + #[cfg(feature = "full")] pub use compiler::{Compiler, CompilerInterface}; diff --git a/crates/oxc/src/napi/isolated_declarations.rs b/crates/oxc/src/napi/isolated_declarations.rs new file mode 100644 index 0000000000000..db2583e872354 --- /dev/null +++ b/crates/oxc/src/napi/isolated_declarations.rs @@ -0,0 +1,24 @@ +use napi_derive::napi; + +use super::source_map::SourceMap; + +#[napi(object)] +pub struct IsolatedDeclarationsResult { + pub code: String, + pub map: Option, + pub errors: Vec, +} + +#[napi(object)] +#[derive(Debug, Default, Clone, Copy)] +pub struct IsolatedDeclarationsOptions { + /// Do not emit declarations for code that has an @internal annotation in its JSDoc comment. + /// This is an internal compiler option; use at your own risk, because the compiler does not check that the result is valid. + /// + /// Default: `false` + /// + /// See + pub strip_internal: Option, + + pub sourcemap: Option, +} diff --git a/crates/oxc/src/napi/mod.rs b/crates/oxc/src/napi/mod.rs new file mode 100644 index 0000000000000..d61a33ec2eb00 --- /dev/null +++ b/crates/oxc/src/napi/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "parser")] +pub mod parse; + +#[cfg(feature = "sourcemap")] +pub mod source_map; + +#[cfg(feature = "isolated_declarations")] +pub mod isolated_declarations; + +#[cfg(feature = "transformer")] +pub mod transform; diff --git a/crates/oxc/src/napi/parse.rs b/crates/oxc/src/napi/parse.rs new file mode 100644 index 0000000000000..9f2c94dd685ad --- /dev/null +++ b/crates/oxc/src/napi/parse.rs @@ -0,0 +1,36 @@ +use napi_derive::napi; + +/// Babel Parser Options +/// +/// +#[napi(object)] +#[derive(Default)] +pub struct ParserOptions { + #[napi(ts_type = "'script' | 'module' | 'unambiguous' | undefined")] + pub source_type: Option, + pub source_filename: Option, + /// Emit `ParenthesizedExpression` in AST. + /// + /// If this option is true, parenthesized expressions are represented by + /// (non-standard) `ParenthesizedExpression` nodes that have a single `expression` property + /// containing the expression inside parentheses. + /// + /// Default: true + pub preserve_parens: Option, +} + +#[napi(object)] +pub struct ParseResult { + pub program: String, + pub comments: Vec, + pub errors: Vec, +} + +#[napi(object)] +pub struct Comment { + #[napi(ts_type = "'Line' | 'Block'")] + pub r#type: &'static str, + pub value: String, + pub start: u32, + pub end: u32, +} diff --git a/napi/transform/src/sourcemap.rs b/crates/oxc/src/napi/source_map.rs similarity index 100% rename from napi/transform/src/sourcemap.rs rename to crates/oxc/src/napi/source_map.rs diff --git a/napi/transform/src/options.rs b/crates/oxc/src/napi/transform.rs similarity index 67% rename from napi/transform/src/options.rs rename to crates/oxc/src/napi/transform.rs index 2dc26f524dc65..c1c6b2925891d 100644 --- a/napi/transform/src/options.rs +++ b/crates/oxc/src/napi/transform.rs @@ -1,15 +1,52 @@ +// NOTE: Types must be aligned with [@types/babel__core](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/babel__core/index.d.ts). + #![allow(rustdoc::bare_urls)] use std::path::PathBuf; use napi::Either; use napi_derive::napi; -use oxc_transformer::{ - ArrowFunctionsOptions, ES2015Options, ReactJsxRuntime, ReactOptions, ReactRefreshOptions, - RewriteExtensionsMode, TypeScriptOptions, -}; +use rustc_hash::FxHashMap; + +use oxc_transformer::{JsxRuntime, RewriteExtensionsMode}; + +use super::{isolated_declarations::IsolatedDeclarationsOptions, source_map::SourceMap}; + +#[derive(Default)] +#[napi(object)] +pub struct TransformResult { + /// The transformed code. + /// + /// If parsing failed, this will be an empty string. + pub code: String, -use crate::IsolatedDeclarationsOptions; + /// The source map for the transformed code. + /// + /// This will be set if {@link TransformOptions#sourcemap} is `true`. + pub map: Option, + + /// The `.d.ts` declaration file for the transformed code. Declarations are + /// only generated if `declaration` is set to `true` and a TypeScript file + /// is provided. + /// + /// If parsing failed and `declaration` is set, this will be an empty string. + /// + /// @see {@link TypeScriptOptions#declaration} + /// @see [declaration tsconfig option](https://www.typescriptlang.org/tsconfig/#declaration) + pub declaration: Option, + + /// Declaration source map. Only generated if both + /// {@link TypeScriptOptions#declaration declaration} and + /// {@link TransformOptions#sourcemap sourcemap} are set to `true`. + pub declaration_map: Option, + + /// Parse and transformation errors. + /// + /// Oxc's parser recovers from common syntax errors, meaning that + /// transformed code may still be available even if there are errors in this + /// list. + pub errors: Vec, +} /// Options for transforming a JavaScript or TypeScript file. /// @@ -20,19 +57,14 @@ pub struct TransformOptions { #[napi(ts_type = "'script' | 'module' | 'unambiguous' | undefined")] pub source_type: Option, + /// Treat the source text as `js`, `jsx`, `ts`, or `tsx`. + #[napi(ts_type = "'js' | 'jsx' | 'ts' | 'tsx'")] + pub lang: Option, + /// The current working directory. Used to resolve relative paths in other /// options. pub cwd: Option, - /// Configure how TypeScript is transformed. - pub typescript: Option, - - /// Configure how TSX and JSX are transformed. - pub react: Option, - - /// Enable ES2015 transformations. - pub es2015: Option, - /// Enable source map generation. /// /// When `true`, the `sourceMap` field of transform result objects will be populated. @@ -41,6 +73,23 @@ pub struct TransformOptions { /// /// @see {@link SourceMap} pub sourcemap: Option, + + /// Configure how TypeScript is transformed. + pub typescript: Option, + + /// Configure how TSX and JSX are transformed. + pub jsx: Option, + + /// Enable ES2015 transformations. + pub es2015: Option, + + /// Define Plugin + #[napi(ts_type = "Record")] + pub define: Option>, + + /// Inject Plugin + #[napi(ts_type = "Record")] + pub inject: Option>>>, } impl From for oxc_transformer::TransformOptions { @@ -48,7 +97,7 @@ impl From for oxc_transformer::TransformOptions { Self { cwd: options.cwd.map(PathBuf::from).unwrap_or_default(), typescript: options.typescript.map(Into::into).unwrap_or_default(), - react: options.react.map(Into::into).unwrap_or_default(), + react: options.jsx.map(Into::into).unwrap_or_default(), es2015: options.es2015.map(Into::into).unwrap_or_default(), ..Self::default() } @@ -57,7 +106,7 @@ impl From for oxc_transformer::TransformOptions { #[napi(object)] #[derive(Default)] -pub struct TypeScriptBindingOptions { +pub struct TypeScriptOptions { pub jsx_pragma: Option, pub jsx_pragma_frag: Option, pub only_remove_type_imports: Option, @@ -83,10 +132,10 @@ pub struct TypeScriptBindingOptions { pub rewrite_import_extensions: Option>, } -impl From for TypeScriptOptions { - fn from(options: TypeScriptBindingOptions) -> Self { - let ops = TypeScriptOptions::default(); - TypeScriptOptions { +impl From for oxc_transformer::TypeScriptOptions { + fn from(options: TypeScriptOptions) -> Self { + let ops = oxc_transformer::TypeScriptOptions::default(); + oxc_transformer::TypeScriptOptions { jsx_pragma: options.jsx_pragma.map(Into::into).unwrap_or(ops.jsx_pragma), jsx_pragma_frag: options.jsx_pragma_frag.map(Into::into).unwrap_or(ops.jsx_pragma_frag), only_remove_type_imports: options @@ -119,7 +168,7 @@ impl From for TypeScriptOptions { /// /// @see {@link https://babeljs.io/docs/babel-plugin-transform-react-jsx#options} #[napi(object)] -pub struct ReactBindingOptions { +pub struct JsxOptions { /// Decides which runtime to use. /// /// - 'automatic' - auto-import the correct JSX factories @@ -196,16 +245,16 @@ pub struct ReactBindingOptions { /// Conforms to the implementation in {@link https://github.com/facebook/react/tree/main/packages/react-refresh} /// /// @default false - pub refresh: Option>, + pub refresh: Option>, } -impl From for ReactOptions { - fn from(options: ReactBindingOptions) -> Self { - let ops = ReactOptions::default(); - ReactOptions { +impl From for oxc_transformer::JsxOptions { + fn from(options: JsxOptions) -> Self { + let ops = oxc_transformer::JsxOptions::default(); + oxc_transformer::JsxOptions { runtime: match options.runtime.as_deref() { - Some("classic") => ReactJsxRuntime::Classic, - /* "automatic" */ _ => ReactJsxRuntime::Automatic, + Some("classic") => JsxRuntime::Classic, + /* "automatic" */ _ => JsxRuntime::Automatic, }, development: options.development.unwrap_or(ops.development), throw_if_namespace: options.throw_if_namespace.unwrap_or(ops.throw_if_namespace), @@ -216,8 +265,8 @@ impl From for ReactOptions { use_built_ins: options.use_built_ins, use_spread: options.use_spread, refresh: options.refresh.and_then(|value| match value { - Either::A(b) => b.then(ReactRefreshOptions::default), - Either::B(options) => Some(ReactRefreshOptions::from(options)), + Either::A(b) => b.then(oxc_transformer::ReactRefreshOptions::default), + Either::B(options) => Some(oxc_transformer::ReactRefreshOptions::from(options)), }), ..Default::default() } @@ -225,7 +274,7 @@ impl From for ReactOptions { } #[napi(object)] -pub struct ReactRefreshBindingOptions { +pub struct ReactRefreshOptions { /// Specify the identifier of the refresh registration variable. /// /// @default `$RefreshReg$`. @@ -239,10 +288,10 @@ pub struct ReactRefreshBindingOptions { pub emit_full_signatures: Option, } -impl From for ReactRefreshOptions { - fn from(options: ReactRefreshBindingOptions) -> Self { - let ops = ReactRefreshOptions::default(); - ReactRefreshOptions { +impl From for oxc_transformer::ReactRefreshOptions { + fn from(options: ReactRefreshOptions) -> Self { + let ops = oxc_transformer::ReactRefreshOptions::default(); + oxc_transformer::ReactRefreshOptions { refresh_reg: options.refresh_reg.unwrap_or(ops.refresh_reg), refresh_sig: options.refresh_sig.unwrap_or(ops.refresh_sig), emit_full_signatures: options.emit_full_signatures.unwrap_or(ops.emit_full_signatures), @@ -251,7 +300,7 @@ impl From for ReactRefreshOptions { } #[napi(object)] -pub struct ArrowFunctionsBindingOptions { +pub struct ArrowFunctionsOptions { /// This option enables the following: /// * Wrap the generated function in .bind(this) and keeps uses of this inside the function as-is, instead of using a renamed this. /// * Add a runtime check to ensure the functions are not instantiated. @@ -261,20 +310,20 @@ pub struct ArrowFunctionsBindingOptions { pub spec: Option, } -impl From for ArrowFunctionsOptions { - fn from(options: ArrowFunctionsBindingOptions) -> Self { - ArrowFunctionsOptions { spec: options.spec.unwrap_or_default() } +impl From for oxc_transformer::ArrowFunctionsOptions { + fn from(options: ArrowFunctionsOptions) -> Self { + oxc_transformer::ArrowFunctionsOptions { spec: options.spec.unwrap_or_default() } } } #[napi(object)] -pub struct ES2015BindingOptions { +pub struct Es2015Options { /// Transform arrow functions into function expressions. - pub arrow_function: Option, + pub arrow_function: Option, } -impl From for ES2015Options { - fn from(options: ES2015BindingOptions) -> Self { - ES2015Options { arrow_function: options.arrow_function.map(Into::into) } +impl From for oxc_transformer::ES2015Options { + fn from(options: Es2015Options) -> Self { + oxc_transformer::ES2015Options { arrow_function: options.arrow_function.map(Into::into) } } } diff --git a/crates/oxc_allocator/CHANGELOG.md b/crates/oxc_allocator/CHANGELOG.md index b543884940664..01d92b48beaf4 100644 --- a/crates/oxc_allocator/CHANGELOG.md +++ b/crates/oxc_allocator/CHANGELOG.md @@ -4,6 +4,16 @@ 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.31.0] - 2024-10-08 + +### Performance + +- 5db9b30 allocator: Use lower bound of size hint when creating Vecs from an iterator (#6194) (DonIsaac) + +### Refactor + +- f7d1136 allocator: Remove unnecessary `Vec` impl (#6213) (overlookmotel) + ## [0.30.2] - 2024-09-27 ### Documentation diff --git a/crates/oxc_allocator/Cargo.toml b/crates/oxc_allocator/Cargo.toml index eda122be44c16..cf8d1284a5df9 100644 --- a/crates/oxc_allocator/Cargo.toml +++ b/crates/oxc_allocator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_allocator" -version = "0.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_allocator/src/boxed.rs b/crates/oxc_allocator/src/boxed.rs index 5ef4eef87a7e8..46f5a753376e3 100644 --- a/crates/oxc_allocator/src/boxed.rs +++ b/crates/oxc_allocator/src/boxed.rs @@ -76,6 +76,25 @@ impl<'alloc, T> Box<'alloc, T> { } } +impl<'alloc, T: ?Sized> Box<'alloc, T> { + /// Create a [`Box`] from a raw pointer to a value. + /// + /// The [`Box`] takes ownership of the data pointed to by `ptr`. + /// + /// # SAFETY + /// Data pointed to by `ptr` must live as long as `'alloc`. + /// This requirement is met if the pointer was obtained from other data in the arena. + /// + /// Data pointed to by `ptr` must *only* be used for this `Box`. i.e. it must be unique, + /// with no other aliases. You must not, for example, create 2 `Box`es from the same pointer. + /// + /// `ptr` must have been created from a `*mut T` or `&mut T` (not a `*const T` / `&T`). + #[inline] + pub(crate) const unsafe fn from_non_null(ptr: NonNull) -> Self { + Self(ptr, PhantomData) + } +} + impl<'alloc, T: ?Sized> ops::Deref for Box<'alloc, T> { type Target = T; @@ -148,6 +167,7 @@ impl<'alloc, T: Hash> Hash for Box<'alloc, T> { pub struct Address(usize); impl<'a, T> Box<'a, T> { + /// Get the memory address of a value allocated in the arena. #[inline] pub fn address(&self) -> Address { Address(ptr::addr_of!(**self) as usize) diff --git a/crates/oxc_allocator/src/clone_in.rs b/crates/oxc_allocator/src/clone_in.rs index 79d1a6364ca59..3610bd7a1edc2 100644 --- a/crates/oxc_allocator/src/clone_in.rs +++ b/crates/oxc_allocator/src/clone_in.rs @@ -20,8 +20,13 @@ use crate::{Allocator, Box, Vec}; /// However, it **isn't** guaranteed. /// pub trait CloneIn<'new_alloc>: Sized { + /// The type of the cloned object. + /// + /// This should always be `Self` with a different lifetime. type Cloned; + /// Clone `self` into the given `allocator`. `allocator` may be the same one + /// that `self` is already in. fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned; } diff --git a/crates/oxc_allocator/src/convert.rs b/crates/oxc_allocator/src/convert.rs index 8fb99fea03626..2f9122b817a0e 100644 --- a/crates/oxc_allocator/src/convert.rs +++ b/crates/oxc_allocator/src/convert.rs @@ -2,17 +2,19 @@ use crate::{Allocator, Box}; -/// This trait works similarly to the standard library `From` trait, It comes with a similar -/// implementation containing blanket implementation for `IntoIn`, reflective implementation and a +/// This trait works similarly to the standard library [`From`] trait, It comes with a similar +/// implementation containing blanket implementation for [`IntoIn`], reflective implementation and a /// bunch of primitive conversions from Rust types to their arena equivalent. pub trait FromIn<'a, T>: Sized { + /// Converts to this type from the input type within the given `allocator`. fn from_in(value: T, allocator: &'a Allocator) -> Self; } -/// This trait works similarly to the standard library `Into` trait. -/// It is similar to `FromIn` is reflective, A `FromIn` implementation also implicitly implements -/// `IntoIn` for the opposite type. +/// This trait works similarly to the standard library [`Into`] trait. +/// It is similar to [`FromIn`] is reflective, A [`FromIn`] implementation also implicitly implements +/// [`IntoIn`] for the opposite type. pub trait IntoIn<'a, T>: Sized { + /// Converts this type into the (usually inferred) input type within the given `allocator`. fn into_in(self, allocator: &'a Allocator) -> T; } diff --git a/crates/oxc_allocator/src/lib.rs b/crates/oxc_allocator/src/lib.rs index b07443c3d83c3..90460f81ead9a 100644 --- a/crates/oxc_allocator/src/lib.rs +++ b/crates/oxc_allocator/src/lib.rs @@ -38,7 +38,7 @@ //! let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default()); //! assert!(parsed.errors.is_empty()); //! ``` - +#![warn(missing_docs)] use std::{ convert::From, ops::{Deref, DerefMut}, diff --git a/crates/oxc_allocator/src/vec.rs b/crates/oxc_allocator/src/vec.rs index d3d226121edb7..653fc0e928cd5 100644 --- a/crates/oxc_allocator/src/vec.rs +++ b/crates/oxc_allocator/src/vec.rs @@ -7,6 +7,7 @@ use std::{ fmt::Debug, hash::{Hash, Hasher}, ops, + ptr::NonNull, }; use allocator_api2::vec; @@ -14,7 +15,7 @@ use bumpalo::Bump; #[cfg(any(feature = "serialize", test))] use serde::{ser::SerializeSeq, Serialize, Serializer}; -use crate::Allocator; +use crate::{Allocator, Box}; /// Bumpalo Vec #[derive(Debug, PartialEq, Eq)] @@ -92,14 +93,52 @@ impl<'alloc, T> Vec<'alloc, T> { Self(vec::Vec::with_capacity_in(capacity, allocator)) } + /// Create a new [`Vec`] whose elements are taken from an iterator and + /// allocated in the given `allocator`. + /// + /// This is behaviorially identical to [`FromIterator::from_iter`]. #[inline] pub fn from_iter_in>(iter: I, allocator: &'alloc Allocator) -> Self { let iter = iter.into_iter(); - let capacity = iter.size_hint().1.unwrap_or(0); + let hint = iter.size_hint(); + let capacity = hint.1.unwrap_or(hint.0); let mut vec = vec::Vec::with_capacity_in(capacity, &**allocator); vec.extend(iter); Self(vec) } + + /// Converts the vector into [`Box<[T]>`][owned slice]. + /// + /// Any excess capacity the vector has will not be included in the slice. + /// The excess memory will be leaked in the arena (i.e. not reused by another allocation). + /// + /// # Examples + /// + /// ``` + /// use oxc_allocator::{Allocator, Vec}; + /// + /// let allocator = Allocator::default(); + /// let mut v = Vec::with_capacity_in(10, &allocator); + /// v.extend([1, 2, 3]); + /// let b = v.into_boxed_slice(); + /// + /// assert_eq!(&*b, &[1, 2, 3]); + /// assert_eq!(b.len(), 3); + /// ``` + /// + /// [owned slice]: Box + pub fn into_boxed_slice(self) -> Box<'alloc, [T]> { + let slice = self.0.leak(); + let ptr = NonNull::from(slice); + // SAFETY: `ptr` points to a valid slice `[T]`. + // `allocator_api2::vec::Vec::leak` consumes the inner `Vec` without dropping it. + // Lifetime of returned `Box<'alloc, [T]>` is same as lifetime of consumed `Vec<'alloc, T>`, + // so data in the `Box` must be valid for its lifetime. + // `Vec` uniquely owned the data, and we have consumed the `Vec`, so the new `Box` has + // unique ownership of the data (no aliasing). + // `ptr` was created from a `&mut [T]`. + unsafe { Box::from_non_null(ptr) } + } } impl<'alloc, T> ops::Deref for Vec<'alloc, T> { @@ -142,14 +181,6 @@ impl<'alloc, T> ops::Index for Vec<'alloc, T> { } } -impl<'alloc, T> ops::Index for &'alloc Vec<'alloc, T> { - type Output = T; - - fn index(&self, index: usize) -> &Self::Output { - self.0.index(index) - } -} - // Unused right now. // impl<'alloc, T> ops::IndexMut for Vec<'alloc, T> { // fn index_mut(&mut self, index: usize) -> &mut Self::Output { @@ -185,7 +216,7 @@ impl<'alloc, T: Hash> Hash for Vec<'alloc, T> { #[cfg(test)] mod test { use super::Vec; - use crate::Allocator; + use crate::{Allocator, Box}; #[test] fn vec_with_capacity() { @@ -218,4 +249,19 @@ mod test { program } } + + #[test] + fn vec_to_boxed_slice() { + let allocator = Allocator::default(); + let mut v = Vec::with_capacity_in(10, &allocator); + v.extend([1, 2, 3]); + + let b = v.into_boxed_slice(); + // Check return value is an `oxc_allocator::Box`, not an `allocator_api2::boxed::Box` + let b: Box<[u8]> = b; + + assert_eq!(&*b, &[1, 2, 3]); + // Check length of slice is equal to what `v.len()` was, not `v.capacity()` + assert_eq!(b.len(), 3); + } } diff --git a/crates/oxc_ast/CHANGELOG.md b/crates/oxc_ast/CHANGELOG.md index 914ff6d5c4bbc..0e428acaeb070 100644 --- a/crates/oxc_ast/CHANGELOG.md +++ b/crates/oxc_ast/CHANGELOG.md @@ -4,6 +4,20 @@ 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.31.0] - 2024-10-08 + +- 01b878e parser: [**BREAKING**] Use `BindingIdentifier` for `namespace` declaration names (#6003) (DonIsaac) + +- 5a73a66 regular_expression: [**BREAKING**] Simplify public APIs (#6262) (leaysgur) + +### Features + +- 9e62396 syntax_operations: Add crate `oxc_ecmascript` (#6202) (Boshen) + +### Refactor + +- acab777 regular_expression: Misc fixes (#6234) (leaysgur) + ## [0.30.2] - 2024-09-27 ### Features diff --git a/crates/oxc_ast/Cargo.toml b/crates/oxc_ast/Cargo.toml index fe23fc31361bd..8680c7e2a1325 100644 --- a/crates/oxc_ast/Cargo.toml +++ b/crates/oxc_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_ast" -version = "0.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_ast/src/ast/comment.rs b/crates/oxc_ast/src/ast/comment.rs new file mode 100644 index 0000000000000..a52892f9c9bda --- /dev/null +++ b/crates/oxc_ast/src/ast/comment.rs @@ -0,0 +1,112 @@ +use oxc_allocator::CloneIn; +use oxc_ast_macros::ast; +use oxc_span::{cmp::ContentEq, hash::ContentHash, Span}; + +#[ast] +#[generate_derive(CloneIn, ContentEq, ContentHash)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub enum CommentKind { + #[default] + Line = 0, + Block = 1, +} + +#[ast] +#[generate_derive(CloneIn, ContentEq, ContentHash)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub enum CommentPosition { + /// Comments prior to a token until another token or trailing comment. + /// + /// e.g. + /// + /// ``` + /// /* leading */ token; + /// /* leading */ + /// // leading + /// token; + /// ``` + #[default] + Leading = 0, + + /// Comments tailing a token until a newline. + /// e.g. `token /* trailing */ // trailing` + Trailing = 1, +} + +#[ast] +#[generate_derive(CloneIn, ContentEq, ContentHash)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub struct Comment { + /// The span of the comment text (without leading/trailing delimiters). + pub span: Span, + + /// Line or block comment + pub kind: CommentKind, + + /// Leading or trailing comment + pub position: CommentPosition, + + /// Start of token this leading comment is attached to. + /// `/* Leading */ token` + /// ^ This start + /// NOTE: Trailing comment attachment is not computed yet. + pub attached_to: u32, + + /// Whether this comment has a preceding newline. + /// Used to avoid becoming a trailing comment in codegen. + pub preceded_by_newline: bool, + + /// Whether this comment has a tailing newline. + pub followed_by_newline: bool, +} + +impl Comment { + #[inline] + pub fn new(start: u32, end: u32, kind: CommentKind) -> Self { + let span = Span::new(start, end); + Self { + span, + kind, + position: CommentPosition::Trailing, + attached_to: 0, + preceded_by_newline: false, + followed_by_newline: false, + } + } + + pub fn is_line(self) -> bool { + self.kind == CommentKind::Line + } + + pub fn is_block(self) -> bool { + self.kind == CommentKind::Block + } + + pub fn is_leading(self) -> bool { + self.position == CommentPosition::Leading + } + + pub fn is_trailing(self) -> bool { + self.position == CommentPosition::Trailing + } + + pub fn real_span(&self) -> Span { + Span::new(self.real_span_start(), self.real_span_end()) + } + + pub fn real_span_end(&self) -> u32 { + match self.kind { + CommentKind::Line => self.span.end, + // length of `*/` + CommentKind::Block => self.span.end + 2, + } + } + + pub fn real_span_start(&self) -> u32 { + self.span.start - 2 + } + + pub fn is_jsdoc(&self, source_text: &str) -> bool { + self.is_leading() && self.is_block() && self.span.source_text(source_text).starts_with('*') + } +} diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index e06d43eb38e4a..719e69c543ba7 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -39,6 +39,11 @@ pub struct Program<'a> { #[serde(flatten)] pub span: Span, pub source_type: SourceType, + #[serde(skip)] + pub source_text: &'a str, + /// Sorted comments + #[serde(skip)] + pub comments: Vec<'a, Comment>, pub hashbang: Option>, pub directives: Vec<'a, Directive<'a>>, pub body: Vec<'a, Statement<'a>>, diff --git a/crates/oxc_ast/src/ast/mod.rs b/crates/oxc_ast/src/ast/mod.rs index 3370e1c231408..19a3c0b505cbe 100644 --- a/crates/oxc_ast/src/ast/mod.rs +++ b/crates/oxc_ast/src/ast/mod.rs @@ -175,6 +175,7 @@ //! //! If you are seeing compile-time errors in `src/ast/macros.rs`, this will be the cause. +pub(crate) mod comment; pub(crate) mod js; pub(crate) mod jsx; pub(crate) mod literal; @@ -191,4 +192,4 @@ pub use oxc_syntax::{ }, }; -pub use self::{js::*, jsx::*, literal::*, ts::*}; +pub use self::{comment::*, js::*, jsx::*, literal::*, ts::*}; diff --git a/crates/oxc_ast/src/ast/ts.rs b/crates/oxc_ast/src/ast/ts.rs index ae3327211f08a..1555888052394 100644 --- a/crates/oxc_ast/src/ast/ts.rs +++ b/crates/oxc_ast/src/ast/ts.rs @@ -347,10 +347,12 @@ pub struct TSConditionalType<'a> { /// The type before `extends` in the test expression. pub check_type: TSType<'a>, /// The type `check_type` is being tested against. + #[scope(enter_before)] pub extends_type: TSType<'a>, /// The type evaluated to if the test is true. pub true_type: TSType<'a>, /// The type evaluated to if the test is false. + #[scope(exit_before)] pub false_type: TSType<'a>, #[serde(skip)] #[clone_in(default)] @@ -1145,10 +1147,10 @@ pub struct TSIndexSignature<'a> { pub struct TSCallSignatureDeclaration<'a> { #[serde(flatten)] pub span: Span, + pub type_parameters: Option>>, pub this_param: Option>, pub params: Box<'a, FormalParameters<'a>>, pub return_type: Option>>, - pub type_parameters: Option>>, } #[ast] @@ -1186,10 +1188,10 @@ pub struct TSMethodSignature<'a> { pub computed: bool, pub optional: bool, pub kind: TSMethodSignatureKind, + pub type_parameters: Option>>, pub this_param: Option>>, pub params: Box<'a, FormalParameters<'a>>, pub return_type: Option>>, - pub type_parameters: Option>>, #[serde(skip)] #[clone_in(default)] pub scope_id: Cell>, @@ -1205,9 +1207,9 @@ pub struct TSMethodSignature<'a> { pub struct TSConstructSignatureDeclaration<'a> { #[serde(flatten)] pub span: Span, + pub type_parameters: Option>>, pub params: Box<'a, FormalParameters<'a>>, pub return_type: Option>>, - pub type_parameters: Option>>, #[serde(skip)] #[clone_in(default)] pub scope_id: Cell>, @@ -1391,7 +1393,7 @@ pub enum TSModuleDeclarationKind { #[cfg_attr(feature = "serialize", derive(Serialize, Tsify))] #[serde(untagged)] pub enum TSModuleDeclarationName<'a> { - Identifier(IdentifierName<'a>) = 0, + Identifier(BindingIdentifier<'a>) = 0, StringLiteral(StringLiteral<'a>) = 1, } @@ -1560,10 +1562,17 @@ pub enum TSImportAttributeName<'a> { pub struct TSFunctionType<'a> { #[serde(flatten)] pub span: Span, + /// Generic type parameters + /// + /// ```ts + /// type T = (x: U) => U; + /// // ^ + /// ``` + pub type_parameters: Option>>, /// `this` parameter /// /// ```ts - /// type T = (this: string, a: number) => void + /// type T = (this: string, a: number) => void; /// // ^^^^^^^^^^^^ /// ``` pub this_param: Option>>, @@ -1571,17 +1580,10 @@ pub struct TSFunctionType<'a> { pub params: Box<'a, FormalParameters<'a>>, /// Return type of the function. /// ```ts - /// type T = () => void + /// type T = () => void; /// // ^^^^ /// ``` pub return_type: Box<'a, TSTypeAnnotation<'a>>, - /// Generic type parameters - /// - /// ```ts - /// type T = (x: T) => T - /// // ^ - /// ``` - pub type_parameters: Option>>, } #[ast(visit)] @@ -1593,9 +1595,9 @@ pub struct TSConstructorType<'a> { #[serde(flatten)] pub span: Span, pub r#abstract: bool, + pub type_parameters: Option>>, pub params: Box<'a, FormalParameters<'a>>, pub return_type: Box<'a, TSTypeAnnotation<'a>>, - pub type_parameters: Option>>, } /// TypeScript Mapped Type diff --git a/crates/oxc_ast/src/ast_builder_impl.rs b/crates/oxc_ast/src/ast_builder_impl.rs index 69c1e636e4f53..1c4e7e1aa9a71 100644 --- a/crates/oxc_ast/src/ast_builder_impl.rs +++ b/crates/oxc_ast/src/ast_builder_impl.rs @@ -112,6 +112,28 @@ impl<'a> AstBuilder<'a> { mem::replace(decl, empty_decl) } + #[inline] + pub fn move_variable_declaration( + self, + decl: &mut VariableDeclaration<'a>, + ) -> VariableDeclaration<'a> { + let empty_decl = self.variable_declaration( + Span::default(), + VariableDeclarationKind::Var, + self.vec(), + false, + ); + mem::replace(decl, empty_decl) + } + + pub fn move_array_expression_element( + self, + element: &mut ArrayExpressionElement<'a>, + ) -> ArrayExpressionElement<'a> { + let empty_element = self.array_expression_element_elision(Span::default()); + mem::replace(element, empty_element) + } + #[inline] pub fn move_vec(self, vec: &mut Vec<'a, T>) -> Vec<'a, T> { mem::replace(vec, self.vec()) diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index 5b93a1fda3080..7ec9a77ec9bbd 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, cell::Cell, fmt}; use oxc_allocator::{Box, FromIn, Vec}; -use oxc_span::{Atom, GetSpan, SourceType, Span}; +use oxc_span::{Atom, GetSpan, Span}; use oxc_syntax::{ operator::UnaryOperator, reference::ReferenceId, @@ -28,18 +28,6 @@ export interface FormalParameterRest extends Span { } "#; -impl<'a> Program<'a> { - pub fn new( - span: Span, - source_type: SourceType, - directives: Vec<'a, Directive<'a>>, - hashbang: Option>, - body: Vec<'a, Statement<'a>>, - ) -> Self { - Self { span, source_type, directives, hashbang, body, scope_id: Cell::default() } - } -} - impl<'a> Program<'a> { pub fn is_empty(&self) -> bool { self.body.is_empty() && self.directives.is_empty() @@ -269,23 +257,6 @@ impl<'a> Expression<'a> { matches!(self, Expression::BinaryExpression(_) | Expression::LogicalExpression(_)) } - /// Returns literal's value converted to the Boolean type - /// returns `true` when node is truthy, `false` when node is falsy, `None` when it cannot be determined. - /// - /// 1. If argument is a Boolean, return argument. - /// 2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0ℤ, or the empty String, return false. - pub fn to_boolean(&self) -> Option { - match self { - Self::BooleanLiteral(lit) => Some(lit.value), - Self::NullLiteral(_) => Some(false), - Self::NumericLiteral(lit) => Some(lit.value != 0.0), - Self::BigIntLiteral(lit) => Some(!lit.is_zero()), - Self::RegExpLiteral(_) => Some(true), - Self::StringLiteral(lit) => Some(!lit.value.is_empty()), - _ => None, - } - } - pub fn get_member_expr(&self) -> Option<&MemberExpression<'a>> { match self.get_inner_expression() { Expression::ChainExpression(chain_expr) => chain_expr.expression.as_member_expression(), @@ -382,14 +353,6 @@ impl<'a> ArrayExpressionElement<'a> { } } -impl<'a> ObjectExpression<'a> { - /// Returns `true` if this object has a property named `__proto__` - pub fn has_proto(&self) -> bool { - use crate::syntax_directed_operations::PropName; - self.properties.iter().any(|p| p.prop_name().is_some_and(|name| name.0 == "__proto__")) - } -} - impl<'a> PropertyKey<'a> { pub fn static_name(&self) -> Option> { match self { diff --git a/crates/oxc_ast/src/generated/assert_layouts.rs b/crates/oxc_ast/src/generated/assert_layouts.rs index 049f15187c37b..df250fa8229ab 100644 --- a/crates/oxc_ast/src/generated/assert_layouts.rs +++ b/crates/oxc_ast/src/generated/assert_layouts.rs @@ -55,14 +55,16 @@ const _: () = { assert!(offset_of!(StringLiteral, span) == 0usize); assert!(offset_of!(StringLiteral, value) == 8usize); - assert!(size_of::() == 112usize); + assert!(size_of::() == 160usize); assert!(align_of::() == 8usize); assert!(offset_of!(Program, span) == 0usize); assert!(offset_of!(Program, source_type) == 8usize); - assert!(offset_of!(Program, hashbang) == 16usize); - assert!(offset_of!(Program, directives) == 40usize); - assert!(offset_of!(Program, body) == 72usize); - assert!(offset_of!(Program, scope_id) == 104usize); + assert!(offset_of!(Program, source_text) == 16usize); + assert!(offset_of!(Program, comments) == 32usize); + assert!(offset_of!(Program, hashbang) == 64usize); + assert!(offset_of!(Program, directives) == 88usize); + assert!(offset_of!(Program, body) == 120usize); + assert!(offset_of!(Program, scope_id) == 152usize); assert!(size_of::() == 16usize); assert!(align_of::() == 8usize); @@ -1040,10 +1042,10 @@ const _: () = { assert!(size_of::() == 64usize); assert!(align_of::() == 8usize); assert!(offset_of!(TSCallSignatureDeclaration, span) == 0usize); - assert!(offset_of!(TSCallSignatureDeclaration, this_param) == 8usize); - assert!(offset_of!(TSCallSignatureDeclaration, params) == 40usize); - assert!(offset_of!(TSCallSignatureDeclaration, return_type) == 48usize); - assert!(offset_of!(TSCallSignatureDeclaration, type_parameters) == 56usize); + assert!(offset_of!(TSCallSignatureDeclaration, type_parameters) == 8usize); + assert!(offset_of!(TSCallSignatureDeclaration, this_param) == 16usize); + assert!(offset_of!(TSCallSignatureDeclaration, params) == 48usize); + assert!(offset_of!(TSCallSignatureDeclaration, return_type) == 56usize); assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); @@ -1055,18 +1057,18 @@ const _: () = { assert!(offset_of!(TSMethodSignature, computed) == 24usize); assert!(offset_of!(TSMethodSignature, optional) == 25usize); assert!(offset_of!(TSMethodSignature, kind) == 26usize); - assert!(offset_of!(TSMethodSignature, this_param) == 32usize); - assert!(offset_of!(TSMethodSignature, params) == 40usize); - assert!(offset_of!(TSMethodSignature, return_type) == 48usize); - assert!(offset_of!(TSMethodSignature, type_parameters) == 56usize); + assert!(offset_of!(TSMethodSignature, type_parameters) == 32usize); + assert!(offset_of!(TSMethodSignature, this_param) == 40usize); + assert!(offset_of!(TSMethodSignature, params) == 48usize); + assert!(offset_of!(TSMethodSignature, return_type) == 56usize); assert!(offset_of!(TSMethodSignature, scope_id) == 64usize); assert!(size_of::() == 40usize); assert!(align_of::() == 8usize); assert!(offset_of!(TSConstructSignatureDeclaration, span) == 0usize); - assert!(offset_of!(TSConstructSignatureDeclaration, params) == 8usize); - assert!(offset_of!(TSConstructSignatureDeclaration, return_type) == 16usize); - assert!(offset_of!(TSConstructSignatureDeclaration, type_parameters) == 24usize); + assert!(offset_of!(TSConstructSignatureDeclaration, type_parameters) == 8usize); + assert!(offset_of!(TSConstructSignatureDeclaration, params) == 16usize); + assert!(offset_of!(TSConstructSignatureDeclaration, return_type) == 24usize); assert!(offset_of!(TSConstructSignatureDeclaration, scope_id) == 32usize); assert!(size_of::() == 32usize); @@ -1091,19 +1093,19 @@ const _: () = { assert!(size_of::() == 16usize); assert!(align_of::() == 8usize); - assert!(size_of::() == 64usize); + assert!(size_of::() == 72usize); assert!(align_of::() == 8usize); assert!(offset_of!(TSModuleDeclaration, span) == 0usize); assert!(offset_of!(TSModuleDeclaration, id) == 8usize); - assert!(offset_of!(TSModuleDeclaration, body) == 40usize); - assert!(offset_of!(TSModuleDeclaration, kind) == 56usize); - assert!(offset_of!(TSModuleDeclaration, declare) == 57usize); - assert!(offset_of!(TSModuleDeclaration, scope_id) == 60usize); + assert!(offset_of!(TSModuleDeclaration, body) == 48usize); + assert!(offset_of!(TSModuleDeclaration, kind) == 64usize); + assert!(offset_of!(TSModuleDeclaration, declare) == 65usize); + assert!(offset_of!(TSModuleDeclaration, scope_id) == 68usize); assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 32usize); + assert!(size_of::() == 40usize); assert!(align_of::() == 8usize); assert!(size_of::() == 16usize); @@ -1161,18 +1163,18 @@ const _: () = { assert!(size_of::() == 40usize); assert!(align_of::() == 8usize); assert!(offset_of!(TSFunctionType, span) == 0usize); - assert!(offset_of!(TSFunctionType, this_param) == 8usize); - assert!(offset_of!(TSFunctionType, params) == 16usize); - assert!(offset_of!(TSFunctionType, return_type) == 24usize); - assert!(offset_of!(TSFunctionType, type_parameters) == 32usize); + assert!(offset_of!(TSFunctionType, type_parameters) == 8usize); + assert!(offset_of!(TSFunctionType, this_param) == 16usize); + assert!(offset_of!(TSFunctionType, params) == 24usize); + assert!(offset_of!(TSFunctionType, return_type) == 32usize); assert!(size_of::() == 40usize); assert!(align_of::() == 8usize); assert!(offset_of!(TSConstructorType, span) == 0usize); assert!(offset_of!(TSConstructorType, r#abstract) == 8usize); - assert!(offset_of!(TSConstructorType, params) == 16usize); - assert!(offset_of!(TSConstructorType, return_type) == 24usize); - assert!(offset_of!(TSConstructorType, type_parameters) == 32usize); + assert!(offset_of!(TSConstructorType, type_parameters) == 16usize); + assert!(offset_of!(TSConstructorType, params) == 24usize); + assert!(offset_of!(TSConstructorType, return_type) == 32usize); assert!(size_of::() == 56usize); assert!(align_of::() == 8usize); @@ -1374,6 +1376,21 @@ const _: () = { assert!(offset_of!(JSXText, span) == 0usize); assert!(offset_of!(JSXText, value) == 8usize); + assert!(size_of::() == 1usize); + assert!(align_of::() == 1usize); + + assert!(size_of::() == 1usize); + assert!(align_of::() == 1usize); + + assert!(size_of::() == 20usize); + assert!(align_of::() == 4usize); + assert!(offset_of!(Comment, span) == 0usize); + assert!(offset_of!(Comment, kind) == 8usize); + assert!(offset_of!(Comment, position) == 9usize); + assert!(offset_of!(Comment, attached_to) == 12usize); + assert!(offset_of!(Comment, preceded_by_newline) == 16usize); + assert!(offset_of!(Comment, followed_by_newline) == 17usize); + assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); @@ -1412,24 +1429,6 @@ const _: () = { assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 72usize); - assert!(align_of::() == 8usize); - assert!(offset_of!(RegularExpression, span) == 0usize); - assert!(offset_of!(RegularExpression, pattern) == 8usize); - assert!(offset_of!(RegularExpression, flags) == 56usize); - - assert!(size_of::() == 16usize); - assert!(align_of::() == 4usize); - assert!(offset_of!(Flags, span) == 0usize); - assert!(offset_of!(Flags, global) == 8usize); - assert!(offset_of!(Flags, ignore_case) == 9usize); - assert!(offset_of!(Flags, multiline) == 10usize); - assert!(offset_of!(Flags, unicode) == 11usize); - assert!(offset_of!(Flags, sticky) == 12usize); - assert!(offset_of!(Flags, dot_all) == 13usize); - assert!(offset_of!(Flags, has_indices) == 14usize); - assert!(offset_of!(Flags, unicode_sets) == 15usize); - assert!(size_of::() == 48usize); assert!(align_of::() == 8usize); assert!(offset_of!(Pattern, span) == 0usize); @@ -1445,7 +1444,7 @@ const _: () = { assert!(offset_of!(Alternative, span) == 0usize); assert!(offset_of!(Alternative, body) == 8usize); - assert!(size_of::() == 24usize); + assert!(size_of::() == 16usize); assert!(align_of::() == 8usize); assert!(size_of::() == 12usize); @@ -1465,7 +1464,7 @@ const _: () = { assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 64usize); + assert!(size_of::() == 56usize); assert!(align_of::() == 8usize); assert!(offset_of!(Quantifier, span) == 0usize); assert!(offset_of!(Quantifier, min) == 8usize); @@ -1513,7 +1512,7 @@ const _: () = { assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 24usize); + assert!(size_of::() == 16usize); assert!(align_of::() == 8usize); assert!(size_of::() == 40usize); @@ -1540,18 +1539,23 @@ const _: () = { assert!(offset_of!(CapturingGroup, name) == 8usize); assert!(offset_of!(CapturingGroup, body) == 24usize); - assert!(size_of::() == 56usize); + assert!(size_of::() == 64usize); assert!(align_of::() == 8usize); assert!(offset_of!(IgnoreGroup, span) == 0usize); - assert!(offset_of!(IgnoreGroup, enabling_modifiers) == 8usize); - assert!(offset_of!(IgnoreGroup, disabling_modifiers) == 11usize); - assert!(offset_of!(IgnoreGroup, body) == 16usize); + assert!(offset_of!(IgnoreGroup, modifiers) == 8usize); + assert!(offset_of!(IgnoreGroup, body) == 24usize); + + assert!(size_of::() == 16usize); + assert!(align_of::() == 4usize); + assert!(offset_of!(Modifiers, span) == 0usize); + assert!(offset_of!(Modifiers, enabling) == 8usize); + assert!(offset_of!(Modifiers, disabling) == 11usize); - assert!(size_of::() == 3usize); - assert!(align_of::() == 1usize); - assert!(offset_of!(ModifierFlags, ignore_case) == 0usize); - assert!(offset_of!(ModifierFlags, sticky) == 1usize); - assert!(offset_of!(ModifierFlags, multiline) == 2usize); + assert!(size_of::() == 3usize); + assert!(align_of::() == 1usize); + assert!(offset_of!(Modifier, ignore_case) == 0usize); + assert!(offset_of!(Modifier, multiline) == 1usize); + assert!(offset_of!(Modifier, sticky) == 2usize); assert!(size_of::() == 12usize); assert!(align_of::() == 4usize); @@ -1610,14 +1614,16 @@ const _: () = { assert!(offset_of!(StringLiteral, span) == 0usize); assert!(offset_of!(StringLiteral, value) == 8usize); - assert!(size_of::() == 64usize); + assert!(size_of::() == 88usize); assert!(align_of::() == 4usize); assert!(offset_of!(Program, span) == 0usize); assert!(offset_of!(Program, source_type) == 8usize); - assert!(offset_of!(Program, hashbang) == 12usize); - assert!(offset_of!(Program, directives) == 28usize); - assert!(offset_of!(Program, body) == 44usize); - assert!(offset_of!(Program, scope_id) == 60usize); + assert!(offset_of!(Program, source_text) == 12usize); + assert!(offset_of!(Program, comments) == 20usize); + assert!(offset_of!(Program, hashbang) == 36usize); + assert!(offset_of!(Program, directives) == 52usize); + assert!(offset_of!(Program, body) == 68usize); + assert!(offset_of!(Program, scope_id) == 84usize); assert!(size_of::() == 8usize); assert!(align_of::() == 4usize); @@ -2595,10 +2601,10 @@ const _: () = { assert!(size_of::() == 44usize); assert!(align_of::() == 4usize); assert!(offset_of!(TSCallSignatureDeclaration, span) == 0usize); - assert!(offset_of!(TSCallSignatureDeclaration, this_param) == 8usize); - assert!(offset_of!(TSCallSignatureDeclaration, params) == 32usize); - assert!(offset_of!(TSCallSignatureDeclaration, return_type) == 36usize); - assert!(offset_of!(TSCallSignatureDeclaration, type_parameters) == 40usize); + assert!(offset_of!(TSCallSignatureDeclaration, type_parameters) == 8usize); + assert!(offset_of!(TSCallSignatureDeclaration, this_param) == 12usize); + assert!(offset_of!(TSCallSignatureDeclaration, params) == 36usize); + assert!(offset_of!(TSCallSignatureDeclaration, return_type) == 40usize); assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); @@ -2610,18 +2616,18 @@ const _: () = { assert!(offset_of!(TSMethodSignature, computed) == 16usize); assert!(offset_of!(TSMethodSignature, optional) == 17usize); assert!(offset_of!(TSMethodSignature, kind) == 18usize); - assert!(offset_of!(TSMethodSignature, this_param) == 20usize); - assert!(offset_of!(TSMethodSignature, params) == 24usize); - assert!(offset_of!(TSMethodSignature, return_type) == 28usize); - assert!(offset_of!(TSMethodSignature, type_parameters) == 32usize); + assert!(offset_of!(TSMethodSignature, type_parameters) == 20usize); + assert!(offset_of!(TSMethodSignature, this_param) == 24usize); + assert!(offset_of!(TSMethodSignature, params) == 28usize); + assert!(offset_of!(TSMethodSignature, return_type) == 32usize); assert!(offset_of!(TSMethodSignature, scope_id) == 36usize); assert!(size_of::() == 24usize); assert!(align_of::() == 4usize); assert!(offset_of!(TSConstructSignatureDeclaration, span) == 0usize); - assert!(offset_of!(TSConstructSignatureDeclaration, params) == 8usize); - assert!(offset_of!(TSConstructSignatureDeclaration, return_type) == 12usize); - assert!(offset_of!(TSConstructSignatureDeclaration, type_parameters) == 16usize); + assert!(offset_of!(TSConstructSignatureDeclaration, type_parameters) == 8usize); + assert!(offset_of!(TSConstructSignatureDeclaration, params) == 12usize); + assert!(offset_of!(TSConstructSignatureDeclaration, return_type) == 16usize); assert!(offset_of!(TSConstructSignatureDeclaration, scope_id) == 20usize); assert!(size_of::() == 20usize); @@ -2646,19 +2652,19 @@ const _: () = { assert!(size_of::() == 12usize); assert!(align_of::() == 4usize); - assert!(size_of::() == 44usize); + assert!(size_of::() == 48usize); assert!(align_of::() == 4usize); assert!(offset_of!(TSModuleDeclaration, span) == 0usize); assert!(offset_of!(TSModuleDeclaration, id) == 8usize); - assert!(offset_of!(TSModuleDeclaration, body) == 28usize); - assert!(offset_of!(TSModuleDeclaration, kind) == 36usize); - assert!(offset_of!(TSModuleDeclaration, declare) == 37usize); - assert!(offset_of!(TSModuleDeclaration, scope_id) == 40usize); + assert!(offset_of!(TSModuleDeclaration, body) == 32usize); + assert!(offset_of!(TSModuleDeclaration, kind) == 40usize); + assert!(offset_of!(TSModuleDeclaration, declare) == 41usize); + assert!(offset_of!(TSModuleDeclaration, scope_id) == 44usize); assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 20usize); + assert!(size_of::() == 24usize); assert!(align_of::() == 4usize); assert!(size_of::() == 8usize); @@ -2716,18 +2722,18 @@ const _: () = { assert!(size_of::() == 24usize); assert!(align_of::() == 4usize); assert!(offset_of!(TSFunctionType, span) == 0usize); - assert!(offset_of!(TSFunctionType, this_param) == 8usize); - assert!(offset_of!(TSFunctionType, params) == 12usize); - assert!(offset_of!(TSFunctionType, return_type) == 16usize); - assert!(offset_of!(TSFunctionType, type_parameters) == 20usize); + assert!(offset_of!(TSFunctionType, type_parameters) == 8usize); + assert!(offset_of!(TSFunctionType, this_param) == 12usize); + assert!(offset_of!(TSFunctionType, params) == 16usize); + assert!(offset_of!(TSFunctionType, return_type) == 20usize); assert!(size_of::() == 24usize); assert!(align_of::() == 4usize); assert!(offset_of!(TSConstructorType, span) == 0usize); assert!(offset_of!(TSConstructorType, r#abstract) == 8usize); - assert!(offset_of!(TSConstructorType, params) == 12usize); - assert!(offset_of!(TSConstructorType, return_type) == 16usize); - assert!(offset_of!(TSConstructorType, type_parameters) == 20usize); + assert!(offset_of!(TSConstructorType, type_parameters) == 12usize); + assert!(offset_of!(TSConstructorType, params) == 16usize); + assert!(offset_of!(TSConstructorType, return_type) == 20usize); assert!(size_of::() == 36usize); assert!(align_of::() == 4usize); @@ -2929,6 +2935,21 @@ const _: () = { assert!(offset_of!(JSXText, span) == 0usize); assert!(offset_of!(JSXText, value) == 8usize); + assert!(size_of::() == 1usize); + assert!(align_of::() == 1usize); + + assert!(size_of::() == 1usize); + assert!(align_of::() == 1usize); + + assert!(size_of::() == 20usize); + assert!(align_of::() == 4usize); + assert!(offset_of!(Comment, span) == 0usize); + assert!(offset_of!(Comment, kind) == 8usize); + assert!(offset_of!(Comment, position) == 9usize); + assert!(offset_of!(Comment, attached_to) == 12usize); + assert!(offset_of!(Comment, preceded_by_newline) == 16usize); + assert!(offset_of!(Comment, followed_by_newline) == 17usize); + assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); @@ -2967,24 +2988,6 @@ const _: () = { assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 56usize); - assert!(align_of::() == 4usize); - assert!(offset_of!(RegularExpression, span) == 0usize); - assert!(offset_of!(RegularExpression, pattern) == 8usize); - assert!(offset_of!(RegularExpression, flags) == 40usize); - - assert!(size_of::() == 16usize); - assert!(align_of::() == 4usize); - assert!(offset_of!(Flags, span) == 0usize); - assert!(offset_of!(Flags, global) == 8usize); - assert!(offset_of!(Flags, ignore_case) == 9usize); - assert!(offset_of!(Flags, multiline) == 10usize); - assert!(offset_of!(Flags, unicode) == 11usize); - assert!(offset_of!(Flags, sticky) == 12usize); - assert!(offset_of!(Flags, dot_all) == 13usize); - assert!(offset_of!(Flags, has_indices) == 14usize); - assert!(offset_of!(Flags, unicode_sets) == 15usize); - assert!(size_of::() == 32usize); assert!(align_of::() == 4usize); assert!(offset_of!(Pattern, span) == 0usize); @@ -3000,7 +3003,7 @@ const _: () = { assert!(offset_of!(Alternative, span) == 0usize); assert!(offset_of!(Alternative, body) == 8usize); - assert!(size_of::() == 20usize); + assert!(size_of::() == 12usize); assert!(align_of::() == 4usize); assert!(size_of::() == 12usize); @@ -3020,7 +3023,7 @@ const _: () = { assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 56usize); + assert!(size_of::() == 48usize); assert!(align_of::() == 8usize); assert!(offset_of!(Quantifier, span) == 0usize); assert!(offset_of!(Quantifier, min) == 8usize); @@ -3068,7 +3071,7 @@ const _: () = { assert!(size_of::() == 1usize); assert!(align_of::() == 1usize); - assert!(size_of::() == 20usize); + assert!(size_of::() == 8usize); assert!(align_of::() == 4usize); assert!(size_of::() == 40usize); @@ -3095,18 +3098,23 @@ const _: () = { assert!(offset_of!(CapturingGroup, name) == 8usize); assert!(offset_of!(CapturingGroup, body) == 16usize); - assert!(size_of::() == 40usize); + assert!(size_of::() == 48usize); assert!(align_of::() == 4usize); assert!(offset_of!(IgnoreGroup, span) == 0usize); - assert!(offset_of!(IgnoreGroup, enabling_modifiers) == 8usize); - assert!(offset_of!(IgnoreGroup, disabling_modifiers) == 11usize); - assert!(offset_of!(IgnoreGroup, body) == 16usize); - - assert!(size_of::() == 3usize); - assert!(align_of::() == 1usize); - assert!(offset_of!(ModifierFlags, ignore_case) == 0usize); - assert!(offset_of!(ModifierFlags, sticky) == 1usize); - assert!(offset_of!(ModifierFlags, multiline) == 2usize); + assert!(offset_of!(IgnoreGroup, modifiers) == 8usize); + assert!(offset_of!(IgnoreGroup, body) == 24usize); + + assert!(size_of::() == 16usize); + assert!(align_of::() == 4usize); + assert!(offset_of!(Modifiers, span) == 0usize); + assert!(offset_of!(Modifiers, enabling) == 8usize); + assert!(offset_of!(Modifiers, disabling) == 11usize); + + assert!(size_of::() == 3usize); + assert!(align_of::() == 1usize); + assert!(offset_of!(Modifier, ignore_case) == 0usize); + assert!(offset_of!(Modifier, multiline) == 1usize); + assert!(offset_of!(Modifier, sticky) == 2usize); assert!(size_of::() == 12usize); assert!(align_of::() == 4usize); diff --git a/crates/oxc_ast/src/generated/ast_builder.rs b/crates/oxc_ast/src/generated/ast_builder.rs index 41f53072c2cc6..334cb53823b6a 100644 --- a/crates/oxc_ast/src/generated/ast_builder.rs +++ b/crates/oxc_ast/src/generated/ast_builder.rs @@ -221,19 +221,35 @@ impl<'a> AstBuilder<'a> { /// ## Parameters /// - span: The [`Span`] covering this node /// - source_type + /// - source_text + /// - comments: Sorted comments /// - hashbang /// - directives /// - body #[inline] - pub fn program( + pub fn program( self, span: Span, source_type: SourceType, + source_text: S, + comments: Vec<'a, Comment>, hashbang: Option>, directives: Vec<'a, Directive<'a>>, body: Vec<'a, Statement<'a>>, - ) -> Program<'a> { - Program { span, source_type, hashbang, directives, body, scope_id: Default::default() } + ) -> Program<'a> + where + S: IntoIn<'a, &'a str>, + { + Program { + span, + source_type, + source_text: source_text.into_in(self.allocator), + comments, + hashbang, + directives, + body, + scope_id: Default::default(), + } } /// Builds a [`Program`] and stores it in the memory arena. @@ -243,19 +259,29 @@ impl<'a> AstBuilder<'a> { /// ## Parameters /// - span: The [`Span`] covering this node /// - source_type + /// - source_text + /// - comments: Sorted comments /// - hashbang /// - directives /// - body #[inline] - pub fn alloc_program( + pub fn alloc_program( self, span: Span, source_type: SourceType, + source_text: S, + comments: Vec<'a, Comment>, hashbang: Option>, directives: Vec<'a, Directive<'a>>, body: Vec<'a, Statement<'a>>, - ) -> Box<'a, Program<'a>> { - Box::new_in(self.program(span, source_type, hashbang, directives, body), self.allocator) + ) -> Box<'a, Program<'a>> + where + S: IntoIn<'a, &'a str>, + { + Box::new_in( + self.program(span, source_type, source_text, comments, hashbang, directives, body), + self.allocator, + ) } /// Build a [`Expression::BooleanLiteral`] @@ -8755,29 +8781,29 @@ impl<'a> AstBuilder<'a> { /// ## Parameters /// - span: The [`Span`] covering this node /// - r#abstract + /// - type_parameters /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_type_constructor_type( self, span: Span, r#abstract: bool, - params: T1, - return_type: T2, - type_parameters: T3, + type_parameters: T1, + params: T2, + return_type: T3, ) -> TSType<'a> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, { TSType::TSConstructorType(self.alloc(self.ts_constructor_type( span, r#abstract, + type_parameters, params, return_type, - type_parameters, ))) } @@ -8796,31 +8822,31 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters: Generic type parameters /// - this_param: `this` parameter /// - params: Function parameters. Akin to [`Function::params`]. /// - return_type: Return type of the function. - /// - type_parameters: Generic type parameters #[inline] pub fn ts_type_function_type( self, span: Span, - this_param: T1, - params: T2, - return_type: T3, - type_parameters: T4, + type_parameters: T1, + this_param: T2, + params: T3, + return_type: T4, ) -> TSType<'a> where - T1: IntoIn<'a, Option>>>, - T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T3: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, - T4: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Option>>>, + T3: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T4: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, { TSType::TSFunctionType(self.alloc(self.ts_function_type( span, + type_parameters, this_param, params, return_type, - type_parameters, ))) } @@ -10773,30 +10799,30 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters /// - this_param /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_signature_call_signature_declaration( self, span: Span, + type_parameters: T1, this_param: Option>, - params: T1, - return_type: T2, - type_parameters: T3, + params: T2, + return_type: T3, ) -> TSSignature<'a> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Option>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Option>>>, { TSSignature::TSCallSignatureDeclaration(self.alloc(self.ts_call_signature_declaration( span, + type_parameters, this_param, params, return_type, - type_parameters, ))) } @@ -10815,24 +10841,24 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_signature_construct_signature_declaration( self, span: Span, - params: T1, - return_type: T2, - type_parameters: T3, + type_parameters: T1, + params: T2, + return_type: T3, ) -> TSSignature<'a> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Option>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Option>>>, { TSSignature::TSConstructSignatureDeclaration(self.alloc( - self.ts_construct_signature_declaration(span, params, return_type, type_parameters), + self.ts_construct_signature_declaration(span, type_parameters, params, return_type), )) } @@ -10858,10 +10884,10 @@ impl<'a> AstBuilder<'a> { /// - computed /// - optional /// - kind + /// - type_parameters /// - this_param /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_signature_method_signature( self, @@ -10870,16 +10896,16 @@ impl<'a> AstBuilder<'a> { computed: bool, optional: bool, kind: TSMethodSignatureKind, - this_param: T1, - params: T2, - return_type: T3, - type_parameters: T4, + type_parameters: T1, + this_param: T2, + params: T3, + return_type: T4, ) -> TSSignature<'a> where - T1: IntoIn<'a, Option>>>, - T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T3: IntoIn<'a, Option>>>, - T4: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Option>>>, + T3: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T4: IntoIn<'a, Option>>>, { TSSignature::TSMethodSignature(self.alloc(self.ts_method_signature( span, @@ -10887,10 +10913,10 @@ impl<'a> AstBuilder<'a> { computed, optional, kind, + type_parameters, this_param, params, return_type, - type_parameters, ))) } @@ -10963,30 +10989,30 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters /// - this_param /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_call_signature_declaration( self, span: Span, + type_parameters: T1, this_param: Option>, - params: T1, - return_type: T2, - type_parameters: T3, + params: T2, + return_type: T3, ) -> TSCallSignatureDeclaration<'a> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Option>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Option>>>, { TSCallSignatureDeclaration { span, + type_parameters: type_parameters.into_in(self.allocator), this_param, params: params.into_in(self.allocator), return_type: return_type.into_in(self.allocator), - type_parameters: type_parameters.into_in(self.allocator), } } @@ -10996,31 +11022,31 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters /// - this_param /// - params /// - return_type - /// - type_parameters #[inline] pub fn alloc_ts_call_signature_declaration( self, span: Span, + type_parameters: T1, this_param: Option>, - params: T1, - return_type: T2, - type_parameters: T3, + params: T2, + return_type: T3, ) -> Box<'a, TSCallSignatureDeclaration<'a>> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Option>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Option>>>, { Box::new_in( self.ts_call_signature_declaration( span, + type_parameters, this_param, params, return_type, - type_parameters, ), self.allocator, ) @@ -11036,10 +11062,10 @@ impl<'a> AstBuilder<'a> { /// - computed /// - optional /// - kind + /// - type_parameters /// - this_param /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_method_signature( self, @@ -11048,16 +11074,16 @@ impl<'a> AstBuilder<'a> { computed: bool, optional: bool, kind: TSMethodSignatureKind, - this_param: T1, - params: T2, - return_type: T3, - type_parameters: T4, + type_parameters: T1, + this_param: T2, + params: T3, + return_type: T4, ) -> TSMethodSignature<'a> where - T1: IntoIn<'a, Option>>>, - T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T3: IntoIn<'a, Option>>>, - T4: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Option>>>, + T3: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T4: IntoIn<'a, Option>>>, { TSMethodSignature { span, @@ -11065,10 +11091,10 @@ impl<'a> AstBuilder<'a> { computed, optional, kind, + type_parameters: type_parameters.into_in(self.allocator), this_param: this_param.into_in(self.allocator), params: params.into_in(self.allocator), return_type: return_type.into_in(self.allocator), - type_parameters: type_parameters.into_in(self.allocator), scope_id: Default::default(), } } @@ -11083,10 +11109,10 @@ impl<'a> AstBuilder<'a> { /// - computed /// - optional /// - kind + /// - type_parameters /// - this_param /// - params /// - return_type - /// - type_parameters #[inline] pub fn alloc_ts_method_signature( self, @@ -11095,16 +11121,16 @@ impl<'a> AstBuilder<'a> { computed: bool, optional: bool, kind: TSMethodSignatureKind, - this_param: T1, - params: T2, - return_type: T3, - type_parameters: T4, + type_parameters: T1, + this_param: T2, + params: T3, + return_type: T4, ) -> Box<'a, TSMethodSignature<'a>> where - T1: IntoIn<'a, Option>>>, - T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T3: IntoIn<'a, Option>>>, - T4: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Option>>>, + T3: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T4: IntoIn<'a, Option>>>, { Box::new_in( self.ts_method_signature( @@ -11113,10 +11139,10 @@ impl<'a> AstBuilder<'a> { computed, optional, kind, + type_parameters, this_param, params, return_type, - type_parameters, ), self.allocator, ) @@ -11128,27 +11154,27 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_construct_signature_declaration( self, span: Span, - params: T1, - return_type: T2, - type_parameters: T3, + type_parameters: T1, + params: T2, + return_type: T3, ) -> TSConstructSignatureDeclaration<'a> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Option>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Option>>>, { TSConstructSignatureDeclaration { span, + type_parameters: type_parameters.into_in(self.allocator), params: params.into_in(self.allocator), return_type: return_type.into_in(self.allocator), - type_parameters: type_parameters.into_in(self.allocator), scope_id: Default::default(), } } @@ -11159,24 +11185,24 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters /// - params /// - return_type - /// - type_parameters #[inline] pub fn alloc_ts_construct_signature_declaration( self, span: Span, - params: T1, - return_type: T2, - type_parameters: T3, + type_parameters: T1, + params: T2, + return_type: T3, ) -> Box<'a, TSConstructSignatureDeclaration<'a>> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Option>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Option>>>, { Box::new_in( - self.ts_construct_signature_declaration(span, params, return_type, type_parameters), + self.ts_construct_signature_declaration(span, type_parameters, params, return_type), self.allocator, ) } @@ -11423,9 +11449,9 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node - /// - name + /// - name: The identifier name being bound. #[inline] - pub fn ts_module_declaration_name_identifier_name( + pub fn ts_module_declaration_name_binding_identifier( self, span: Span, name: A, @@ -11433,17 +11459,17 @@ impl<'a> AstBuilder<'a> { where A: IntoIn<'a, Atom<'a>>, { - TSModuleDeclarationName::Identifier(self.identifier_name(span, name)) + TSModuleDeclarationName::Identifier(self.binding_identifier(span, name)) } - /// Convert a [`IdentifierName`] into a [`TSModuleDeclarationName::Identifier`] + /// Convert a [`BindingIdentifier`] into a [`TSModuleDeclarationName::Identifier`] #[inline] - pub fn ts_module_declaration_name_from_identifier_name( + pub fn ts_module_declaration_name_from_binding_identifier( self, inner: T, ) -> TSModuleDeclarationName<'a> where - T: IntoIn<'a, IdentifierName<'a>>, + T: IntoIn<'a, BindingIdentifier<'a>>, { TSModuleDeclarationName::Identifier(inner.into_in(self.allocator)) } @@ -11946,31 +11972,31 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters: Generic type parameters /// - this_param: `this` parameter /// - params: Function parameters. Akin to [`Function::params`]. /// - return_type: Return type of the function. - /// - type_parameters: Generic type parameters #[inline] pub fn ts_function_type( self, span: Span, - this_param: T1, - params: T2, - return_type: T3, - type_parameters: T4, + type_parameters: T1, + this_param: T2, + params: T3, + return_type: T4, ) -> TSFunctionType<'a> where - T1: IntoIn<'a, Option>>>, - T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T3: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, - T4: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Option>>>, + T3: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T4: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, { TSFunctionType { span, + type_parameters: type_parameters.into_in(self.allocator), this_param: this_param.into_in(self.allocator), params: params.into_in(self.allocator), return_type: return_type.into_in(self.allocator), - type_parameters: type_parameters.into_in(self.allocator), } } @@ -11980,27 +12006,27 @@ impl<'a> AstBuilder<'a> { /// /// ## Parameters /// - span: The [`Span`] covering this node + /// - type_parameters: Generic type parameters /// - this_param: `this` parameter /// - params: Function parameters. Akin to [`Function::params`]. /// - return_type: Return type of the function. - /// - type_parameters: Generic type parameters #[inline] pub fn alloc_ts_function_type( self, span: Span, - this_param: T1, - params: T2, - return_type: T3, - type_parameters: T4, + type_parameters: T1, + this_param: T2, + params: T3, + return_type: T4, ) -> Box<'a, TSFunctionType<'a>> where - T1: IntoIn<'a, Option>>>, - T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T3: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, - T4: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Option>>>, + T3: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T4: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, { Box::new_in( - self.ts_function_type(span, this_param, params, return_type, type_parameters), + self.ts_function_type(span, type_parameters, this_param, params, return_type), self.allocator, ) } @@ -12012,29 +12038,29 @@ impl<'a> AstBuilder<'a> { /// ## Parameters /// - span: The [`Span`] covering this node /// - r#abstract + /// - type_parameters /// - params /// - return_type - /// - type_parameters #[inline] pub fn ts_constructor_type( self, span: Span, r#abstract: bool, - params: T1, - return_type: T2, - type_parameters: T3, + type_parameters: T1, + params: T2, + return_type: T3, ) -> TSConstructorType<'a> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, { TSConstructorType { span, r#abstract, + type_parameters: type_parameters.into_in(self.allocator), params: params.into_in(self.allocator), return_type: return_type.into_in(self.allocator), - type_parameters: type_parameters.into_in(self.allocator), } } @@ -12045,25 +12071,25 @@ impl<'a> AstBuilder<'a> { /// ## Parameters /// - span: The [`Span`] covering this node /// - r#abstract + /// - type_parameters /// - params /// - return_type - /// - type_parameters #[inline] pub fn alloc_ts_constructor_type( self, span: Span, r#abstract: bool, - params: T1, - return_type: T2, - type_parameters: T3, + type_parameters: T1, + params: T2, + return_type: T3, ) -> Box<'a, TSConstructorType<'a>> where - T1: IntoIn<'a, Box<'a, FormalParameters<'a>>>, - T2: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, - T3: IntoIn<'a, Option>>>, + T1: IntoIn<'a, Option>>>, + T2: IntoIn<'a, Box<'a, FormalParameters<'a>>>, + T3: IntoIn<'a, Box<'a, TSTypeAnnotation<'a>>>, { Box::new_in( - self.ts_constructor_type(span, r#abstract, params, return_type, type_parameters), + self.ts_constructor_type(span, r#abstract, type_parameters, params, return_type), self.allocator, ) } diff --git a/crates/oxc_ast/src/generated/derive_clone_in.rs b/crates/oxc_ast/src/generated/derive_clone_in.rs index 798007b74b4ff..e60a6643d3b87 100644 --- a/crates/oxc_ast/src/generated/derive_clone_in.rs +++ b/crates/oxc_ast/src/generated/derive_clone_in.rs @@ -5,6 +5,9 @@ use oxc_allocator::{Allocator, CloneIn}; +#[allow(clippy::wildcard_imports)] +use crate::ast::comment::*; + #[allow(clippy::wildcard_imports)] use crate::ast::js::*; @@ -112,6 +115,8 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for Program<'old_alloc> { Program { span: CloneIn::clone_in(&self.span, allocator), source_type: CloneIn::clone_in(&self.source_type, allocator), + source_text: CloneIn::clone_in(&self.source_text, allocator), + comments: CloneIn::clone_in(&self.comments, allocator), hashbang: CloneIn::clone_in(&self.hashbang, allocator), directives: CloneIn::clone_in(&self.directives, allocator), body: CloneIn::clone_in(&self.body, allocator), @@ -3378,10 +3383,10 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for TSCallSignatureDeclaration< fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned { TSCallSignatureDeclaration { span: CloneIn::clone_in(&self.span, allocator), + type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), this_param: CloneIn::clone_in(&self.this_param, allocator), params: CloneIn::clone_in(&self.params, allocator), return_type: CloneIn::clone_in(&self.return_type, allocator), - type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), } } } @@ -3406,10 +3411,10 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for TSMethodSignature<'old_allo computed: CloneIn::clone_in(&self.computed, allocator), optional: CloneIn::clone_in(&self.optional, allocator), kind: CloneIn::clone_in(&self.kind, allocator), + type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), this_param: CloneIn::clone_in(&self.this_param, allocator), params: CloneIn::clone_in(&self.params, allocator), return_type: CloneIn::clone_in(&self.return_type, allocator), - type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), scope_id: Default::default(), } } @@ -3420,9 +3425,9 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for TSConstructSignatureDeclara fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned { TSConstructSignatureDeclaration { span: CloneIn::clone_in(&self.span, allocator), + type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), params: CloneIn::clone_in(&self.params, allocator), return_type: CloneIn::clone_in(&self.return_type, allocator), - type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), scope_id: Default::default(), } } @@ -3641,10 +3646,10 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for TSFunctionType<'old_alloc> fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned { TSFunctionType { span: CloneIn::clone_in(&self.span, allocator), + type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), this_param: CloneIn::clone_in(&self.this_param, allocator), params: CloneIn::clone_in(&self.params, allocator), return_type: CloneIn::clone_in(&self.return_type, allocator), - type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), } } } @@ -3655,9 +3660,9 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for TSConstructorType<'old_allo TSConstructorType { span: CloneIn::clone_in(&self.span, allocator), r#abstract: CloneIn::clone_in(&self.r#abstract, allocator), + type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), params: CloneIn::clone_in(&self.params, allocator), return_type: CloneIn::clone_in(&self.return_type, allocator), - type_parameters: CloneIn::clone_in(&self.type_parameters, allocator), } } } @@ -4230,3 +4235,37 @@ impl<'old_alloc, 'new_alloc> CloneIn<'new_alloc> for JSXText<'old_alloc> { } } } + +impl<'alloc> CloneIn<'alloc> for CommentKind { + type Cloned = CommentKind; + fn clone_in(&self, _: &'alloc Allocator) -> Self::Cloned { + match self { + Self::Line => CommentKind::Line, + Self::Block => CommentKind::Block, + } + } +} + +impl<'alloc> CloneIn<'alloc> for CommentPosition { + type Cloned = CommentPosition; + fn clone_in(&self, _: &'alloc Allocator) -> Self::Cloned { + match self { + Self::Leading => CommentPosition::Leading, + Self::Trailing => CommentPosition::Trailing, + } + } +} + +impl<'alloc> CloneIn<'alloc> for Comment { + type Cloned = Comment; + fn clone_in(&self, allocator: &'alloc Allocator) -> Self::Cloned { + Comment { + span: CloneIn::clone_in(&self.span, allocator), + kind: CloneIn::clone_in(&self.kind, allocator), + position: CloneIn::clone_in(&self.position, allocator), + attached_to: CloneIn::clone_in(&self.attached_to, allocator), + preceded_by_newline: CloneIn::clone_in(&self.preceded_by_newline, allocator), + followed_by_newline: CloneIn::clone_in(&self.followed_by_newline, allocator), + } + } +} diff --git a/crates/oxc_ast/src/generated/derive_content_eq.rs b/crates/oxc_ast/src/generated/derive_content_eq.rs index 783c23b7c7e79..2ea7edb1e6cdb 100644 --- a/crates/oxc_ast/src/generated/derive_content_eq.rs +++ b/crates/oxc_ast/src/generated/derive_content_eq.rs @@ -5,6 +5,9 @@ use oxc_span::cmp::ContentEq; +#[allow(clippy::wildcard_imports)] +use crate::ast::comment::*; + #[allow(clippy::wildcard_imports)] use crate::ast::js::*; @@ -92,6 +95,8 @@ impl<'a> ContentEq for StringLiteral<'a> { impl<'a> ContentEq for Program<'a> { fn content_eq(&self, other: &Self) -> bool { ContentEq::content_eq(&self.source_type, &other.source_type) + && ContentEq::content_eq(&self.source_text, &other.source_text) + && ContentEq::content_eq(&self.comments, &other.comments) && ContentEq::content_eq(&self.hashbang, &other.hashbang) && ContentEq::content_eq(&self.directives, &other.directives) && ContentEq::content_eq(&self.body, &other.body) @@ -3455,10 +3460,10 @@ impl<'a> ContentEq for TSIndexSignature<'a> { impl<'a> ContentEq for TSCallSignatureDeclaration<'a> { fn content_eq(&self, other: &Self) -> bool { - ContentEq::content_eq(&self.this_param, &other.this_param) + ContentEq::content_eq(&self.type_parameters, &other.type_parameters) + && ContentEq::content_eq(&self.this_param, &other.this_param) && ContentEq::content_eq(&self.params, &other.params) && ContentEq::content_eq(&self.return_type, &other.return_type) - && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) } } @@ -3474,18 +3479,18 @@ impl<'a> ContentEq for TSMethodSignature<'a> { && ContentEq::content_eq(&self.computed, &other.computed) && ContentEq::content_eq(&self.optional, &other.optional) && ContentEq::content_eq(&self.kind, &other.kind) + && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) && ContentEq::content_eq(&self.this_param, &other.this_param) && ContentEq::content_eq(&self.params, &other.params) && ContentEq::content_eq(&self.return_type, &other.return_type) - && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) } } impl<'a> ContentEq for TSConstructSignatureDeclaration<'a> { fn content_eq(&self, other: &Self) -> bool { - ContentEq::content_eq(&self.params, &other.params) + ContentEq::content_eq(&self.type_parameters, &other.type_parameters) + && ContentEq::content_eq(&self.params, &other.params) && ContentEq::content_eq(&self.return_type, &other.return_type) - && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) } } @@ -3657,19 +3662,19 @@ impl<'a> ContentEq for TSImportAttributeName<'a> { impl<'a> ContentEq for TSFunctionType<'a> { fn content_eq(&self, other: &Self) -> bool { - ContentEq::content_eq(&self.this_param, &other.this_param) + ContentEq::content_eq(&self.type_parameters, &other.type_parameters) + && ContentEq::content_eq(&self.this_param, &other.this_param) && ContentEq::content_eq(&self.params, &other.params) && ContentEq::content_eq(&self.return_type, &other.return_type) - && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) } } impl<'a> ContentEq for TSConstructorType<'a> { fn content_eq(&self, other: &Self) -> bool { ContentEq::content_eq(&self.r#abstract, &other.r#abstract) + && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) && ContentEq::content_eq(&self.params, &other.params) && ContentEq::content_eq(&self.return_type, &other.return_type) - && ContentEq::content_eq(&self.type_parameters, &other.type_parameters) } } @@ -4211,3 +4216,25 @@ impl<'a> ContentEq for JSXText<'a> { ContentEq::content_eq(&self.value, &other.value) } } + +impl ContentEq for CommentKind { + fn content_eq(&self, other: &Self) -> bool { + self == other + } +} + +impl ContentEq for CommentPosition { + fn content_eq(&self, other: &Self) -> bool { + self == other + } +} + +impl ContentEq for Comment { + fn content_eq(&self, other: &Self) -> bool { + ContentEq::content_eq(&self.kind, &other.kind) + && ContentEq::content_eq(&self.position, &other.position) + && ContentEq::content_eq(&self.attached_to, &other.attached_to) + && ContentEq::content_eq(&self.preceded_by_newline, &other.preceded_by_newline) + && ContentEq::content_eq(&self.followed_by_newline, &other.followed_by_newline) + } +} diff --git a/crates/oxc_ast/src/generated/derive_content_hash.rs b/crates/oxc_ast/src/generated/derive_content_hash.rs index dfe5280b67d5f..e7f5d3196c4de 100644 --- a/crates/oxc_ast/src/generated/derive_content_hash.rs +++ b/crates/oxc_ast/src/generated/derive_content_hash.rs @@ -7,6 +7,9 @@ use std::{hash::Hasher, mem::discriminant}; use oxc_span::hash::ContentHash; +#[allow(clippy::wildcard_imports)] +use crate::ast::comment::*; + #[allow(clippy::wildcard_imports)] use crate::ast::js::*; @@ -70,6 +73,8 @@ impl<'a> ContentHash for StringLiteral<'a> { impl<'a> ContentHash for Program<'a> { fn content_hash(&self, state: &mut H) { ContentHash::content_hash(&self.source_type, state); + ContentHash::content_hash(&self.source_text, state); + ContentHash::content_hash(&self.comments, state); ContentHash::content_hash(&self.hashbang, state); ContentHash::content_hash(&self.directives, state); ContentHash::content_hash(&self.body, state); @@ -1846,10 +1851,10 @@ impl<'a> ContentHash for TSIndexSignature<'a> { impl<'a> ContentHash for TSCallSignatureDeclaration<'a> { fn content_hash(&self, state: &mut H) { + ContentHash::content_hash(&self.type_parameters, state); ContentHash::content_hash(&self.this_param, state); ContentHash::content_hash(&self.params, state); ContentHash::content_hash(&self.return_type, state); - ContentHash::content_hash(&self.type_parameters, state); } } @@ -1865,18 +1870,18 @@ impl<'a> ContentHash for TSMethodSignature<'a> { ContentHash::content_hash(&self.computed, state); ContentHash::content_hash(&self.optional, state); ContentHash::content_hash(&self.kind, state); + ContentHash::content_hash(&self.type_parameters, state); ContentHash::content_hash(&self.this_param, state); ContentHash::content_hash(&self.params, state); ContentHash::content_hash(&self.return_type, state); - ContentHash::content_hash(&self.type_parameters, state); } } impl<'a> ContentHash for TSConstructSignatureDeclaration<'a> { fn content_hash(&self, state: &mut H) { + ContentHash::content_hash(&self.type_parameters, state); ContentHash::content_hash(&self.params, state); ContentHash::content_hash(&self.return_type, state); - ContentHash::content_hash(&self.type_parameters, state); } } @@ -2020,19 +2025,19 @@ impl<'a> ContentHash for TSImportAttributeName<'a> { impl<'a> ContentHash for TSFunctionType<'a> { fn content_hash(&self, state: &mut H) { + ContentHash::content_hash(&self.type_parameters, state); ContentHash::content_hash(&self.this_param, state); ContentHash::content_hash(&self.params, state); ContentHash::content_hash(&self.return_type, state); - ContentHash::content_hash(&self.type_parameters, state); } } impl<'a> ContentHash for TSConstructorType<'a> { fn content_hash(&self, state: &mut H) { ContentHash::content_hash(&self.r#abstract, state); + ContentHash::content_hash(&self.type_parameters, state); ContentHash::content_hash(&self.params, state); ContentHash::content_hash(&self.return_type, state); - ContentHash::content_hash(&self.type_parameters, state); } } @@ -2373,3 +2378,25 @@ impl<'a> ContentHash for JSXText<'a> { ContentHash::content_hash(&self.value, state); } } + +impl ContentHash for CommentKind { + fn content_hash(&self, state: &mut H) { + ContentHash::content_hash(&discriminant(self), state); + } +} + +impl ContentHash for CommentPosition { + fn content_hash(&self, state: &mut H) { + ContentHash::content_hash(&discriminant(self), state); + } +} + +impl ContentHash for Comment { + fn content_hash(&self, state: &mut H) { + ContentHash::content_hash(&self.kind, state); + ContentHash::content_hash(&self.position, state); + ContentHash::content_hash(&self.attached_to, state); + ContentHash::content_hash(&self.preceded_by_newline, state); + ContentHash::content_hash(&self.followed_by_newline, state); + } +} diff --git a/crates/oxc_ast/src/generated/visit.rs b/crates/oxc_ast/src/generated/visit.rs index 274c2765f238c..0f973a526d429 100644 --- a/crates/oxc_ast/src/generated/visit.rs +++ b/crates/oxc_ast/src/generated/visit.rs @@ -1913,23 +1913,23 @@ pub mod walk { pub fn walk_ts_conditional_type<'a, V: Visit<'a>>(visitor: &mut V, it: &TSConditionalType<'a>) { let kind = AstKind::TSConditionalType(visitor.alloc(it)); visitor.enter_node(kind); - visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); visitor.visit_ts_type(&it.check_type); + visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); visitor.visit_ts_type(&it.extends_type); visitor.visit_ts_type(&it.true_type); - visitor.visit_ts_type(&it.false_type); visitor.leave_scope(); + visitor.visit_ts_type(&it.false_type); visitor.leave_node(kind); } #[inline] pub fn walk_ts_constructor_type<'a, V: Visit<'a>>(visitor: &mut V, it: &TSConstructorType<'a>) { // NOTE: AstKind doesn't exists! - visitor.visit_formal_parameters(&it.params); - visitor.visit_ts_type_annotation(&it.return_type); if let Some(type_parameters) = &it.type_parameters { visitor.visit_ts_type_parameter_declaration(type_parameters); } + visitor.visit_formal_parameters(&it.params); + visitor.visit_ts_type_annotation(&it.return_type); } #[inline] @@ -2090,14 +2090,14 @@ pub mod walk { #[inline] pub fn walk_ts_function_type<'a, V: Visit<'a>>(visitor: &mut V, it: &TSFunctionType<'a>) { // NOTE: AstKind doesn't exists! + if let Some(type_parameters) = &it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } if let Some(this_param) = &it.this_param { visitor.visit_ts_this_parameter(this_param); } visitor.visit_formal_parameters(&it.params); visitor.visit_ts_type_annotation(&it.return_type); - if let Some(type_parameters) = &it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } } #[inline] @@ -2423,6 +2423,9 @@ pub mod walk { it: &TSCallSignatureDeclaration<'a>, ) { // NOTE: AstKind doesn't exists! + if let Some(type_parameters) = &it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } if let Some(this_param) = &it.this_param { visitor.visit_ts_this_parameter(this_param); } @@ -2430,9 +2433,6 @@ pub mod walk { if let Some(return_type) = &it.return_type { visitor.visit_ts_type_annotation(return_type); } - if let Some(type_parameters) = &it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } } #[inline] @@ -2443,13 +2443,13 @@ pub mod walk { let kind = AstKind::TSConstructSignatureDeclaration(visitor.alloc(it)); visitor.enter_node(kind); visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); + if let Some(type_parameters) = &it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } visitor.visit_formal_parameters(&it.params); if let Some(return_type) = &it.return_type { visitor.visit_ts_type_annotation(return_type); } - if let Some(type_parameters) = &it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } visitor.leave_scope(); visitor.leave_node(kind); } @@ -2460,6 +2460,9 @@ pub mod walk { visitor.enter_node(kind); visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); visitor.visit_property_key(&it.key); + if let Some(type_parameters) = &it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } if let Some(this_param) = &it.this_param { visitor.visit_ts_this_parameter(this_param); } @@ -2467,9 +2470,6 @@ pub mod walk { if let Some(return_type) = &it.return_type { visitor.visit_ts_type_annotation(return_type); } - if let Some(type_parameters) = &it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } visitor.leave_scope(); visitor.leave_node(kind); } @@ -3888,7 +3888,7 @@ pub mod walk { it: &TSModuleDeclarationName<'a>, ) { match it { - TSModuleDeclarationName::Identifier(it) => visitor.visit_identifier_name(it), + TSModuleDeclarationName::Identifier(it) => visitor.visit_binding_identifier(it), TSModuleDeclarationName::StringLiteral(it) => visitor.visit_string_literal(it), } } diff --git a/crates/oxc_ast/src/generated/visit_mut.rs b/crates/oxc_ast/src/generated/visit_mut.rs index eb5460f88fbc4..333343110a3fa 100644 --- a/crates/oxc_ast/src/generated/visit_mut.rs +++ b/crates/oxc_ast/src/generated/visit_mut.rs @@ -1956,12 +1956,12 @@ pub mod walk_mut { ) { let kind = AstType::TSConditionalType; visitor.enter_node(kind); - visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); visitor.visit_ts_type(&mut it.check_type); + visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); visitor.visit_ts_type(&mut it.extends_type); visitor.visit_ts_type(&mut it.true_type); - visitor.visit_ts_type(&mut it.false_type); visitor.leave_scope(); + visitor.visit_ts_type(&mut it.false_type); visitor.leave_node(kind); } @@ -1971,11 +1971,11 @@ pub mod walk_mut { it: &mut TSConstructorType<'a>, ) { // NOTE: AstType doesn't exists! - visitor.visit_formal_parameters(&mut it.params); - visitor.visit_ts_type_annotation(&mut it.return_type); if let Some(type_parameters) = &mut it.type_parameters { visitor.visit_ts_type_parameter_declaration(type_parameters); } + visitor.visit_formal_parameters(&mut it.params); + visitor.visit_ts_type_annotation(&mut it.return_type); } #[inline] @@ -2157,14 +2157,14 @@ pub mod walk_mut { it: &mut TSFunctionType<'a>, ) { // NOTE: AstType doesn't exists! + if let Some(type_parameters) = &mut it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } if let Some(this_param) = &mut it.this_param { visitor.visit_ts_this_parameter(this_param); } visitor.visit_formal_parameters(&mut it.params); visitor.visit_ts_type_annotation(&mut it.return_type); - if let Some(type_parameters) = &mut it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } } #[inline] @@ -2514,6 +2514,9 @@ pub mod walk_mut { it: &mut TSCallSignatureDeclaration<'a>, ) { // NOTE: AstType doesn't exists! + if let Some(type_parameters) = &mut it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } if let Some(this_param) = &mut it.this_param { visitor.visit_ts_this_parameter(this_param); } @@ -2521,9 +2524,6 @@ pub mod walk_mut { if let Some(return_type) = &mut it.return_type { visitor.visit_ts_type_annotation(return_type); } - if let Some(type_parameters) = &mut it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } } #[inline] @@ -2534,13 +2534,13 @@ pub mod walk_mut { let kind = AstType::TSConstructSignatureDeclaration; visitor.enter_node(kind); visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); + if let Some(type_parameters) = &mut it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } visitor.visit_formal_parameters(&mut it.params); if let Some(return_type) = &mut it.return_type { visitor.visit_ts_type_annotation(return_type); } - if let Some(type_parameters) = &mut it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } visitor.leave_scope(); visitor.leave_node(kind); } @@ -2554,6 +2554,9 @@ pub mod walk_mut { visitor.enter_node(kind); visitor.enter_scope(ScopeFlags::empty(), &it.scope_id); visitor.visit_property_key(&mut it.key); + if let Some(type_parameters) = &mut it.type_parameters { + visitor.visit_ts_type_parameter_declaration(type_parameters); + } if let Some(this_param) = &mut it.this_param { visitor.visit_ts_this_parameter(this_param); } @@ -2561,9 +2564,6 @@ pub mod walk_mut { if let Some(return_type) = &mut it.return_type { visitor.visit_ts_type_annotation(return_type); } - if let Some(type_parameters) = &mut it.type_parameters { - visitor.visit_ts_type_parameter_declaration(type_parameters); - } visitor.leave_scope(); visitor.leave_node(kind); } @@ -4109,7 +4109,7 @@ pub mod walk_mut { it: &mut TSModuleDeclarationName<'a>, ) { match it { - TSModuleDeclarationName::Identifier(it) => visitor.visit_identifier_name(it), + TSModuleDeclarationName::Identifier(it) => visitor.visit_binding_identifier(it), TSModuleDeclarationName::StringLiteral(it) => visitor.visit_string_literal(it), } } diff --git a/crates/oxc_ast/src/lib.rs b/crates/oxc_ast/src/lib.rs index 006fa94352287..eab91afe0b717 100644 --- a/crates/oxc_ast/src/lib.rs +++ b/crates/oxc_ast/src/lib.rs @@ -33,7 +33,6 @@ mod ast_builder_impl; mod ast_impl; mod ast_kind_impl; pub mod precedence; -pub mod syntax_directed_operations; mod trivia; mod generated { @@ -58,10 +57,11 @@ pub use generated::{ast_builder, ast_kind}; pub use num_bigint::BigUint; pub use crate::{ + ast::comment::{Comment, CommentKind, CommentPosition}, ast_builder::AstBuilder, ast_builder_impl::NONE, ast_kind::{AstKind, AstType}, - trivia::{Comment, CommentKind, CommentPosition, SortedComments, Trivias}, + trivia::{comments_range, has_comments_between, CommentsRange}, visit::{Visit, VisitMut}, }; diff --git a/crates/oxc_ast/src/precedence.rs b/crates/oxc_ast/src/precedence.rs index 56e54b46ed199..12d84eeb0f7b9 100644 --- a/crates/oxc_ast/src/precedence.rs +++ b/crates/oxc_ast/src/precedence.rs @@ -2,9 +2,10 @@ use oxc_syntax::precedence::{GetPrecedence, Precedence}; use crate::ast::{ match_member_expression, AssignmentExpression, AwaitExpression, BinaryExpression, - CallExpression, ComputedMemberExpression, ConditionalExpression, Expression, ImportExpression, - LogicalExpression, MemberExpression, NewExpression, PrivateFieldExpression, SequenceExpression, - StaticMemberExpression, TSTypeAssertion, UnaryExpression, UpdateExpression, YieldExpression, + CallExpression, ChainExpression, ComputedMemberExpression, ConditionalExpression, Expression, + ImportExpression, LogicalExpression, MemberExpression, NewExpression, PrivateFieldExpression, + SequenceExpression, StaticMemberExpression, TSTypeAssertion, UnaryExpression, UpdateExpression, + YieldExpression, }; impl<'a> GetPrecedence for Expression<'a> { @@ -103,6 +104,12 @@ impl<'a> GetPrecedence for NewExpression<'a> { } } +impl<'a> GetPrecedence for ChainExpression<'a> { + fn precedence(&self) -> Precedence { + Precedence::Member + } +} + impl<'a> GetPrecedence for MemberExpression<'a> { fn precedence(&self) -> Precedence { Precedence::Member diff --git a/crates/oxc_ast/src/syntax_directed_operations/mod.rs b/crates/oxc_ast/src/syntax_directed_operations/mod.rs deleted file mode 100644 index f44cc8bd4958a..0000000000000 --- a/crates/oxc_ast/src/syntax_directed_operations/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! [ECMA262 Syntax-Directed Operations](https://tc39.es/ecma262/#sec-syntax-directed-operations) - -mod bound_names; -mod is_simple_parameter_list; -mod private_bound_identifiers; -mod prop_name; - -pub use self::{ - bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList, - private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName, -}; diff --git a/crates/oxc_ast/src/trivia.rs b/crates/oxc_ast/src/trivia.rs index e00f2c7e3afd2..cd9ae1df92c1e 100644 --- a/crates/oxc_ast/src/trivia.rs +++ b/crates/oxc_ast/src/trivia.rs @@ -2,163 +2,22 @@ use std::{ iter::FusedIterator, - ops::{Bound, Deref, RangeBounds}, - sync::Arc, + ops::{Bound, RangeBounds}, }; use oxc_span::Span; -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum CommentKind { - Line, - Block, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum CommentPosition { - /// Comments prior to a token until another token or trailing comment. - /// - /// e.g. - /// - /// ``` - /// /* leading */ token; - /// /* leading */ - /// // leading - /// token; - /// ``` - Leading, - - /// Comments tailing a token until a newline. - /// e.g. `token /* trailing */ // trailing` - Trailing, -} - -/// Single or multiline comment -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct Comment { - /// The span of the comment text (without leading/trailing delimiters). - pub span: Span, - - /// Line or block comment - pub kind: CommentKind, - - /// Leading or trailing comment - pub position: CommentPosition, - - /// Start of token this leading comment is attached to. - /// `/* Leading */ token` - /// ^ This start - /// NOTE: Trailing comment attachment is not computed yet. - pub attached_to: u32, - - /// Whether this comment has a preceding newline. - /// Used to avoid becoming a trailing comment in codegen. - pub preceded_by_newline: bool, - - /// Whether this comment has a tailing newline. - pub followed_by_newline: bool, -} - -impl Comment { - #[inline] - pub fn new(start: u32, end: u32, kind: CommentKind) -> Self { - let span = Span::new(start, end); - Self { - span, - kind, - position: CommentPosition::Trailing, - attached_to: 0, - preceded_by_newline: false, - followed_by_newline: false, - } - } - - pub fn is_line(self) -> bool { - self.kind == CommentKind::Line - } - - pub fn is_block(self) -> bool { - self.kind == CommentKind::Block - } - - pub fn is_leading(self) -> bool { - self.position == CommentPosition::Leading - } - - pub fn is_trailing(self) -> bool { - self.position == CommentPosition::Trailing - } - - pub fn real_span(&self) -> Span { - Span::new(self.real_span_start(), self.real_span_end()) - } +use crate::ast::comment::*; - pub fn real_span_end(&self) -> u32 { - match self.kind { - CommentKind::Line => self.span.end, - // length of `*/` - CommentKind::Block => self.span.end + 2, - } - } - - pub fn real_span_start(&self) -> u32 { - self.span.start - 2 - } - - pub fn is_jsdoc(&self, source_text: &str) -> bool { - self.is_leading() && self.is_block() && self.span.source_text(source_text).starts_with('*') - } +pub fn comments_range(comments: &[Comment], range: R) -> CommentsRange<'_> +where + R: RangeBounds, +{ + CommentsRange::new(comments, range.start_bound().cloned(), range.end_bound().cloned()) } -/// Sorted set of unique trivia comments, in ascending order by starting position. -pub type SortedComments = Box<[Comment]>; - -#[derive(Debug, Clone, Default)] -pub struct Trivias(Arc); - -#[derive(Debug, Default)] -pub struct TriviasImpl { - /// Unique comments, ordered by increasing span-start. - comments: SortedComments, - - irregular_whitespaces: Box<[Span]>, -} - -impl Deref for Trivias { - type Target = TriviasImpl; - - #[inline] - fn deref(&self) -> &Self::Target { - self.0.as_ref() - } -} - -impl Trivias { - pub fn new(comments: SortedComments, irregular_whitespaces: Vec) -> Trivias { - Self(Arc::new(TriviasImpl { - comments, - irregular_whitespaces: irregular_whitespaces.into_boxed_slice(), - })) - } - - pub fn comments(&self) -> impl Iterator { - self.comments.iter() - } - - pub fn comments_range(&self, range: R) -> CommentsRange<'_> - where - R: RangeBounds, - { - CommentsRange::new(&self.comments, range.start_bound().cloned(), range.end_bound().cloned()) - } - - pub fn has_comments_between(&self, span: Span) -> bool { - self.comments_range(span.start..span.end).count() > 0 - } - - pub fn irregular_whitespaces(&self) -> &[Span] { - &self.irregular_whitespaces - } +pub fn has_comments_between(comments: &[Comment], span: Span) -> bool { + comments_range(comments, span.start..span.end).count() > 0 } /// Double-ended iterator over a range of comments, by starting position. @@ -241,7 +100,7 @@ mod test { #[test] fn test_comments_range() { - let comments: SortedComments = vec![ + let comments = vec![ Comment::new(0, 4, CommentKind::Line), Comment::new(5, 9, CommentKind::Line), Comment::new(10, 13, CommentKind::Line), @@ -250,10 +109,9 @@ mod test { ] .into_boxed_slice(); let full_len = comments.len(); - let trivias = Trivias::new(comments, vec![]); - assert_eq!(trivias.comments_range(..).count(), full_len); - assert_eq!(trivias.comments_range(1..).count(), full_len.saturating_sub(1)); - assert_eq!(trivias.comments_range(..18).count(), full_len.saturating_sub(1)); - assert_eq!(trivias.comments_range(..=18).count(), full_len); + assert_eq!(comments_range(&comments, ..).count(), full_len); + assert_eq!(comments_range(&comments, 1..).count(), full_len.saturating_sub(1)); + assert_eq!(comments_range(&comments, ..18).count(), full_len.saturating_sub(1)); + assert_eq!(comments_range(&comments, ..=18).count(), full_len); } } diff --git a/crates/oxc_ast_macros/Cargo.toml b/crates/oxc_ast_macros/Cargo.toml index 88c7100689a75..27cda301d9522 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.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_cfg/CHANGELOG.md b/crates/oxc_cfg/CHANGELOG.md index 124967785e7dd..6744c8367b678 100644 --- a/crates/oxc_cfg/CHANGELOG.md +++ b/crates/oxc_cfg/CHANGELOG.md @@ -4,6 +4,21 @@ 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.31.0] - 2024-10-08 + +- 95ca01c cfg: [**BREAKING**] Make BasicBlock::unreachable private (#6321) (DonIsaac) + +### Features + +- fa4d505 cfg: Derive more base traits for CFG blocks (#6320) (DonIsaac) +- 14275b1 cfg: Color-code edges in CFG dot diagrams (#6314) (DonIsaac) + +### Refactor + +- 40932f7 cfg: Use IndexVec for storing basic blocks (#6323) (DonIsaac) +- a1e0d30 cfg: Add type alias for Graph (#6322) (DonIsaac) +- 7672793 cfg: Move block data types to separate file (#6319) (DonIsaac) + ## [0.29.0] - 2024-09-13 ### Refactor diff --git a/crates/oxc_cfg/Cargo.toml b/crates/oxc_cfg/Cargo.toml index cee54384e2091..e4c072975bd9a 100644 --- a/crates/oxc_cfg/Cargo.toml +++ b/crates/oxc_cfg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_cfg" -version = "0.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true @@ -21,9 +21,11 @@ test = false doctest = false [dependencies] +oxc_index = { workspace = true } oxc_syntax = { workspace = true } bitflags = { workspace = true } itertools = { workspace = true } +nonmax = { workspace = true } petgraph = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/oxc_cfg/src/block.rs b/crates/oxc_cfg/src/block.rs new file mode 100644 index 0000000000000..9d94e065c896d --- /dev/null +++ b/crates/oxc_cfg/src/block.rs @@ -0,0 +1,74 @@ +use oxc_syntax::node::NodeId; + +#[derive(Debug, Clone)] +pub struct BasicBlock { + pub instructions: Vec, + unreachable: bool, +} + +impl BasicBlock { + pub(crate) fn new() -> Self { + BasicBlock { instructions: Vec::new(), unreachable: false } + } + + pub fn instructions(&self) -> &Vec { + &self.instructions + } + + #[inline] + pub fn is_unreachable(&self) -> bool { + self.unreachable + } + + #[inline] + pub fn mark_as_unreachable(&mut self) { + self.unreachable = true; + } + + #[inline] + pub fn mark_as_reachable(&mut self) { + self.unreachable = false; + } +} + +#[derive(Debug, Clone)] +pub struct Instruction { + pub kind: InstructionKind, + pub node_id: Option, +} + +impl Instruction { + pub fn new(kind: InstructionKind, node_id: Option) -> Self { + Self { kind, node_id } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstructionKind { + Unreachable, + Statement, + Return(ReturnInstructionKind), + Break(LabeledInstruction), + Continue(LabeledInstruction), + Throw, + Condition, + Iteration(IterationInstructionKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReturnInstructionKind { + ImplicitUndefined, + NotImplicitUndefined, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LabeledInstruction { + Labeled, + Unlabeled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IterationInstructionKind { + Of, + In, +} diff --git a/crates/oxc_cfg/src/builder/context.rs b/crates/oxc_cfg/src/builder/context.rs index 3097f3053213b..48cd6f659a0a0 100644 --- a/crates/oxc_cfg/src/builder/context.rs +++ b/crates/oxc_cfg/src/builder/context.rs @@ -1,5 +1,5 @@ use super::ControlFlowGraphBuilder; -use crate::{BasicBlockId, EdgeType}; +use crate::{BlockNodeId, EdgeType}; bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -15,9 +15,9 @@ bitflags::bitflags! { pub(super) struct Ctx<'a> { flags: CtxFlags, label: Option<&'a str>, - entries: Vec<(CtxFlags, BasicBlockId)>, - break_jmp: Option, - continue_jmp: Option, + entries: Vec<(CtxFlags, BlockNodeId)>, + break_jmp: Option, + continue_jmp: Option, } impl<'a> Ctx<'a> { @@ -29,11 +29,11 @@ impl<'a> Ctx<'a> { self.label.as_ref().is_some_and(|it| *it == label) } - fn r#break(&mut self, entry: BasicBlockId) { + fn r#break(&mut self, entry: BlockNodeId) { self.entries.push((CtxFlags::BREAK, entry)); } - fn r#continue(&mut self, entry: BasicBlockId) { + fn r#continue(&mut self, entry: BlockNodeId) { self.entries.push((CtxFlags::CONTINUE, entry)); } } @@ -41,19 +41,19 @@ impl<'a> Ctx<'a> { pub trait CtxCursor { #![allow(clippy::return_self_not_must_use)] /// Marks the break jump position in the current context. - fn mark_break(self, jmp_pos: BasicBlockId) -> Self; + fn mark_break(self, jmp_pos: BlockNodeId) -> Self; /// Marks the continue jump position in the current context. - fn mark_continue(self, jmp_pos: BasicBlockId) -> Self; + fn mark_continue(self, jmp_pos: BlockNodeId) -> Self; /// Creates a break entry in the current context. - fn r#break(self, bb: BasicBlockId) -> Self; + fn r#break(self, bb: BlockNodeId) -> Self; /// Creates a continue entry in the current context. - fn r#continue(self, bb: BasicBlockId) -> Self; + fn r#continue(self, bb: BlockNodeId) -> Self; } pub struct QueryCtx<'a, 'c>(&'c mut ControlFlowGraphBuilder<'a>, /* label */ Option<&'a str>); impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> { - fn mark_break(self, jmp_pos: BasicBlockId) -> Self { + fn mark_break(self, jmp_pos: BlockNodeId) -> Self { self.0.in_break_context(self.1, |ctx| { debug_assert!(ctx.break_jmp.is_none()); ctx.break_jmp = Some(jmp_pos); @@ -61,7 +61,7 @@ impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> { self } - fn mark_continue(self, jmp_pos: BasicBlockId) -> Self { + fn mark_continue(self, jmp_pos: BlockNodeId) -> Self { self.0.in_continue_context(self.1, |ctx| { debug_assert!(ctx.continue_jmp.is_none()); ctx.continue_jmp = Some(jmp_pos); @@ -69,14 +69,14 @@ impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> { self } - fn r#break(self, bb: BasicBlockId) -> Self { + fn r#break(self, bb: BlockNodeId) -> Self { self.0.in_break_context(self.1, |ctx| { ctx.r#break(bb); }); self } - fn r#continue(self, bb: BasicBlockId) -> Self { + fn r#continue(self, bb: BlockNodeId) -> Self { self.0.in_continue_context(self.1, |ctx| { ctx.r#continue(bb); }); @@ -192,24 +192,24 @@ impl<'a, 'c> RefCtxCursor<'a, 'c> { } impl<'a, 'c> CtxCursor for RefCtxCursor<'a, 'c> { - fn mark_break(self, jmp_pos: BasicBlockId) -> Self { + fn mark_break(self, jmp_pos: BlockNodeId) -> Self { debug_assert!(self.0.break_jmp.is_none()); self.0.break_jmp = Some(jmp_pos); self } - fn mark_continue(self, jmp_pos: BasicBlockId) -> Self { + fn mark_continue(self, jmp_pos: BlockNodeId) -> Self { debug_assert!(self.0.continue_jmp.is_none()); self.0.continue_jmp = Some(jmp_pos); self } - fn r#break(self, bb: BasicBlockId) -> Self { + fn r#break(self, bb: BlockNodeId) -> Self { self.0.r#break(bb); self } - fn r#continue(self, bb: BasicBlockId) -> Self { + fn r#continue(self, bb: BlockNodeId) -> Self { self.0.r#continue(bb); self } diff --git a/crates/oxc_cfg/src/builder/mod.rs b/crates/oxc_cfg/src/builder/mod.rs index 2edc48da38223..c325bbe264ea8 100644 --- a/crates/oxc_cfg/src/builder/mod.rs +++ b/crates/oxc_cfg/src/builder/mod.rs @@ -2,28 +2,29 @@ mod context; use context::Ctx; pub use context::{CtxCursor, CtxFlags}; +use oxc_index::IndexVec; use oxc_syntax::node::NodeId; use petgraph::Direction; use super::{ - BasicBlock, BasicBlockId, ControlFlowGraph, EdgeType, ErrorEdgeKind, Graph, Instruction, + BasicBlock, BlockNodeId, ControlFlowGraph, EdgeType, ErrorEdgeKind, Graph, Instruction, InstructionKind, IterationInstructionKind, LabeledInstruction, }; -use crate::ReturnInstructionKind; +use crate::{BasicBlockId, ReturnInstructionKind}; #[derive(Debug, Default)] -struct ErrorHarness(ErrorEdgeKind, BasicBlockId); +struct ErrorHarness(ErrorEdgeKind, BlockNodeId); #[derive(Debug, Default)] pub struct ControlFlowGraphBuilder<'a> { - pub graph: Graph, - pub basic_blocks: Vec, - pub current_node_ix: BasicBlockId, + pub graph: Graph, + pub basic_blocks: IndexVec, + pub current_node_ix: BlockNodeId, ctx_stack: Vec>, /// Contains the error unwinding path represented as a stack of `ErrorHarness`es error_path: Vec, /// Stack of finalizers, the top most element is always the appropriate one for current node. - finalizers: Vec>, + finalizers: Vec>, } impl<'a> ControlFlowGraphBuilder<'a> { @@ -36,7 +37,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { } /// # Panics - pub fn basic_block(&self, basic_block: BasicBlockId) -> &BasicBlock { + pub fn basic_block(&self, basic_block: BlockNodeId) -> &BasicBlock { let idx = *self .graph .node_weight(basic_block) @@ -47,7 +48,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { } /// # Panics - pub fn basic_block_mut(&mut self, basic_block: BasicBlockId) -> &mut BasicBlock { + pub fn basic_block_mut(&mut self, basic_block: BlockNodeId) -> &mut BasicBlock { let idx = *self .graph .node_weight(basic_block) @@ -57,15 +58,14 @@ impl<'a> ControlFlowGraphBuilder<'a> { .expect("expected `self.current_node_ix` to be a valid node index in self.graph") } - pub(self) fn new_basic_block(&mut self) -> BasicBlockId { + pub(self) fn new_basic_block(&mut self) -> BlockNodeId { // current length would be the index of block we are adding on the next line. - let basic_block_ix = self.basic_blocks.len(); - self.basic_blocks.push(BasicBlock::new()); + let basic_block_ix = self.basic_blocks.push(BasicBlock::new()); self.graph.add_node(basic_block_ix) } #[must_use] - pub fn new_basic_block_function(&mut self) -> BasicBlockId { + pub fn new_basic_block_function(&mut self) -> BlockNodeId { // we might want to differentiate between function blocks and normal blocks down the road. self.new_basic_block_normal() } @@ -73,7 +73,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { /// # Panics /// if there is no error harness to attach to. #[must_use] - pub fn new_basic_block_normal(&mut self) -> BasicBlockId { + pub fn new_basic_block_normal(&mut self) -> BlockNodeId { let graph_ix = self.new_basic_block(); self.current_node_ix = graph_ix; @@ -89,12 +89,12 @@ impl<'a> ControlFlowGraphBuilder<'a> { graph_ix } - pub fn add_edge(&mut self, a: BasicBlockId, b: BasicBlockId, weight: EdgeType) { + pub fn add_edge(&mut self, a: BlockNodeId, b: BlockNodeId, weight: EdgeType) { if matches!(weight, EdgeType::NewFunction) { - self.basic_block_mut(b).unreachable = false; - } else if matches!(weight, EdgeType::Unreachable) || self.basic_block(a).unreachable { + self.basic_block_mut(b).mark_as_reachable(); + } else if matches!(weight, EdgeType::Unreachable) || self.basic_block(a).is_unreachable() { if self.graph.edges_directed(b, Direction::Incoming).count() == 0 { - self.basic_block_mut(b).unreachable = true; + self.basic_block_mut(b).mark_as_unreachable(); } } else if !self .basic_block(b) @@ -102,7 +102,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { .iter() .any(|it| matches!(it, Instruction { kind: InstructionKind::Unreachable, .. })) { - self.basic_block_mut(b).unreachable = false; + self.basic_block_mut(b).mark_as_reachable(); } self.graph.add_edge(a, b, weight); } @@ -117,7 +117,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { /// Creates and push a new `BasicBlockId` onto `self.error_path` stack. /// Returns the `BasicBlockId` of the created error harness block. - pub fn attach_error_harness(&mut self, kind: ErrorEdgeKind) -> BasicBlockId { + pub fn attach_error_harness(&mut self, kind: ErrorEdgeKind) -> BlockNodeId { let graph_ix = self.new_basic_block(); self.error_path.push(ErrorHarness(kind, graph_ix)); graph_ix @@ -126,7 +126,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { /// # Panics /// if there is no error harness pushed onto the stack, /// Or last harness doesn't match the expected `BasicBlockId`. - pub fn release_error_harness(&mut self, expect: BasicBlockId) { + pub fn release_error_harness(&mut self, expect: BlockNodeId) { let harness = self .error_path .pop() @@ -139,7 +139,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { /// Creates and push a new `BasicBlockId` onto `self.finalizers` stack. /// Returns the `BasicBlockId` of the created finalizer block. - pub fn attach_finalizer(&mut self) -> BasicBlockId { + pub fn attach_finalizer(&mut self) -> BlockNodeId { let graph_ix = self.new_basic_block(); self.finalizers.push(Some(graph_ix)); graph_ix @@ -156,7 +156,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { /// # Panics /// if last finalizer doesn't match the expected `BasicBlockId`. - pub fn release_finalizer(&mut self, expect: BasicBlockId) { + pub fn release_finalizer(&mut self, expect: BlockNodeId) { // return early if there is no finalizer. let Some(finalizer) = self.finalizers.pop() else { return }; assert_eq!( @@ -166,7 +166,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { ); } - pub fn append_condition_to(&mut self, block: BasicBlockId, node: Option) { + pub fn append_condition_to(&mut self, block: BlockNodeId, node: Option) { self.push_instruction_to(block, InstructionKind::Condition, node); } @@ -211,7 +211,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { let current_node_ix = self.current_node_ix; let basic_block_with_unreachable_graph_ix = self.new_basic_block_normal(); self.push_instruction(InstructionKind::Unreachable, None); - self.current_basic_block().unreachable = true; + self.current_basic_block().mark_as_unreachable(); self.add_edge( current_node_ix, basic_block_with_unreachable_graph_ix, @@ -228,7 +228,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { #[inline] pub(self) fn push_instruction_to( &mut self, - block: BasicBlockId, + block: BlockNodeId, kind: InstructionKind, node_id: Option, ) { diff --git a/crates/oxc_cfg/src/dot.rs b/crates/oxc_cfg/src/dot.rs index bb71eae682f4e..9076aab4d85b7 100644 --- a/crates/oxc_cfg/src/dot.rs +++ b/crates/oxc_cfg/src/dot.rs @@ -1,11 +1,11 @@ -// use oxc_ast::{ -// ast::{BreakStatement, ContinueStatement}, -// AstKind, -// }; +use std::{borrow::Cow, fmt}; + +use itertools::Itertools as _; use petgraph::{ dot::{Config, Dot}, visit::EdgeRef, }; +use rustc_hash::FxHashMap; use super::IterationInstructionKind; use crate::{ @@ -26,19 +26,31 @@ impl DisplayDot for ControlFlowGraph { &[Config::EdgeNoLabel, Config::NodeNoLabel], &|_graph, edge| { let weight = edge.weight(); - let label = format!("label = \"{weight:?}\" "); + let mut attrs = Attrs::default().with("label", format!("{weight:?}")); + if matches!(weight, EdgeType::Unreachable) - || self.basic_block(edge.source()).unreachable + || self.basic_block(edge.source()).is_unreachable() { - format!("{label}, style = \"dotted\" ") - } else { - label + attrs += ("style", "dotted"); + } else if matches!(weight, EdgeType::Error(_)) { + attrs += ("color", "red"); + }; + + format!("{attrs:?}") + }, + &|_graph, node| { + let block = &self.basic_blocks[*node.1]; + let mut attrs = Attrs::default().with("label", block.display_dot()); + + if *node.1 == 0 { + attrs += ("color", "green"); + } + if block.is_unreachable() { + attrs += ("style", "dotted"); } + + format!("{attrs:?}") }, - &|_graph, node| format!( - "label = {:?} ", - self.basic_blocks[*node.1].display_dot().trim() - ), ) ) } @@ -46,11 +58,7 @@ impl DisplayDot for ControlFlowGraph { impl DisplayDot for BasicBlock { fn display_dot(&self) -> String { - self.instructions().iter().fold(String::new(), |mut acc, it| { - acc.push_str(it.display_dot().as_str()); - acc.push('\n'); - acc - }) + self.instructions().iter().map(DisplayDot::display_dot).join("\n") } } @@ -77,3 +85,101 @@ impl DisplayDot for Instruction { .to_string() } } + +#[derive(Clone)] +pub enum Attr<'a> { + String(Cow<'a, str>), + Identifier(Cow<'a, str>), + Int(i64), +} +impl<'a> Attr<'a> { + #[inline] + #[must_use] + pub fn ident(identifier: S) -> Self + where + S: Into>, + { + Self::Identifier(identifier.into()) + } +} + +impl fmt::Debug for Attr<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Int(i) => write!(f, "{i}"), + Self::String(s) => write!(f, "{s:?}"), + Self::Identifier(ident) => write!(f, "{ident}"), // display instead of debug + } + } +} + +impl<'a> From<&'a str> for Attr<'a> { + fn from(value: &'a str) -> Self { + Self::String(Cow::Borrowed(value)) + } +} + +impl From for Attr<'static> { + fn from(value: String) -> Self { + Self::String(Cow::Owned(value)) + } +} + +impl From for Attr<'_> { + fn from(value: i64) -> Self { + Self::Int(value) + } +} + +#[derive(Default)] +pub struct Attrs<'a>(FxHashMap, Attr<'a>>); +impl<'a> Attrs<'a> { + #[must_use] + #[inline] + pub fn with(mut self, key: K, value: V) -> Self + where + K: Into>, + V: Into>, + { + self += (key, value); + self + } +} + +impl<'a, K, V> FromIterator<(K, V)> for Attrs<'a> +where + K: Into>, + V: Into>, +{ + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().map(|(k, v)| (k.into(), v.into())).collect()) + } +} + +impl<'a, K, V> std::ops::AddAssign<(K, V)> for Attrs<'a> +where + K: Into>, + V: Into>, +{ + fn add_assign(&mut self, (key, value): (K, V)) { + self.0.insert(key.into(), value.into()); + } +} + +impl fmt::Debug for Attrs<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + return Ok(()); + } + + let l = self.0.len(); + for (i, (k, v)) in self.0.iter().enumerate() { + write!(f, "{k}={v:?}")?; + if i < l - 1 { + write!(f, ", ")?; + } + } + + Ok(()) + } +} diff --git a/crates/oxc_cfg/src/lib.rs b/crates/oxc_cfg/src/lib.rs index 2e126aa444e55..0bc8ba3c6082f 100644 --- a/crates/oxc_cfg/src/lib.rs +++ b/crates/oxc_cfg/src/lib.rs @@ -1,13 +1,16 @@ +mod block; mod builder; -mod dot; +pub mod dot; pub mod visit; +use std::fmt; + use itertools::Itertools; -use oxc_syntax::node::NodeId; +use nonmax::NonMaxU32; +use oxc_index::{Idx, IndexVec}; use petgraph::{ - stable_graph::NodeIndex, visit::{Control, DfsEvent, EdgeRef}, - Direction, Graph, + Direction, }; pub mod graph { @@ -19,68 +22,42 @@ pub mod graph { } } +pub use block::*; pub use builder::{ControlFlowGraphBuilder, CtxCursor, CtxFlags}; pub use dot::DisplayDot; use visit::set_depth_first_search; -pub type BasicBlockId = NodeIndex; +pub type BlockNodeId = petgraph::stable_graph::NodeIndex; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BasicBlockId(NonMaxU32); -#[derive(Debug)] -pub struct BasicBlock { - pub instructions: Vec, - pub unreachable: bool, -} - -impl BasicBlock { - fn new() -> Self { - BasicBlock { instructions: Vec::new(), unreachable: false } +impl Idx for BasicBlockId { + #[allow(clippy::cast_possible_truncation)] + fn from_usize(idx: usize) -> Self { + assert!(idx < u32::MAX as usize); + // SAFETY: We just checked `idx` is valid for `NonMaxU32` + Self(unsafe { NonMaxU32::new_unchecked(idx as u32) }) } - pub fn instructions(&self) -> &Vec { - &self.instructions + fn index(self) -> usize { + self.0.get() as usize } } -#[derive(Debug, Clone)] -pub struct Instruction { - pub kind: InstructionKind, - pub node_id: Option, -} - -impl Instruction { - pub fn new(kind: InstructionKind, node_id: Option) -> Self { - Self { kind, node_id } +impl PartialEq for BasicBlockId { + #[inline] + fn eq(&self, other: &u32) -> bool { + self.0.get() == *other } } -#[derive(Debug, Clone)] -pub enum InstructionKind { - Unreachable, - Statement, - Return(ReturnInstructionKind), - Break(LabeledInstruction), - Continue(LabeledInstruction), - Throw, - Condition, - Iteration(IterationInstructionKind), -} -#[derive(Debug, Clone)] -pub enum ReturnInstructionKind { - ImplicitUndefined, - NotImplicitUndefined, -} - -#[derive(Debug, Clone)] -pub enum LabeledInstruction { - Labeled, - Unlabeled, +impl fmt::Display for BasicBlockId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } } -#[derive(Debug, Clone)] -pub enum IterationInstructionKind { - Of, - In, -} +pub type Graph = petgraph::graph::DiGraph; #[derive(Debug, Clone)] pub enum EdgeType { @@ -121,35 +98,35 @@ pub enum EvalConstConditionResult { #[derive(Debug)] pub struct ControlFlowGraph { - pub graph: Graph, - pub basic_blocks: Vec, + pub graph: Graph, + pub basic_blocks: IndexVec, } impl ControlFlowGraph { - pub fn graph(&self) -> &Graph { + pub fn graph(&self) -> &Graph { &self.graph } /// # Panics - pub fn basic_block(&self, id: BasicBlockId) -> &BasicBlock { + pub fn basic_block(&self, id: BlockNodeId) -> &BasicBlock { let ix = *self.graph.node_weight(id).expect("expected a valid node id in self.graph"); self.basic_blocks.get(ix).expect("expected a valid node id in self.basic_blocks") } /// # Panics - pub fn basic_block_mut(&mut self, id: BasicBlockId) -> &mut BasicBlock { + pub fn basic_block_mut(&mut self, id: BlockNodeId) -> &mut BasicBlock { let ix = *self.graph.node_weight(id).expect("expected a valid node id in self.graph"); self.basic_blocks.get_mut(ix).expect("expected a valid node id in self.basic_blocks") } - pub fn is_reachable(&self, from: BasicBlockId, to: BasicBlockId) -> bool { + pub fn is_reachable(&self, from: BlockNodeId, to: BlockNodeId) -> bool { self.is_reachable_filtered(from, to, |_| Control::Continue) } - pub fn is_reachable_filtered Control>( + pub fn is_reachable_filtered Control>( &self, - from: BasicBlockId, - to: BasicBlockId, + from: BlockNodeId, + to: BlockNodeId, filter: F, ) -> bool { if from == to { @@ -184,16 +161,13 @@ impl ControlFlowGraph { /// Otherwise returns `Some(loop_start, loop_end)`. pub fn is_infinite_loop_start( &self, - node: BasicBlockId, + node: BlockNodeId, try_eval_const_condition: F, - ) -> Option<(BasicBlockId, BasicBlockId)> + ) -> Option<(BlockNodeId, BlockNodeId)> where F: Fn(&Instruction) -> EvalConstConditionResult, { - fn get_jump_target( - graph: &Graph, - node: BasicBlockId, - ) -> Option { + fn get_jump_target(graph: &Graph, node: BlockNodeId) -> Option { graph .edges_directed(node, Direction::Outgoing) .find_or_first(|e| matches!(e.weight(), EdgeType::Jump)) @@ -246,7 +220,7 @@ impl ControlFlowGraph { } } - pub fn is_cyclic(&self, node: BasicBlockId) -> bool { + pub fn is_cyclic(&self, node: BlockNodeId) -> bool { set_depth_first_search(&self.graph, Some(node), |event| match event { DfsEvent::BackEdge(_, id) if id == node => Err(()), _ => Ok(()), diff --git a/crates/oxc_cfg/src/visit.rs b/crates/oxc_cfg/src/visit.rs index 72f3d651ab001..2e65e0f03f033 100644 --- a/crates/oxc_cfg/src/visit.rs +++ b/crates/oxc_cfg/src/visit.rs @@ -6,18 +6,18 @@ use petgraph::{ }; use rustc_hash::FxHashSet; -use crate::BasicBlockId; +use crate::BlockNodeId; /// # Panics pub fn neighbors_filtered_by_edge_weight( graph: &Graph, - node: BasicBlockId, + node: BlockNodeId, edge_filter: &F, visitor: &mut G, ) -> Vec where F: Fn(&EdgeWeight) -> Option, - G: FnMut(&BasicBlockId, State) -> (State, bool), + G: FnMut(&BlockNodeId, State) -> (State, bool), { let mut q = vec![]; let mut final_states = vec![]; diff --git a/crates/oxc_codegen/CHANGELOG.md b/crates/oxc_codegen/CHANGELOG.md index 8d4047c252e4b..217d4f57d7e28 100644 --- a/crates/oxc_codegen/CHANGELOG.md +++ b/crates/oxc_codegen/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) until v1.0.0. +## [0.31.0] - 2024-10-08 + +- 020bb80 codegen: [**BREAKING**] Change to `CodegenReturn::code` and `CodegenReturn::map` (#6310) (Boshen) + +### Bug Fixes + +- 84b2d07 codegen: Converts line comment to block comment if it is a `PURE` comment (#6356) (Dunqing) + +### Refactor + + ## [0.30.5] - 2024-09-29 ### Refactor diff --git a/crates/oxc_codegen/Cargo.toml b/crates/oxc_codegen/Cargo.toml index b92cfc0c0d4f3..1ba96c14f8414 100644 --- a/crates/oxc_codegen/Cargo.toml +++ b/crates/oxc_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_codegen" -version = "0.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true @@ -20,7 +20,6 @@ workspace = true doctest = false [dependencies] -cow-utils = { workspace = true } oxc_allocator = { workspace = true } oxc_ast = { workspace = true } oxc_index = { workspace = true } @@ -29,7 +28,9 @@ oxc_sourcemap = { workspace = true } oxc_span = { workspace = true } oxc_syntax = { workspace = true, features = ["to_js_string"] } +assert-unchecked = { workspace = true } bitflags = { workspace = true } +cow-utils = { workspace = true } daachorse = { workspace = true } nonmax = { workspace = true } once_cell = { workspace = true } diff --git a/crates/oxc_codegen/examples/codegen.rs b/crates/oxc_codegen/examples/codegen.rs index 9fceb36b286ba..3edfcfbaabf37 100644 --- a/crates/oxc_codegen/examples/codegen.rs +++ b/crates/oxc_codegen/examples/codegen.rs @@ -2,7 +2,7 @@ use std::{env, path::Path}; use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CodegenOptions, CommentOptions}; +use oxc_codegen::{CodeGenerator, CodegenOptions}; use oxc_parser::{Parser, ParserReturn}; use oxc_span::SourceType; use pico_args::Arguments; @@ -24,7 +24,7 @@ fn main() -> std::io::Result<()> { let printed = { let Some(ret) = parse(&allocator, &source_text, source_type) else { return Ok(()) }; - codegen(&source_text, &ret, minify) + codegen(&ret, minify) }; println!("First time:"); println!("{printed}"); @@ -35,7 +35,7 @@ fn main() -> std::io::Result<()> { let Some(ret) = parse(&allocator, &printed, source_type) else { return Ok(()) }; println!("Second time:"); - let printed = codegen(&printed, &ret, minify); + let printed = codegen(&ret, minify); println!("{printed}"); // Check syntax error parse(&allocator, &printed, source_type); @@ -59,14 +59,9 @@ fn parse<'a>( Some(ret) } -fn codegen(source_text: &str, ret: &ParserReturn<'_>, minify: bool) -> String { +fn codegen(ret: &ParserReturn<'_>, minify: bool) -> String { CodeGenerator::new() - .enable_comment( - source_text, - ret.trivias.clone(), - CommentOptions { preserve_annotate_comments: true }, - ) .with_options(CodegenOptions { minify, ..CodegenOptions::default() }) .build(&ret.program) - .source_text + .code } diff --git a/crates/oxc_codegen/examples/sourcemap.rs b/crates/oxc_codegen/examples/sourcemap.rs index 779ef4caeefc2..78b16aafc1629 100644 --- a/crates/oxc_codegen/examples/sourcemap.rs +++ b/crates/oxc_codegen/examples/sourcemap.rs @@ -3,7 +3,7 @@ use std::{env, path::Path}; use base64::{prelude::BASE64_STANDARD, Engine}; use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CodegenReturn}; +use oxc_codegen::{CodeGenerator, CodegenOptions, CodegenReturn}; use oxc_parser::Parser; use oxc_span::SourceType; @@ -27,19 +27,17 @@ fn main() -> std::io::Result<()> { return Ok(()); } - let CodegenReturn { source_text, source_map } = CodeGenerator::new() - .enable_source_map(path.to_string_lossy().as_ref(), &source_text) + let CodegenReturn { code, map } = CodeGenerator::new() + .with_options(CodegenOptions { + source_map_path: Some(path.to_path_buf()), + ..CodegenOptions::default() + }) .build(&ret.program); - if let Some(source_map) = source_map { + if let Some(source_map) = map { let result = source_map.to_json_string(); - let hash = BASE64_STANDARD.encode(format!( - "{}\0{}{}\0{}", - source_text.len(), - source_text, - result.len(), - result - )); + let hash = + BASE64_STANDARD.encode(format!("{}\0{}{}\0{}", code.len(), code, result.len(), result)); println!("https://evanw.github.io/source-map-visualization/#{hash}"); } diff --git a/crates/oxc_codegen/src/binary_expr_visitor.rs b/crates/oxc_codegen/src/binary_expr_visitor.rs index 852699f465636..ae03f6d27c036 100644 --- a/crates/oxc_codegen/src/binary_expr_visitor.rs +++ b/crates/oxc_codegen/src/binary_expr_visitor.rs @@ -155,7 +155,7 @@ impl<'a> BinaryExpressionVisitor<'a> { && self.ctx.intersects(Context::FORBID_IN)); if self.wrap { - p.print_char(b'('); + p.print_ascii_byte(b'('); self.ctx &= Context::FORBID_IN.not(); } @@ -200,7 +200,7 @@ impl<'a> BinaryExpressionVisitor<'a> { p.print_soft_space(); self.e.right().gen_expr(p, self.right_precedence, self.ctx & Context::FORBID_IN); if self.wrap { - p.print_char(b')'); + p.print_ascii_byte(b')'); } } } diff --git a/crates/oxc_codegen/src/code_buffer.rs b/crates/oxc_codegen/src/code_buffer.rs new file mode 100644 index 0000000000000..34ade405f163c --- /dev/null +++ b/crates/oxc_codegen/src/code_buffer.rs @@ -0,0 +1,527 @@ +use assert_unchecked::assert_unchecked; + +/// A string builder for constructing source code. +/// +/// `CodeBuffer` provides safe abstractions over a byte array. +/// Essentially same as `String` but with additional methods. +/// +/// Use one of the various `print_*` methods to add text into the buffer. +/// When you are done, call [`into_string`] to extract the final [`String`]. +/// +/// # Example +/// ``` +/// use oxc_codegen::CodeBuffer; +/// let mut code = CodeBuffer::new(); +/// +/// // mock settings +/// let is_public = true; +/// +/// if is_public { +/// code.print_str("export ") +/// } +/// code.print_str("function foo() {\n"); +/// code.print_str(" console.log('Hello, world!');\n"); +/// code.print_str("}\n"); +/// +/// let source = code.into_string(); +/// ``` +/// +/// [`into_string`]: CodeBuffer::into_string +#[derive(Debug, Default, Clone)] +pub struct CodeBuffer { + /// INVARIANT: `buf` is a valid UTF-8 string. + buf: Vec, +} + +impl CodeBuffer { + /// Create a new empty `CodeBuffer`. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// + /// // use `code` to build new source text + /// code.print_str("fn main() { println!(\"Hello, world!\"); }"); + /// let source_text = code.into_string(); + /// ``` + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Create a new, empty `CodeBuffer` with the specified capacity. + /// + /// The buffer will be able to hold at least `capacity` bytes without reallocating. + /// This method is allowed to allocate for more bytes than `capacity`. + /// If `capacity` is 0, the buffer will not allocate. + /// + /// It is important to note that although the returned buffer has the + /// minimum *capacity* specified, the buffer will have a zero *length*. + /// + /// # Panics + /// Panics if the new capacity exceeds `isize::MAX` bytes. + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + Self { buf: Vec::with_capacity(capacity) } + } + + /// Returns the number of bytes in the buffer. + /// + /// This is *not* the same as the number of characters in the buffer, + /// since non-ASCII characters require multiple bytes. + #[inline] + pub fn len(&self) -> usize { + self.buf.len() + } + + /// Returns `true` if the buffer contains no characters. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// assert!(code.is_empty()); + /// + /// code.print_char('c'); + /// assert!(!code.is_empty()); + /// ``` + #[inline] + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + /// Reserves capacity for at least `additional` more bytes in the buffer. + /// + /// The buffer may reserve more space to speculatively avoid frequent reallocations. + /// After calling `reserve`, capacity will be greater than or equal to `self.len() + additional`. + /// Does nothing if capacity is already sufficient. + /// + /// # Panics + /// Panics if the new capacity exceeds `isize::MAX` bytes. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::default(); + /// code.reserve(10); + /// ``` + #[inline] + pub fn reserve(&mut self, additional: usize) { + self.buf.reserve(additional); + } + + /// Peek the `n`th character from the end of the buffer. + /// + /// When `n` is zero, the last character is returned. + /// Returns [`None`] if `n` exceeds the length of the buffer. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// code.print_str("foo"); + /// + /// assert_eq!(code.peek_nth_char_back(0), Some('o')); + /// assert_eq!(code.peek_nth_char_back(2), Some('f')); + /// assert_eq!(code.peek_nth_char_back(3), None); + /// ``` + #[inline] + #[must_use = "Peeking is pointless if the peeked char isn't used"] + pub fn peek_nth_char_back(&self, n: usize) -> Option { + // SAFETY: All methods of `CodeBuffer` ensure `buf` is valid UTF-8 + unsafe { std::str::from_utf8_unchecked(&self.buf) }.chars().nth_back(n) + } + + /// Peek the `n`th byte from the end of the buffer. + /// + /// When `n` is zero, the last byte is returned. + /// Returns [`None`] if `n` exceeds the length of the buffer. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// code.print_str("foo"); + /// + /// assert_eq!(code.peek_nth_byte_back(0), Some(b'o')); + /// assert_eq!(code.peek_nth_byte_back(2), Some(b'f')); + /// assert_eq!(code.peek_nth_byte_back(3), None); + /// ``` + #[inline] + #[must_use = "Peeking is pointless if the peeked char isn't used"] + pub fn peek_nth_byte_back(&self, n: usize) -> Option { + let len = self.len(); + if n < len { + Some(self.buf[len - 1 - n]) + } else { + None + } + } + + /// Peek the last byte from the end of the buffer. + #[inline] + pub fn last_byte(&self) -> Option { + self.buf.last().copied() + } + + /// Peek the last char from the end of the buffer. + #[inline] + pub fn last_char(&self) -> Option { + self.peek_nth_char_back(0) + } + + /// Push a single ASCII byte into the buffer. + /// + /// # Panics + /// Panics if `byte` is not an ASCII byte (`0 - 0x7F`). + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// code.print_ascii_byte(b'f'); + /// code.print_ascii_byte(b'o'); + /// code.print_ascii_byte(b'o'); + /// + /// let source = code.into_string(); + /// assert_eq!(source, "foo"); + /// ``` + #[inline] + pub fn print_ascii_byte(&mut self, byte: u8) { + // When this method is inlined, and the value of `byte` is known, this assertion should + // get optimized away by the compiler. e.g. `code_buffer.print_ascii_byte(b' ')`. + assert!(byte.is_ascii(), "byte {byte} is not ASCII"); + + // SAFETY: `byte` is an ASCII character + unsafe { self.print_byte_unchecked(byte) } + } + + /// Push a byte to the buffer, without checking that the buffer still represents a valid + /// UTF-8 string. + /// + /// If you are looking to print a byte you know is valid ASCII, prefer [`print_ascii_byte`]. + /// If you are not certain, you may use [`print_char`] as a safe alternative. + /// + /// # SAFETY + /// The caller must ensure that, after 1 or more sequential calls, the buffer represents + /// a valid UTF-8 string. + /// + /// It is safe for a single call to temporarily result in invalid UTF-8, as long as + /// UTF-8 integrity is restored before calls to any other `print_*` method or + /// [`into_string`]. This lets you, for example, print an 4-byte Unicode character + /// using 4 separate calls to this method. However, consider using [`print_bytes_unchecked`] + /// instead for that use case. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// // Safe: 'a' is a valid ASCII character. Its UTF-8 representation only + /// // requires a single byte. + /// unsafe { code.print_byte_unchecked(b'a') }; + /// + /// let not_ascii = '⚓'; + /// let bytes = not_ascii.to_string().into_bytes(); + /// // Safe: after this loop completes, `code` returns to a valid state. + /// for byte in bytes { + /// unsafe { code.print_byte_unchecked(byte) }; + /// } + /// + /// // NOT SAFE: `ch` exceeds the ASCII segment range. `code` is no longer valid UTF-8 + /// // unsafe { code.print_byte_unchecked(0xFF) }; + /// ``` + /// + /// [`print_ascii_byte`]: CodeBuffer::print_ascii_byte + /// [`print_char`]: CodeBuffer::print_char + /// [`into_string`]: CodeBuffer::into_string + /// [`print_bytes_unchecked`]: CodeBuffer::print_bytes_unchecked + #[inline] + pub unsafe fn print_byte_unchecked(&mut self, byte: u8) { + // By default, `self.buf.push(byte)` results in quite verbose assembly, because the default + // branch is for the "buf is full to capacity" case. + // + // That's not ideal because growth strategy is doubling, so e.g. when the `Vec` has just grown + // from 1024 bytes to 2048 bytes, it won't need to grow again until another 1024 bytes have + // been pushed. "Needs to grow" is a very rare occurrence. + // + // So we use `push_slow` to move the complicated logic for the "needs to grow" path out of + // `print_byte_unchecked`, leaving a fast path for the common "there is sufficient capacity" case. + // https://godbolt.org/z/Kv8sEoEed + // https://github.com/oxc-project/oxc/pull/6148#issuecomment-2381635390 + #[cold] + #[inline(never)] + fn push_slow(code_buffer: &mut CodeBuffer, byte: u8) { + let buf = &mut code_buffer.buf; + // SAFETY: We only call this function below if `buf.len() == buf.capacity()`. + // This function is not inlined, so we need this assertion to assist compiler to + // understand this fact. + unsafe { assert_unchecked!(buf.len() == buf.capacity()) } + buf.push(byte); + } + + #[expect(clippy::if_not_else)] + if self.buf.len() != self.buf.capacity() { + self.buf.push(byte); + } else { + push_slow(self, byte); + } + } + + /// Push a single Unicode character into the buffer. + /// + /// When pushing multiple characters, consider choosing [`print_str`] over this method + /// since it's much more efficient. If you really want to insert only a single character + /// and you're certain it's ASCII, consider using [`print_ascii_byte`]. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// + /// code.print_char('f'); + /// code.print_char('o'); + /// code.print_char('o'); + /// + /// assert_eq!(String::from(code), "foo"); + /// ``` + /// + /// [`print_str`]: CodeBuffer::print_str + /// [`print_ascii_byte`]: CodeBuffer::print_ascii_byte + #[inline] + pub fn print_char(&mut self, ch: char) { + let mut b = [0; 4]; + self.buf.extend(ch.encode_utf8(&mut b).as_bytes()); + } + + /// Push a string into the buffer. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// code.print_str("function main() { console.log('Hello, world!') }"); + /// ``` + #[inline] + pub fn print_str>(&mut self, s: S) { + self.buf.extend(s.as_ref().as_bytes()); + } + + /// Push a sequence of ASCII characters into the buffer. + /// + /// # Panics + /// Panics if any byte in the iterator is not ASCII. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// + /// code.print_ascii_bytes([b'f', b'o', b'o']); + /// assert_eq!(String::from(code), "foo"); + /// ``` + pub fn print_ascii_bytes(&mut self, bytes: I) + where + I: IntoIterator, + { + let iter = bytes.into_iter(); + let hint = iter.size_hint(); + self.buf.reserve(hint.1.unwrap_or(hint.0)); + for byte in iter { + self.print_ascii_byte(byte); + } + } + + /// Print a sequence of bytes without checking that the buffer still + /// represents a valid UTF-8 string. + /// + /// # Safety + /// + /// The caller must ensure that, after this method call, the buffer represents + /// a valid UTF-8 string. In practice, this means only two cases are valid: + /// + /// 1. Both the buffer and the byte sequence are valid UTF-8, + /// 2. The buffer became invalid after a call to [`print_byte_unchecked`] and `bytes` completes + /// any incomplete Unicode characters, returning the buffer to a valid state. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// + /// // Indent to a dynamic level. + /// // Sound because all elements in this iterator are ASCII characters. + /// unsafe { + /// code.print_bytes_unchecked(std::iter::repeat(b' ').take(4)); + /// } + /// ``` + /// + /// [`print_byte_unchecked`]: CodeBuffer::print_byte_unchecked + #[inline] + pub unsafe fn print_bytes_unchecked(&mut self, bytes: I) + where + I: IntoIterator, + { + self.buf.extend(bytes); + } + + /// Get contents of buffer as a byte slice. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// code.print_str("foo"); + /// assert_eq!(code.as_bytes(), &[b'f', b'o', b'o']); + /// ``` + #[inline] + pub fn as_bytes(&self) -> &[u8] { + &self.buf + } + + /// Consume buffer and return source code as a `String`. + /// + /// # Example + /// ``` + /// use oxc_codegen::CodeBuffer; + /// let mut code = CodeBuffer::new(); + /// code.print_str("console.log('foo');"); + /// + /// let source = code.into_string(); + /// assert_eq!(source, "console.log('foo');"); + /// ``` + #[must_use] + #[inline] + pub fn into_string(self) -> String { + if cfg!(debug_assertions) { + String::from_utf8(self.buf).unwrap() + } else { + // SAFETY: All methods of `CodeBuffer` ensure `buf` is valid UTF-8 + unsafe { String::from_utf8_unchecked(self.buf) } + } + } +} + +impl AsRef<[u8]> for CodeBuffer { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From for String { + #[inline] + fn from(code: CodeBuffer) -> Self { + code.into_string() + } +} + +#[cfg(test)] +mod test { + use super::CodeBuffer; + + #[test] + fn empty() { + let code = CodeBuffer::default(); + assert!(code.is_empty()); + assert_eq!(code.len(), 0); + assert_eq!(String::from(code), ""); + } + + #[test] + fn string_isomorphism() { + let s = "Hello, world!"; + let mut code = CodeBuffer::with_capacity(s.len()); + code.print_str(s); + assert_eq!(code.len(), s.len()); + assert_eq!(String::from(code), s.to_string()); + } + + #[test] + fn into_string() { + let s = "Hello, world!"; + let mut code = CodeBuffer::with_capacity(s.len()); + code.print_str(s); + + let source = code.into_string(); + assert_eq!(source, s); + } + + #[test] + #[allow(clippy::byte_char_slices)] + fn print_ascii_byte() { + let mut code = CodeBuffer::new(); + code.print_ascii_byte(b'f'); + code.print_ascii_byte(b'o'); + code.print_ascii_byte(b'o'); + + assert_eq!(code.len(), 3); + assert_eq!(code.as_bytes(), &[b'f', b'o', b'o']); + assert_eq!(String::from(code), "foo"); + } + + #[test] + #[allow(clippy::byte_char_slices)] + fn print_byte_unchecked() { + let mut code = CodeBuffer::new(); + // SAFETY: These bytes are all ASCII + unsafe { + code.print_byte_unchecked(b'f'); + code.print_byte_unchecked(b'o'); + code.print_byte_unchecked(b'o'); + } + + assert_eq!(code.len(), 3); + assert_eq!(code.as_bytes(), &[b'f', b'o', b'o']); + assert_eq!(String::from(code), "foo"); + } + + #[test] + #[allow(clippy::byte_char_slices)] + fn print_ascii_bytes() { + let mut code = CodeBuffer::new(); + code.print_ascii_bytes([b'f', b'o', b'o']); + + assert_eq!(code.len(), 3); + assert_eq!(code.as_bytes(), &[b'f', b'o', b'o']); + assert_eq!(String::from(code), "foo"); + } + + #[test] + fn peek_nth_char_back() { + let mut code = CodeBuffer::new(); + code.print_str("bar"); + + assert_eq!(code.peek_nth_char_back(0), Some('r')); + assert_eq!(code.peek_nth_char_back(1), Some('a')); + assert_eq!(code.peek_nth_char_back(2), Some('b')); + assert_eq!(code.peek_nth_char_back(3), None); + } + + #[test] + fn peek_nth_byte_back() { + let mut code = CodeBuffer::new(); + code.print_str("bar"); + + assert_eq!(code.peek_nth_byte_back(0), Some(b'r')); + assert_eq!(code.peek_nth_byte_back(1), Some(b'a')); + assert_eq!(code.peek_nth_byte_back(2), Some(b'b')); + assert_eq!(code.peek_nth_byte_back(3), None); + } + + #[test] + fn last_byte() { + let mut code = CodeBuffer::new(); + assert_eq!(code.last_byte(), None); + code.print_str("bar"); + assert_eq!(code.last_byte(), Some(b'r')); + } + + #[test] + fn last_char() { + let mut code = CodeBuffer::new(); + assert_eq!(code.last_char(), None); + code.print_str("bar"); + assert_eq!(code.last_char(), Some('r')); + } +} diff --git a/crates/oxc_codegen/src/comment.rs b/crates/oxc_codegen/src/comment.rs index 29bf12f4891eb..3db8da578844a 100644 --- a/crates/oxc_codegen/src/comment.rs +++ b/crates/oxc_codegen/src/comment.rs @@ -2,7 +2,7 @@ use daachorse::DoubleArrayAhoCorasick; use once_cell::sync::Lazy; use rustc_hash::FxHashMap; -use oxc_ast::{Comment, CommentKind, Trivias}; +use oxc_ast::{Comment, CommentKind}; use oxc_syntax::identifier::is_line_terminator; use crate::Codegen; @@ -16,13 +16,9 @@ static ANNOTATION_MATCHER: Lazy> = Lazy::new(|| { pub(crate) type CommentsMap = FxHashMap>; impl<'a> Codegen<'a> { - pub(crate) fn preserve_annotate_comments(&self) -> bool { - self.comment_options.preserve_annotate_comments && !self.options.minify - } - - pub(crate) fn build_comments(&mut self, trivias: &Trivias) { - for comment in trivias.comments().copied() { - self.comments.entry(comment.attached_to).or_default().push(comment); + pub(crate) fn build_comments(&mut self, comments: &[Comment]) { + for comment in comments { + self.comments.entry(comment.attached_to).or_default().push(*comment); } } @@ -31,35 +27,33 @@ impl<'a> Codegen<'a> { } pub(crate) fn has_annotation_comment(&self, start: u32) -> bool { - if !self.preserve_annotate_comments() { + if !self.options.print_annotation_comments() { return false; } - let Some(source_text) = self.source_text else { return false }; self.comments.get(&start).is_some_and(|comments| { - comments.iter().any(|comment| Self::is_annotation_comment(comment, source_text)) + comments.iter().any(|comment| self.is_annotation_comment(comment)) }) } pub(crate) fn has_non_annotation_comment(&self, start: u32) -> bool { - if !self.preserve_annotate_comments() { + if !self.options.print_annotation_comments() { return self.has_comment(start); } - let Some(source_text) = self.source_text else { return false }; self.comments.get(&start).is_some_and(|comments| { - comments.iter().any(|comment| !Self::is_annotation_comment(comment, source_text)) + comments.iter().any(|comment| !self.is_annotation_comment(comment)) }) } /// Weather to keep leading comments. - fn is_leading_comments(comment: &Comment, source_text: &str) -> bool { - (comment.is_jsdoc(source_text) || (comment.is_line() && Self::is_annotation_comment(comment, source_text))) + fn is_leading_comments(&self, comment: &Comment) -> bool { + (comment.is_jsdoc(self.source_text) || (comment.is_line() && self.is_annotation_comment(comment))) && comment.preceded_by_newline // webpack comment `/*****/` - && !comment.span.source_text(source_text).chars().all(|c| c == '*') + && !comment.span.source_text(self.source_text).chars().all(|c| c == '*') } - fn print_comment(&mut self, comment: &Comment, source_text: &str) { - let comment_source = comment.real_span().source_text(source_text); + fn print_comment(&mut self, comment: &Comment) { + let comment_source = comment.real_span().source_text(self.source_text); match comment.kind { CommentKind::Line => { self.print_str(comment_source); @@ -84,18 +78,16 @@ impl<'a> Codegen<'a> { if self.options.minify { return; } - let Some(source_text) = self.source_text else { return }; let Some(comments) = self.comments.remove(&start) else { return; }; - let (comments, unused_comments): (Vec<_>, Vec<_>) = comments - .into_iter() - .partition(|comment| Self::is_leading_comments(comment, source_text)); + let (comments, unused_comments): (Vec<_>, Vec<_>) = + comments.into_iter().partition(|comment| self.is_leading_comments(comment)); if comments.first().is_some_and(|c| c.preceded_by_newline) { // Skip printing newline if this comment is already on a newline. - if self.peek_nth(0).is_some_and(|c| c != '\n' && c != '\t') { + if self.last_byte().is_some_and(|b| b != b'\n' && b != b'\t') { self.print_hard_newline(); self.print_indent(); } @@ -107,7 +99,7 @@ impl<'a> Codegen<'a> { self.print_indent(); } - self.print_comment(comment, source_text); + self.print_comment(comment); } if comments.last().is_some_and(|c| c.is_line() || c.followed_by_newline) { @@ -120,27 +112,32 @@ impl<'a> Codegen<'a> { } } - fn is_annotation_comment(comment: &Comment, source_text: &str) -> bool { - let comment_content = comment.span.source_text(source_text); + fn is_annotation_comment(&self, comment: &Comment) -> bool { + let comment_content = comment.span.source_text(self.source_text); ANNOTATION_MATCHER.find_iter(comment_content).count() != 0 } pub(crate) fn print_annotation_comments(&mut self, node_start: u32) { - if !self.preserve_annotate_comments() { + if !self.options.print_annotation_comments() { return; } // If there is has annotation comments awaiting move to here, print them. let start = self.start_of_annotation_comment.take().unwrap_or(node_start); - let Some(source_text) = self.source_text else { return }; let Some(comments) = self.comments.remove(&start) else { return }; for comment in comments { - if !Self::is_annotation_comment(&comment, source_text) { + if !self.is_annotation_comment(&comment) { continue; } - self.print_str(comment.real_span().source_text(source_text)); + if comment.is_line() { + self.print_str("/*"); + self.print_str(comment.span.source_text(self.source_text)); + self.print_str("*/"); + } else { + self.print_str(comment.real_span().source_text(self.source_text)); + } self.print_hard_space(); } } @@ -149,12 +146,10 @@ impl<'a> Codegen<'a> { if self.options.minify { return false; } - let Some(source_text) = self.source_text else { return false }; let Some(comments) = self.comments.remove(&start) else { return false }; - let (annotation_comments, comments): (Vec<_>, Vec<_>) = comments - .into_iter() - .partition(|comment| Self::is_annotation_comment(comment, source_text)); + let (annotation_comments, comments): (Vec<_>, Vec<_>) = + comments.into_iter().partition(|comment| self.is_annotation_comment(comment)); if !annotation_comments.is_empty() { self.comments.insert(start, annotation_comments); @@ -163,7 +158,7 @@ impl<'a> Codegen<'a> { for comment in &comments { self.print_hard_newline(); self.print_indent(); - self.print_comment(comment, source_text); + self.print_comment(comment); } if comments.is_empty() { diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 93fc53b9fd29b..9e746c64ffaaf 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, ops::Not}; +use std::ops::Not; use cow_utils::CowUtils; #[allow(clippy::wildcard_imports)] @@ -64,7 +64,7 @@ impl<'a> Gen for Directive<'a> { p.wrap_quote(|p, _| { p.print_str(self.directive.as_str()); }); - p.print_char(b';'); + p.print_ascii_byte(b';'); p.print_soft_newline(); } } @@ -168,9 +168,9 @@ impl<'a> Gen for IfStatement<'a> { fn print_if(if_stmt: &IfStatement<'_>, p: &mut Codegen, ctx: Context) { p.print_str("if"); p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); p.print_expression(&if_stmt.test); - p.print_char(b')'); + p.print_ascii_byte(b')'); match &if_stmt.consequent { Statement::BlockStatement(block) => { @@ -252,7 +252,7 @@ impl<'a> Gen for ForStatement<'a> { p.print_indent(); p.print_str("for"); p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); if let Some(init) = &self.init { init.print(p, Context::FORBID_IN); @@ -272,7 +272,7 @@ impl<'a> Gen for ForStatement<'a> { p.print_expression(update); } - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_body(&self.body, false, ctx); } } @@ -283,14 +283,14 @@ impl<'a> Gen for ForInStatement<'a> { p.print_indent(); p.print_str("for"); p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); self.left.print(p, Context::empty().and_forbid_in(false)); p.print_soft_space(); p.print_space_before_identifier(); p.print_str("in"); p.print_hard_space(); p.print_expression(&self.right); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_body(&self.body, false, ctx); } } @@ -304,13 +304,13 @@ impl<'a> Gen for ForOfStatement<'a> { p.print_str(" await"); } p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); self.left.print(p, ctx); p.print_soft_space(); p.print_space_before_identifier(); p.print_str("of "); self.right.print_expr(p, Precedence::Comma, Context::empty()); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_body(&self.body, false, ctx); } } @@ -347,9 +347,9 @@ impl<'a> Gen for WhileStatement<'a> { p.print_indent(); p.print_str("while"); p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); p.print_expression(&self.test); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_body(&self.body, false, ctx); } } @@ -372,9 +372,9 @@ impl<'a> Gen for DoWhileStatement<'a> { } p.print_str("while"); p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); p.print_expression(&self.test); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_semicolon_after_statement(); } } @@ -420,9 +420,9 @@ impl<'a> Gen for SwitchStatement<'a> { p.print_indent(); p.print_str("switch"); p.print_soft_space(); - p.print_char(b'('); + p.print_ascii_byte(b'('); p.print_expression(&self.discriminant); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_soft_space(); p.print_curly_braces(self.span, self.cases.is_empty(), |p| { for case in &self.cases { @@ -535,9 +535,9 @@ impl<'a> Gen for WithStatement<'a> { p.add_source_mapping(self.span.start); p.print_indent(); p.print_str("with"); - p.print_char(b'('); + p.print_ascii_byte(b'('); p.print_expression(&self.object); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_body(&self.body, false, ctx); } } @@ -558,7 +558,7 @@ impl<'a> Gen for VariableDeclaration<'a> { p.print_str("declare "); } - if p.preserve_annotate_comments() + if p.options.print_annotation_comments() && p.start_of_annotation_comment.is_none() && matches!(self.kind, VariableDeclarationKind::Const) && matches!(self.declarations.first(), Some(VariableDeclarator { init: Some(init), .. }) if init.is_function()) @@ -585,7 +585,7 @@ impl<'a> Gen for VariableDeclarator<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { self.id.kind.print(p, ctx); if self.definite { - p.print_char(b'!'); + p.print_ascii_byte(b'!'); } if self.id.optional { p.print_str("?"); @@ -621,7 +621,7 @@ impl<'a> Gen for Function<'a> { } p.print_str("function"); if self.generator { - p.print_char(b'*'); + p.print_ascii_byte(b'*'); p.print_soft_space(); } if let Some(id) = &self.id { @@ -631,7 +631,7 @@ impl<'a> Gen for Function<'a> { if let Some(type_parameters) = &self.type_parameters { type_parameters.print(p, ctx); } - p.print_char(b'('); + p.print_ascii_byte(b'('); if let Some(this_param) = &self.this_param { this_param.print(p, ctx); if !self.params.is_empty() || self.params.rest.is_some() { @@ -640,7 +640,7 @@ impl<'a> Gen for Function<'a> { p.print_soft_space(); } self.params.print(p, ctx); - p.print_char(b')'); + p.print_ascii_byte(b')'); if let Some(return_type) = &self.return_type { p.print_str(": "); return_type.print(p, ctx); @@ -714,9 +714,9 @@ impl<'a> Gen for ImportDeclaration<'a> { p.print_soft_space(); p.print_str("from"); p.print_soft_space(); - p.print_char(b'"'); + p.print_ascii_byte(b'"'); p.print_str(self.source.value.as_str()); - p.print_char(b'"'); + p.print_ascii_byte(b'"'); if let Some(with_clause) = &self.with_clause { p.print_hard_space(); with_clause.print(p, ctx); @@ -761,7 +761,7 @@ impl<'a> Gen for ImportDeclaration<'a> { p.print_soft_space(); } in_block = true; - p.print_char(b'{'); + p.print_ascii_byte(b'{'); p.print_soft_space(); } @@ -781,7 +781,7 @@ impl<'a> Gen for ImportDeclaration<'a> { } if in_block { p.print_soft_space(); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); } p.print_str(" from "); } @@ -825,7 +825,7 @@ impl<'a> Gen for ExportNamedDeclaration<'a> { p.add_source_mapping(self.span.start); p.print_indent(); - if p.preserve_annotate_comments() { + if p.options.print_annotation_comments() { match &self.declaration { Some(Declaration::FunctionDeclaration(_)) => { p.print_annotation_comments(self.span.start); @@ -871,13 +871,13 @@ impl<'a> Gen for ExportNamedDeclaration<'a> { } } None => { - p.print_char(b'{'); + p.print_ascii_byte(b'{'); if !self.specifiers.is_empty() { p.print_soft_space(); p.print_list(&self.specifiers, ctx); p.print_soft_space(); } - p.print_char(b'}'); + p.print_ascii_byte(b'}'); if let Some(source) = &self.source { p.print_soft_space(); p.print_str("from"); @@ -954,7 +954,7 @@ impl<'a> Gen for ExportAllDeclaration<'a> { if self.export_kind.is_type() { p.print_str("type "); } - p.print_char(b'*'); + p.print_ascii_byte(b'*'); if let Some(exported) = &self.exported { p.print_str(" as "); @@ -1194,20 +1194,17 @@ impl<'a> Gen for BigIntLiteral<'a> { impl<'a> Gen for RegExpLiteral<'a> { fn gen(&self, p: &mut Codegen, _ctx: Context) { p.add_source_mapping(self.span.start); - let last = p.peek_nth(0); - let pattern_text = p.source_text.map_or_else( - || Cow::Owned(self.regex.pattern.to_string()), - |src| self.regex.pattern.source_text(src), - ); + let last = p.last_byte(); + let pattern_text = self.regex.pattern.source_text(p.source_text); // Avoid forming a single-line comment or " GenExpr for ComputedMemberExpression<'a> { if self.optional { p.print_str("?."); } - p.print_char(b'['); + p.print_ascii_byte(b'['); self.expression.print_expr(p, Precedence::Lowest, Context::empty()); - p.print_char(b']'); + p.print_ascii_byte(b']'); } } @@ -1343,12 +1340,12 @@ impl<'a> GenExpr for StaticMemberExpression<'a> { fn gen_expr(&self, p: &mut Codegen, _precedence: Precedence, ctx: Context) { self.object.print_expr(p, Precedence::Postfix, ctx.intersection(Context::FORBID_CALL)); if self.optional { - p.print_char(b'?'); + p.print_ascii_byte(b'?'); } else if p.need_space_before_dot == p.code_len() { // `0.toExponential()` is invalid, add a space before the dot, `0 .toExponential()` is valid p.print_hard_space(); } - p.print_char(b'.'); + p.print_ascii_byte(b'.'); self.property.print(p, ctx); } } @@ -1359,7 +1356,7 @@ impl<'a> GenExpr for PrivateFieldExpression<'a> { if self.optional { p.print_str("?"); } - p.print_char(b'.'); + p.print_ascii_byte(b'.'); self.field.print(p, ctx); } } @@ -1385,21 +1382,23 @@ impl<'a> GenExpr for CallExpression<'a> { if let Some(type_parameters) = &self.type_parameters { type_parameters.print(p, ctx); } - p.print_char(b'('); - let has_comment = (self.span.end > 0 && p.has_comment(self.span.end - 1)) + p.print_ascii_byte(b'('); + let has_comment_before_right_paren = + self.span.end > 0 && p.has_comment(self.span.end - 1); + let has_comment = has_comment_before_right_paren || self.arguments.iter().any(|item| p.has_comment(item.span().start)); if has_comment { p.indent(); p.print_list_with_comments(&self.arguments, ctx); // Handle `/* comment */);` - if !p.print_expr_comments(self.span.end - 1) { + if !has_comment_before_right_paren || !p.print_expr_comments(self.span.end - 1) { p.print_soft_newline(); } p.dedent(); } else { p.print_list(&self.arguments, ctx); } - p.print_char(b')'); + p.print_ascii_byte(b')'); p.add_source_mapping(self.span.end); }); } @@ -1440,7 +1439,7 @@ impl<'a> Gen for ArrayExpression<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { let is_multi_line = self.elements.len() > 2; p.add_source_mapping(self.span.start); - p.print_char(b'['); + p.print_ascii_byte(b'['); if is_multi_line { p.indent(); } @@ -1465,7 +1464,7 @@ impl<'a> Gen for ArrayExpression<'a> { p.print_indent(); } p.add_source_mapping(self.span.end); - p.print_char(b']'); + p.print_ascii_byte(b']'); } } @@ -1477,7 +1476,7 @@ impl<'a> GenExpr for ObjectExpression<'a> { let wrap = p.start_of_stmt == n || p.start_of_arrow_expr == n; p.wrap(wrap, |p| { p.add_source_mapping(self.span.start); - p.print_char(b'{'); + p.print_ascii_byte(b'{'); if is_multi_line { p.indent(); } @@ -1501,7 +1500,7 @@ impl<'a> GenExpr for ObjectExpression<'a> { p.print_soft_space(); } p.add_source_mapping(self.span.end); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); }); } } @@ -1540,18 +1539,18 @@ impl<'a> Gen for ObjectProperty<'a> { p.print_str("*"); } if self.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); } self.key.print(p, ctx); if self.computed { - p.print_char(b']'); + p.print_ascii_byte(b']'); } if let Some(type_parameters) = &func.type_parameters { type_parameters.print(p, ctx); } - p.print_char(b'('); + p.print_ascii_byte(b'('); func.params.print(p, ctx); - p.print_char(b')'); + p.print_ascii_byte(b')'); if let Some(body) = &func.body { p.print_soft_space(); body.print(p, ctx); @@ -1570,13 +1569,13 @@ impl<'a> Gen for ObjectProperty<'a> { } if self.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); } if !shorthand { self.key.print(p, ctx); } if self.computed { - p.print_char(b']'); + p.print_ascii_byte(b']'); } if !shorthand { p.print_colon(); @@ -1615,9 +1614,9 @@ impl<'a> GenExpr for ArrowFunctionExpression<'a> { type_parameters.print(p, ctx); } p.add_source_mapping(self.span.start); - p.print_char(b'('); + p.print_ascii_byte(b'('); self.params.print(p, ctx); - p.print_char(b')'); + p.print_ascii_byte(b')'); if let Some(return_type) = &self.return_type { p.print_str(":"); p.print_soft_space(); @@ -1645,7 +1644,7 @@ impl<'a> GenExpr for YieldExpression<'a> { p.print_space_before_identifier(); p.print_str("yield"); if self.delegate { - p.print_char(b'*'); + p.print_ascii_byte(b'*'); p.print_soft_space(); } if let Some(argument) = self.argument.as_ref() { @@ -1740,7 +1739,7 @@ impl<'a> GenExpr for LogicalExpression<'a> { precedence, ctx, left_precedence: Precedence::Lowest, - left_ctx: Context::empty(), + left_ctx: ctx, operator: BinaryishOperator::Logical(self.operator), wrap: false, right_precedence: Precedence::Lowest, @@ -1759,7 +1758,7 @@ impl<'a> GenExpr for ConditionalExpression<'a> { p.wrap(wrap, |p| { self.test.print_expr(p, Precedence::Conditional, ctx & Context::FORBID_IN); p.print_soft_space(); - p.print_char(b'?'); + p.print_ascii_byte(b'?'); p.print_soft_space(); self.consequent.print_expr(p, Precedence::Yield, Context::empty()); p.print_soft_space(); @@ -1831,7 +1830,7 @@ impl<'a> Gen for AssignmentTargetPattern<'a> { impl<'a> Gen for ArrayAssignmentTarget<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); - p.print_char(b'['); + p.print_ascii_byte(b'['); for (index, item) in self.elements.iter().enumerate() { if index != 0 { p.print_comma(); @@ -1851,7 +1850,7 @@ impl<'a> Gen for ArrayAssignmentTarget<'a> { if self.trailing_comma.is_some() { p.print_comma(); } - p.print_char(b']'); + p.print_ascii_byte(b']'); p.add_source_mapping(self.span.end); } } @@ -1859,7 +1858,7 @@ impl<'a> Gen for ArrayAssignmentTarget<'a> { impl<'a> Gen for ObjectAssignmentTarget<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); - p.print_char(b'{'); + p.print_ascii_byte(b'{'); p.print_list(&self.properties, ctx); if let Some(target) = &self.rest { if !self.properties.is_empty() { @@ -1868,7 +1867,7 @@ impl<'a> Gen for ObjectAssignmentTarget<'a> { p.add_source_mapping(self.span.start); target.print(p, ctx); } - p.print_char(b'}'); + p.print_ascii_byte(b'}'); p.add_source_mapping(self.span.end); } } @@ -1932,9 +1931,9 @@ impl<'a> Gen for AssignmentTargetPropertyProperty<'a> { ident.print(p, ctx); } key @ match_expression!(PropertyKey) => { - p.print_char(b'['); + p.print_ascii_byte(b'['); key.to_expression().print_expr(p, Precedence::Comma, Context::empty()); - p.print_char(b']'); + p.print_ascii_byte(b']'); } } p.print_colon(); @@ -1951,9 +1950,9 @@ impl<'a> Gen for AssignmentTargetRest<'a> { } impl<'a> GenExpr for SequenceExpression<'a> { - fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, _ctx: Context) { + fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { p.wrap(precedence >= self.precedence(), |p| { - p.print_expressions(&self.expressions, Precedence::Lowest, Context::empty()); + p.print_expressions(&self.expressions, Precedence::Lowest, ctx); }); } } @@ -1961,7 +1960,8 @@ impl<'a> GenExpr for SequenceExpression<'a> { impl<'a> GenExpr for ImportExpression<'a> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { let wrap = precedence >= Precedence::New || ctx.intersects(Context::FORBID_CALL); - let has_comment = (self.span.end > 0 && p.has_comment(self.span.end - 1)) + let has_comment_before_right_paren = self.span.end > 0 && p.has_comment(self.span.end - 1); + let has_comment = has_comment_before_right_paren || p.has_comment(self.source.span().start) || self.arguments.first().is_some_and(|argument| p.has_comment(argument.span().start)); @@ -1995,14 +1995,14 @@ impl<'a> GenExpr for ImportExpression<'a> { } p.dedent(); } - p.print_char(b')'); + p.print_ascii_byte(b')'); }); } } impl<'a> Gen for TemplateLiteral<'a> { fn gen(&self, p: &mut Codegen, _ctx: Context) { - p.print_char(b'`'); + p.print_ascii_byte(b'`'); let mut expressions = self.expressions.iter(); for quasi in &self.quasis { @@ -2012,11 +2012,11 @@ impl<'a> Gen for TemplateLiteral<'a> { if let Some(expr) = expressions.next() { p.print_str("${"); p.print_expression(expr); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); } } - p.print_char(b'`'); + p.print_ascii_byte(b'`'); } } @@ -2050,12 +2050,12 @@ impl<'a> GenExpr for AwaitExpression<'a> { impl<'a> GenExpr for ChainExpression<'a> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { - match &self.expression { + p.wrap(precedence >= Precedence::Postfix, |p| match &self.expression { ChainElement::CallExpression(expr) => expr.print_expr(p, precedence, ctx), match_member_expression!(ChainElement) => { self.expression.to_member_expression().print_expr(p, precedence, ctx); } - } + }); } } @@ -2071,7 +2071,7 @@ impl<'a> GenExpr for NewExpression<'a> { p.add_source_mapping(self.span.start); p.print_str("new "); self.callee.print_expr(p, Precedence::New, Context::FORBID_CALL); - p.print_char(b'('); + p.print_ascii_byte(b'('); let has_comment = p.has_comment(self.span.end - 1) || self.arguments.iter().any(|item| p.has_comment(item.span().start)); if has_comment { @@ -2085,32 +2085,32 @@ impl<'a> GenExpr for NewExpression<'a> { } else { p.print_list(&self.arguments, ctx); } - p.print_char(b')'); + p.print_ascii_byte(b')'); }); } } impl<'a> GenExpr for TSAsExpression<'a> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { - p.print_char(b'('); - p.print_char(b'('); - self.expression.print_expr(p, precedence, Context::default()); - p.print_char(b')'); - p.print_str(" as "); - self.type_annotation.print(p, ctx); - p.print_char(b')'); + let wrap = precedence >= Precedence::Shift; + + p.wrap(wrap, |p| { + self.expression.print_expr(p, Precedence::Exponentiation, ctx); + p.print_str(" as "); + self.type_annotation.print(p, ctx); + }); } } impl<'a> GenExpr for TSSatisfiesExpression<'a> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { - p.print_char(b'('); - p.print_char(b'('); + p.print_ascii_byte(b'('); + p.print_ascii_byte(b'('); self.expression.print_expr(p, precedence, Context::default()); - p.print_char(b')'); + p.print_ascii_byte(b')'); p.print_str(" satisfies "); self.type_annotation.print(p, ctx); - p.print_char(b')'); + p.print_ascii_byte(b')'); } } @@ -2119,7 +2119,7 @@ impl<'a> GenExpr for TSNonNullExpression<'a> { p.wrap(matches!(self.expression, Expression::ParenthesizedExpression(_)), |p| { self.expression.print_expr(p, precedence, ctx); }); - p.print_char(b'!'); + p.print_ascii_byte(b'!'); if p.options.minify { p.print_hard_space(); } @@ -2156,7 +2156,7 @@ impl<'a> Gen for MetaProperty<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); self.meta.print(p, ctx); - p.print_char(b'.'); + p.print_ascii_byte(b'.'); self.property.print(p, ctx); } } @@ -2267,7 +2267,7 @@ impl<'a> Gen for JSXMemberExpressionObject<'a> { impl<'a> Gen for JSXMemberExpression<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { self.object.print(p, ctx); - p.print_char(b'.'); + p.print_ascii_byte(b'.'); self.property.print(p, ctx); } } @@ -2326,9 +2326,9 @@ impl<'a> Gen for JSXExpression<'a> { impl<'a> Gen for JSXExpressionContainer<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { - p.print_char(b'{'); + p.print_ascii_byte(b'{'); self.expression.print(p, ctx); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); } } @@ -2339,9 +2339,9 @@ impl<'a> Gen for JSXAttributeValue<'a> { Self::Element(el) => el.print(p, ctx), Self::StringLiteral(lit) => { let quote = if lit.value.contains('"') { b'\'' } else { b'"' }; - p.print_char(quote); + p.print_ascii_byte(quote); p.print_str(&lit.value); - p.print_char(quote); + p.print_ascii_byte(quote); } Self::ExpressionContainer(expr_container) => expr_container.print(p, ctx), } @@ -2352,7 +2352,7 @@ impl<'a> Gen for JSXSpreadAttribute<'a> { fn gen(&self, p: &mut Codegen, _ctx: Context) { p.print_str("{..."); self.argument.print_expr(p, Precedence::Comma, Context::empty()); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); } } @@ -2368,7 +2368,7 @@ impl<'a> Gen for JSXAttributeItem<'a> { impl<'a> Gen for JSXOpeningElement<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); - p.print_char(b'<'); + p.print_ascii_byte(b'<'); self.name.print(p, ctx); for attr in &self.attributes { match attr { @@ -2385,7 +2385,7 @@ impl<'a> Gen for JSXOpeningElement<'a> { p.print_soft_space(); p.print_str("/"); } - p.print_char(b'>'); + p.print_ascii_byte(b'>'); } } @@ -2394,7 +2394,7 @@ impl<'a> Gen for JSXClosingElement<'a> { p.add_source_mapping(self.span.start); p.print_str("'); + p.print_ascii_byte(b'>'); } } @@ -2513,21 +2513,21 @@ impl<'a> Gen for MethodDefinition<'a> { } if self.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); } self.key.print(p, ctx); if self.computed { - p.print_char(b']'); + p.print_ascii_byte(b']'); } if self.optional { - p.print_char(b'?'); + p.print_ascii_byte(b'?'); } if let Some(type_parameters) = self.value.type_parameters.as_ref() { type_parameters.print(p, ctx); } - p.print_char(b'('); + p.print_ascii_byte(b'('); self.value.params.print(p, ctx); - p.print_char(b')'); + p.print_ascii_byte(b')'); if let Some(return_type) = &self.value.return_type { p.print_colon(); p.print_soft_space(); @@ -2566,11 +2566,11 @@ impl<'a> Gen for PropertyDefinition<'a> { p.print_str("readonly "); } if self.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); } self.key.print(p, ctx); if self.computed { - p.print_char(b']'); + p.print_ascii_byte(b']'); } if self.optional { p.print_str("?"); @@ -2609,13 +2609,13 @@ impl<'a> Gen for AccessorProperty<'a> { p.print_str("accessor"); if self.computed { p.print_soft_space(); - p.print_char(b'['); + p.print_ascii_byte(b'['); } else { p.print_hard_space(); } self.key.print(p, ctx); if self.computed { - p.print_char(b']'); + p.print_ascii_byte(b']'); } if let Some(type_annotation) = &self.type_annotation { p.print_colon(); @@ -2634,7 +2634,7 @@ impl<'a> Gen for AccessorProperty<'a> { impl<'a> Gen for PrivateIdentifier<'a> { fn gen(&self, p: &mut Codegen, _ctx: Context) { p.add_source_mapping_for_name(self.span, &self.name); - p.print_char(b'#'); + p.print_ascii_byte(b'#'); p.print_str(self.name.as_str()); } } @@ -2667,7 +2667,7 @@ impl<'a> Gen for BindingPatternKind<'a> { impl<'a> Gen for ObjectPattern<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); - p.print_char(b'{'); + p.print_ascii_byte(b'{'); if !self.is_empty() { p.print_soft_space(); } @@ -2681,7 +2681,7 @@ impl<'a> Gen for ObjectPattern<'a> { if !self.is_empty() { p.print_soft_space(); } - p.print_char(b'}'); + p.print_ascii_byte(b'}'); p.add_source_mapping(self.span.end); } } @@ -2690,7 +2690,7 @@ impl<'a> Gen for BindingProperty<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); if self.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); } let mut shorthand = false; @@ -2718,7 +2718,7 @@ impl<'a> Gen for BindingProperty<'a> { self.key.print(p, ctx); } if self.computed { - p.print_char(b']'); + p.print_ascii_byte(b']'); } if !shorthand { p.print_colon(); @@ -2739,7 +2739,7 @@ impl<'a> Gen for BindingRestElement<'a> { impl<'a> Gen for ArrayPattern<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span.start); - p.print_char(b'['); + p.print_ascii_byte(b'['); for (index, item) in self.elements.iter().enumerate() { if index != 0 { p.print_comma(); @@ -2756,7 +2756,7 @@ impl<'a> Gen for ArrayPattern<'a> { p.print_soft_space(); rest.print(p, ctx); } - p.print_char(b']'); + p.print_ascii_byte(b']'); p.add_source_mapping(self.span.end); } } @@ -2790,7 +2790,7 @@ impl<'a> Gen for Decorator<'a> { } p.add_source_mapping(self.span.start); - p.print_char(b'@'); + p.print_ascii_byte(b'@'); let wrap = need_wrap(&self.expression); p.wrap(wrap, |p| { self.expression.print_expr(p, Precedence::Lowest, Context::empty()); @@ -2810,7 +2810,7 @@ impl<'a> Gen for TSClassImplements<'a> { impl<'a> Gen for TSTypeParameterDeclaration<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { let is_multi_line = self.params.len() >= 2; - p.print_char(b'<'); + p.print_ascii_byte(b'<'); if is_multi_line { p.indent(); } @@ -2831,7 +2831,7 @@ impl<'a> Gen for TSTypeParameterDeclaration<'a> { p.dedent(); p.print_indent(); } - p.print_char(b'>'); + p.print_ascii_byte(b'>'); } } @@ -2920,9 +2920,9 @@ impl<'a> Gen for TSUnionType<'a> { impl<'a> Gen for TSParenthesizedType<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { - p.print_char(b'('); + p.print_ascii_byte(b'('); self.type_annotation.print(p, ctx); - p.print_char(b')'); + p.print_ascii_byte(b')'); } } @@ -3230,9 +3230,9 @@ impl<'a> Gen for TSSignature<'a> { p.print_str("readonly "); } if signature.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); signature.key.print(p, ctx); - p.print_char(b']'); + p.print_ascii_byte(b']'); } else { match &signature.key { PropertyKey::StaticIdentifier(key) => { @@ -3296,9 +3296,9 @@ impl<'a> Gen for TSSignature<'a> { TSMethodSignatureKind::Set => p.print_str("set "), } if signature.computed { - p.print_char(b'['); + p.print_ascii_byte(b'['); signature.key.print(p, ctx); - p.print_char(b']'); + p.print_ascii_byte(b']'); } else { match &signature.key { PropertyKey::StaticIdentifier(key) => { @@ -3370,7 +3370,7 @@ impl<'a> Gen for TSImportType<'a> { } p.print_str(")"); if let Some(qualifier) = &self.qualifier { - p.print_char(b'.'); + p.print_ascii_byte(b'.'); qualifier.print(p, ctx); } if let Some(type_parameters) = &self.type_parameters { @@ -3381,18 +3381,18 @@ impl<'a> Gen for TSImportType<'a> { impl<'a> Gen for TSImportAttributes<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { - p.print_char(b'{'); + p.print_ascii_byte(b'{'); p.print_soft_space(); self.attributes_keyword.print(p, ctx); p.print_str(":"); p.print_soft_space(); - p.print_char(b'{'); + p.print_ascii_byte(b'{'); p.print_soft_space(); p.print_list(&self.elements, ctx); p.print_soft_space(); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); p.print_soft_space(); - p.print_char(b'}'); + p.print_ascii_byte(b'}'); } } @@ -3488,7 +3488,7 @@ impl<'a> Gen for TSModuleDeclaration<'a> { loop { match body { TSModuleDeclarationBody::TSModuleDeclaration(b) => { - p.print_char(b'.'); + p.print_ascii_byte(b'.'); b.id.print(p, ctx); if let Some(b) = &b.body { body = b; diff --git a/crates/oxc_codegen/src/lib.rs b/crates/oxc_codegen/src/lib.rs index 40871c85cc1b6..27cca0eb325c3 100644 --- a/crates/oxc_codegen/src/lib.rs +++ b/crates/oxc_codegen/src/lib.rs @@ -4,29 +4,29 @@ //! * [esbuild](https://github.com/evanw/esbuild/blob/main/internal/js_printer/js_printer.go) mod binary_expr_visitor; +mod code_buffer; mod comment; mod context; mod gen; mod operator; mod sourcemap_builder; -use std::borrow::Cow; +use std::{borrow::Cow, path::PathBuf}; -use oxc_ast::{ - ast::{BindingIdentifier, BlockStatement, Expression, IdentifierReference, Program, Statement}, - Trivias, +use oxc_ast::ast::{ + BindingIdentifier, BlockStatement, Expression, IdentifierReference, Program, Statement, }; use oxc_mangler::Mangler; use oxc_span::{GetSpan, Span}; use oxc_syntax::{ - identifier::is_identifier_part, + identifier::{is_identifier_part, is_identifier_part_ascii}, operator::{BinaryOperator, UnaryOperator, UpdateOperator}, precedence::Precedence, }; use crate::{ - binary_expr_visitor::BinaryExpressionVisitor, comment::CommentsMap, operator::Operator, - sourcemap_builder::SourcemapBuilder, + binary_expr_visitor::BinaryExpressionVisitor, code_buffer::CodeBuffer, comment::CommentsMap, + operator::Operator, sourcemap_builder::SourcemapBuilder, }; pub use crate::{ context::Context, @@ -36,7 +36,7 @@ pub use crate::{ /// Code generator without whitespace removal. pub type CodeGenerator<'a> = Codegen<'a>; -#[derive(Default, Clone, Copy)] +#[derive(Debug, Clone)] pub struct CodegenOptions { /// Use single quotes instead of double quotes. /// @@ -47,52 +47,63 @@ pub struct CodegenOptions { /// /// Default is `false`. pub minify: bool, + + /// Print comments? + /// + /// Default is `true`. + pub comments: bool, + + /// Print annotation comments, e.g. `/* #__PURE__ */` and `/* #__NO_SIDE_EFFECTS__ */`. + /// + /// Only takes into effect when `comments` is false. + /// + /// Default is `false`. + pub annotation_comments: bool, + + pub source_map_path: Option, } -#[derive(Default, Clone, Copy)] -pub struct CommentOptions { - /// Enable preserve annotate comments, like `/* #__PURE__ */` and `/* #__NO_SIDE_EFFECTS__ */`. - pub preserve_annotate_comments: bool, +impl Default for CodegenOptions { + fn default() -> Self { + Self { + single_quote: false, + minify: false, + comments: true, + annotation_comments: false, + source_map_path: None, + } + } +} + +impl CodegenOptions { + fn print_annotation_comments(&self) -> bool { + !self.minify && (self.comments || self.annotation_comments) + } } /// Output from [`Codegen::build`] pub struct CodegenReturn { /// The generated source code. - pub source_text: String, + pub code: String, + /// The source map from the input source code to the generated source code. /// - /// You must use [`Codegen::enable_source_map`] for this to be [`Some`]. - pub source_map: Option, + /// You must set [`CodegenOptions::source_map_path`] for this to be [`Some`]. + pub map: Option, } pub struct Codegen<'a> { - options: CodegenOptions, - comment_options: CommentOptions, + pub(crate) options: CodegenOptions, /// Original source code of the AST - source_text: Option<&'a str>, + source_text: &'a str, - trivias: Trivias, comments: CommentsMap, - /// Start of comment that needs to be moved to the before VariableDeclarator - /// - /// For example: - /// ```js - /// /* @__NO_SIDE_EFFECTS__ */ export const a = function() { - /// }, b = 10000; - /// ``` - /// Should be generated as: - /// ```js - /// export const /* @__NO_SIDE_EFFECTS__ */ a = function() { - /// }, b = 10000; - /// ``` - start_of_annotation_comment: Option, - mangler: Option, /// Output Code - code: Vec, + code: CodeBuffer, // states prev_op_end: usize, @@ -109,6 +120,19 @@ pub struct Codegen<'a> { start_of_stmt: usize, start_of_arrow_expr: usize, start_of_default_export: usize, + /// Start of comment that needs to be moved to the before VariableDeclarator + /// + /// For example: + /// ```js + /// /* @__NO_SIDE_EFFECTS__ */ export const a = function() { + /// }, b = 10000; + /// ``` + /// Should be generated as: + /// ```js + /// export const /* @__NO_SIDE_EFFECTS__ */ a = function() { + /// }, b = 10000; + /// ``` + start_of_annotation_comment: Option, /// Track the current indentation level indent: u32, @@ -127,13 +151,13 @@ impl<'a> Default for Codegen<'a> { } impl<'a> From> for String { - fn from(mut val: Codegen<'a>) -> Self { + fn from(val: Codegen<'a>) -> Self { val.into_source_text() } } impl<'a> From> for Cow<'a, str> { - fn from(mut val: Codegen<'a>) -> Self { + fn from(val: Codegen<'a>) -> Self { Cow::Owned(val.into_source_text()) } } @@ -144,13 +168,11 @@ impl<'a> Codegen<'a> { pub fn new() -> Self { Self { options: CodegenOptions::default(), - comment_options: CommentOptions::default(), - source_text: None, - trivias: Trivias::default(), + source_text: "", comments: CommentsMap::default(), start_of_annotation_comment: None, mangler: None, - code: vec![], + code: CodeBuffer::default(), needs_semicolon: false, need_space_before_dot: 0, print_next_indent_as_space: false, @@ -167,55 +189,10 @@ impl<'a> Codegen<'a> { } } - /// Initialize the output code buffer to reduce memory reallocation. - /// Minification will reduce by at least half of the original size. - #[must_use] - pub fn with_capacity(mut self, source_text_len: usize) -> Self { - let capacity = if self.options.minify { source_text_len / 2 } else { source_text_len }; - // ensure space for at least `capacity` additional bytes without clobbering existing - // allocations. - self.code.reserve(capacity); - self - } - #[must_use] pub fn with_options(mut self, options: CodegenOptions) -> Self { - self.options = options; self.quote = if options.single_quote { b'\'' } else { b'"' }; - self - } - - /// Adds the source text of the original AST. - /// - /// The source code will be used with comments or for improving the generated output. It also - /// pre-allocates memory for the output code using [`Codegen::with_capacity`]. Note that if you - /// use this method alongside your own call to [`Codegen::with_capacity`], the larger of the - /// two will be used. - #[must_use] - pub fn with_source_text(mut self, source_text: &'a str) -> Self { - self.source_text = Some(source_text); - self.with_capacity(source_text.len()) - } - - /// Also sets the [Self::with_source_text] - #[must_use] - pub fn enable_comment( - mut self, - source_text: &'a str, - trivias: Trivias, - options: CommentOptions, - ) -> Self { - self.comment_options = options; - self.build_comments(&trivias); - self.trivias = trivias; - self.with_source_text(source_text) - } - - #[must_use] - pub fn enable_source_map(mut self, source_name: &str, source_text: &str) -> Self { - let mut sourcemap_builder = SourcemapBuilder::default(); - sourcemap_builder.with_name_and_source(source_name, source_text); - self.sourcemap_builder = Some(sourcemap_builder); + self.options = options; self } @@ -226,30 +203,41 @@ impl<'a> Codegen<'a> { } #[must_use] - pub fn build(mut self, program: &Program<'_>) -> CodegenReturn { + pub fn build(mut self, program: &Program<'a>) -> CodegenReturn { + self.quote = if self.options.single_quote { b'\'' } else { b'"' }; + self.source_text = program.source_text; + self.code.reserve(program.source_text.len()); + if self.options.print_annotation_comments() { + self.build_comments(&program.comments); + } + if let Some(path) = &self.options.source_map_path { + self.sourcemap_builder = Some(SourcemapBuilder::new(path, program.source_text)); + } + program.print(&mut self, Context::default()); - let source_text = self.into_source_text(); - let source_map = self.sourcemap_builder.map(SourcemapBuilder::into_sourcemap); - CodegenReturn { source_text, source_map } + let code = self.code.into_string(); + let map = self.sourcemap_builder.map(SourcemapBuilder::into_sourcemap); + CodegenReturn { code, map } } #[must_use] - pub fn into_source_text(&mut self) -> String { - // SAFETY: criteria of `from_utf8_unchecked` are met. - - unsafe { String::from_utf8_unchecked(std::mem::take(&mut self.code)) } + pub fn into_source_text(self) -> String { + self.code.into_string() } - /// Push a single character into the buffer + /// Push a single ASCII byte into the buffer. + /// + /// # Panics + /// Panics if `byte` is not an ASCII byte (`0 - 0x7F`). #[inline] - pub fn print_char(&mut self, ch: u8) { - self.code.push(ch); + pub fn print_ascii_byte(&mut self, byte: u8) { + self.code.print_ascii_byte(byte); } /// Push str into the buffer #[inline] pub fn print_str(&mut self, s: &str) { - self.code.extend(s.as_bytes()); + self.code.print_str(s); } #[inline] @@ -260,7 +248,7 @@ impl<'a> Codegen<'a> { // Private APIs impl<'a> Codegen<'a> { - fn code(&self) -> &Vec { + fn code(&self) -> &CodeBuffer { &self.code } @@ -271,51 +259,64 @@ impl<'a> Codegen<'a> { #[inline] fn print_soft_space(&mut self) { if !self.options.minify { - self.print_char(b' '); + self.print_ascii_byte(b' '); } } #[inline] fn print_hard_space(&mut self) { - self.print_char(b' '); + self.print_ascii_byte(b' '); } #[inline] fn print_soft_newline(&mut self) { if !self.options.minify { - self.print_char(b'\n'); + self.print_ascii_byte(b'\n'); } } #[inline] fn print_hard_newline(&mut self) { - self.print_char(b'\n'); + self.print_ascii_byte(b'\n'); } #[inline] fn print_semicolon(&mut self) { - self.print_char(b';'); + self.print_ascii_byte(b';'); } #[inline] fn print_comma(&mut self) { - self.print_char(b','); + self.print_ascii_byte(b','); } #[inline] fn print_space_before_identifier(&mut self) { - if self - .peek_nth(0) - .is_some_and(|ch| is_identifier_part(ch) || self.prev_reg_exp_end == self.code.len()) - { - self.print_hard_space(); + let Some(byte) = self.last_byte() else { return }; + + if self.prev_reg_exp_end != self.code.len() { + let is_identifier = if byte.is_ascii() { + // Fast path for ASCII (very common case) + is_identifier_part_ascii(byte as char) + } else { + is_identifier_part(self.last_char().unwrap()) + }; + if !is_identifier { + return; + } } + + self.print_hard_space(); } #[inline] - fn peek_nth(&self, n: usize) -> Option { - // SAFETY: criteria of `from_utf8_unchecked` are met. - unsafe { std::str::from_utf8_unchecked(self.code()) }.chars().nth_back(n) + fn last_byte(&self) -> Option { + self.code.last_byte() + } + + #[inline] + fn last_char(&self) -> Option { + self.code.last_char() } #[inline] @@ -342,7 +343,10 @@ impl<'a> Codegen<'a> { self.print_next_indent_as_space = false; return; } - self.code.extend(std::iter::repeat(b'\t').take(self.indent as usize)); + // SAFETY: this iterator only yields tabs, which are always valid ASCII characters. + unsafe { + self.code.print_bytes_unchecked(std::iter::repeat(b'\t').take(self.indent as usize)); + } } #[inline] @@ -369,12 +373,12 @@ impl<'a> Codegen<'a> { #[inline] fn print_colon(&mut self) { - self.print_char(b':'); + self.print_ascii_byte(b':'); } #[inline] fn print_equal(&mut self) { - self.print_char(b'='); + self.print_ascii_byte(b'='); } fn print_sequence(&mut self, items: &[T], ctx: Context) { @@ -386,7 +390,7 @@ impl<'a> Codegen<'a> { fn print_curly_braces(&mut self, span: Span, single_line: bool, op: F) { self.add_source_mapping(span.start); - self.print_char(b'{'); + self.print_ascii_byte(b'{'); if !single_line { self.print_soft_newline(); self.indent(); @@ -397,12 +401,12 @@ impl<'a> Codegen<'a> { self.print_indent(); } self.add_source_mapping(span.end); - self.print_char(b'}'); + self.print_ascii_byte(b'}'); } fn print_block_start(&mut self, position: u32) { self.add_source_mapping(position); - self.print_char(b'{'); + self.print_ascii_byte(b'{'); self.print_soft_newline(); self.indent(); } @@ -411,7 +415,7 @@ impl<'a> Codegen<'a> { self.dedent(); self.print_indent(); self.add_source_mapping(position); - self.print_char(b'}'); + self.print_ascii_byte(b'}'); } fn print_body(&mut self, stmt: &Statement<'_>, need_space: bool, ctx: Context) { @@ -543,7 +547,11 @@ impl<'a> Codegen<'a> { || ((prev == bin_op_sub || prev == un_op_neg) && (next == bin_op_sub || next == un_op_neg || next == un_op_pre_dec)) || (prev == un_op_post_dec && next == bin_op_gt) - || (prev == un_op_not && next == un_op_pre_dec && self.peek_nth(1) == Some('<')) + || (prev == un_op_not + && next == un_op_pre_dec + // `prev == UnaryOperator::LogicalNot` which means last byte is ASCII, + // and therefore previous character is 1 byte from end of buffer + && self.code.peek_nth_byte_back(1) == Some(b'<')) { self.print_hard_space(); } @@ -552,30 +560,30 @@ impl<'a> Codegen<'a> { #[inline] fn wrap(&mut self, wrap: bool, mut f: F) { if wrap { - self.print_char(b'('); + self.print_ascii_byte(b'('); } f(self); if wrap { - self.print_char(b')'); + self.print_ascii_byte(b')'); } } #[inline] fn wrap_quote(&mut self, mut f: F) { - self.print_char(self.quote); + self.print_ascii_byte(self.quote); f(self, self.quote); - self.print_char(self.quote); + self.print_ascii_byte(self.quote); } fn add_source_mapping(&mut self, position: u32) { if let Some(sourcemap_builder) = self.sourcemap_builder.as_mut() { - sourcemap_builder.add_source_mapping(&self.code, position, None); + sourcemap_builder.add_source_mapping(self.code.as_bytes(), position, None); } } fn add_source_mapping_for_name(&mut self, span: Span, name: &str) { if let Some(sourcemap_builder) = self.sourcemap_builder.as_mut() { - sourcemap_builder.add_source_mapping_for_name(&self.code, span, name); + sourcemap_builder.add_source_mapping_for_name(self.code.as_bytes(), span, name); } } } diff --git a/crates/oxc_codegen/src/sourcemap_builder.rs b/crates/oxc_codegen/src/sourcemap_builder.rs index 31ddb113d7588..a520ef9c3b738 100644 --- a/crates/oxc_codegen/src/sourcemap_builder.rs +++ b/crates/oxc_codegen/src/sourcemap_builder.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use nonmax::NonMaxU32; use oxc_index::{Idx, IndexVec}; @@ -73,27 +73,23 @@ pub struct SourcemapBuilder { generated_column: u32, } -impl Default for SourcemapBuilder { - fn default() -> Self { +impl SourcemapBuilder { + pub fn new(path: &Path, source_text: &str) -> Self { + let mut sourcemap_builder = oxc_sourcemap::SourceMapBuilder::default(); + let line_offset_tables = Self::generate_line_offset_tables(source_text); + let source_id = + sourcemap_builder.set_source_and_content(path.to_string_lossy().as_ref(), source_text); Self { - source_id: 0, - original_source: "".into(), + source_id, + original_source: Arc::from(source_text), last_generated_update: 0, last_position: None, - line_offset_tables: LineOffsetTables::default(), - sourcemap_builder: oxc_sourcemap::SourceMapBuilder::default(), + line_offset_tables, + sourcemap_builder, generated_line: 0, generated_column: 0, } } -} - -impl SourcemapBuilder { - pub fn with_name_and_source(&mut self, name: &str, source: &str) { - self.line_offset_tables = Self::generate_line_offset_tables(source); - self.source_id = self.sourcemap_builder.set_source_and_content(name, source); - self.original_source = source.into(); - } pub fn into_sourcemap(self) -> oxc_sourcemap::SourceMap { self.sourcemap_builder.into_sourcemap() @@ -392,8 +388,7 @@ mod test { } fn assert_mapping(source: &str, mappings: &[(u32, u32, u32)]) { - let mut builder = SourcemapBuilder::default(); - builder.with_name_and_source("x.js", source); + let mut builder = SourcemapBuilder::new(Path::new("x.js"), source); for (position, expected_line, expected_col) in mappings.iter().copied() { let (line, col) = builder.search_original_line_and_column(position); assert_eq!( @@ -407,8 +402,7 @@ mod test { #[test] fn add_source_mapping() { fn create_mappings(source: &str, line: u32, column: u32) { - let mut builder = SourcemapBuilder::default(); - builder.with_name_and_source("x.js", source); + let mut builder = SourcemapBuilder::new(Path::new("x.js"), source); let output: Vec = source.as_bytes().into(); for (i, _ch) in source.char_indices() { #[allow(clippy::cast_possible_truncation)] @@ -444,8 +438,7 @@ mod test { #[test] fn add_source_mapping_for_name() { let output = "ac".as_bytes(); - let mut builder = SourcemapBuilder::default(); - builder.with_name_and_source("x.js", "ab"); + let mut builder = SourcemapBuilder::new(Path::new("x.js"), "ab"); builder.add_source_mapping_for_name(output, Span::new(0, 1), "a"); builder.add_source_mapping_for_name(output, Span::new(1, 2), "c"); let sm = builder.into_sourcemap(); @@ -464,8 +457,7 @@ mod test { #[test] fn add_source_mapping_for_unordered_position() { let output = "".as_bytes(); - let mut builder = SourcemapBuilder::default(); - builder.with_name_and_source("x.js", "ab"); + let mut builder = SourcemapBuilder::new(Path::new("x.js"), "ab"); builder.add_source_mapping(output, 1, None); builder.add_source_mapping(output, 0, None); let sm = builder.into_sourcemap(); diff --git a/crates/oxc_codegen/tests/integration/esbuild.rs b/crates/oxc_codegen/tests/integration/esbuild.rs index ed655a3e39778..a59411642fa96 100644 --- a/crates/oxc_codegen/tests/integration/esbuild.rs +++ b/crates/oxc_codegen/tests/integration/esbuild.rs @@ -1,4 +1,6 @@ -//! Tests ported from [esbuild](https://github.com/evanw/esbuild/blob/main/internal/js_printer/js_printer_test.go#L164) +//! Tests ported from `esbuild` +//! * +//! * use crate::tester::{test, test_minify}; @@ -180,11 +182,10 @@ fn test_new() { // test_minify("new x() ** 2", "new x**2;"); // Test preservation of Webpack-specific comments - // TODO: Not support trailing comments yet - // test( - // "new Worker(// webpackFoo: 1\n // webpackBar: 2\n 'path');", - // "new Worker(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\"\n);\n", - // ); + test( + "new Worker(// webpackFoo: 1\n // webpackBar: 2\n 'path');", + "new Worker(\n\t// webpackFoo: 1\n\t// webpackBar: 2\n\t\"path\"\n);\n", + ); test( "new Worker(/* webpackFoo: 1 */ /* webpackBar: 2 */ 'path');", "new Worker(\n\t/* webpackFoo: 1 */\n\t/* webpackBar: 2 */\n\t\"path\"\n);\n", @@ -243,12 +244,12 @@ fn test_call() { // testMangleMinify(t, "(1 ? eval : 2)?.(x)", "eval?.(x);"); // Webpack-specific comments - // TODO: Not support trailing comments yet - // test( - // "import(// webpackFoo: 1\n // webpackBar: 2\n 'path');", - // "import(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\"\n);\n", - // ); - // test( "import(// webpackFoo: 1\n // webpackBar: 2\n 'path', {type: 'module'});", "import(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\",\n { type: \"module\" }\n);\n"); + test( + "require(// webpackFoo: 1\n // webpackBar: 2\n 'path');", + "require(\n\t// webpackFoo: 1\n\t// webpackBar: 2\n\t\"path\"\n);\n", + ); + test( "require(// webpackFoo: 1\n // webpackBar: 2\n 'path', {type: 'module'});", + "require(\n\t// webpackFoo: 1\n\t// webpackBar: 2\n\t\"path\",\n\t{ type: \"module\" }\n);\n"); test( "require(/* webpackFoo: 1 */ /* webpackBar: 2 */ 'path');", "require(\n\t/* webpackFoo: 1 */\n\t/* webpackBar: 2 */\n\t\"path\"\n);\n", @@ -657,12 +658,11 @@ fn test_decorators() { fn test_import() { test("import('path');", "import(\"path\");\n"); // The semicolon must not be a separate statement - // TODO: Not support trailing comments yet - // test( - // "import(// webpackFoo: 1\n // webpackBar: 2\n 'path');", - // "import(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\"\n);\n", - // ); - // test( "import(// webpackFoo: 1\n // webpackBar: 2\n 'path', {type: 'module'});", "import(\n // webpackFoo: 1\n // webpackBar: 2\n \"path\",\n { type: \"module\" }\n);\n"); + test( + "import(// webpackFoo: 1\n // webpackBar: 2\n 'path');", + "import(\n\t// webpackFoo: 1\n\t// webpackBar: 2\n\t\"path\"\n);\n", + ); + test( "import(// webpackFoo: 1\n // webpackBar: 2\n 'path', {type: 'module'});", "import(\n\t// webpackFoo: 1\n\t// webpackBar: 2\n\t\"path\",\n\t{ type: \"module\" }\n);\n"); test( "import(/* webpackFoo: 1 */ /* webpackBar: 2 */ 'path');", "import(\n\t/* webpackFoo: 1 */\n\t/* webpackBar: 2 */\n\t\"path\"\n);\n", @@ -1155,3 +1155,33 @@ fn test_using() { test_minify("await using x = y", "await using x=y;"); test_minify("await using x = y, z = _", "await using x=y,z=_;"); } + +#[test] +fn test_preserve_optional_chain_parentheses() { + test("a?.b.c", "a?.b.c;\n"); + test("(a?.b).c", "(a?.b).c;\n"); + test("a?.b.c.d", "a?.b.c.d;\n"); + test("(a?.b.c).d", "(a?.b.c).d;\n"); + test("a?.b[c]", "a?.b[c];\n"); + test("(a?.b)[c]", "(a?.b)[c];\n"); + test("a?.b(c)", "a?.b(c);\n"); + test("(a?.b)(c)", "(a?.b)(c);\n"); + + test("a?.[b][c]", "a?.[b][c];\n"); + test("(a?.[b])[c]", "(a?.[b])[c];\n"); + test("a?.[b][c][d]", "a?.[b][c][d];\n"); + test("(a?.[b][c])[d]", "(a?.[b][c])[d];\n"); + test("a?.[b].c", "a?.[b].c;\n"); + test("(a?.[b]).c", "(a?.[b]).c;\n"); + test("a?.[b](c)", "a?.[b](c);\n"); + test("(a?.[b])(c)", "(a?.[b])(c);\n"); + + test("a?.(b)(c)", "a?.(b)(c);\n"); + test("(a?.(b))(c)", "(a?.(b))(c);\n"); + test("a?.(b)(c)(d)", "a?.(b)(c)(d);\n"); + test("(a?.(b)(c))(d)", "(a?.(b)(c))(d);\n"); + test("a?.(b).c", "a?.(b).c;\n"); + test("(a?.(b)).c", "(a?.(b)).c;\n"); + test("a?.(b)[c]", "a?.(b)[c];\n"); + test("(a?.(b))[c]", "(a?.(b))[c];\n"); +} diff --git a/crates/oxc_codegen/tests/integration/main.rs b/crates/oxc_codegen/tests/integration/main.rs index 31327f9a484d0..b2c5c4f9f15c1 100644 --- a/crates/oxc_codegen/tests/integration/main.rs +++ b/crates/oxc_codegen/tests/integration/main.rs @@ -7,7 +7,7 @@ pub mod ts; pub mod unit; use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CodegenOptions, CommentOptions}; +use oxc_codegen::{CodeGenerator, CodegenOptions}; use oxc_parser::Parser; use oxc_span::SourceType; @@ -17,13 +17,8 @@ pub fn codegen(source_text: &str) -> String { let ret = Parser::new(&allocator, source_text, source_type).parse(); CodeGenerator::new() .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) - .enable_comment( - source_text, - ret.trivias, - CommentOptions { preserve_annotate_comments: true }, - ) .build(&ret.program) - .source_text + .code } pub fn snapshot(name: &str, cases: &[&str]) { diff --git a/crates/oxc_codegen/tests/integration/pure_comments.rs b/crates/oxc_codegen/tests/integration/pure_comments.rs index 74d3cb65bb86f..5ceb65abec09c 100644 --- a/crates/oxc_codegen/tests/integration/pure_comments.rs +++ b/crates/oxc_codegen/tests/integration/pure_comments.rs @@ -129,6 +129,72 @@ const defineSSRCustomElement = () => { return /* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ /* #__NO_SIDE_EFFECTS__ */ defineCustomElement(options, extraOptions, hydrate); }; ", + " + const Component = // #__PURE__ + React.forwardRef((props, ref) => {}); + ", + // Copy from + " + function bar() {} + let bare = foo(bar); + + let at_yes = /* @__PURE__ */ foo(bar); + let at_no = /* @__PURE__ */ foo(bar()); + let new_at_yes = /* @__PURE__ */ new foo(bar); + let new_at_no = /* @__PURE__ */ new foo(bar()); + + let nospace_at_yes = /*@__PURE__*/ foo(bar); + let nospace_at_no = /*@__PURE__*/ foo(bar()); + let nospace_new_at_yes = /*@__PURE__*/ new foo(bar); + let nospace_new_at_no = /*@__PURE__*/ new foo(bar()); + + let num_yes = /* #__PURE__ */ foo(bar); + let num_no = /* #__PURE__ */ foo(bar()); + let new_num_yes = /* #__PURE__ */ new foo(bar); + let new_num_no = /* #__PURE__ */ new foo(bar()); + + let nospace_num_yes = /*#__PURE__*/ foo(bar); + let nospace_num_no = /*#__PURE__*/ foo(bar()); + let nospace_new_num_yes = /*#__PURE__*/ new foo(bar); + let nospace_new_num_no = /*#__PURE__*/ new foo(bar()); + + let dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar); + let dot_no = /* @__PURE__ */ foo(sideEffect()).dot(bar()); + let new_dot_yes = /* @__PURE__ */ new foo(sideEffect()).dot(bar); + let new_dot_no = /* @__PURE__ */ new foo(sideEffect()).dot(bar()); + + let nested_yes = [1, /* @__PURE__ */ foo(bar), 2]; + let nested_no = [1, /* @__PURE__ */ foo(bar()), 2]; + let new_nested_yes = [1, /* @__PURE__ */ new foo(bar), 2]; + let new_nested_no = [1, /* @__PURE__ */ new foo(bar()), 2]; + + let single_at_yes = // @__PURE__ + foo(bar); + let single_at_no = // @__PURE__ + foo(bar()); + let new_single_at_yes = // @__PURE__ + new foo(bar); + let new_single_at_no = // @__PURE__ + new foo(bar()); + + let single_num_yes = // #__PURE__ + foo(bar); + let single_num_no = // #__PURE__ + foo(bar()); + let new_single_num_yes = // #__PURE__ + new foo(bar); + let new_single_num_no = // #__PURE__ + new foo(bar()); + + let bad_no = /* __PURE__ */ foo(bar); + let new_bad_no = /* __PURE__ */ new foo(bar); + + let parens_no = (/* @__PURE__ */ foo)(bar); + let new_parens_no = new (/* @__PURE__ */ foo)(bar); + + let exp_no = /* @__PURE__ */ foo() ** foo(); + let new_exp_no = /* @__PURE__ */ new foo() ** foo(); + ", ]; snapshot("pure_comments", &cases); diff --git a/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap b/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap index 67fc6499a6e5b..b653834f5bc41 100644 --- a/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap +++ b/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap @@ -269,3 +269,131 @@ const defineSSRCustomElement = () => { const defineSSRCustomElement = () => { return /* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ /* #__NO_SIDE_EFFECTS__ */ defineCustomElement(options, extraOptions, hydrate); }; + +########## 20 + + const Component = // #__PURE__ + React.forwardRef((props, ref) => {}); + +---------- +const Component = /* #__PURE__*/ React.forwardRef((props, ref) => {}); + +########## 21 + + function bar() {} + let bare = foo(bar); + + let at_yes = /* @__PURE__ */ foo(bar); + let at_no = /* @__PURE__ */ foo(bar()); + let new_at_yes = /* @__PURE__ */ new foo(bar); + let new_at_no = /* @__PURE__ */ new foo(bar()); + + let nospace_at_yes = /*@__PURE__*/ foo(bar); + let nospace_at_no = /*@__PURE__*/ foo(bar()); + let nospace_new_at_yes = /*@__PURE__*/ new foo(bar); + let nospace_new_at_no = /*@__PURE__*/ new foo(bar()); + + let num_yes = /* #__PURE__ */ foo(bar); + let num_no = /* #__PURE__ */ foo(bar()); + let new_num_yes = /* #__PURE__ */ new foo(bar); + let new_num_no = /* #__PURE__ */ new foo(bar()); + + let nospace_num_yes = /*#__PURE__*/ foo(bar); + let nospace_num_no = /*#__PURE__*/ foo(bar()); + let nospace_new_num_yes = /*#__PURE__*/ new foo(bar); + let nospace_new_num_no = /*#__PURE__*/ new foo(bar()); + + let dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar); + let dot_no = /* @__PURE__ */ foo(sideEffect()).dot(bar()); + let new_dot_yes = /* @__PURE__ */ new foo(sideEffect()).dot(bar); + let new_dot_no = /* @__PURE__ */ new foo(sideEffect()).dot(bar()); + + let nested_yes = [1, /* @__PURE__ */ foo(bar), 2]; + let nested_no = [1, /* @__PURE__ */ foo(bar()), 2]; + let new_nested_yes = [1, /* @__PURE__ */ new foo(bar), 2]; + let new_nested_no = [1, /* @__PURE__ */ new foo(bar()), 2]; + + let single_at_yes = // @__PURE__ + foo(bar); + let single_at_no = // @__PURE__ + foo(bar()); + let new_single_at_yes = // @__PURE__ + new foo(bar); + let new_single_at_no = // @__PURE__ + new foo(bar()); + + let single_num_yes = // #__PURE__ + foo(bar); + let single_num_no = // #__PURE__ + foo(bar()); + let new_single_num_yes = // #__PURE__ + new foo(bar); + let new_single_num_no = // #__PURE__ + new foo(bar()); + + let bad_no = /* __PURE__ */ foo(bar); + let new_bad_no = /* __PURE__ */ new foo(bar); + + let parens_no = (/* @__PURE__ */ foo)(bar); + let new_parens_no = new (/* @__PURE__ */ foo)(bar); + + let exp_no = /* @__PURE__ */ foo() ** foo(); + let new_exp_no = /* @__PURE__ */ new foo() ** foo(); + +---------- +function bar() {} +let bare = foo(bar); +let at_yes = /* @__PURE__ */ foo(bar); +let at_no = /* @__PURE__ */ foo(bar()); +let new_at_yes = /* @__PURE__ */ new foo(bar); +let new_at_no = /* @__PURE__ */ new foo(bar()); +let nospace_at_yes = /*@__PURE__*/ foo(bar); +let nospace_at_no = /*@__PURE__*/ foo(bar()); +let nospace_new_at_yes = /*@__PURE__*/ new foo(bar); +let nospace_new_at_no = /*@__PURE__*/ new foo(bar()); +let num_yes = /* #__PURE__ */ foo(bar); +let num_no = /* #__PURE__ */ foo(bar()); +let new_num_yes = /* #__PURE__ */ new foo(bar); +let new_num_no = /* #__PURE__ */ new foo(bar()); +let nospace_num_yes = /*#__PURE__*/ foo(bar); +let nospace_num_no = /*#__PURE__*/ foo(bar()); +let nospace_new_num_yes = /*#__PURE__*/ new foo(bar); +let nospace_new_num_no = /*#__PURE__*/ new foo(bar()); +let dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar); +let dot_no = /* @__PURE__ */ foo(sideEffect()).dot(bar()); +let new_dot_yes = /* @__PURE__ */ new foo(sideEffect()).dot(bar); +let new_dot_no = /* @__PURE__ */ new foo(sideEffect()).dot(bar()); +let nested_yes = [ + 1, + /* @__PURE__ */ foo(bar), + 2 +]; +let nested_no = [ + 1, + /* @__PURE__ */ foo(bar()), + 2 +]; +let new_nested_yes = [ + 1, + /* @__PURE__ */ new foo(bar), + 2 +]; +let new_nested_no = [ + 1, + /* @__PURE__ */ new foo(bar()), + 2 +]; +let single_at_yes = /* @__PURE__*/ foo(bar); +let single_at_no = /* @__PURE__*/ foo(bar()); +let new_single_at_yes = /* @__PURE__*/ new foo(bar); +let new_single_at_no = /* @__PURE__*/ new foo(bar()); +let single_num_yes = /* #__PURE__*/ foo(bar); +let single_num_no = /* #__PURE__*/ foo(bar()); +let new_single_num_yes = /* #__PURE__*/ new foo(bar); +let new_single_num_no = /* #__PURE__*/ new foo(bar()); +let bad_no = foo(bar); +let new_bad_no = new foo(bar); +let parens_no = foo(bar); +let new_parens_no = new foo(bar); +let exp_no = /* @__PURE__ */ foo() ** foo(); +let new_exp_no = /* @__PURE__ */ new foo() ** foo(); diff --git a/crates/oxc_codegen/tests/integration/snapshots/ts.snap b/crates/oxc_codegen/tests/integration/snapshots/ts.snap index c5bb3fddcaae7..727c54bba0952 100644 --- a/crates/oxc_codegen/tests/integration/snapshots/ts.snap +++ b/crates/oxc_codegen/tests/integration/snapshots/ts.snap @@ -159,7 +159,7 @@ a = x!; ########## 25 b = (x as y); ---------- -b = ((x) as y); +b = x as y; ########## 26 c = foo; @@ -210,3 +210,18 @@ export type Component< E extends EmitsOptions | Record = {}, S extends Record = any > = ConcreteComponent | ComponentPublicInstanceConstructor; + +########## 32 +(a || b) as any +---------- +(a || b) as any; + +########## 33 +(a ** b) as any +---------- +(a ** b) as any; + +########## 34 +(function g() {}) as any +---------- +(function g() {}) as any; diff --git a/crates/oxc_codegen/tests/integration/tester.rs b/crates/oxc_codegen/tests/integration/tester.rs index 10b2d6acf202f..5f8d8a3993171 100644 --- a/crates/oxc_codegen/tests/integration/tester.rs +++ b/crates/oxc_codegen/tests/integration/tester.rs @@ -1,5 +1,5 @@ use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CodegenOptions, CommentOptions}; +use oxc_codegen::{CodeGenerator, CodegenOptions}; use oxc_parser::Parser; use oxc_span::SourceType; @@ -7,14 +7,7 @@ pub fn test(source_text: &str, expected: &str) { let source_type = SourceType::jsx(); let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); - let result = CodeGenerator::new() - .enable_comment( - source_text, - ret.trivias, - CommentOptions { preserve_annotate_comments: true }, - ) - .build(&ret.program) - .source_text; + let result = CodeGenerator::new().build(&ret.program).code; assert_eq!(result, expected, "\nfor source: {source_text:?}"); } @@ -22,7 +15,7 @@ pub fn test_without_source(source_text: &str, expected: &str) { let source_type = SourceType::jsx(); let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); - let result = CodeGenerator::new().build(&ret.program).source_text; + let result = CodeGenerator::new().build(&ret.program).code; assert_eq!(result, expected, "\nfor source: {source_text:?}"); } @@ -33,6 +26,6 @@ pub fn test_minify(source_text: &str, expected: &str) { let result = CodeGenerator::new() .with_options(CodegenOptions { minify: true, ..CodegenOptions::default() }) .build(&ret.program) - .source_text; + .code; assert_eq!(result, expected, "\nfor minify source: {source_text}"); } diff --git a/crates/oxc_codegen/tests/integration/ts.rs b/crates/oxc_codegen/tests/integration/ts.rs index 1ee5d448b786f..69c805f7e1b7f 100644 --- a/crates/oxc_codegen/tests/integration/ts.rs +++ b/crates/oxc_codegen/tests/integration/ts.rs @@ -50,7 +50,10 @@ export type Component< > = | ConcreteComponent | ComponentPublicInstanceConstructor -" +", +"(a || b) as any", +"(a ** b) as any", +"(function g() {}) as any", ]; snapshot("ts", &cases); diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index 203e2495b8759..702822be42c2d 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -271,3 +271,17 @@ fn vite_special_comments() { "import(\n\t/* @vite-ignore */\n\tmodule1Url\n).then((module1) => {\n\tself.postMessage(module.default + module1.msg1 + import.meta.env.BASE_URL);\n});\n", ); } + +// followup from https://github.com/oxc-project/oxc/pull/6422 +#[test] +fn in_expr_in_sequence_in_for_loop_init() { + test( + "for (l = ('foo' in bar), i; i < 10; i += 1) {}", + "for (l = (\"foo\" in bar), i; i < 10; i += 1) {}\n", + ); + + test( + "for (('hidden' in a) && (m = a.hidden), r = 0; s > r; r++) {}", + "for ((\"hidden\" in a) && (m = a.hidden), r = 0; s > r; r++) {}\n", + ); +} diff --git a/crates/oxc_data_structures/CHANGELOG.md b/crates/oxc_data_structures/CHANGELOG.md new file mode 100644 index 0000000000000..0ceb2dd4bb11f --- /dev/null +++ b/crates/oxc_data_structures/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +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.31.0] - 2024-10-08 + +### Features + +- 7566c2d data_structures: Add `as_slice` + `as_mut_slice` methods to stacks (#6216) (overlookmotel) +- c3c3447 data_structures: Add `oxc_data_structures` crate; add stack (#6206) (Boshen) + +### Refactor + +- cc57541 data_structures: `NonEmptyStack::len` hint that `len` is never 0 (#6220) (overlookmotel) +- 147a5d5 data_structures: Remove `is_empty` methods for non-empty stacks (#6219) (overlookmotel) +- 61805fd data_structures: Add debug assertion to `SparseStack` (#6218) (overlookmotel) +- dbfa0bc data_structures: Add `len` method to `StackCommon` trait (#6215) (overlookmotel) + diff --git a/crates/oxc_data_structures/Cargo.toml b/crates/oxc_data_structures/Cargo.toml new file mode 100644 index 0000000000000..9f387c7889bce --- /dev/null +++ b/crates/oxc_data_structures/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "oxc_data_structures" +version = "0.31.0" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +include = ["/src"] +keywords.workspace = true +license.workspace = true +publish = true +repository.workspace = true +rust-version.workspace = true +description.workspace = true + +[lints] +workspace = true + +[lib] +test = true +doctest = false + +[dependencies] +assert-unchecked = { workspace = true } diff --git a/crates/oxc_data_structures/src/lib.rs b/crates/oxc_data_structures/src/lib.rs new file mode 100644 index 0000000000000..1a3c20dca0f6c --- /dev/null +++ b/crates/oxc_data_structures/src/lib.rs @@ -0,0 +1,3 @@ +//! Data structures used across other oxc crates. +#![warn(missing_docs)] +pub mod stack; diff --git a/crates/oxc_transformer/src/helpers/stack/capacity.rs b/crates/oxc_data_structures/src/stack/capacity.rs similarity index 100% rename from crates/oxc_transformer/src/helpers/stack/capacity.rs rename to crates/oxc_data_structures/src/stack/capacity.rs diff --git a/crates/oxc_transformer/src/helpers/stack/common.rs b/crates/oxc_data_structures/src/stack/common.rs similarity index 93% rename from crates/oxc_transformer/src/helpers/stack/common.rs rename to crates/oxc_data_structures/src/stack/common.rs index 5499b416ce81e..e63c27e0d7cfc 100644 --- a/crates/oxc_transformer/src/helpers/stack/common.rs +++ b/crates/oxc_data_structures/src/stack/common.rs @@ -3,7 +3,7 @@ use std::{ alloc::{self, Layout}, mem::{align_of, size_of}, - ptr, + ptr, slice, }; use assert_unchecked::assert_unchecked; @@ -11,7 +11,7 @@ use assert_unchecked::assert_unchecked; use super::{NonNull, StackCapacity}; pub trait StackCommon: StackCapacity { - // Getter setter methods defined by implementer + // Getter + setter methods defined by implementer fn start(&self) -> NonNull; fn end(&self) -> NonNull; fn cursor(&self) -> NonNull; @@ -19,6 +19,9 @@ pub trait StackCommon: StackCapacity { fn set_end(&mut self, end: NonNull); fn set_cursor(&mut self, cursor: NonNull); + // Defined by implementer + fn len(&self) -> usize; + /// Make allocation of `capacity_bytes` bytes, aligned for `T`. /// /// # Panics @@ -98,12 +101,12 @@ pub trait StackCommon: StackCapacity { /// /// # SAFETY /// * Stack must be allocated. - /// * Stack must contain `len` initialized entries, starting at `self.start()`. + /// * Stack must contain `self.len()` initialized entries, starting at `self.start()`. #[inline] - unsafe fn drop_contents(&self, len: usize) { + unsafe fn drop_contents(&self) { // Drop contents. Next line copied from `std`'s `Vec`. // SAFETY: Caller guarantees stack contains `len` initialized entries, starting at `start`. - ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.start().as_ptr(), len)); + ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.start().as_ptr(), self.len())); } /// Get layout for allocation of `capacity_bytes` bytes. @@ -183,6 +186,20 @@ pub trait StackCommon: StackCapacity { self.end().byte_offset_from(self.start()) as usize } } + + /// Get contents of stack as a slice `&[T]`. + #[inline] + fn as_slice(&self) -> &[T] { + // SAFETY: Stack always contains `self.len()` entries, starting at `self.start()` + unsafe { slice::from_raw_parts(self.start().as_ptr(), self.len()) } + } + + /// Get contents of stack as a mutable slice `&mut [T]`. + #[inline] + fn as_mut_slice(&mut self) -> &mut [T] { + // SAFETY: Stack always contains `self.len()` entries, starting at `self.start()` + unsafe { slice::from_raw_parts_mut(self.start().as_ptr(), self.len()) } + } } /// Make allocation of with provided layout. diff --git a/crates/oxc_data_structures/src/stack/mod.rs b/crates/oxc_data_structures/src/stack/mod.rs new file mode 100644 index 0000000000000..7c0619018fc86 --- /dev/null +++ b/crates/oxc_data_structures/src/stack/mod.rs @@ -0,0 +1,18 @@ +//! Contains the following FILO data structures: +//! - [`Stack`]: A growable stack +//! - [`SparseStack`]: A stack that can have empty entries +//! - [`NonEmptyStack`]: A growable stack that can never be empty, allowing for more efficient +//! operations +mod capacity; +mod common; +mod non_empty; +mod non_null; +mod sparse; +mod standard; + +use capacity::StackCapacity; +use common::StackCommon; +pub use non_empty::NonEmptyStack; +use non_null::NonNull; +pub use sparse::SparseStack; +pub use standard::Stack; diff --git a/crates/oxc_transformer/src/helpers/stack/non_empty.rs b/crates/oxc_data_structures/src/stack/non_empty.rs similarity index 92% rename from crates/oxc_transformer/src/helpers/stack/non_empty.rs rename to crates/oxc_data_structures/src/stack/non_empty.rs index ef7403ae9634d..82754d7a710c0 100644 --- a/crates/oxc_transformer/src/helpers/stack/non_empty.rs +++ b/crates/oxc_data_structures/src/stack/non_empty.rs @@ -1,6 +1,9 @@ #![expect(clippy::unnecessary_safety_comment)] -use std::mem::size_of; +use std::{ + mem::size_of, + ops::{Deref, DerefMut}, +}; use super::{NonNull, StackCapacity, StackCommon}; @@ -86,6 +89,21 @@ impl StackCommon for NonEmptyStack { fn set_cursor(&mut self, cursor: NonNull) { self.cursor = cursor; } + + #[inline] + fn len(&self) -> usize { + // SAFETY: `self.start` and `self.cursor` are both derived from same pointer. + // `self.cursor` is always >= `self.start`. + // Distance between pointers is always a multiple of `size_of::()`. + let offset = unsafe { self.cursor_offset() }; + + // When stack has 1 entry, `start - cursor == 0`, so add 1 to get number of entries. + // SAFETY: Capacity cannot exceed `Self::MAX_CAPACITY`, which is `<= isize::MAX`, + // and offset can't exceed capacity, so `+ 1` cannot wrap around. + // `checked_add(1).unwrap_unchecked()` instead of just `+ 1` to hint to compiler + // that return value can never be zero. + unsafe { offset.checked_add(1).unwrap_unchecked() } + } } impl NonEmptyStack { @@ -132,7 +150,8 @@ impl NonEmptyStack { /// # Panics /// Panics if `T` is a zero-sized type. /// - /// # SAFETY + /// # Safety + /// /// * `capacity` must not be 0. /// * `capacity` must not exceed [`Self::MAX_CAPACITY`]. #[inline] @@ -253,8 +272,9 @@ impl NonEmptyStack { /// Pop value from stack, without checking that stack isn't empty. /// - /// # SAFETY - /// Stack must have at least 2 entries, so that after pop, it still has at least 1. + /// # Safety + /// + /// * Stack must have at least 2 entries, so that after pop, it still has at least 1. #[inline] pub unsafe fn pop_unchecked(&mut self) -> T { debug_assert!(self.cursor > self.start); @@ -273,15 +293,15 @@ impl NonEmptyStack { /// Number of entries is always at least 1. Stack is never empty. #[inline] pub fn len(&self) -> usize { - // SAFETY: `self.start` and `self.cursor` are both derived from same pointer. - // `self.cursor` is always >= `self.start`. - // Distance between pointers is always a multiple of `size_of::()`. - let offset = unsafe { self.cursor_offset() }; + >::len(self) + } - // When stack has 1 entry, `start - cursor == 0`, so add 1 to get number of entries. - // SAFETY: Capacity cannot exceed `Self::MAX_CAPACITY`, which is `<= isize::MAX`, - // and offset can't exceed capacity, so `+ 1` cannot wrap around. - offset + 1 + /// Get if stack is empty. Always returns `false`. + #[inline] + pub fn is_empty(&self) -> bool { + // This method is pointless, as the stack is never empty. But provide it to override + // the default method from `slice::is_empty` which is inherited via `Deref` + false } /// Get capacity. @@ -289,6 +309,18 @@ impl NonEmptyStack { pub fn capacity(&self) -> usize { >::capacity(self) } + + /// Get contents of stack as a slice `&[T]`. + #[inline] + pub fn as_slice(&self) -> &[T] { + >::as_slice(self) + } + + /// Get contents of stack as a mutable slice `&mut [T]`. + #[inline] + pub fn as_mut_slice(&mut self) -> &mut [T] { + >::as_mut_slice(self) + } } impl Drop for NonEmptyStack { @@ -296,7 +328,7 @@ impl Drop for NonEmptyStack { // Drop contents. // SAFETY: Stack is always allocated, and contains `self.len()` initialized entries, // starting at `self.start`. - unsafe { self.drop_contents(self.len()) }; + unsafe { self.drop_contents() }; // Drop the memory // SAFETY: Stack is always allocated. @@ -304,6 +336,22 @@ impl Drop for NonEmptyStack { } } +impl Deref for NonEmptyStack { + type Target = [T]; + + #[inline] + fn deref(&self) -> &[T] { + self.as_slice() + } +} + +impl DerefMut for NonEmptyStack { + #[inline] + fn deref_mut(&mut self) -> &mut [T] { + self.as_mut_slice() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxc_transformer/src/helpers/stack/non_null.rs b/crates/oxc_data_structures/src/stack/non_null.rs similarity index 95% rename from crates/oxc_transformer/src/helpers/stack/non_null.rs rename to crates/oxc_data_structures/src/stack/non_null.rs index fdc6ca05202b9..573e0fe4c5ada 100644 --- a/crates/oxc_transformer/src/helpers/stack/non_null.rs +++ b/crates/oxc_data_structures/src/stack/non_null.rs @@ -14,8 +14,9 @@ use std::{cmp::Ordering, ptr::NonNull as NativeNonNull}; #[derive(Debug)] pub struct NonNull(NativeNonNull); -#[expect(dead_code, clippy::incompatible_msrv)] -unsafe fn non_null_add_is_not_stable(ptr: NativeNonNull) -> NativeNonNull { +#[cfg(clippy)] +#[expect(clippy::incompatible_msrv)] +unsafe fn _non_null_add_is_not_stable(ptr: NativeNonNull) -> NativeNonNull { ptr.add(1) } diff --git a/crates/oxc_transformer/src/helpers/stack/sparse.rs b/crates/oxc_data_structures/src/stack/sparse.rs similarity index 97% rename from crates/oxc_transformer/src/helpers/stack/sparse.rs rename to crates/oxc_data_structures/src/stack/sparse.rs index 943b0e0a093bf..847d05c62f352 100644 --- a/crates/oxc_transformer/src/helpers/stack/sparse.rs +++ b/crates/oxc_data_structures/src/stack/sparse.rs @@ -30,11 +30,16 @@ pub struct SparseStack { values: Stack, } +impl Default for SparseStack { + fn default() -> Self { + Self::new() + } +} + impl SparseStack { /// Maximum capacity for entries (either `Some` or `None`). /// /// Effectively unlimited on 64-bit systems. - #[expect(dead_code)] pub const MAX_TOTAL_CAPACITY: usize = NonEmptyStack::::MAX_CAPACITY; /// Maximum capacity for filled entries (`Some`). @@ -44,7 +49,6 @@ impl SparseStack { /// Both are effectively unlimited on 64-bit systems. /// /// [`MAX_TOTAL_CAPACITY`]: Self::MAX_TOTAL_CAPACITY - #[expect(dead_code)] pub const MAX_FILLED_CAPACITY: usize = Stack::::MAX_CAPACITY; /// Create new `SparseStack`. @@ -70,7 +74,6 @@ impl SparseStack { /// * `total_capacity` must not exceed `Self::MAX_TOTAL_CAPACITY`. /// * `filled_capacity` must not exceed `Self::MAX_FILLED_CAPACITY`. #[inline] - #[expect(dead_code)] pub fn with_capacity(total_capacity: usize, filled_capacity: usize) -> Self { Self { has_values: NonEmptyStack::with_capacity(total_capacity, false), @@ -172,6 +175,7 @@ impl SparseStack { self.values.push(init()); } + debug_assert!(!self.values.is_empty()); // SAFETY: Last `self.has_values` is only `true` if there's a corresponding value in `self.values`. // This invariant is maintained in `push`, `pop`, `take_last`, and `last_or_init`. // Here either last `self.has_values` was already `true`, or it's just been set to `true` @@ -183,6 +187,7 @@ impl SparseStack { /// /// Number of entries is always at least 1. Stack is never empty. #[inline] + #[expect(clippy::len_without_is_empty)] // `is_empty` method is pointless. It's never empty. pub fn len(&self) -> usize { self.has_values.len() } @@ -191,7 +196,6 @@ impl SparseStack { /// /// Capacity is always at least 1. Stack is never empty. #[inline] - #[expect(dead_code)] pub fn total_capacity(&self) -> usize { self.has_values.capacity() } @@ -202,7 +206,6 @@ impl SparseStack { /// /// [`total_capacity`]: Self::total_capacity #[inline] - #[expect(dead_code)] pub fn filled_capacity(&self) -> usize { self.values.capacity() } diff --git a/crates/oxc_transformer/src/helpers/stack/standard.rs b/crates/oxc_data_structures/src/stack/standard.rs similarity index 94% rename from crates/oxc_transformer/src/helpers/stack/standard.rs rename to crates/oxc_data_structures/src/stack/standard.rs index c722773c5d312..5c8bc67ef306d 100644 --- a/crates/oxc_transformer/src/helpers/stack/standard.rs +++ b/crates/oxc_data_structures/src/stack/standard.rs @@ -1,6 +1,9 @@ #![expect(clippy::unnecessary_safety_comment)] -use std::mem::size_of; +use std::{ + mem::size_of, + ops::{Deref, DerefMut}, +}; use super::{NonNull, StackCapacity, StackCommon}; @@ -36,6 +39,12 @@ pub struct Stack { end: NonNull, } +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} + impl StackCapacity for Stack {} impl StackCommon for Stack { @@ -68,6 +77,13 @@ impl StackCommon for Stack { fn set_cursor(&mut self, cursor: NonNull) { self.cursor = cursor; } + + fn len(&self) -> usize { + // SAFETY: `self.start` and `self.cursor` are both derived from same pointer. + // `self.cursor` is always >= `self.start`. + // Distance between pointers is always a multiple of `size_of::()`. + unsafe { self.cursor_offset() } + } } impl Stack { @@ -117,7 +133,8 @@ impl Stack { /// # Panics /// Panics if `T` is a zero-sized type. /// - /// # SAFETY + /// # Safety + /// /// * `capacity` must not be 0. /// * `capacity` must not exceed [`Self::MAX_CAPACITY`]. #[inline] @@ -154,7 +171,6 @@ impl Stack { /// Get reference to last value on stack. #[inline] - #[cfg_attr(not(test), expect(dead_code))] pub fn last(&self) -> Option<&T> { #[expect(clippy::if_not_else)] if !self.is_empty() { @@ -167,8 +183,9 @@ impl Stack { /// Get reference to last value on stack, without checking stack isn't empty. /// - /// # SAFETY - /// Stack must not be empty. + /// # Safety + /// + /// * Stack must not be empty. #[inline] pub unsafe fn last_unchecked(&self) -> &T { debug_assert!(self.end > self.start); @@ -182,7 +199,6 @@ impl Stack { /// Get mutable reference to last value on stack. #[inline] - #[cfg_attr(not(test), expect(dead_code))] pub fn last_mut(&mut self) -> Option<&mut T> { #[expect(clippy::if_not_else)] if !self.is_empty() { @@ -195,8 +211,9 @@ impl Stack { /// Get mutable reference to last value on stack, without checking stack isn't empty. /// - /// # SAFETY - /// Stack must not be empty. + /// # Safety + /// + /// * Stack must not be empty. #[inline] pub unsafe fn last_mut_unchecked(&mut self) -> &mut T { debug_assert!(self.end > self.start); @@ -265,7 +282,6 @@ impl Stack { /// Pop value from stack. #[inline] - #[cfg_attr(not(test), expect(dead_code))] pub fn pop(&mut self) -> Option { #[expect(clippy::if_not_else)] if !self.is_empty() { @@ -278,8 +294,9 @@ impl Stack { /// Pop value from stack, without checking that stack isn't empty. /// - /// # SAFETY - /// Stack must not be empty. + /// # Safety + /// + /// * Stack must not be empty. #[inline] pub unsafe fn pop_unchecked(&mut self) -> T { debug_assert!(self.end > self.start); @@ -296,10 +313,7 @@ impl Stack { /// Get number of entries on stack. #[inline] pub fn len(&self) -> usize { - // SAFETY: `self.start` and `self.cursor` are both derived from same pointer. - // `self.cursor` is always >= `self.start`. - // Distance between pointers is always a multiple of `size_of::()`. - unsafe { self.cursor_offset() } + >::len(self) } /// Get if stack is empty. @@ -313,6 +327,18 @@ impl Stack { pub fn capacity(&self) -> usize { >::capacity(self) } + + /// Get contents of stack as a slice `&[T]`. + #[inline] + pub fn as_slice(&self) -> &[T] { + >::as_slice(self) + } + + /// Get contents of stack as a mutable slice `&mut [T]`. + #[inline] + pub fn as_mut_slice(&mut self) -> &mut [T] { + >::as_mut_slice(self) + } } impl Drop for Stack { @@ -325,7 +351,7 @@ impl Drop for Stack { if !self.is_empty() { // SAFETY: Checked above that stack is allocated. // Stack contains `self.len()` initialized entries, starting at `self.start` - unsafe { self.drop_contents(self.len()) }; + unsafe { self.drop_contents() }; } // Drop the memory @@ -334,6 +360,22 @@ impl Drop for Stack { } } +impl Deref for Stack { + type Target = [T]; + + #[inline] + fn deref(&self) -> &[T] { + self.as_slice() + } +} + +impl DerefMut for Stack { + #[inline] + fn deref_mut(&mut self) -> &mut [T] { + self.as_mut_slice() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxc_diagnostics/Cargo.toml b/crates/oxc_diagnostics/Cargo.toml index 86c8663248f94..f5bf592cad930 100644 --- a/crates/oxc_diagnostics/Cargo.toml +++ b/crates/oxc_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_diagnostics" -version = "0.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true diff --git a/crates/oxc_ecmascript/CHANGELOG.md b/crates/oxc_ecmascript/CHANGELOG.md new file mode 100644 index 0000000000000..9cbef2906d6da --- /dev/null +++ b/crates/oxc_ecmascript/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +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.31.0] - 2024-10-08 + +### Features + +- 9e62396 syntax_operations: Add crate `oxc_ecmascript` (#6202) (Boshen) + diff --git a/crates/oxc_ecmascript/Cargo.toml b/crates/oxc_ecmascript/Cargo.toml new file mode 100644 index 0000000000000..124e4672b9c49 --- /dev/null +++ b/crates/oxc_ecmascript/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "oxc_ecmascript" +version = "0.31.0" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +include = ["/src"] +keywords.workspace = true +license.workspace = true +publish = true +repository.workspace = true +rust-version.workspace = true +description.workspace = true + +[lints] +workspace = true + +[lib] +test = true +doctest = false + +[dependencies] +oxc_ast = { workspace = true } +oxc_span = { workspace = true } +oxc_syntax = { workspace = true, features = ["to_js_string"] } + +num-bigint = { workspace = true } +num-traits = { workspace = true } + +[features] +default = [] +side_effects = [] +constant_evaluation = ["side_effects"] diff --git a/crates/oxc_ast/src/syntax_directed_operations/bound_names.rs b/crates/oxc_ecmascript/src/bound_names.rs similarity index 93% rename from crates/oxc_ast/src/syntax_directed_operations/bound_names.rs rename to crates/oxc_ecmascript/src/bound_names.rs index ce9ca5d413eb8..aebffc84c6c39 100644 --- a/crates/oxc_ast/src/syntax_directed_operations/bound_names.rs +++ b/crates/oxc_ecmascript/src/bound_names.rs @@ -1,4 +1,9 @@ -use crate::ast::*; +use oxc_ast::ast::{ + ArrayPattern, AssignmentPattern, BindingIdentifier, BindingPattern, BindingPatternKind, + BindingRestElement, Class, Declaration, ExportNamedDeclaration, FormalParameter, + FormalParameters, Function, ImportDeclaration, ImportDeclarationSpecifier, ModuleDeclaration, + ObjectPattern, VariableDeclaration, +}; /// [`BoundName`](https://tc39.es/ecma262/#sec-static-semantics-boundnames) pub trait BoundName<'a> { diff --git a/crates/oxc_minifier/src/node_util/is_literal_value.rs b/crates/oxc_ecmascript/src/constant_evaluation/is_litral_value.rs similarity index 95% rename from crates/oxc_minifier/src/node_util/is_literal_value.rs rename to crates/oxc_ecmascript/src/constant_evaluation/is_litral_value.rs index 996a02ed4f31c..5c57359c85c9b 100644 --- a/crates/oxc_minifier/src/node_util/is_literal_value.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/is_litral_value.rs @@ -1,3 +1,4 @@ +#[allow(clippy::wildcard_imports)] use oxc_ast::ast::*; /// Returns true if this is a literal value. We define a literal value as any node that evaluates @@ -66,7 +67,8 @@ impl<'a, 'b> IsLiteralValue<'a, 'b> for ObjectProperty<'a> { impl<'a, 'b> IsLiteralValue<'a, 'b> for PropertyKey<'a> { fn is_literal_value(&self, include_functions: bool) -> bool { match self { - Self::StaticIdentifier(_) | Self::PrivateIdentifier(_) => false, + Self::StaticIdentifier(_) => true, + Self::PrivateIdentifier(_) => false, match_expression!(Self) => self.to_expression().is_literal_value(include_functions), } } diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs new file mode 100644 index 0000000000000..a9b1e2358b88e --- /dev/null +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -0,0 +1,363 @@ +mod is_litral_value; +mod r#type; +mod value; + +use std::borrow::Cow; + +use num_bigint::BigInt; +use num_traits::{One, Zero}; + +#[allow(clippy::wildcard_imports)] +use oxc_ast::ast::*; + +use crate::{side_effects::MayHaveSideEffects, ToInt32, ToJsString}; + +pub use self::{is_litral_value::IsLiteralValue, r#type::ValueType, value::ConstantValue}; + +pub trait ConstantEvaluation<'a> { + fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool { + matches!(ident.name.as_str(), "undefined" | "NaN" | "Infinity") + } + + fn resolve_binding(&self, ident: &IdentifierReference<'a>) -> Option> { + match ident.name.as_str() { + "undefined" if self.is_global_reference(ident) => Some(ConstantValue::Undefined), + "NaN" if self.is_global_reference(ident) => Some(ConstantValue::Number(f64::NAN)), + "Infinity" if self.is_global_reference(ident) => { + Some(ConstantValue::Number(f64::INFINITY)) + } + _ => None, + } + } + + fn get_side_free_number_value(&self, expr: &Expression<'a>) -> Option { + let value = self.eval_to_number(expr); + // Calculating the number value, if any, is likely to be faster than calculating side effects, + // and there are only a very few cases where we can compute a number value, but there could + // also be side effects. e.g. `void doSomething()` has value NaN, regardless of the behavior + // of `doSomething()` + if value.is_some() && expr.may_have_side_effects() { + None + } else { + value + } + } + + fn eval_to_boolean(&self, expr: &Expression<'a>) -> Option { + match expr { + Expression::Identifier(ident) => match ident.name.as_str() { + "undefined" | "NaN" if self.is_global_reference(ident) => Some(false), + "Infinity" if self.is_global_reference(ident) => Some(true), + _ => None, + }, + Expression::LogicalExpression(logical_expr) => { + match logical_expr.operator { + // true && true -> true + // true && false -> false + // a && true -> None + LogicalOperator::And => { + let left = self.eval_to_boolean(&logical_expr.left); + let right = self.eval_to_boolean(&logical_expr.right); + match (left, right) { + (Some(true), Some(true)) => Some(true), + (Some(false), _) | (_, Some(false)) => Some(false), + (None, _) | (_, None) => None, + } + } + // true || false -> true + // false || false -> false + // a || b -> None + LogicalOperator::Or => { + let left = self.eval_to_boolean(&logical_expr.left); + let right = self.eval_to_boolean(&logical_expr.right); + match (left, right) { + (Some(true), _) | (_, Some(true)) => Some(true), + (Some(false), Some(false)) => Some(false), + (None, _) | (_, None) => None, + } + } + LogicalOperator::Coalesce => None, + } + } + Expression::SequenceExpression(sequence_expr) => { + // For sequence expression, the value is the value of the RHS. + sequence_expr.expressions.last().and_then(|e| self.eval_to_boolean(e)) + } + Expression::UnaryExpression(unary_expr) => { + match unary_expr.operator { + UnaryOperator::Void => Some(false), + + UnaryOperator::BitwiseNot + | UnaryOperator::UnaryPlus + | UnaryOperator::UnaryNegation => { + // `~0 -> true` `+1 -> true` `+0 -> false` `-0 -> false` + self.eval_to_number(expr).map(|value| !value.is_zero()) + } + UnaryOperator::LogicalNot => { + // !true -> false + self.eval_to_boolean(&unary_expr.argument).map(|b| !b) + } + _ => None, + } + } + Expression::AssignmentExpression(assign_expr) => { + match assign_expr.operator { + AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr => None, + // For ASSIGN, the value is the value of the RHS. + _ => self.eval_to_boolean(&assign_expr.right), + } + } + expr => { + use crate::ToBoolean; + expr.to_boolean() + } + } + } + + fn eval_to_number(&self, expr: &Expression<'a>) -> Option { + match expr { + Expression::Identifier(ident) => match ident.name.as_str() { + "undefined" | "NaN" if self.is_global_reference(ident) => Some(f64::NAN), + "Infinity" if self.is_global_reference(ident) => Some(f64::INFINITY), + _ => None, + }, + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::UnaryPlus => self.eval_to_number(&unary_expr.argument), + UnaryOperator::UnaryNegation => { + self.eval_to_number(&unary_expr.argument).map(|v| -v) + } + UnaryOperator::LogicalNot => { + self.eval_to_boolean(expr).map(|b| if b { 1_f64 } else { 0_f64 }) + } + UnaryOperator::Void => Some(f64::NAN), + _ => None, + }, + expr => { + use crate::ToNumber; + expr.to_number() + } + } + } + + fn eval_to_big_int(&self, expr: &Expression<'a>) -> Option { + match expr { + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::UnaryPlus => self.eval_to_big_int(&unary_expr.argument), + UnaryOperator::UnaryNegation => { + self.eval_to_big_int(&unary_expr.argument).map(|v| -v) + } + _ => None, + }, + Expression::BigIntLiteral(_) => { + use crate::ToBigInt; + expr.to_big_int() + } + _ => None, + } + } + + fn eval_expression(&self, expr: &Expression<'a>) -> Option> { + match expr { + Expression::BinaryExpression(e) => self.eval_binary_expression(e), + Expression::LogicalExpression(e) => self.eval_logical_expression(e), + Expression::UnaryExpression(e) => self.eval_unary_expression(e), + Expression::Identifier(ident) => self.resolve_binding(ident), + Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)), + Expression::StringLiteral(lit) => { + Some(ConstantValue::String(Cow::Borrowed(lit.value.as_str()))) + } + _ => None, + } + } + + fn eval_binary_expression(&self, expr: &BinaryExpression<'a>) -> Option> { + match expr.operator { + BinaryOperator::Addition => { + let left = &expr.left; + let right = &expr.right; + if left.may_have_side_effects() || right.may_have_side_effects() { + return None; + } + let left_type = ValueType::from(left); + let right_type = ValueType::from(right); + if left_type.is_string() || right_type.is_string() { + let lval = self.eval_expression(&expr.left)?; + let rval = self.eval_expression(&expr.right)?; + let lstr = lval.to_js_string()?; + let rstr = rval.to_js_string()?; + return Some(ConstantValue::String(lstr + rstr)); + } + if left_type.is_number() || right_type.is_number() { + let lval = self.eval_expression(&expr.left)?; + let rval = self.eval_expression(&expr.right)?; + let lnum = lval.into_number()?; + let rnum = rval.into_number()?; + return Some(ConstantValue::Number(lnum + rnum)); + } + None + } + BinaryOperator::Subtraction + | BinaryOperator::Division + | BinaryOperator::Remainder + | BinaryOperator::Multiplication + | BinaryOperator::Exponential => { + let lval = self.eval_to_number(&expr.left)?; + let rval = self.eval_to_number(&expr.right)?; + let val = match expr.operator { + BinaryOperator::Subtraction => lval - rval, + BinaryOperator::Division => { + if rval.is_zero() { + if lval.is_sign_positive() { + f64::INFINITY + } else { + f64::NEG_INFINITY + } + } else { + lval / rval + } + } + BinaryOperator::Remainder => { + if !rval.is_zero() && rval.is_finite() { + lval % rval + } else if rval.is_infinite() { + f64::NAN + } else { + return None; + } + } + BinaryOperator::Multiplication => lval * rval, + BinaryOperator::Exponential => lval.powf(rval), + _ => unreachable!(), + }; + Some(ConstantValue::Number(val)) + } + #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + BinaryOperator::ShiftLeft + | BinaryOperator::ShiftRight + | BinaryOperator::ShiftRightZeroFill => { + let left_num = self.get_side_free_number_value(&expr.left); + let right_num = self.get_side_free_number_value(&expr.right); + if let (Some(left_val), Some(right_val)) = (left_num, right_num) { + if left_val.fract() != 0.0 || right_val.fract() != 0.0 { + return None; + } + // only the lower 5 bits are used when shifting, so don't do anything + // if the shift amount is outside [0,32) + if !(0.0..32.0).contains(&right_val) { + return None; + } + let right_val_int = right_val as u32; + let bits = left_val.to_int_32(); + + let result_val: f64 = match expr.operator { + BinaryOperator::ShiftLeft => f64::from(bits.wrapping_shl(right_val_int)), + BinaryOperator::ShiftRight => f64::from(bits.wrapping_shr(right_val_int)), + BinaryOperator::ShiftRightZeroFill => { + // JavaScript always treats the result of >>> as unsigned. + // We must force Rust to do the same here. + let bits = bits as u32; + let res = bits.wrapping_shr(right_val_int); + f64::from(res) + } + _ => unreachable!(), + }; + return Some(ConstantValue::Number(result_val)); + } + None + } + _ => None, + } + } + + fn eval_logical_expression(&self, expr: &LogicalExpression<'a>) -> Option> { + match expr.operator { + LogicalOperator::And => { + if self.eval_to_boolean(&expr.left) == Some(true) { + self.eval_expression(&expr.right) + } else { + self.eval_expression(&expr.left) + } + } + _ => None, + } + } + + fn eval_unary_expression(&self, expr: &UnaryExpression<'a>) -> Option> { + match expr.operator { + UnaryOperator::Typeof => { + if !expr.argument.is_literal_value(true) { + return None; + } + let s = match &expr.argument { + Expression::FunctionExpression(_) => "function", + Expression::StringLiteral(_) => "string", + Expression::NumericLiteral(_) => "number", + Expression::BooleanLiteral(_) => "boolean", + Expression::NullLiteral(_) + | Expression::ObjectExpression(_) + | Expression::ArrayExpression(_) => "object", + Expression::UnaryExpression(e) if e.operator == UnaryOperator::Void => { + "undefined" + } + Expression::BigIntLiteral(_) => "bigint", + Expression::Identifier(ident) => match ident.name.as_str() { + "undefined" if self.is_global_reference(ident) => "undefined", + "NaN" | "Infinity" if self.is_global_reference(ident) => "number", + _ => return None, + }, + _ => return None, + }; + Some(ConstantValue::String(Cow::Borrowed(s))) + } + UnaryOperator::Void => { + if (!expr.argument.is_number() || !expr.argument.is_number_0()) + && !expr.may_have_side_effects() + { + return Some(ConstantValue::Undefined); + } + None + } + UnaryOperator::LogicalNot => { + // Don't fold !0 and !1 back to false. + if let Expression::NumericLiteral(n) = &expr.argument { + if n.value.is_zero() || n.value.is_one() { + return None; + } + } + self.eval_to_boolean(&expr.argument).map(|b| !b).map(ConstantValue::Boolean) + } + UnaryOperator::UnaryPlus => { + self.eval_to_number(&expr.argument).map(ConstantValue::Number) + } + UnaryOperator::UnaryNegation => { + let ty = ValueType::from(&expr.argument); + match ty { + ValueType::BigInt => { + self.eval_to_big_int(&expr.argument).map(|v| -v).map(ConstantValue::BigInt) + } + ValueType::Number => self + .eval_to_number(&expr.argument) + .map(|v| if v.is_nan() { v } else { -v }) + .map(ConstantValue::Number), + _ => None, + } + } + UnaryOperator::BitwiseNot => { + let ty = ValueType::from(&expr.argument); + match ty { + ValueType::BigInt => { + self.eval_to_big_int(&expr.argument).map(|v| !v).map(ConstantValue::BigInt) + } + #[expect(clippy::cast_lossless)] + ValueType::Number => self + .eval_to_number(&expr.argument) + .map(|v| !v.to_int_32()) + .map(|v| v as f64) + .map(ConstantValue::Number), + _ => None, + } + } + UnaryOperator::Delete => None, + } + } +} diff --git a/crates/oxc_minifier/src/ty.rs b/crates/oxc_ecmascript/src/constant_evaluation/type.rs similarity index 64% rename from crates/oxc_minifier/src/ty.rs rename to crates/oxc_ecmascript/src/constant_evaluation/type.rs index 445a42e1e06ba..f5e5527c2c954 100644 --- a/crates/oxc_minifier/src/ty.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/type.rs @@ -1,22 +1,39 @@ -use oxc_ast::ast::*; +use oxc_ast::ast::Expression; use oxc_syntax::operator::{BinaryOperator, UnaryOperator}; /// JavaScript Language Type /// /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Ty { - BigInt, - Boolean, +pub enum ValueType { + Undefined, // a.k.a `Void` in closure compiler Null, Number, + BigInt, + String, + Boolean, Object, - Str, - Void, Undetermined, } -impl<'a> From<&Expression<'a>> for Ty { +impl ValueType { + pub fn is_string(self) -> bool { + matches!(self, Self::String) + } + + pub fn is_number(self) -> bool { + matches!(self, Self::Number) + } +} + +/// `get_known_value_type` +/// +/// Evaluate and attempt to determine which primitive value type it could resolve to. +/// Without proper type information some assumptions had to be made for operations that could +/// result in a BigInt or a Number. If there is not enough information available to determine one +/// or the other then we assume Number in order to maintain historical behavior of the compiler and +/// avoid breaking projects that relied on this behavior. +impl<'a> From<&Expression<'a>> for ValueType { fn from(expr: &Expression<'a>) -> Self { // TODO: complete this match expr { @@ -24,18 +41,18 @@ impl<'a> From<&Expression<'a>> for Ty { Expression::BooleanLiteral(_) => Self::Boolean, Expression::NullLiteral(_) => Self::Null, Expression::NumericLiteral(_) => Self::Number, - Expression::StringLiteral(_) => Self::Str, + Expression::StringLiteral(_) => Self::String, Expression::ObjectExpression(_) | Expression::ArrayExpression(_) | Expression::RegExpLiteral(_) | Expression::FunctionExpression(_) => Self::Object, Expression::Identifier(ident) => match ident.name.as_str() { - "undefined" => Self::Void, + "undefined" => Self::Undefined, "NaN" | "Infinity" => Self::Number, _ => Self::Undetermined, }, Expression::UnaryExpression(unary_expr) => match unary_expr.operator { - UnaryOperator::Void => Self::Void, + UnaryOperator::Void => Self::Undefined, UnaryOperator::UnaryNegation => { let argument_ty = Self::from(&unary_expr.argument); if argument_ty == Self::BigInt { @@ -45,29 +62,29 @@ impl<'a> From<&Expression<'a>> for Ty { } UnaryOperator::UnaryPlus => Self::Number, UnaryOperator::LogicalNot => Self::Boolean, - UnaryOperator::Typeof => Self::Str, + UnaryOperator::Typeof => Self::String, _ => Self::Undetermined, }, Expression::BinaryExpression(binary_expr) => match binary_expr.operator { BinaryOperator::Addition => { let left_ty = Self::from(&binary_expr.left); let right_ty = Self::from(&binary_expr.right); - - if left_ty == Self::Str || right_ty == Self::Str { - return Self::Str; + if left_ty == Self::String || right_ty == Self::String { + return Self::String; } - // There are some pretty weird cases for object types: // {} + [] === "0" // [] + {} === "[object Object]" if left_ty == Self::Object || right_ty == Self::Object { return Self::Undetermined; } - Self::Undetermined } _ => Self::Undetermined, }, + Expression::SequenceExpression(e) => { + e.expressions.last().map_or(ValueType::Undetermined, Self::from) + } _ => Self::Undetermined, } } diff --git a/crates/oxc_ecmascript/src/constant_evaluation/value.rs b/crates/oxc_ecmascript/src/constant_evaluation/value.rs new file mode 100644 index 0000000000000..4f4f60a4e7b0e --- /dev/null +++ b/crates/oxc_ecmascript/src/constant_evaluation/value.rs @@ -0,0 +1,73 @@ +use std::borrow::Cow; + +use num_bigint::BigInt; + +use crate::ToJsString; + +#[derive(Debug, PartialEq)] +pub enum ConstantValue<'a> { + Number(f64), + BigInt(BigInt), + String(Cow<'a, str>), + Boolean(bool), + Undefined, +} + +impl<'a> ConstantValue<'a> { + pub fn is_number(&self) -> bool { + matches!(self, Self::Number(_)) + } + + pub fn is_big_int(&self) -> bool { + matches!(self, Self::BigInt(_)) + } + + pub fn is_string(&self) -> bool { + matches!(self, Self::String(_)) + } + + pub fn is_boolean(&self) -> bool { + matches!(self, Self::Boolean(_)) + } + + pub fn is_undefined(&self) -> bool { + matches!(self, Self::Undefined) + } + + pub fn into_string(self) -> Option> { + match self { + Self::String(s) => Some(s), + _ => None, + } + } + + pub fn into_number(self) -> Option { + match self { + Self::Number(s) => Some(s), + _ => None, + } + } + + pub fn into_boolean(self) -> Option { + match self { + Self::Boolean(s) => Some(s), + _ => None, + } + } +} + +impl<'a> ToJsString<'a> for ConstantValue<'a> { + fn to_js_string(&self) -> Option> { + match self { + Self::Number(n) => { + use oxc_syntax::number::ToJsString; + Some(Cow::Owned(n.to_js_string())) + } + // FIXME: to js number string + Self::BigInt(n) => Some(Cow::Owned(n.to_string() + "n")), + Self::String(s) => Some(s.clone()), + Self::Boolean(b) => Some(Cow::Borrowed(if *b { "true" } else { "false" })), + Self::Undefined => Some(Cow::Borrowed("undefined")), + } + } +} diff --git a/crates/oxc_ast/src/syntax_directed_operations/is_simple_parameter_list.rs b/crates/oxc_ecmascript/src/is_simple_parameter_list.rs similarity index 91% rename from crates/oxc_ast/src/syntax_directed_operations/is_simple_parameter_list.rs rename to crates/oxc_ecmascript/src/is_simple_parameter_list.rs index be773071ffa87..a1369f6bd54a5 100644 --- a/crates/oxc_ast/src/syntax_directed_operations/is_simple_parameter_list.rs +++ b/crates/oxc_ecmascript/src/is_simple_parameter_list.rs @@ -1,4 +1,4 @@ -use crate::ast::*; +use oxc_ast::ast::FormalParameters; /// [`IsSimpleParameterList`](https://tc39.es/ecma262/#sec-static-semantics-issimpleparameterlist) pub trait IsSimpleParameterList { diff --git a/crates/oxc_ecmascript/src/lib.rs b/crates/oxc_ecmascript/src/lib.rs new file mode 100644 index 0000000000000..770bc716eda59 --- /dev/null +++ b/crates/oxc_ecmascript/src/lib.rs @@ -0,0 +1,34 @@ +//! Methods defined in the [ECMAScript Language Specification](https://tc39.es/ecma262). + +// [Syntax-Directed Operations](https://tc39.es/ecma262/#sec-syntax-directed-operations) +mod bound_names; +mod is_simple_parameter_list; +mod private_bound_identifiers; +mod prop_name; + +// Abstract Operations +mod string_char_at; +mod string_index_of; +mod string_last_index_of; +mod string_to_big_int; +mod string_to_number; +mod to_big_int; +mod to_boolean; +mod to_int_32; +mod to_number; +mod to_string; + +#[cfg(feature = "constant_evaluation")] +pub mod constant_evaluation; + +#[cfg(feature = "side_effects")] +pub mod side_effects; + +pub use self::{ + bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList, + private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName, + string_char_at::StringCharAt, string_index_of::StringIndexOf, + string_last_index_of::StringLastIndexOf, string_to_big_int::StringToBigInt, + string_to_number::StringToNumber, to_big_int::ToBigInt, to_boolean::ToBoolean, + to_int_32::ToInt32, to_number::ToNumber, to_string::ToJsString, +}; diff --git a/crates/oxc_ast/src/syntax_directed_operations/private_bound_identifiers.rs b/crates/oxc_ecmascript/src/private_bound_identifiers.rs similarity index 92% rename from crates/oxc_ast/src/syntax_directed_operations/private_bound_identifiers.rs rename to crates/oxc_ecmascript/src/private_bound_identifiers.rs index 4c1a1ee026e86..68fe360935908 100644 --- a/crates/oxc_ast/src/syntax_directed_operations/private_bound_identifiers.rs +++ b/crates/oxc_ecmascript/src/private_bound_identifiers.rs @@ -1,4 +1,7 @@ -use crate::ast::*; +use oxc_ast::ast::{ + AccessorProperty, ClassElement, MethodDefinition, PrivateIdentifier, PropertyDefinition, + PropertyKey, +}; /// [`PrivateBoundIdentifiers`](https://tc39.es/ecma262/#sec-static-semantics-privateboundidentifiers) pub trait PrivateBoundIdentifiers { diff --git a/crates/oxc_ast/src/syntax_directed_operations/prop_name.rs b/crates/oxc_ecmascript/src/prop_name.rs similarity index 92% rename from crates/oxc_ast/src/syntax_directed_operations/prop_name.rs rename to crates/oxc_ecmascript/src/prop_name.rs index 0b56af7261372..f72f863bb0442 100644 --- a/crates/oxc_ast/src/syntax_directed_operations/prop_name.rs +++ b/crates/oxc_ecmascript/src/prop_name.rs @@ -1,7 +1,9 @@ +use oxc_ast::ast::{ + ClassElement, MethodDefinition, ObjectProperty, ObjectPropertyKind, PropertyDefinition, + PropertyKey, PropertyKind, +}; use oxc_span::Span; -use crate::ast::*; - /// [`PropName`](https://tc39.es/ecma262/#sec-static-semantics-propname) pub trait PropName { fn prop_name(&self) -> Option<(&str, Span)>; diff --git a/crates/oxc_ecmascript/src/side_effects/check_for_state_change.rs b/crates/oxc_ecmascript/src/side_effects/check_for_state_change.rs new file mode 100644 index 0000000000000..f4666b9ae8af9 --- /dev/null +++ b/crates/oxc_ecmascript/src/side_effects/check_for_state_change.rs @@ -0,0 +1,322 @@ +#[allow(clippy::wildcard_imports)] +use oxc_ast::ast::*; +use oxc_syntax::operator::UnaryOperator; + +/// A "simple" operator is one whose children are expressions, has no direct side-effects. +fn is_simple_unary_operator(operator: UnaryOperator) -> bool { + operator != UnaryOperator::Delete +} + +/// Returns true if some node in n's subtree changes application state. If +/// `check_for_new_objects` is true, we assume that newly created mutable objects (like object +/// literals) change state. Otherwise, we assume that they have no side effects. +/// +/// Ported from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/AstAnalyzer.java#L241) +pub trait CheckForStateChange<'a, 'b> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool; +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for Expression<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::NumericLiteral(_) + | Self::BooleanLiteral(_) + | Self::StringLiteral(_) + | Self::BigIntLiteral(_) + | Self::NullLiteral(_) + | Self::RegExpLiteral(_) + | Self::MetaProperty(_) + | Self::ThisExpression(_) + | Self::ClassExpression(_) + | Self::FunctionExpression(_) => false, + Self::TemplateLiteral(template) => template + .expressions + .iter() + .any(|expr| expr.check_for_state_change(check_for_new_objects)), + Self::Identifier(_ident) => + /* TODO: ident.reference_flags == ReferenceFlags::Write */ + { + false + } + Self::UnaryExpression(unary_expr) => { + unary_expr.check_for_state_change(check_for_new_objects) + } + Self::ParenthesizedExpression(p) => { + p.expression.check_for_state_change(check_for_new_objects) + } + Self::ConditionalExpression(p) => { + p.test.check_for_state_change(check_for_new_objects) + || p.consequent.check_for_state_change(check_for_new_objects) + || p.alternate.check_for_state_change(check_for_new_objects) + } + Self::SequenceExpression(s) => { + s.expressions.iter().any(|expr| expr.check_for_state_change(check_for_new_objects)) + } + Self::BinaryExpression(binary_expr) => { + binary_expr.check_for_state_change(check_for_new_objects) + } + Self::ObjectExpression(object_expr) => { + if check_for_new_objects { + return true; + } + + object_expr + .properties + .iter() + .any(|property| property.check_for_state_change(check_for_new_objects)) + } + Self::ArrayExpression(array_expr) => { + if check_for_new_objects { + return true; + } + array_expr + .elements + .iter() + .any(|element| element.check_for_state_change(check_for_new_objects)) + } + _ => true, + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for UnaryExpression<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + if is_simple_unary_operator(self.operator) { + return self.argument.check_for_state_change(check_for_new_objects); + } + true + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for BinaryExpression<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + let left = self.left.check_for_state_change(check_for_new_objects); + let right = self.right.check_for_state_change(check_for_new_objects); + + left || right + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for ArrayExpressionElement<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::SpreadElement(element) => element.check_for_state_change(check_for_new_objects), + match_expression!(Self) => { + self.to_expression().check_for_state_change(check_for_new_objects) + } + Self::Elision(_) => false, + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for ObjectPropertyKind<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::ObjectProperty(method) => method.check_for_state_change(check_for_new_objects), + Self::SpreadProperty(spread_element) => { + spread_element.check_for_state_change(check_for_new_objects) + } + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for SpreadElement<'a> { + fn check_for_state_change(&self, _check_for_new_objects: bool) -> bool { + // Object-rest and object-spread may trigger a getter. + // TODO: Closure Compiler assumes that getters may side-free when set `assumeGettersArePure`. + // https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/AstAnalyzer.java#L282 + true + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for ObjectProperty<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.key.check_for_state_change(check_for_new_objects) + || self.value.check_for_state_change(check_for_new_objects) + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for PropertyKey<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::StaticIdentifier(_) | Self::PrivateIdentifier(_) => false, + match_expression!(Self) => { + self.to_expression().check_for_state_change(check_for_new_objects) + } + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for ForStatementLeft<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + match_assignment_target!(Self) => { + self.to_assignment_target().check_for_state_change(check_for_new_objects) + } + ForStatementLeft::VariableDeclaration(variable_declaration) => { + variable_declaration.check_for_state_change(check_for_new_objects) + } + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for VariableDeclaration<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.declarations.iter().any(|decl| decl.check_for_state_change(check_for_new_objects)) + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for VariableDeclarator<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.id.check_for_state_change(check_for_new_objects) + || self + .init + .as_ref() + .map_or(false, |init| init.check_for_state_change(check_for_new_objects)) + } +} + +impl CheckForStateChange<'_, '_> for BindingPattern<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match &self.kind { + BindingPatternKind::BindingIdentifier(_) => false, + BindingPatternKind::ObjectPattern(object_pattern) => { + object_pattern + .properties + .iter() + .any(|element| element.check_for_state_change(check_for_new_objects)) + || object_pattern + .rest + .as_ref() + .is_some_and(|rest| rest.check_for_state_change(check_for_new_objects)) + } + BindingPatternKind::ArrayPattern(array_pattern) => { + array_pattern.elements.iter().any(|element| { + element.as_ref().is_some_and(|element| { + element.check_for_state_change(check_for_new_objects) + }) + }) || array_pattern + .rest + .as_ref() + .is_some_and(|rest| rest.check_for_state_change(check_for_new_objects)) + } + BindingPatternKind::AssignmentPattern(assignment_pattern) => { + assignment_pattern.left.check_for_state_change(check_for_new_objects) + && assignment_pattern.right.check_for_state_change(check_for_new_objects) + } + } + } +} + +impl CheckForStateChange<'_, '_> for BindingRestElement<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.argument.check_for_state_change(check_for_new_objects) + } +} + +impl CheckForStateChange<'_, '_> for BindingProperty<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.key.check_for_state_change(check_for_new_objects) + || self.value.check_for_state_change(check_for_new_objects) + } +} + +impl CheckForStateChange<'_, '_> for AssignmentTarget<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + AssignmentTarget::AssignmentTargetIdentifier(_) => false, + AssignmentTarget::TSAsExpression(ts_as_expression) => { + ts_as_expression.expression.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::TSSatisfiesExpression(ts_satisfies_expression) => { + ts_satisfies_expression.expression.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::TSNonNullExpression(ts_non_null_expression) => { + ts_non_null_expression.expression.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::TSTypeAssertion(ts_type_assertion) => { + ts_type_assertion.expression.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::TSInstantiationExpression(ts_instantiation_expression) => { + ts_instantiation_expression.expression.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::ComputedMemberExpression(computed_member_expression) => { + computed_member_expression.object.check_for_state_change(check_for_new_objects) + || computed_member_expression + .expression + .check_for_state_change(check_for_new_objects) + } + AssignmentTarget::StaticMemberExpression(static_member_expression) => { + static_member_expression.object.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::PrivateFieldExpression(private_field_expression) => { + private_field_expression.object.check_for_state_change(check_for_new_objects) + } + AssignmentTarget::ArrayAssignmentTarget(array_assignment_target) => { + array_assignment_target.elements.iter().any(|element| { + element.as_ref().is_some_and(|element| { + element.check_for_state_change(check_for_new_objects) + }) + }) || array_assignment_target + .rest + .as_ref() + .is_some_and(|rest| rest.check_for_state_change(check_for_new_objects)) + } + AssignmentTarget::ObjectAssignmentTarget(object_assignment_target) => { + object_assignment_target + .properties + .iter() + .any(|property| property.check_for_state_change(check_for_new_objects)) + || object_assignment_target + .rest + .as_ref() + .is_some_and(|rest| rest.check_for_state_change(check_for_new_objects)) + } + } + } +} + +impl CheckForStateChange<'_, '_> for AssignmentTargetProperty<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + AssignmentTargetProperty::AssignmentTargetPropertyIdentifier( + assignment_target_property_identifier, + ) => assignment_target_property_identifier + .init + .as_ref() + .map_or(false, |init| init.check_for_state_change(check_for_new_objects)), + AssignmentTargetProperty::AssignmentTargetPropertyProperty( + assignment_target_property_property, + ) => { + assignment_target_property_property + .name + .check_for_state_change(check_for_new_objects) + || assignment_target_property_property + .binding + .check_for_state_change(check_for_new_objects) + } + } + } +} + +impl CheckForStateChange<'_, '_> for AssignmentTargetRest<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.target.check_for_state_change(check_for_new_objects) + } +} + +impl CheckForStateChange<'_, '_> for AssignmentTargetMaybeDefault<'_> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + match_assignment_target!(Self) => { + self.to_assignment_target().check_for_state_change(check_for_new_objects) + } + Self::AssignmentTargetWithDefault(assignment_target_with_default) => { + assignment_target_with_default.binding.check_for_state_change(check_for_new_objects) + && assignment_target_with_default + .init + .check_for_state_change(check_for_new_objects) + } + } + } +} diff --git a/crates/oxc_minifier/src/node_util/may_have_side_effects.rs b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs similarity index 78% rename from crates/oxc_minifier/src/node_util/may_have_side_effects.rs rename to crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs index 32ff26a4b2043..c08cd96ae6fcd 100644 --- a/crates/oxc_minifier/src/node_util/may_have_side_effects.rs +++ b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs @@ -1,4 +1,4 @@ -use oxc_ast::ast::*; +use oxc_ast::ast::{Expression, ForStatementLeft, PropertyKey, UnaryExpression}; use super::check_for_state_change::CheckForStateChange; @@ -18,3 +18,5 @@ where impl<'a, 'b> MayHaveSideEffects<'a, 'b> for Expression<'a> {} impl<'a, 'b> MayHaveSideEffects<'a, 'b> for UnaryExpression<'a> {} +impl<'a, 'b> MayHaveSideEffects<'a, 'b> for ForStatementLeft<'a> {} +impl<'a, 'b> MayHaveSideEffects<'a, 'b> for PropertyKey<'a> {} diff --git a/crates/oxc_ecmascript/src/side_effects/mod.rs b/crates/oxc_ecmascript/src/side_effects/mod.rs new file mode 100644 index 0000000000000..2993d15e2bdb3 --- /dev/null +++ b/crates/oxc_ecmascript/src/side_effects/mod.rs @@ -0,0 +1,5 @@ +mod check_for_state_change; +mod may_have_side_effects; + +pub use check_for_state_change::CheckForStateChange; +pub use may_have_side_effects::MayHaveSideEffects; diff --git a/crates/oxc_ecmascript/src/string_char_at.rs b/crates/oxc_ecmascript/src/string_char_at.rs new file mode 100644 index 0000000000000..fc018f1ee4129 --- /dev/null +++ b/crates/oxc_ecmascript/src/string_char_at.rs @@ -0,0 +1,34 @@ +use crate::ToInt32; + +pub trait StringCharAt { + /// `String.prototype.charAt ( pos )` + /// + fn char_at(&self, index: Option) -> Option; +} + +impl StringCharAt for &str { + #[expect(clippy::cast_sign_loss)] + fn char_at(&self, index: Option) -> Option { + let index = index.map_or(0, |x| x.to_int_32() as isize); + if index < 0 { + None + } else { + self.chars().nth(index as usize) + } + } +} + +#[cfg(test)] +mod test { + + #[test] + fn test_evaluate_string_char_at() { + use crate::string_char_at::StringCharAt; + assert_eq!("test".char_at(Some(0.0)), Some('t')); + assert_eq!("test".char_at(Some(1.0)), Some('e')); + assert_eq!("test".char_at(Some(2.0)), Some('s')); + assert_eq!("test".char_at(Some(-1.0)), None); + assert_eq!("test".char_at(Some(-1.1)), None); + assert_eq!("test".char_at(Some(-1_073_741_825.0)), None); + } +} diff --git a/crates/oxc_ecmascript/src/string_index_of.rs b/crates/oxc_ecmascript/src/string_index_of.rs new file mode 100644 index 0000000000000..86f81ad2218e4 --- /dev/null +++ b/crates/oxc_ecmascript/src/string_index_of.rs @@ -0,0 +1,50 @@ +use crate::ToInt32; + +pub trait StringIndexOf { + /// `String.prototype.indexOf ( searchString [ , position ] )` + /// + fn index_of(&self, search_value: Option<&str>, from_index: Option) -> isize; +} + +impl StringIndexOf for &str { + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + fn index_of(&self, search_value: Option<&str>, from_index: Option) -> isize { + let from_index = from_index.map_or(0, |x| x.to_int_32().max(0)) as usize; + let Some(search_value) = search_value else { + return -1; + }; + let result = self.chars().skip(from_index).collect::().find(search_value); + result.map(|index| index + from_index).map_or(-1, |index| index as isize) + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_string_index_of() { + use super::StringIndexOf; + + assert_eq!("test test test".index_of(Some("t"), Some(0.0)), 0); + assert_eq!("test test test".index_of(Some("t"), Some(1.0)), 3); + assert_eq!("test test test".index_of(Some("t"), Some(4.0)), 5); + assert_eq!("test test test".index_of(Some("t"), Some(4.1)), 5); + assert_eq!("test test test".index_of(Some("t"), Some(0.0)), 0); + assert_eq!("test test test".index_of(Some("t"), Some(-1.0)), 0); + assert_eq!("test test test".index_of(Some("t"), Some(-1.0)), 0); + assert_eq!("test test test".index_of(Some("t"), Some(-1.1)), 0); + assert_eq!("test test test".index_of(Some("t"), Some(-1_073_741_825.0)), 0); + assert_eq!("test test test".index_of(Some("e"), Some(0.0)), 1); + assert_eq!("test test test".index_of(Some("s"), Some(0.0)), 2); + assert_eq!("test test test".index_of(Some("test"), Some(4.0)), 5); + assert_eq!("test test test".index_of(Some("test"), Some(5.0)), 5); + assert_eq!("test test test".index_of(Some("test"), Some(6.0)), 10); + assert_eq!("test test test".index_of(Some("test"), Some(0.0)), 0); + assert_eq!("test test test".index_of(Some("test"), Some(-1.0)), 0); + assert_eq!("test test test".index_of(Some("not found"), Some(-1.0)), -1); + assert_eq!("test test test".index_of(Some("test"), Some(-1.0)), 0); + assert_eq!("test test test".index_of(Some("test"), Some(-1_073_741_825.0)), 0); + assert_eq!("test test test".index_of(Some("test"), Some(0.0)), 0); + assert_eq!("test test test".index_of(Some("notpresent"), Some(0.0)), -1); + assert_eq!("test test test".index_of(None, Some(0.0)), -1); + } +} diff --git a/crates/oxc_ecmascript/src/string_last_index_of.rs b/crates/oxc_ecmascript/src/string_last_index_of.rs new file mode 100644 index 0000000000000..889f48f71bf1e --- /dev/null +++ b/crates/oxc_ecmascript/src/string_last_index_of.rs @@ -0,0 +1,40 @@ +use crate::ToInt32; + +pub trait StringLastIndexOf { + /// `String.prototype.lastIndexOf ( searchString [ , position ] )` + /// + fn last_index_of(&self, search_value: Option<&str>, from_index: Option) -> isize; +} + +impl StringLastIndexOf for &str { + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + fn last_index_of(&self, search_value: Option<&str>, from_index: Option) -> isize { + let Some(search_value) = search_value else { return -1 }; + let from_index = + from_index.map_or(usize::MAX, |x| x.to_int_32().max(0) as usize + search_value.len()); + self.chars() + .take(from_index) + .collect::() + .rfind(search_value) + .map_or(-1, |index| index as isize) + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_prototype_last_index_of() { + use super::StringLastIndexOf; + assert_eq!("test test test".last_index_of(Some("test"), Some(15.0)), 10); + assert_eq!("test test test".last_index_of(Some("test"), Some(14.0)), 10); + assert_eq!("test test test".last_index_of(Some("test"), Some(10.0)), 10); + assert_eq!("test test test".last_index_of(Some("test"), Some(9.0)), 5); + assert_eq!("test test test".last_index_of(Some("test"), Some(6.0)), 5); + assert_eq!("test test test".last_index_of(Some("test"), Some(5.0)), 5); + assert_eq!("test test test".last_index_of(Some("test"), Some(4.0)), 0); + assert_eq!("test test test".last_index_of(Some("test"), Some(0.0)), 0); + assert_eq!("test test test".last_index_of(Some("notpresent"), Some(0.0)), -1); + assert_eq!("test test test".last_index_of(None, Some(1.0)), -1); + assert_eq!("abcdef".last_index_of(Some("b"), None), 1); + } +} diff --git a/crates/oxc_ecmascript/src/string_to_big_int.rs b/crates/oxc_ecmascript/src/string_to_big_int.rs new file mode 100644 index 0000000000000..844366e9f62c6 --- /dev/null +++ b/crates/oxc_ecmascript/src/string_to_big_int.rs @@ -0,0 +1,42 @@ +use num_traits::Zero; + +use num_bigint::BigInt; + +/// `StringToBigInt` +/// +/// +pub trait StringToBigInt<'a> { + fn string_to_big_int(&self) -> Option; +} + +impl<'a> StringToBigInt<'a> for &str { + fn string_to_big_int(&self) -> Option { + if self.contains('\u{000b}') { + // vertical tab is not always whitespace + return None; + } + + let s = self.trim(); + + if s.is_empty() { + return Some(BigInt::zero()); + } + + if s.len() > 2 && s.starts_with('0') { + let radix: u32 = match s.chars().nth(1) { + Some('x' | 'X') => 16, + Some('o' | 'O') => 8, + Some('b' | 'B') => 2, + _ => 0, + }; + + if radix == 0 { + return None; + } + + return BigInt::parse_bytes(s[2..].as_bytes(), radix); + } + + return BigInt::parse_bytes(s.as_bytes(), 10); + } +} diff --git a/crates/oxc_ecmascript/src/string_to_number.rs b/crates/oxc_ecmascript/src/string_to_number.rs new file mode 100644 index 0000000000000..297a5f7c25ad3 --- /dev/null +++ b/crates/oxc_ecmascript/src/string_to_number.rs @@ -0,0 +1,75 @@ +pub trait StringToNumber { + fn string_to_number(&self) -> f64; +} + +/// `StringToNumber` +/// +/// +impl StringToNumber for &str { + fn string_to_number(&self) -> f64 { + let s = self.trim_matches(is_trimmable_whitespace); + + match s { + "" => return 0.0, + "-Infinity" => return f64::NEG_INFINITY, + "Infinity" | "+Infinity" => return f64::INFINITY, + // Make sure that no further variants of "infinity" are parsed. + "inf" | "-inf" | "+inf" => return f64::NAN, + _ => {} + } + + let mut bytes = s.bytes(); + + if s.len() > 2 && bytes.next() == Some(b'0') { + let radix: u32 = match bytes.next() { + Some(b'x' | b'X') => 16, + Some(b'o' | b'O') => 8, + Some(b'b' | b'B') => 2, + _ => 0, + }; + + if radix != 0 { + let s = &s[2..]; + + // Fast path + if let Ok(value) = u32::from_str_radix(s, radix) { + return f64::from(value); + } + + // Slow path + let mut value: f64 = 0.0; + for c in bytes { + if let Some(digit) = char::from(c).to_digit(radix) { + value = value.mul_add(f64::from(radix), f64::from(digit)); + } else { + return f64::NAN; + } + } + return value; + } + } + + s.parse::().unwrap_or(f64::NAN) + } +} + +// +/// Helper function to check if a `char` is trimmable. +pub(crate) const fn is_trimmable_whitespace(c: char) -> bool { + // The rust implementation of `trim` does not regard the same characters whitespace as ecma standard does + // + // Rust uses \p{White_Space} by default, which also includes: + // `\u{0085}' (next line) + // And does not include: + // '\u{FEFF}' (zero width non-breaking space) + // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space + matches!( + c, + '\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' | + // Unicode Space_Separator category + '\u{1680}' | '\u{2000}' + ..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' | + // Line terminators: https://tc39.es/ecma262/#sec-line-terminators + '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}' + ) +} diff --git a/crates/oxc_ecmascript/src/to_big_int.rs b/crates/oxc_ecmascript/src/to_big_int.rs new file mode 100644 index 0000000000000..8fed6fd11ef4d --- /dev/null +++ b/crates/oxc_ecmascript/src/to_big_int.rs @@ -0,0 +1,70 @@ +use num_bigint::BigInt; +use num_traits::{One, Zero}; + +use oxc_ast::ast::Expression; +use oxc_syntax::operator::UnaryOperator; + +use crate::{StringToBigInt, ToBoolean, ToJsString}; + +/// `ToBigInt` +/// +/// +pub trait ToBigInt<'a> { + fn to_big_int(&self) -> Option; +} + +impl<'a> ToBigInt<'a> for Expression<'a> { + #[expect(clippy::cast_possible_truncation)] + fn to_big_int(&self) -> Option { + match self { + Expression::NumericLiteral(number_literal) => { + let value = number_literal.value; + if value.abs() < 2_f64.powi(53) && value.fract() == 0.0 { + Some(BigInt::from(value as i64)) + } else { + None + } + } + Expression::BigIntLiteral(bigint_literal) => { + let value = bigint_literal.raw.as_str().trim_end_matches('n').string_to_big_int(); + debug_assert!(value.is_some(), "Failed to parse {}", bigint_literal.raw); + value + } + Expression::BooleanLiteral(bool_literal) => { + if bool_literal.value { + Some(BigInt::one()) + } else { + Some(BigInt::zero()) + } + } + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::LogicalNot => { + self.to_boolean().map( + |boolean| { + if boolean { + BigInt::one() + } else { + BigInt::zero() + } + }, + ) + } + UnaryOperator::UnaryNegation => { + unary_expr.argument.to_big_int().map(std::ops::Neg::neg) + } + UnaryOperator::BitwiseNot => { + unary_expr.argument.to_big_int().map(std::ops::Not::not) + } + UnaryOperator::UnaryPlus => unary_expr.argument.to_big_int(), + _ => None, + }, + Expression::StringLiteral(string_literal) => { + string_literal.value.as_str().string_to_big_int() + } + Expression::TemplateLiteral(_) => { + self.to_js_string().and_then(|value| value.as_ref().string_to_big_int()) + } + _ => None, + } + } +} diff --git a/crates/oxc_ecmascript/src/to_boolean.rs b/crates/oxc_ecmascript/src/to_boolean.rs new file mode 100644 index 0000000000000..b210291f94285 --- /dev/null +++ b/crates/oxc_ecmascript/src/to_boolean.rs @@ -0,0 +1,46 @@ +use oxc_ast::ast::Expression; + +/// `ToBoolean` +/// +/// +pub trait ToBoolean<'a> { + fn to_boolean(&self) -> Option; +} + +impl<'a> ToBoolean<'a> for Expression<'a> { + fn to_boolean(&self) -> Option { + // 1. If argument is a Boolean, return argument. + // 2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0ℤ, or the empty String, return false. + // 3. NOTE: This step is replaced in section B.3.6.1. + // 4. Return true. + match self { + Expression::Identifier(ident) => match ident.name.as_str() { + "NaN" | "undefined" => Some(false), + "Infinity" => Some(true), + _ => None, + }, + Expression::RegExpLiteral(_) + | Expression::ArrayExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::ClassExpression(_) + | Expression::FunctionExpression(_) + | Expression::NewExpression(_) + | Expression::ObjectExpression(_) => Some(true), + Expression::NullLiteral(_) => Some(false), + Expression::BooleanLiteral(boolean_literal) => Some(boolean_literal.value), + Expression::NumericLiteral(number_literal) => Some(number_literal.value != 0.0), + Expression::BigIntLiteral(big_int_literal) => Some(!big_int_literal.is_zero()), + Expression::StringLiteral(string_literal) => Some(!string_literal.value.is_empty()), + Expression::TemplateLiteral(template_literal) => { + // only for `` + template_literal + .quasis + .first() + .filter(|quasi| quasi.tail) + .and_then(|quasi| quasi.value.cooked.as_ref()) + .map(|cooked| !cooked.is_empty()) + } + _ => None, + } + } +} diff --git a/crates/oxc_ecmascript/src/to_int_32.rs b/crates/oxc_ecmascript/src/to_int_32.rs new file mode 100644 index 0000000000000..d0a69a48cf16d --- /dev/null +++ b/crates/oxc_ecmascript/src/to_int_32.rs @@ -0,0 +1,83 @@ +/// Converts a 64-bit floating point number to an `i32` according to the [`ToInt32`][ToInt32] algorithm. +/// +/// [ToInt32]: https://tc39.es/ecma262/#sec-toint32 +/// +/// This is copied from [Boa](https://github.com/boa-dev/boa/blob/61567687cf4bfeca6bd548c3e72b6965e74b2461/core/engine/src/builtins/number/conversions.rs) +pub trait ToInt32 { + fn to_int_32(&self) -> i32; +} + +impl ToInt32 for f64 { + #[allow(clippy::float_cmp, clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + fn to_int_32(&self) -> i32 { + const SIGN_MASK: u64 = 0x8000_0000_0000_0000; + const EXPONENT_MASK: u64 = 0x7FF0_0000_0000_0000; + const SIGNIFICAND_MASK: u64 = 0x000F_FFFF_FFFF_FFFF; + const HIDDEN_BIT: u64 = 0x0010_0000_0000_0000; + const PHYSICAL_SIGNIFICAND_SIZE: i32 = 52; // Excludes the hidden bit. + const SIGNIFICAND_SIZE: i32 = 53; + + const EXPONENT_BIAS: i32 = 0x3FF + PHYSICAL_SIGNIFICAND_SIZE; + const DENORMAL_EXPONENT: i32 = -EXPONENT_BIAS + 1; + + fn is_denormal(number: f64) -> bool { + (number.to_bits() & EXPONENT_MASK) == 0 + } + + fn exponent(number: f64) -> i32 { + if is_denormal(number) { + return DENORMAL_EXPONENT; + } + + let d64 = number.to_bits(); + let biased_e = ((d64 & EXPONENT_MASK) >> PHYSICAL_SIGNIFICAND_SIZE) as i32; + + biased_e - EXPONENT_BIAS + } + + fn significand(number: f64) -> u64 { + let d64 = number.to_bits(); + let significand = d64 & SIGNIFICAND_MASK; + + if is_denormal(number) { + significand + } else { + significand + HIDDEN_BIT + } + } + + fn sign(number: f64) -> i64 { + if (number.to_bits() & SIGN_MASK) == 0 { + 1 + } else { + -1 + } + } + + let number = *self; + + if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) { + let i = number as i32; + if f64::from(i) == number { + return i; + } + } + + let exponent = exponent(number); + let bits = if exponent < 0 { + if exponent <= -SIGNIFICAND_SIZE { + return 0; + } + + significand(number) >> -exponent + } else { + if exponent > 31 { + return 0; + } + + (significand(number) << exponent) & 0xFFFF_FFFF + }; + + (sign(number) * (bits as i64)) as i32 + } +} diff --git a/crates/oxc_ecmascript/src/to_number.rs b/crates/oxc_ecmascript/src/to_number.rs new file mode 100644 index 0000000000000..6aa905a404f92 --- /dev/null +++ b/crates/oxc_ecmascript/src/to_number.rs @@ -0,0 +1,35 @@ +#[allow(clippy::wildcard_imports)] +use oxc_ast::ast::*; + +/// `ToNumber` +/// +/// +pub trait ToNumber<'a> { + fn to_number(&self) -> Option; +} + +impl<'a> ToNumber<'a> for Expression<'a> { + fn to_number(&self) -> Option { + match self { + Expression::NumericLiteral(number_literal) => Some(number_literal.value), + Expression::BooleanLiteral(bool_literal) => { + if bool_literal.value { + Some(1.0) + } else { + Some(0.0) + } + } + Expression::NullLiteral(_) => Some(0.0), + Expression::Identifier(ident) => match ident.name.as_str() { + "Infinity" => Some(f64::INFINITY), + "NaN" | "undefined" => Some(f64::NAN), + _ => None, + }, + Expression::StringLiteral(lit) => { + use crate::StringToNumber; + Some(lit.value.as_str().string_to_number()) + } + _ => None, + } + } +} diff --git a/crates/oxc_ecmascript/src/to_string.rs b/crates/oxc_ecmascript/src/to_string.rs new file mode 100644 index 0000000000000..8dcca1540f7b6 --- /dev/null +++ b/crates/oxc_ecmascript/src/to_string.rs @@ -0,0 +1,113 @@ +use std::borrow::Cow; + +#[allow(clippy::wildcard_imports)] +use oxc_ast::ast::*; +use oxc_syntax::operator::UnaryOperator; + +use crate::ToBoolean; + +/// `ToString` +/// +/// +pub trait ToJsString<'a> { + fn to_js_string(&self) -> Option>; +} + +impl<'a> ToJsString<'a> for Expression<'a> { + fn to_js_string(&self) -> Option> { + match self { + Expression::StringLiteral(lit) => lit.to_js_string(), + Expression::TemplateLiteral(lit) => lit.to_js_string(), + Expression::Identifier(ident) => ident.to_js_string(), + Expression::NumericLiteral(lit) => lit.to_js_string(), + Expression::BigIntLiteral(lit) => lit.to_js_string(), + Expression::NullLiteral(lit) => lit.to_js_string(), + Expression::BooleanLiteral(lit) => lit.to_js_string(), + Expression::UnaryExpression(e) => e.to_js_string(), + Expression::ArrayExpression(e) => e.to_js_string(), + Expression::ObjectExpression(e) => e.to_js_string(), + _ => None, + } + } +} + +impl<'a> ToJsString<'a> for StringLiteral<'a> { + fn to_js_string(&self) -> Option> { + Some(Cow::Borrowed(self.value.as_str())) + } +} + +impl<'a> ToJsString<'a> for TemplateLiteral<'a> { + fn to_js_string(&self) -> Option> { + let mut str = String::new(); + for (i, quasi) in self.quasis.iter().enumerate() { + str.push_str(quasi.value.cooked.as_ref()?); + + if i < self.expressions.len() { + let expr = &self.expressions[i]; + let value = expr.to_js_string()?; + str.push_str(&value); + } + } + Some(Cow::Owned(str)) + } +} + +impl<'a> ToJsString<'a> for IdentifierReference<'a> { + fn to_js_string(&self) -> Option> { + let name = self.name.as_str(); + matches!(name, "undefined" | "Infinity" | "NaN").then(|| Cow::Borrowed(name)) + } +} + +impl<'a> ToJsString<'a> for NumericLiteral<'a> { + fn to_js_string(&self) -> Option> { + // FIXME: to js number string + Some(Cow::Owned(self.value.to_string())) + } +} + +impl<'a> ToJsString<'a> for BigIntLiteral<'a> { + fn to_js_string(&self) -> Option> { + // FIXME: to js bigint string + Some(Cow::Owned(self.raw.to_string())) + } +} + +impl<'a> ToJsString<'a> for BooleanLiteral { + fn to_js_string(&self) -> Option> { + Some(Cow::Borrowed(if self.value { "true" } else { "false" })) + } +} + +impl<'a> ToJsString<'a> for NullLiteral { + fn to_js_string(&self) -> Option> { + Some(Cow::Borrowed("null")) + } +} + +impl<'a> ToJsString<'a> for UnaryExpression<'a> { + fn to_js_string(&self) -> Option> { + match self.operator { + UnaryOperator::Void => Some(Cow::Borrowed("undefined")), + UnaryOperator::LogicalNot => self + .argument + .to_boolean() + .map(|boolean| Cow::Borrowed(if boolean { "false" } else { "true" })), + _ => None, + } + } +} + +impl<'a> ToJsString<'a> for ArrayExpression<'a> { + fn to_js_string(&self) -> Option> { + // TODO: https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L302-L303 + None + } +} + +impl<'a> ToJsString<'a> for ObjectExpression<'a> { + fn to_js_string(&self) -> Option> { + Some(Cow::Borrowed("[object Object]")) + } +} diff --git a/crates/oxc_index/Cargo.toml b/crates/oxc_index/Cargo.toml index 72d3c289cbde9..3556f0cd46981 100644 --- a/crates/oxc_index/Cargo.toml +++ b/crates/oxc_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_index" -version = "0.30.5" +version = "0.31.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 c17a20e05538d..f9d05d4070677 100644 --- a/crates/oxc_isolated_declarations/CHANGELOG.md +++ b/crates/oxc_isolated_declarations/CHANGELOG.md @@ -4,6 +4,21 @@ 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.31.0] - 2024-10-08 + +- 020bb80 codegen: [**BREAKING**] Change to `CodegenReturn::code` and `CodegenReturn::map` (#6310) (Boshen) + +### Features + +- 9e62396 syntax_operations: Add crate `oxc_ecmascript` (#6202) (Boshen) + +### Bug Fixes + +- e9eeae0 isolated-declarations: False positive for function with a type asserted parameters (#6181) (Dunqing) + +### Refactor + + ## [0.30.3] - 2024-09-27 ### Bug Fixes diff --git a/crates/oxc_isolated_declarations/Cargo.toml b/crates/oxc_isolated_declarations/Cargo.toml index 0309938fc1c40..481b30485bd5e 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.30.5" +version = "0.31.0" authors.workspace = true categories.workspace = true edition.workspace = true @@ -27,6 +27,7 @@ test = false oxc_allocator = { workspace = true } oxc_ast = { workspace = true } oxc_diagnostics = { workspace = true } +oxc_ecmascript = { workspace = true } oxc_span = { workspace = true } oxc_syntax = { workspace = true, features = ["to_js_string"] } diff --git a/crates/oxc_isolated_declarations/examples/isolated_declarations.rs b/crates/oxc_isolated_declarations/examples/isolated_declarations.rs index c20970ea0f4c4..ce354f662ae42 100644 --- a/crates/oxc_isolated_declarations/examples/isolated_declarations.rs +++ b/crates/oxc_isolated_declarations/examples/isolated_declarations.rs @@ -2,7 +2,7 @@ use std::{env, path::Path}; use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CommentOptions}; +use oxc_codegen::CodeGenerator; use oxc_isolated_declarations::{IsolatedDeclarations, IsolatedDeclarationsOptions}; use oxc_parser::Parser; use oxc_span::SourceType; @@ -32,21 +32,10 @@ fn main() { println!("Original:\n"); println!("{source_text}\n"); - let id_ret = IsolatedDeclarations::new( - &allocator, - &source_text, - &ret.trivias, - IsolatedDeclarationsOptions { strip_internal: true }, - ) - .build(&ret.program); - let printed = CodeGenerator::new() - .enable_comment( - &source_text, - ret.trivias, - CommentOptions { preserve_annotate_comments: false }, - ) - .build(&id_ret.program) - .source_text; + let id_ret = + IsolatedDeclarations::new(&allocator, IsolatedDeclarationsOptions { strip_internal: true }) + .build(&ret.program); + let printed = CodeGenerator::new().build(&id_ret.program).code; println!("Dts Emit:\n"); println!("{printed}\n"); diff --git a/crates/oxc_isolated_declarations/src/declaration.rs b/crates/oxc_isolated_declarations/src/declaration.rs index e0c29c9d141ff..ad88e1f0b11c8 100644 --- a/crates/oxc_isolated_declarations/src/declaration.rs +++ b/crates/oxc_isolated_declarations/src/declaration.rs @@ -6,7 +6,8 @@ use oxc_allocator::Vec; #[allow(clippy::wildcard_imports)] use oxc_ast::ast::*; use oxc_ast::visit::walk_mut::walk_ts_signatures; -use oxc_ast::{syntax_directed_operations::BoundNames, Visit, VisitMut}; +use oxc_ast::{Visit, VisitMut}; +use oxc_ecmascript::BoundNames; use oxc_span::{GetSpan, SPAN}; use oxc_syntax::scope::ScopeFlags; diff --git a/crates/oxc_isolated_declarations/src/enum.rs b/crates/oxc_isolated_declarations/src/enum.rs index 0e9150b12d02b..8ff44ed085319 100644 --- a/crates/oxc_isolated_declarations/src/enum.rs +++ b/crates/oxc_isolated_declarations/src/enum.rs @@ -1,11 +1,13 @@ +use rustc_hash::FxHashMap; + #[allow(clippy::wildcard_imports)] use oxc_ast::ast::*; +use oxc_ecmascript::ToInt32; use oxc_span::{Atom, GetSpan, SPAN}; use oxc_syntax::{ - number::{NumberBase, ToJsInt32, ToJsString}, + number::{NumberBase, ToJsString}, operator::{BinaryOperator, UnaryOperator}, }; -use rustc_hash::FxHashMap; use crate::{diagnostics::enum_member_initializers, IsolatedDeclarations}; @@ -223,22 +225,22 @@ impl<'a> IsolatedDeclarations<'a> { match expr.operator { BinaryOperator::ShiftRight => Some(ConstantValue::Number(f64::from( - left.to_js_int_32().wrapping_shr(right.to_js_int_32() as u32), + left.to_int_32().wrapping_shr(right.to_int_32() as u32), ))), BinaryOperator::ShiftRightZeroFill => Some(ConstantValue::Number(f64::from( - (left.to_js_int_32() as u32).wrapping_shr(right.to_js_int_32() as u32), + (left.to_int_32() as u32).wrapping_shr(right.to_int_32() as u32), ))), BinaryOperator::ShiftLeft => Some(ConstantValue::Number(f64::from( - left.to_js_int_32().wrapping_shl(right.to_js_int_32() as u32), + left.to_int_32().wrapping_shl(right.to_int_32() as u32), ))), BinaryOperator::BitwiseXOR => { - Some(ConstantValue::Number(f64::from(left.to_js_int_32() ^ right.to_js_int_32()))) + Some(ConstantValue::Number(f64::from(left.to_int_32() ^ right.to_int_32()))) } BinaryOperator::BitwiseOR => { - Some(ConstantValue::Number(f64::from(left.to_js_int_32() | right.to_js_int_32()))) + Some(ConstantValue::Number(f64::from(left.to_int_32() | right.to_int_32()))) } BinaryOperator::BitwiseAnd => { - Some(ConstantValue::Number(f64::from(left.to_js_int_32() & right.to_js_int_32()))) + Some(ConstantValue::Number(f64::from(left.to_int_32() & right.to_int_32()))) } BinaryOperator::Multiplication => Some(ConstantValue::Number(left * right)), BinaryOperator::Division => Some(ConstantValue::Number(left / right)), @@ -276,9 +278,7 @@ impl<'a> IsolatedDeclarations<'a> { match expr.operator { UnaryOperator::UnaryPlus => Some(ConstantValue::Number(value)), UnaryOperator::UnaryNegation => Some(ConstantValue::Number(-value)), - UnaryOperator::BitwiseNot => { - Some(ConstantValue::Number(f64::from(!value.to_js_int_32()))) - } + UnaryOperator::BitwiseNot => Some(ConstantValue::Number(f64::from(!value.to_int_32()))), _ => None, } } diff --git a/crates/oxc_isolated_declarations/src/inferrer.rs b/crates/oxc_isolated_declarations/src/inferrer.rs index faa3827085b4f..21856c09b846f 100644 --- a/crates/oxc_isolated_declarations/src/inferrer.rs +++ b/crates/oxc_isolated_declarations/src/inferrer.rs @@ -3,12 +3,10 @@ use oxc_ast::ast::{ ArrowFunctionExpression, BindingPatternKind, Expression, FormalParameter, Function, Statement, TSType, TSTypeAnnotation, UnaryExpression, }; -use oxc_span::{GetSpan, SPAN}; +use oxc_span::SPAN; use crate::{ - diagnostics::{ - array_inferred, inferred_type_of_class_expression, parameter_must_have_explicit_type, - }, + diagnostics::{array_inferred, inferred_type_of_class_expression}, return_type::FunctionReturnType, IsolatedDeclarations, }; @@ -101,12 +99,6 @@ impl<'a> IsolatedDeclarations<'a> { // SAFETY: `ast.copy` is unsound! We need to fix. Some(unsafe { self.ast.copy(&annotation.type_annotation) }) } else { - if let Expression::TSAsExpression(expr) = &pattern.right { - if !expr.type_annotation.is_keyword_or_literal() { - self.error(parameter_must_have_explicit_type(expr.type_annotation.span())); - } - } - self.infer_type_from_expression(&pattern.right) } } else { diff --git a/crates/oxc_isolated_declarations/src/lib.rs b/crates/oxc_isolated_declarations/src/lib.rs index 1601535125cb6..84303623efda5 100644 --- a/crates/oxc_isolated_declarations/src/lib.rs +++ b/crates/oxc_isolated_declarations/src/lib.rs @@ -24,21 +24,27 @@ use std::{cell::RefCell, mem}; use diagnostics::function_with_assigning_properties; use oxc_allocator::{Allocator, CloneIn}; #[allow(clippy::wildcard_imports)] -use oxc_ast::{ast::*, AstBuilder, Trivias, Visit, NONE}; +use oxc_ast::{ast::*, AstBuilder, Visit, NONE}; use oxc_diagnostics::OxcDiagnostic; use oxc_span::{Atom, GetSpan, SourceType, SPAN}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::scope::ScopeTree; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy)] pub struct IsolatedDeclarationsOptions { - /// Do not emit declarations for code that has an @internal annotation in its JSDoc comment. - /// This is an internal compiler option; use at your own risk, because the compiler does not check that the result is valid. - /// + /// Do not emit declarations for code that has an `@internal` annotation in its JSDoc comment. + /// This is an internal compiler option; use at your own risk, because the compiler does not + /// check that the result is valid. + /// + /// Default: `false` + /// + /// ## References + /// [TSConfig - `stripInternal`](https://www.typescriptlang.org/tsconfig/#stripInternal) pub strip_internal: bool, } +#[non_exhaustive] pub struct IsolatedDeclarationsReturn<'a> { pub program: Program<'a>, pub errors: Vec, @@ -59,21 +65,12 @@ pub struct IsolatedDeclarations<'a> { } impl<'a> IsolatedDeclarations<'a> { - pub fn new( - allocator: &'a Allocator, - source_text: &str, - trivias: &Trivias, - options: IsolatedDeclarationsOptions, - ) -> Self { + pub fn new(allocator: &'a Allocator, options: IsolatedDeclarationsOptions) -> Self { let strip_internal = options.strip_internal; - let is_internal_set = strip_internal - .then(|| Self::build_internal_annotations(source_text, trivias)) - .unwrap_or_default(); - Self { ast: AstBuilder::new(allocator), strip_internal, - internal_annotations: is_internal_set, + internal_annotations: FxHashSet::default(), scope: ScopeTree::new(allocator), errors: RefCell::new(vec![]), } @@ -83,10 +80,22 @@ impl<'a> IsolatedDeclarations<'a> { /// /// Returns `Vec` if any errors were collected during the transformation. pub fn build(mut self, program: &Program<'a>) -> IsolatedDeclarationsReturn<'a> { + self.internal_annotations = self + .strip_internal + .then(|| Self::build_internal_annotations(program)) + .unwrap_or_default(); let source_type = SourceType::d_ts(); let directives = self.ast.vec(); let stmts = self.transform_program(program); - let program = self.ast.program(SPAN, source_type, None, directives, stmts); + let program = self.ast.program( + SPAN, + source_type, + program.source_text, + self.ast.vec_from_iter(program.comments.iter().copied()), + None, + directives, + stmts, + ); IsolatedDeclarationsReturn { program, errors: self.take_errors() } } @@ -100,10 +109,10 @@ impl<'a> IsolatedDeclarations<'a> { } /// Build the lookup table for jsdoc `@internal`. - fn build_internal_annotations(source_text: &str, trivias: &Trivias) -> FxHashSet { + fn build_internal_annotations(program: &Program<'a>) -> FxHashSet { let mut set = FxHashSet::default(); - for comment in trivias.comments() { - let has_internal = comment.span.source_text(source_text).contains("@internal"); + for comment in &program.comments { + let has_internal = comment.span.source_text(program.source_text).contains("@internal"); // Use the first jsdoc comment if there are multiple jsdoc comments for the same node. if has_internal && !set.contains(&comment.attached_to) { set.insert(comment.attached_to); diff --git a/crates/oxc_isolated_declarations/src/types.rs b/crates/oxc_isolated_declarations/src/types.rs index e01da5a121075..8ee3b0daa5574 100644 --- a/crates/oxc_isolated_declarations/src/types.rs +++ b/crates/oxc_isolated_declarations/src/types.rs @@ -30,11 +30,11 @@ impl<'a> IsolatedDeclarations<'a> { self.ast.ts_type_function_type( func.span, // SAFETY: `ast.copy` is unsound! We need to fix. + unsafe { self.ast.copy(&func.type_parameters) }, + // SAFETY: `ast.copy` is unsound! We need to fix. unsafe { self.ast.copy(&func.this_param) }, params, return_type, - // SAFETY: `ast.copy` is unsound! We need to fix. - unsafe { self.ast.copy(&func.type_parameters) }, ) }) } @@ -57,11 +57,11 @@ impl<'a> IsolatedDeclarations<'a> { return_type.map(|return_type| { self.ast.ts_type_function_type( func.span, + // SAFETY: `ast.copy` is unsound! We need to fix. + unsafe { self.ast.copy(&func.type_parameters) }, NONE, params, return_type, - // SAFETY: `ast.copy` is unsound! We need to fix. - unsafe { self.ast.copy(&func.type_parameters) }, ) }) } @@ -107,11 +107,11 @@ impl<'a> IsolatedDeclarations<'a> { false, TSMethodSignatureKind::Method, // SAFETY: `ast.copy` is unsound! We need to fix. + unsafe { self.ast.copy(&function.type_parameters) }, + // SAFETY: `ast.copy` is unsound! We need to fix. unsafe { self.ast.copy(&function.this_param) }, params, return_type, - // SAFETY: `ast.copy` is unsound! We need to fix. - unsafe { self.ast.copy(&function.type_parameters) }, )); } } diff --git a/crates/oxc_isolated_declarations/tests/deno/mod.rs b/crates/oxc_isolated_declarations/tests/deno/mod.rs index 211b7f3b37947..b320b6738032c 100644 --- a/crates/oxc_isolated_declarations/tests/deno/mod.rs +++ b/crates/oxc_isolated_declarations/tests/deno/mod.rs @@ -15,14 +15,12 @@ mod tests { let ret = Parser::new(&allocator, source, source_type).parse(); let ret = IsolatedDeclarations::new( &allocator, - source, - &ret.trivias, IsolatedDeclarationsOptions { strip_internal: true }, ) .build(&ret.program); - let actual = CodeGenerator::new().build(&ret.program).source_text; + let actual = CodeGenerator::new().build(&ret.program).code; let expected_program = Parser::new(&allocator, expected, source_type).parse().program; - let expected = CodeGenerator::new().build(&expected_program).source_text; + let expected = CodeGenerator::new().build(&expected_program).code; assert_eq!(actual.trim(), expected.trim()); } diff --git a/crates/oxc_isolated_declarations/tests/fixtures/function-parameters.ts b/crates/oxc_isolated_declarations/tests/fixtures/function-parameters.ts index 4e9efc68b9d19..4d348e1cc0c2f 100644 --- a/crates/oxc_isolated_declarations/tests/fixtures/function-parameters.ts +++ b/crates/oxc_isolated_declarations/tests/fixtures/function-parameters.ts @@ -30,3 +30,5 @@ export const fooBad2 = ({a, b} = { a: 1, b: 2 }): number => { export function withAny(a: any = 1, b: string): void { } export function withUnknown(a: unknown = 1, b: string): void { } + +export function withTypeAssertion(a = /regular-repression-cannot-infer/ as any): void { } \ No newline at end of file diff --git a/crates/oxc_isolated_declarations/tests/mod.rs b/crates/oxc_isolated_declarations/tests/mod.rs index 84531a961d905..7c3d2b184cc0b 100644 --- a/crates/oxc_isolated_declarations/tests/mod.rs +++ b/crates/oxc_isolated_declarations/tests/mod.rs @@ -3,7 +3,7 @@ mod deno; use std::{fs, path::Path, sync::Arc}; use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CommentOptions}; +use oxc_codegen::CodeGenerator; use oxc_isolated_declarations::{IsolatedDeclarations, IsolatedDeclarationsOptions}; use oxc_parser::Parser; use oxc_span::SourceType; @@ -13,21 +13,10 @@ fn transform(path: &Path, source_text: &str) -> String { let source_type = SourceType::from_path(path).unwrap(); let parser_ret = Parser::new(&allocator, source_text, source_type).parse(); - let id_ret = IsolatedDeclarations::new( - &allocator, - source_text, - &parser_ret.trivias, - IsolatedDeclarationsOptions { strip_internal: true }, - ) - .build(&parser_ret.program); - let code = CodeGenerator::new() - .enable_comment( - source_text, - parser_ret.trivias, - CommentOptions { preserve_annotate_comments: false }, - ) - .build(&id_ret.program) - .source_text; + let id_ret = + IsolatedDeclarations::new(&allocator, IsolatedDeclarationsOptions { strip_internal: true }) + .build(&parser_ret.program); + let code = CodeGenerator::new().build(&id_ret.program).code; let mut snapshot = format!("```\n==================== .D.TS ====================\n\n{code}\n\n"); diff --git a/crates/oxc_isolated_declarations/tests/snapshots/function-parameters.snap b/crates/oxc_isolated_declarations/tests/snapshots/function-parameters.snap index 4a017d85c447d..e5769c27b5e7e 100644 --- a/crates/oxc_isolated_declarations/tests/snapshots/function-parameters.snap +++ b/crates/oxc_isolated_declarations/tests/snapshots/function-parameters.snap @@ -17,6 +17,7 @@ export declare function fooBad(): number; export declare const fooBad2: () => number; export declare function withAny(a: any, b: string): void; export declare function withUnknown(a: unknown, b: string): void; +export declare function withTypeAssertion(a?: any): void; ==================== Errors ==================== diff --git a/crates/oxc_language_server/src/linter.rs b/crates/oxc_language_server/src/linter.rs index 9ac26e6966ef7..cd165a04ede8f 100644 --- a/crates/oxc_language_server/src/linter.rs +++ b/crates/oxc_language_server/src/linter.rs @@ -3,6 +3,7 @@ use std::{ fs, path::{Path, PathBuf}, rc::Rc, + str::FromStr, sync::{Arc, OnceLock}, }; @@ -19,9 +20,11 @@ use oxc_span::VALID_EXTENSIONS; use ropey::Rope; use rustc_hash::FxHashSet; use tower_lsp::lsp_types::{ - self, DiagnosticRelatedInformation, DiagnosticSeverity, Position, Range, Url, + self, CodeDescription, DiagnosticRelatedInformation, DiagnosticSeverity, NumberOrString, + Position, Range, Url, }; +const LINT_DOC_LINK_PREFIX: &str = "https://oxc.rs/docs/guide/usage/linter/rules"; #[derive(Debug)] struct ErrorWithPosition { pub start_pos: Position, @@ -107,7 +110,13 @@ impl ErrorWithPosition { ret_range }, ); - + let code = self.miette_err.code().map(|item| item.to_string()); + let code_description = code.as_ref().and_then(|code| { + let (scope, number) = parse_diagnostic_code(code)?; + Some(CodeDescription { + href: Url::from_str(&format!("{LINT_DOC_LINK_PREFIX}/{scope}/{number}")).ok()?, + }) + }); let message = self.miette_err.help().map_or_else( || self.miette_err.to_string(), |help| format!("{}\nhelp: {}", self.miette_err, help), @@ -116,10 +125,10 @@ impl ErrorWithPosition { lsp_types::Diagnostic { range, severity, - code: None, + code: code.map(NumberOrString::String), message, source: Some("oxc".into()), - code_description: None, + code_description, related_information, tags: None, data: None, @@ -176,7 +185,6 @@ impl IsolatedLintHandler { let Some(ref related_info) = d.diagnostic.related_information else { continue; }; - let related_information = Some(vec![DiagnosticRelatedInformation { location: lsp_types::Location { uri: lsp_types::Url::from_file_path(path).unwrap(), @@ -194,7 +202,7 @@ impl IsolatedLintHandler { severity: Some(DiagnosticSeverity::HINT), code: None, message: r.message.clone(), - source: Some("oxc".into()), + source: d.diagnostic.source.clone(), code_description: None, related_information: related_information.clone(), tags: None, @@ -259,12 +267,10 @@ impl IsolatedLintHandler { return Some(Self::wrap_diagnostics(path, &source_text, reports, start)); }; - let program = allocator.alloc(ret.program); - let semantic_ret = SemanticBuilder::new(javascript_source_text) + let semantic_ret = SemanticBuilder::new() .with_cfg(true) - .with_trivias(ret.trivias) .with_check_syntax_error(true) - .build(program); + .build(&ret.program); if !semantic_ret.errors.is_empty() { let reports = semantic_ret @@ -278,7 +284,9 @@ impl IsolatedLintHandler { return Some(Self::wrap_diagnostics(path, &source_text, reports, start)); }; - let result = self.linter.run(path, Rc::new(semantic_ret.semantic)); + let mut semantic = semantic_ret.semantic; + semantic.set_irregular_whitespaces(ret.irregular_whitespaces); + let result = self.linter.run(path, Rc::new(semantic)); let reports = result .into_iter() @@ -288,12 +296,12 @@ impl IsolatedLintHandler { range: Range { start: offset_to_position( (f.span.start + start) as usize, - javascript_source_text, + source_text.as_str(), ) .unwrap_or_default(), end: offset_to_position( (f.span.end + start) as usize, - javascript_source_text, + source_text.as_str(), ) .unwrap_or_default(), }, @@ -380,3 +388,12 @@ fn cmp_range(first: &Range, other: &Range) -> std::cmp::Ordering { o => o, } } + +/// parse `OxcCode` to `Option<(scope, number)>` +fn parse_diagnostic_code(code: &str) -> Option<(&str, &str)> { + if !code.ends_with(')') { + return None; + } + let right_parenthesis_pos = code.rfind('(')?; + Some((&code[0..right_parenthesis_pos], &code[right_parenthesis_pos + 1..code.len() - 1])) +} diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index f55dd1342c66a..daedab95e5fd8 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -7,7 +7,7 @@ use futures::future::join_all; use globset::Glob; use ignore::gitignore::Gitignore; use log::{debug, error, info}; -use oxc_linter::{FixKind, Linter, OxlintOptions}; +use oxc_linter::{FixKind, LinterBuilder, Oxlintrc}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, OnceCell, RwLock, SetError}; use tower_lsp::{ @@ -345,12 +345,13 @@ impl Backend { if let Some(config_path) = config_path { let mut linter = self.server_linter.write().await; *linter = ServerLinter::new_with_linter( - Linter::from_options( - OxlintOptions::default() - .with_fix(FixKind::SafeFix) - .with_config_path(Some(config_path)), + LinterBuilder::from_oxlintrc( + true, + Oxlintrc::from_file(&config_path) + .expect("should have initialized linter with new options"), ) - .expect("should have initialized linter with new options"), + .with_fix(FixKind::SafeFix) + .build(), ); } } diff --git a/crates/oxc_linter/CHANGELOG.md b/crates/oxc_linter/CHANGELOG.md index da3eda5fab752..677bd365739be 100644 --- a/crates/oxc_linter/CHANGELOG.md +++ b/crates/oxc_linter/CHANGELOG.md @@ -4,6 +4,56 @@ 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.9.10] - 2024-10-07 + +- 5a73a66 regular_expression: [**BREAKING**] Simplify public APIs (#6262) (leaysgur) + +### Features + +- 376cc09 linter: Implement `no-throw-literal` (#6144) (dalaoshu) +- 5957214 linter: Allow fixing in files with source offsets (#6197) (camchenry) +- a089e19 linter: Eslint/no-else-return (#4305) (yoho) +- 183739f linter: Implement prefer-await-to-callbacks (#6153) (dalaoshu) +- ae539af linter: Implement no-return-assign (#6108) (Radu Baston) + +### Bug Fixes + +- 9e9808b linter: Fix regression when parsing ts in vue files (#6336) (Boshen) +- 93c6db6 linter: Improve docs and diagnostics message for no-else-return (#6327) (DonIsaac) +- e0a3378 linter: Correct false positive in `unicorn/prefer-string-replace-all` (#6263) (H11) +- ea28ee9 linter: Improve the fixer of `prefer-namespace-keyword` (#6230) (dalaoshu) +- f6a3450 linter: Get correct source offsets for astro files (#6196) (camchenry) +- be0030c linter: Allow whitespace control characters in `no-control-regex` (#6140) (camchenry) +- e7e8ead linter: False positive in `no-return-assign` (#6128) (DonIsaac) + +### Performance + +- ac0a82a linter: Reuse allocator when there are multiple source texts (#6337) (Boshen) +- 50a0029 linter: Do not concat vec in `no-useless-length-check` (#6276) (camchenry) + +### Documentation + +- 7ca70dd linter: Add docs for `ContextHost` and `LintContext` (#6272) (camchenry) +- a949ecb linter: Improve docs for `eslint/getter-return` (#6229) (DonIsaac) +- 14ba263 linter: Improve docs for `eslint-plugin-import` rules (#6131) (dalaoshu) + +### Refactor + +- 642725c linter: Rename vars from `ast_node_id` to `node_id` (#6305) (overlookmotel) +- 8413175 linter: Move shared function from utils to rule (#6127) (dalaoshu) +- ba9c372 linter: Make jest/vitest rule mapping more clear (#6273) (camchenry) +- 82b8f21 linter: Add schemars and serde traits to AllowWarnDeny and RuleCategories (#6119) (DonIsaac) +- ea908f7 linter: Consolidate file loading logic (#6130) (DonIsaac) +- db751f0 linter: Use regexp AST visitor in `no-control-regex` (#6129) (camchenry) +- 3aa7e42 linter: Use RegExp AST visitor for `no-hex-escape` (#6117) (camchenry) +- 9d5b44a linter: Use regex visitor in `no-regex-spaces` (#6063) (camchenry) +- 0d44cf7 linter: Use regex visitor in `no-useless-escape` (#6062) (camchenry) +- eeb8873 linter: Use regex visitor in `no-empty-character-class` (#6058) (camchenry) + +### Testing + +- d883562 linter: Invalid `eslint/no-unused-vars` options (#6228) (DonIsaac) + ## [0.9.9] - 2024-09-27 ### Bug Fixes diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 73874ed8b8fe2..5303d2cd715ba 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxc_linter" -version = "0.9.9" +version = "0.9.10" authors.workspace = true categories.workspace = true edition.workspace = true @@ -25,6 +25,7 @@ oxc_ast = { workspace = true } oxc_cfg = { workspace = true } oxc_codegen = { workspace = true } oxc_diagnostics = { workspace = true } +oxc_ecmascript = { workspace = true } oxc_index = { workspace = true } oxc_macros = { workspace = true } oxc_parser = { workspace = true } @@ -60,5 +61,5 @@ url = { workspace = true } [dev-dependencies] insta = { workspace = true } -markdown = { version = "1.0.0-alpha.19" } +markdown = { workspace = true } project-root = { workspace = true } diff --git a/crates/oxc_linter/examples/linter.rs b/crates/oxc_linter/examples/linter.rs index be0ae5575bd2b..571ec40ca3859 100644 --- a/crates/oxc_linter/examples/linter.rs +++ b/crates/oxc_linter/examples/linter.rs @@ -29,8 +29,7 @@ fn main() -> std::io::Result<()> { return Ok(()); } - let program = allocator.alloc(ret.program); - let semantic_ret = SemanticBuilder::new(&source_text).with_trivias(ret.trivias).build(program); + let semantic_ret = SemanticBuilder::new().build(&ret.program); let mut errors: Vec = vec![]; diff --git a/crates/oxc_linter/src/ast_util.rs b/crates/oxc_linter/src/ast_util.rs index ad8b2f063aa6d..501c7d77e2c65 100644 --- a/crates/oxc_linter/src/ast_util.rs +++ b/crates/oxc_linter/src/ast_util.rs @@ -1,4 +1,5 @@ use oxc_ast::{ast::BindingIdentifier, AstKind}; +use oxc_ecmascript::ToBoolean; use oxc_semantic::{AstNode, IsGlobalReference, NodeId, SymbolId}; use oxc_span::{GetSpan, Span}; use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator}; @@ -286,11 +287,15 @@ pub fn extract_regex_flags<'a>( if args.len() <= 1 { return None; } - let Argument::StringLiteral(flag_arg) = &args[1] else { - return None; + let flag_arg = match &args[1] { + Argument::StringLiteral(flag_arg) => flag_arg.value.clone(), + Argument::TemplateLiteral(template) if template.is_no_substitution_template() => { + template.quasi().expect("no-substitution templates always have a quasi") + } + _ => return None, }; let mut flags = RegExpFlags::empty(); - for ch in flag_arg.value.chars() { + for ch in flag_arg.chars() { let flag = RegExpFlags::try_from(ch).ok()?; flags |= flag; } diff --git a/crates/oxc_linter/src/builder.rs b/crates/oxc_linter/src/builder.rs index c20600620ef1d..5ef238745c678 100644 --- a/crates/oxc_linter/src/builder.rs +++ b/crates/oxc_linter/src/builder.rs @@ -70,7 +70,8 @@ impl LinterBuilder { /// ``` pub fn from_oxlintrc(start_empty: bool, oxlintrc: Oxlintrc) -> Self { // TODO: monorepo config merging, plugin-based extends, etc. - let Oxlintrc { plugins, settings, env, globals, rules: oxlintrc_rules } = oxlintrc; + let Oxlintrc { plugins, settings, env, globals, categories, rules: oxlintrc_rules } = + oxlintrc; let config = LintConfig { settings, env, globals }; let options = LintOptions { plugins, ..Default::default() }; @@ -79,6 +80,10 @@ impl LinterBuilder { let cache = RulesCache::new(options.plugins); let mut builder = Self { rules, options, config, cache }; + if !categories.is_empty() { + builder = builder.with_filters(categories.filters()); + } + { let all_rules = builder.cache.borrow(); oxlintrc_rules.override_rules(&mut builder.rules, all_rules.as_slice()); @@ -536,4 +541,55 @@ mod test { let builder = builder.with_plugins(expected_plugins); assert_eq!(expected_plugins, builder.plugins()); } + + #[test] + fn test_categories() { + let oxlintrc: Oxlintrc = serde_json::from_str( + r#" + { + "categories": { + "correctness": "warn", + "suspicious": "deny" + }, + "rules": { + "no-const-assign": "error" + } + } + "#, + ) + .unwrap(); + let builder = LinterBuilder::from_oxlintrc(false, oxlintrc); + for rule in &builder.rules { + let name = rule.name(); + let plugin = rule.plugin_name(); + let category = rule.category(); + match category { + RuleCategory::Correctness => { + if name == "no-const-assign" { + assert_eq!( + rule.severity, + AllowWarnDeny::Deny, + "no-const-assign should be denied", + ); + } else { + assert_eq!( + rule.severity, + AllowWarnDeny::Warn, + "{plugin}/{name} should be a warning" + ); + } + } + RuleCategory::Suspicious => { + assert_eq!( + rule.severity, + AllowWarnDeny::Deny, + "{plugin}/{name} should be denied" + ); + } + invalid => { + panic!("Found rule {plugin}/{name} with an unexpected category {invalid:?}"); + } + } + } + } } diff --git a/crates/oxc_linter/src/config/categories.rs b/crates/oxc_linter/src/config/categories.rs new file mode 100644 index 0000000000000..ef79217432825 --- /dev/null +++ b/crates/oxc_linter/src/config/categories.rs @@ -0,0 +1,85 @@ +use std::{borrow::Cow, ops::Deref}; + +use rustc_hash::FxHashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{AllowWarnDeny, LintFilter, RuleCategory}; + +/// Configure an entire category of rules all at once. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct OxlintCategories(FxHashMap); + +impl Deref for OxlintCategories { + type Target = FxHashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl OxlintCategories { + pub fn filters(&self) -> impl Iterator + '_ { + self.iter().map(|(category, severity)| LintFilter::new(*severity, *category).unwrap()) + } +} + +impl JsonSchema for OxlintCategories { + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("OxlintCategories") + } + + fn schema_name() -> String { + "OxlintCategories".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + let severity = gen.subschema_for::(); + let mut schema = + gen.subschema_for::>().into_object(); + + { + schema.object().additional_properties = None; + let properties = &mut schema.object().properties; + + properties.insert(RuleCategory::Correctness.as_str().to_string(), severity.clone()); + properties.insert(RuleCategory::Suspicious.as_str().to_string(), severity.clone()); + properties.insert(RuleCategory::Pedantic.as_str().to_string(), severity.clone()); + properties.insert(RuleCategory::Perf.as_str().to_string(), severity.clone()); + properties.insert(RuleCategory::Style.as_str().to_string(), severity.clone()); + properties.insert(RuleCategory::Restriction.as_str().to_string(), severity.clone()); + properties.insert(RuleCategory::Nursery.as_str().to_string(), severity.clone()); + } + + { + let metadata = schema.metadata(); + metadata.title = Some("Rule Categories".to_string()); + + metadata.description = Some( + r#" +Configure an entire category of rules all at once. + +Rules enabled or disabled this way will be overwritten by individual rules in the `rules` field. + +# Example +```json +{ + "categories": { + "correctness": "warn" + }, + "rules": { + "eslint/no-unused-vars": "error" + } +} +``` +"# + .trim() + .to_string(), + ); + + metadata.examples = vec![serde_json::json!({ "correctness": "warn" })]; + } + + schema.into() + } +} diff --git a/crates/oxc_linter/src/config/env.rs b/crates/oxc_linter/src/config/env.rs index 8649a01590fa8..9f11447315421 100644 --- a/crates/oxc_linter/src/config/env.rs +++ b/crates/oxc_linter/src/config/env.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; /// environments](https://eslint.org/docs/v8.x/use/configure/language-options#specifying-environments) /// for what environments are available and what each one provides. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct OxlintEnv(FxHashMap); impl OxlintEnv { diff --git a/crates/oxc_linter/src/config/mod.rs b/crates/oxc_linter/src/config/mod.rs index 229c31fa8912b..7980952d79116 100644 --- a/crates/oxc_linter/src/config/mod.rs +++ b/crates/oxc_linter/src/config/mod.rs @@ -1,3 +1,4 @@ +mod categories; mod env; mod globals; mod oxlintrc; diff --git a/crates/oxc_linter/src/config/oxlintrc.rs b/crates/oxc_linter/src/config/oxlintrc.rs index 100fb7ca6585b..e9754cebcd7d3 100644 --- a/crates/oxc_linter/src/config/oxlintrc.rs +++ b/crates/oxc_linter/src/config/oxlintrc.rs @@ -4,7 +4,10 @@ use oxc_diagnostics::OxcDiagnostic; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use super::{env::OxlintEnv, globals::OxlintGlobals, rules::OxlintRules, settings::OxlintSettings}; +use super::{ + categories::OxlintCategories, env::OxlintEnv, globals::OxlintGlobals, rules::OxlintRules, + settings::OxlintSettings, +}; use crate::{options::LintPlugins, utils::read_to_string}; @@ -45,6 +48,7 @@ use crate::{options::LintPlugins, utils::read_to_string}; #[non_exhaustive] pub struct Oxlintrc { pub plugins: LintPlugins, + pub categories: OxlintCategories, /// See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html). pub rules: OxlintRules, pub settings: OxlintSettings, @@ -90,3 +94,48 @@ impl Oxlintrc { Ok(config) } } + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn test_oxlintrc_de_empty() { + let config: Oxlintrc = serde_json::from_value(json!({})).unwrap(); + assert_eq!(config.plugins, LintPlugins::default()); + assert_eq!(config.rules, OxlintRules::default()); + assert!(config.rules.is_empty()); + assert_eq!(config.settings, OxlintSettings::default()); + assert_eq!(config.env, OxlintEnv::default()); + } + + #[test] + fn test_oxlintrc_de_plugins_empty_array() { + let config: Oxlintrc = serde_json::from_value(json!({ "plugins": [] })).unwrap(); + assert_eq!(config.plugins, LintPlugins::default()); + } + + #[test] + fn test_oxlintrc_de_plugins_enabled_by_default() { + // NOTE(@DonIsaac): creating a Value with `json!` then deserializing it with serde_json::from_value + // Errs with "invalid type: string \"eslint\", expected a borrowed string" and I can't + // figure out why. This seems to work. Why??? + let configs = [ + r#"{ "plugins": ["eslint"] }"#, + r#"{ "plugins": ["oxc"] }"#, + r#"{ "plugins": ["deepscan"] }"#, // alias for oxc + ]; + // ^ these plugins are enabled by default already + for oxlintrc in configs { + let config: Oxlintrc = serde_json::from_str(oxlintrc).unwrap(); + assert_eq!(config.plugins, LintPlugins::default()); + } + } + + #[test] + fn test_oxlintrc_de_plugins_new() { + let config: Oxlintrc = serde_json::from_str(r#"{ "plugins": ["import"] }"#).unwrap(); + assert_eq!(config.plugins, LintPlugins::default().union(LintPlugins::IMPORT)); + } +} diff --git a/crates/oxc_linter/src/config/rules.rs b/crates/oxc_linter/src/config/rules.rs index a53792f3fd435..3c17880247879 100644 --- a/crates/oxc_linter/src/config/rules.rs +++ b/crates/oxc_linter/src/config/rules.rs @@ -23,9 +23,11 @@ type RuleSet = FxHashSet; // // Note: when update document comment, also update `DummyRuleMap`'s description in this file. #[derive(Debug, Clone, Default)] +#[cfg_attr(test, derive(PartialEq))] pub struct OxlintRules(Vec); #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub struct ESLintRule { pub plugin_name: String, pub rule_name: String, diff --git a/crates/oxc_linter/src/config/settings/jsdoc.rs b/crates/oxc_linter/src/config/settings/jsdoc.rs index 2f581897cb409..e7bf9d284f742 100644 --- a/crates/oxc_linter/src/config/settings/jsdoc.rs +++ b/crates/oxc_linter/src/config/settings/jsdoc.rs @@ -8,6 +8,7 @@ use crate::utils::default_true; // #[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct JSDocPluginSettings { /// For all rules but NOT apply to `check-access` and `empty-tags` rule #[serde(default, rename = "ignorePrivate")] @@ -181,6 +182,7 @@ impl JSDocPluginSettings { } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] #[serde(untagged)] enum TagNamePreference { TagNameOnly(String), diff --git a/crates/oxc_linter/src/config/settings/jsx_a11y.rs b/crates/oxc_linter/src/config/settings/jsx_a11y.rs index e6c73f7e3f686..11b823ee38957 100644 --- a/crates/oxc_linter/src/config/settings/jsx_a11y.rs +++ b/crates/oxc_linter/src/config/settings/jsx_a11y.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; // #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct JSXA11yPluginSettings { #[serde(rename = "polymorphicPropName")] pub polymorphic_prop_name: Option, diff --git a/crates/oxc_linter/src/config/settings/mod.rs b/crates/oxc_linter/src/config/settings/mod.rs index 10bfc1d8fd8eb..4a496bc9b554c 100644 --- a/crates/oxc_linter/src/config/settings/mod.rs +++ b/crates/oxc_linter/src/config/settings/mod.rs @@ -13,6 +13,7 @@ use self::{ /// Shared settings for plugins #[derive(Debug, Deserialize, Serialize, Default, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct OxlintSettings { #[serde(default)] #[serde(rename = "jsx-a11y")] diff --git a/crates/oxc_linter/src/config/settings/next.rs b/crates/oxc_linter/src/config/settings/next.rs index 8246e3fc14503..4f289641531af 100644 --- a/crates/oxc_linter/src/config/settings/next.rs +++ b/crates/oxc_linter/src/config/settings/next.rs @@ -4,6 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize, Serializer}; #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct NextPluginSettings { #[serde(default)] #[serde(rename = "rootDir")] diff --git a/crates/oxc_linter/src/config/settings/react.rs b/crates/oxc_linter/src/config/settings/react.rs index 40b0219bd43e3..9cd913dea0fcf 100644 --- a/crates/oxc_linter/src/config/settings/react.rs +++ b/crates/oxc_linter/src/config/settings/react.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; // #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct ReactPluginSettings { #[serde(default)] #[serde(rename = "formComponents")] @@ -31,6 +32,7 @@ impl ReactPluginSettings { // Deserialize helper types #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] #[serde(untagged)] enum CustomComponent { NameOnly(CompactStr), diff --git a/crates/oxc_linter/src/context/host.rs b/crates/oxc_linter/src/context/host.rs index 2ead2913c21f7..f285ce5e60b7e 100644 --- a/crates/oxc_linter/src/context/host.rs +++ b/crates/oxc_linter/src/context/host.rs @@ -34,7 +34,11 @@ use super::{plugin_name_to_prefix, LintContext}; #[must_use] #[non_exhaustive] pub(crate) struct ContextHost<'a> { + /// Shared semantic information about the file being linted, which includes scopes, symbols + /// and AST nodes. See [`Semantic`]. pub(super) semantic: Rc>, + /// Information about specific rules that should be disabled or enabled, via comment directives like + /// `eslint-disable` or `eslint-disable-next-line`. pub(super) disable_directives: DisableDirectives<'a>, /// Diagnostics reported by the linter. /// @@ -46,9 +50,14 @@ pub(crate) struct ContextHost<'a> { /// Set via the `--fix`, `--fix-suggestions`, and `--fix-dangerously` CLI /// flags. pub(super) fix: FixKind, + /// Path to the file being linted. pub(super) file_path: Box, + /// Global linter configuration, such as globals to include and the target + /// environments, and other settings. pub(super) config: Arc, + /// Front-end frameworks that might be in use in the target file. pub(super) frameworks: FrameworkFlags, + /// A list of all available linter plugins. pub(super) plugins: LintPlugins, } @@ -70,8 +79,7 @@ impl<'a> ContextHost<'a> { ); let disable_directives = - DisableDirectivesBuilder::new(semantic.source_text(), semantic.trivias().clone()) - .build(); + DisableDirectivesBuilder::new().build(semantic.source_text(), semantic.comments()); let file_path = file_path.as_ref().to_path_buf().into_boxed_path(); @@ -88,12 +96,14 @@ impl<'a> ContextHost<'a> { .sniff_for_frameworks() } + /// Set the linter configuration for this context. #[inline] pub fn with_config(mut self, config: &Arc) -> Self { self.config = Arc::clone(config); self } + /// Shared reference to the [`Semantic`] analysis of the file. #[inline] pub fn semantic(&self) -> &Semantic<'a> { &self.semantic @@ -115,12 +125,14 @@ impl<'a> ContextHost<'a> { self.semantic.source_type() } + /// Add a diagnostic message to the end of the list of diagnostics. Can be used + /// by any rule to report issues. #[inline] pub(super) fn push_diagnostic(&self, diagnostic: Message<'a>) { self.diagnostics.borrow_mut().push(diagnostic); } - /// Take all diagnostics collected during linting. + /// Take ownership of all diagnostics collected during linting. pub fn take_diagnostics(&self) -> Vec> { // NOTE: diagnostics are only ever borrowed here and in push_diagnostic. // The latter drops the reference as soon as the function returns, so @@ -129,9 +141,10 @@ impl<'a> ContextHost<'a> { std::mem::take(&mut *messages) } + /// Creates a new [`LintContext`] for a specific rule. pub fn spawn(self: Rc, rule: &RuleWithSeverity) -> LintContext<'a> { let rule_name = rule.name(); - let plugin_name = self.map_jest(rule.plugin_name(), rule_name); + let plugin_name = self.map_jest_rule_to_vitest(rule); LintContext { parent: self, @@ -144,6 +157,7 @@ impl<'a> ContextHost<'a> { } } + /// Creates a new [`LintContext`] for testing purposes only. #[cfg(test)] pub(crate) fn spawn_for_test(self: Rc) -> LintContext<'a> { LintContext { @@ -157,10 +171,15 @@ impl<'a> ContextHost<'a> { } } - fn map_jest(&self, plugin_name: &'static str, rule_name: &str) -> &'static str { + /// Maps Jest rule names and maps to Vitest rules when possible, returning the original plugin otherwise. + /// + /// Many Vitest rules are essentially ports of the Jest plugin rules with minor modifications. + /// For these rules, we use the corresponding jest rules with some adjustments for compatibility. + fn map_jest_rule_to_vitest(&self, rule: &RuleWithSeverity) -> &'static str { + let plugin_name = rule.plugin_name(); if self.plugins.has_vitest() && plugin_name == "jest" - && utils::is_jest_rule_adapted_to_vitest(rule_name) + && utils::is_jest_rule_adapted_to_vitest(rule.name()) { "vitest" } else { diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index fc8484f1811b9..987a995cdfb42 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -22,17 +22,28 @@ pub(crate) use host::ContextHost; #[derive(Clone)] #[must_use] +/// Contains all of the state and context specific to this lint rule. Includes information +/// like the rule name, plugin name, and severity of the rule. It also has a reference to +/// the shared linting data [`ContextHost`], which is the same for all rules. pub struct LintContext<'a> { /// Shared context independent of the rule being linted. parent: Rc>, - - // states + /// Name of the plugin this rule belongs to. Example: `eslint`, `unicorn`, `react` current_plugin_name: &'static str, + /// Prefixed version of the plugin name. Examples: + /// - `eslint-plugin-react`, for `react` plugin, + /// - `typescript-eslint`, for `typescript` plugin, + /// - `eslint-plugin-import`, for `import` plugin. current_plugin_prefix: &'static str, + /// Kebab-cased name of the current rule being linted. Example: `no-unused-vars`, `no-undef`. current_rule_name: &'static str, + /// Capabilities of the current rule to fix issues. Indicates whether: + /// - Rule cannot be auto-fixed [`RuleFixMeta::None`] + /// - Rule needs an auto-fix to be written still [`RuleFixMeta::FixPending`] + /// - Rule can be fixed in some cases [`RuleFixMeta::Conditional`] + /// - Rule is fully auto-fixable [`RuleFixMeta::Fixable`] #[cfg(debug_assertions)] current_rule_fix_capabilities: RuleFixMeta, - /// Current rule severity. Allows for user severity overrides, e.g. /// ```json /// // .oxlintrc.json @@ -46,19 +57,23 @@ pub struct LintContext<'a> { } impl<'a> LintContext<'a> { + /// Base URL for the documentation, used to generate rule documentation URLs when a diagnostic is reported. const WEBSITE_BASE_URL: &'static str = "https://oxc.rs/docs/guide/usage/linter/rules"; + /// Set the plugin name for the current rule. pub fn with_plugin_name(mut self, plugin: &'static str) -> Self { self.current_plugin_name = plugin; self.current_plugin_prefix = plugin_name_to_prefix(plugin); self } + /// Set the current rule name. Name should be kebab-cased like: `no-unused-vars` or `no-undef`. pub fn with_rule_name(mut self, name: &'static str) -> Self { self.current_rule_name = name; self } + /// Set the current rule fix capabilities. See [`RuleFixMeta`] for more information. #[cfg(debug_assertions)] pub fn with_rule_fix_capabilities(mut self, capabilities: RuleFixMeta) -> Self { self.current_rule_fix_capabilities = capabilities; @@ -82,6 +97,7 @@ impl<'a> LintContext<'a> { &self.parent.semantic } + /// Get the control flow graph for the current program. #[inline] pub fn cfg(&self) -> &ControlFlowGraph { // SAFETY: `LintContext::new` is the only way to construct a `LintContext` and we always @@ -89,6 +105,7 @@ impl<'a> LintContext<'a> { unsafe { self.semantic().cfg().unwrap_unchecked() } } + /// List of all disable directives in the file being linted. #[inline] pub fn disable_directives(&self) -> &DisableDirectives<'a> { &self.parent.disable_directives @@ -124,6 +141,7 @@ impl<'a> LintContext<'a> { &self.parent.config.settings } + /// Sets of global variables that have been enabled or disabled. #[inline] pub fn globals(&self) -> &OxlintGlobals { &self.parent.config.globals @@ -137,6 +155,12 @@ impl<'a> LintContext<'a> { &self.parent.config.env } + /// Checks if a given variable named is defined as a global variable in the current environment. + /// + /// Example: + /// - `env_contains_var("Date")` returns `true` because it is a global builtin in all environments. + /// - `env_contains_var("HTMLElement")` returns `true` only if the `browser` environment is enabled. + /// - `env_contains_var("globalThis")` returns `true` only if the `es2020` environment or higher is enabled. pub fn env_contains_var(&self, var: &str) -> bool { if GLOBALS["builtin"].contains_key(var) { return true; @@ -153,6 +177,8 @@ impl<'a> LintContext<'a> { /* Diagnostics */ + /// Add a diagnostic message to the list of diagnostics. Outputs a diagnostic with the current rule + /// name, severity, and a link to the rule's documentation URL. fn add_diagnostic(&self, mut message: Message<'a>) { if self.parent.disable_directives.contains(self.current_rule_name, message.span()) { return; @@ -251,6 +277,12 @@ impl<'a> LintContext<'a> { self.diagnostic_with_fix_of_kind(diagnostic, FixKind::DangerousFix, fix); } + /// Report a lint rule violation and provide an automatic fix of a specific kind. + /// + /// The second argument is a [closure] that takes a [`RuleFixer`] and + /// returns something that can turn into a [`RuleFix`]. + /// + /// [closure]: #[allow(clippy::missing_panics_doc)] // only panics in debug mode pub fn diagnostic_with_fix_of_kind( &self, @@ -285,6 +317,7 @@ impl<'a> LintContext<'a> { } } + /// Framework flags, indicating front-end frameworks that might be in use. pub fn frameworks(&self) -> FrameworkFlags { self.parent.frameworks } @@ -325,11 +358,19 @@ impl<'a> LintContext<'a> { } } +/// Gets the prefixed plugin name, given the short plugin name. +/// +/// Example: +/// +/// ```rust +/// assert_eq!(plugin_name_to_prefix("react"), "eslint-plugin-react"); +/// ``` #[inline] fn plugin_name_to_prefix(plugin_name: &'static str) -> &'static str { PLUGIN_PREFIXES.get(plugin_name).copied().unwrap_or(plugin_name) } +/// Map of plugin names to their prefixed versions. const PLUGIN_PREFIXES: phf::Map<&'static str, &'static str> = phf::phf_map! { "import" => "eslint-plugin-import", "jest" => "eslint-plugin-jest", diff --git a/crates/oxc_linter/src/disable_directives.rs b/crates/oxc_linter/src/disable_directives.rs index 510a4053a4cfa..812229f1919b6 100644 --- a/crates/oxc_linter/src/disable_directives.rs +++ b/crates/oxc_linter/src/disable_directives.rs @@ -1,4 +1,4 @@ -use oxc_ast::Trivias; +use oxc_ast::Comment; use oxc_span::Span; use rust_lapper::{Interval, Lapper}; use rustc_hash::FxHashMap; @@ -48,8 +48,6 @@ impl<'a> DisableDirectives<'a> { } pub struct DisableDirectivesBuilder<'a> { - source_text: &'a str, - trivias: Trivias, /// All the disabled rules with their corresponding covering spans intervals: Lapper>, /// Start of `eslint-disable` or `oxlint-disable` @@ -63,10 +61,8 @@ pub struct DisableDirectivesBuilder<'a> { } impl<'a> DisableDirectivesBuilder<'a> { - pub fn new(source_text: &'a str, trivias: Trivias) -> Self { + pub fn new() -> Self { Self { - source_text, - trivias, intervals: Lapper::new(vec![]), disable_all_start: None, disable_start_map: FxHashMap::default(), @@ -75,8 +71,8 @@ impl<'a> DisableDirectivesBuilder<'a> { } } - pub fn build(mut self) -> DisableDirectives<'a> { - self.build_impl(); + pub fn build(mut self, source_text: &'a str, comments: &[Comment]) -> DisableDirectives<'a> { + self.build_impl(source_text, comments); DisableDirectives { intervals: self.intervals, disable_all_comments: self.disable_all_comments.into_boxed_slice(), @@ -89,13 +85,13 @@ impl<'a> DisableDirectivesBuilder<'a> { } #[allow(clippy::cast_possible_truncation)] // for `as u32` - fn build_impl(&mut self) { - let source_len = self.source_text.len() as u32; + fn build_impl(&mut self, source_text: &'a str, comments: &[Comment]) { + let source_len = source_text.len() as u32; // This algorithm iterates through the comments and builds all intervals // for matching disable and enable pairs. // Wrongly ordered matching pairs are not taken into consideration. - for comment in self.trivias.clone().comments() { - let text = comment.span.source_text(self.source_text); + for comment in comments { + let text = comment.span.source_text(source_text); let text = text.trim_start(); if let Some(text) = @@ -112,7 +108,7 @@ impl<'a> DisableDirectivesBuilder<'a> { // `eslint-disable-next-line` else if let Some(text) = text.strip_prefix("-next-line") { // Get the span up to the next new line - let stop = self.source_text[comment.span.end as usize..] + let stop = source_text[comment.span.end as usize..] .lines() .take(2) .fold(comment.span.end, |acc, line| acc + line.len() as u32); @@ -138,7 +134,7 @@ impl<'a> DisableDirectivesBuilder<'a> { // `eslint-disable-line` else if let Some(text) = text.strip_prefix("-line") { // Get the span between the preceding newline to this comment - let start = self.source_text[..=comment.span.start as usize] + let start = source_text[..=comment.span.start as usize] .lines() .next_back() .map_or(0, |line| comment.span.start - (line.len() as u32 - 1)); @@ -369,7 +365,7 @@ fn test() { debugger; debugger; /*{prefix}-disable-line*/ - + debugger; //{prefix}-disable-line no-debugger //{prefix}-disable-next-line no-debugger @@ -393,7 +389,7 @@ fn test() { debugger; debugger; /* {prefix}-disable-line */ - + debugger; // {prefix}-disable-line no-debugger // {prefix}-disable-next-line no-debugger @@ -597,21 +593,21 @@ semi*/ " ), format!( - "debugger; // \t\t {prefix}-disable-line \t\t no-alert, \t\t quotes, \t\t semi \t\t + "debugger; // \t\t {prefix}-disable-line \t\t no-alert, \t\t quotes, \t\t semi \t\t // \t\t {prefix}-disable-next-line \t\t no-alert, \t\t quotes, \t\t semi debugger; debugger; /* \t\t {prefix}-disable-line \t\t no-alert, \t\t quotes, \t\t semi \t\t */ /* \t\t {prefix}-disable-next-line \t\t no-alert, \t\t quotes, \t\t semi */ debugger; /* \t\t {prefix}-disable-next-line - \t\t no-alert, \t\t - \t\t quotes, \t\t + \t\t no-alert, \t\t + \t\t quotes, \t\t \t\t semi \t\t */ debugger; " ), ]; - Tester::new("no-debugger", pass, fail).test(); + Tester::new("no-debugger", pass, fail).intentionally_allow_no_fix_tests().test(); } } diff --git a/crates/oxc_linter/src/fixer/mod.rs b/crates/oxc_linter/src/fixer/mod.rs index 2c51081a2719e..df16517e394dd 100644 --- a/crates/oxc_linter/src/fixer/mod.rs +++ b/crates/oxc_linter/src/fixer/mod.rs @@ -170,7 +170,6 @@ impl<'c, 'a: 'c> RuleFixer<'c, 'a> { #[allow(clippy::unused_self)] pub fn codegen(self) -> CodeGenerator<'a> { CodeGenerator::new() - .with_source_text(self.source_text()) .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) } diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index b75e629d59ba2..b2ad5345d530e 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -26,7 +26,6 @@ use std::{io::Write, path::Path, rc::Rc, sync::Arc}; use config::LintConfig; use context::ContextHost; use options::LintOptions; -use oxc_diagnostics::Error; use oxc_semantic::{AstNode, Semantic}; pub use crate::{ @@ -35,7 +34,7 @@ pub use crate::{ context::LintContext, fixer::FixKind, frameworks::FrameworkFlags, - options::{AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind, OxlintOptions}, + options::{AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind, LintPlugins}, rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleWithSeverity}, service::{LintService, LintServiceOptions}, }; @@ -64,7 +63,7 @@ pub struct Linter { impl Default for Linter { fn default() -> Self { - Self::from_options(OxlintOptions::default()).unwrap() + LinterBuilder::default().build() } } @@ -77,14 +76,6 @@ impl Linter { Self { rules, options, config: Arc::new(config) } } - /// # Errors - /// - /// Returns `Err` if there are any errors parsing the configuration file. - pub fn from_options(options: OxlintOptions) -> Result { - let (rules, config) = options.derive_rules_and_config()?; - Ok(Self { rules, options: options.into(), config: Arc::new(config) }) - } - #[cfg(test)] #[must_use] pub fn with_rules(mut self, rules: Vec) -> Self { diff --git a/crates/oxc_linter/src/loader/partial_loader/astro.rs b/crates/oxc_linter/src/loader/partial_loader/astro.rs index c5199b53bf393..f6a4492bbae8b 100644 --- a/crates/oxc_linter/src/loader/partial_loader/astro.rs +++ b/crates/oxc_linter/src/loader/partial_loader/astro.rs @@ -42,8 +42,9 @@ impl<'a> AstroPartialLoader<'a> { return None; }; - let js_code = - Span::new(start + ASTRO_SPLIT.len() as u32, end).source_text(self.source_text); + // move start to the end of the ASTRO_SPLIT + let start = start + ASTRO_SPLIT.len() as u32; + let js_code = Span::new(start, end).source_text(self.source_text); Some(JavaScriptSource::partial(js_code, SourceType::ts(), start)) } @@ -118,6 +119,7 @@ mod test { let sources = parse_astro(source_text); assert_eq!(sources.len(), 1); assert_eq!(sources[0].source_text.trim(), r#"console.log("Hi");"#); + assert_eq!(sources[0].start, 51); } #[test] @@ -140,7 +142,9 @@ mod test { sources[0].source_text.trim(), "const { message = 'Welcome, world!' } = Astro.props;" ); + assert_eq!(sources[0].start, 12); assert_eq!(sources[1].source_text.trim(), r#"console.log("Hi");"#); + assert_eq!(sources[1].start, 141); } #[test] @@ -158,7 +162,9 @@ mod test { let sources = parse_astro(source_text); assert_eq!(sources.len(), 2); assert!(sources[0].source_text.is_empty()); + assert_eq!(sources[0].start, 102); assert_eq!(sources[1].source_text.trim(), r#"console.log("Hi");"#); + assert_eq!(sources[1].start, 129); } #[test] @@ -176,6 +182,8 @@ mod test { let sources = parse_astro(source_text); assert_eq!(sources.len(), 2); assert!(sources[0].source_text.is_empty()); + assert_eq!(sources[0].start, 104); assert_eq!(sources[1].source_text.trim(), r#"console.log("Hi");"#); + assert_eq!(sources[1].start, 122); } } diff --git a/crates/oxc_linter/src/options/mod.rs b/crates/oxc_linter/src/options/mod.rs index ceb566d5cf108..4ce020dc5f984 100644 --- a/crates/oxc_linter/src/options/mod.rs +++ b/crates/oxc_linter/src/options/mod.rs @@ -2,28 +2,13 @@ mod allow_warn_deny; mod filter; mod plugins; -use std::{convert::From, path::PathBuf}; - -use oxc_diagnostics::Error; -use rustc_hash::FxHashSet; - pub use allow_warn_deny::AllowWarnDeny; pub use filter::{InvalidFilterKind, LintFilter, LintFilterKind}; -pub use plugins::{LintPluginOptions, LintPlugins}; +pub use plugins::LintPlugins; -use crate::{ - config::{LintConfig, Oxlintrc}, - fixer::FixKind, - rules::RULES, - utils::is_jest_rule_adapted_to_vitest, - FrameworkFlags, RuleCategory, RuleEnum, RuleWithSeverity, -}; +use crate::{fixer::FixKind, FrameworkFlags}; -/// Subset of options used directly by the [`Linter`]. Derived from -/// [`OxlintOptions`], which is the public-facing API. Do not expose this -/// outside of this crate. -/// -/// [`Linter`]: crate::Linter +/// Subset of options used directly by the linter. #[derive(Debug, Default, Clone, Copy)] #[cfg_attr(test, derive(PartialEq))] pub(crate) struct LintOptions { @@ -31,271 +16,3 @@ pub(crate) struct LintOptions { pub framework_hints: FrameworkFlags, pub plugins: LintPlugins, } - -impl From for LintOptions { - fn from(options: OxlintOptions) -> Self { - Self { - fix: options.fix, - framework_hints: options.framework_hints, - plugins: options.plugins.into(), - } - } -} - -#[derive(Debug)] -pub struct OxlintOptions { - /// Allow / Deny rules in order. [("allow" / "deny", rule name)] - /// Defaults to [("deny", "correctness")] - pub filter: Vec, - pub config_path: Option, - /// Enable automatic code fixes. Set to [`None`] to disable. - /// - /// The kind represents the riskiest fix that the linter can apply. - pub fix: FixKind, - - pub plugins: LintPluginOptions, - - pub framework_hints: FrameworkFlags, -} - -impl Default for OxlintOptions { - fn default() -> Self { - Self { - filter: vec![LintFilter::warn(RuleCategory::Correctness)], - config_path: None, - fix: FixKind::None, - plugins: LintPluginOptions::default(), - framework_hints: FrameworkFlags::default(), - } - } -} - -impl OxlintOptions { - #[must_use] - pub fn with_filter(mut self, filter: Vec) -> Self { - if !filter.is_empty() { - self.filter = filter; - } - self - } - - #[must_use] - pub fn with_config_path(mut self, filter: Option) -> Self { - self.config_path = filter; - self - } - - /// Set the kind of auto fixes to apply. - /// - /// # Example - /// - /// ``` - /// use oxc_linter::{LintOptions, FixKind}; - /// - /// // turn off all auto fixes. This is default behavior. - /// LintOptions::default().with_fix(FixKind::None); - /// ``` - #[must_use] - pub fn with_fix(mut self, kind: FixKind) -> Self { - self.fix = kind; - self - } - - #[must_use] - pub fn with_react_plugin(mut self, yes: bool) -> Self { - self.plugins.react = yes; - self - } - - #[must_use] - pub fn with_unicorn_plugin(mut self, yes: bool) -> Self { - self.plugins.unicorn = yes; - self - } - - #[must_use] - pub fn with_typescript_plugin(mut self, yes: bool) -> Self { - self.plugins.typescript = yes; - self - } - - #[must_use] - pub fn with_oxc_plugin(mut self, yes: bool) -> Self { - self.plugins.oxc = yes; - self - } - - #[must_use] - pub fn with_import_plugin(mut self, yes: bool) -> Self { - self.plugins.import = yes; - self - } - - #[must_use] - pub fn with_jsdoc_plugin(mut self, yes: bool) -> Self { - self.plugins.jsdoc = yes; - self - } - - #[must_use] - pub fn with_jest_plugin(mut self, yes: bool) -> Self { - self.plugins.jest = yes; - self - } - - #[must_use] - pub fn with_vitest_plugin(mut self, yes: bool) -> Self { - self.plugins.vitest = yes; - self - } - - #[must_use] - pub fn with_jsx_a11y_plugin(mut self, yes: bool) -> Self { - self.plugins.jsx_a11y = yes; - self - } - - #[must_use] - pub fn with_nextjs_plugin(mut self, yes: bool) -> Self { - self.plugins.nextjs = yes; - self - } - - #[must_use] - pub fn with_react_perf_plugin(mut self, yes: bool) -> Self { - self.plugins.react_perf = yes; - self - } - - #[must_use] - pub fn with_promise_plugin(mut self, yes: bool) -> Self { - self.plugins.promise = yes; - self - } - - #[must_use] - pub fn with_node_plugin(mut self, yes: bool) -> Self { - self.plugins.node = yes; - self - } - - #[must_use] - pub fn with_security_plugin(mut self, yes: bool) -> Self { - self.plugins.security = yes; - self - } -} - -impl OxlintOptions { - /// # Errors - /// - /// * Returns `Err` if there are any errors parsing the configuration file. - pub(crate) fn derive_rules_and_config( - &self, - ) -> Result<(Vec, LintConfig), Error> { - let config = self.config_path.as_ref().map(|path| Oxlintrc::from_file(path)).transpose()?; - - let mut rules: FxHashSet = FxHashSet::default(); - let all_rules = self.get_filtered_rules(); - - for (severity, filter) in self.filter.iter().map(Into::into) { - match severity { - AllowWarnDeny::Deny | AllowWarnDeny::Warn => match filter { - LintFilterKind::Category(category) => { - rules.extend( - all_rules - .iter() - .filter(|rule| rule.category() == *category) - .map(|rule| RuleWithSeverity::new(rule.clone(), severity)), - ); - } - LintFilterKind::Rule(_, name) => { - rules.extend( - all_rules - .iter() - .filter(|rule| rule.name() == name) - .map(|rule| RuleWithSeverity::new(rule.clone(), severity)), - ); - } - LintFilterKind::Generic(name_or_category) => { - if name_or_category == "all" { - rules.extend( - all_rules - .iter() - .filter(|rule| rule.category() != RuleCategory::Nursery) - .map(|rule| RuleWithSeverity::new(rule.clone(), severity)), - ); - } else { - rules.extend( - all_rules - .iter() - .filter(|rule| rule.name() == name_or_category) - .map(|rule| RuleWithSeverity::new(rule.clone(), severity)), - ); - } - } - }, - AllowWarnDeny::Allow => match filter { - LintFilterKind::Category(category) => { - rules.retain(|rule| rule.category() != *category); - } - LintFilterKind::Rule(_, name) => { - rules.retain(|rule| rule.name() != name); - } - LintFilterKind::Generic(name_or_category) => { - if name_or_category == "all" { - rules.clear(); - } else { - rules.retain(|rule| rule.name() != name_or_category); - } - } - }, - } - } - - if let Some(config) = &config { - config.rules.override_rules(&mut rules, &all_rules); - } - - let mut rules = rules.into_iter().collect::>(); - - // for stable diagnostics output ordering - rules.sort_unstable_by_key(|rule| rule.id()); - - Ok((rules, config.map(Into::into).unwrap_or_default())) - } - - /// Get final filtered rules by reading `self.xxx_plugin` - fn get_filtered_rules(&self) -> Vec { - RULES - .iter() - .filter(|rule| match rule.plugin_name() { - "react" => self.plugins.react, - "unicorn" => self.plugins.unicorn, - "typescript" => self.plugins.typescript, - "import" => self.plugins.import, - "jsdoc" => self.plugins.jsdoc, - "jest" => { - if self.plugins.jest { - return true; - } - if self.plugins.vitest && is_jest_rule_adapted_to_vitest(rule.name()) { - return true; - } - false - } - "vitest" => self.plugins.vitest, - "jsx_a11y" => self.plugins.jsx_a11y, - "nextjs" => self.plugins.nextjs, - "react_perf" => self.plugins.react_perf, - "oxc" => self.plugins.oxc, - "eslint" | "tree_shaking" => true, - "promise" => self.plugins.promise, - "node" => self.plugins.node, - "security" => self.plugins.security, - name => panic!("Unhandled plugin: {name}"), - }) - .cloned() - .collect::>() - } -} diff --git a/crates/oxc_linter/src/options/plugins.rs b/crates/oxc_linter/src/options/plugins.rs index d991a9f50e8f0..f06d9e4e86a3f 100644 --- a/crates/oxc_linter/src/options/plugins.rs +++ b/crates/oxc_linter/src/options/plugins.rs @@ -1,6 +1,10 @@ use bitflags::bitflags; use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; -use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; +use serde::{ + de::{self, Deserializer}, + ser::Serializer, + Deserialize, Serialize, +}; bitflags! { // NOTE: may be increased to a u32 if needed @@ -151,7 +155,38 @@ impl> FromIterator for LintPlugins { impl<'de> Deserialize<'de> for LintPlugins { fn deserialize>(deserializer: D) -> Result { - Vec::<&str>::deserialize(deserializer).map(|vec| vec.into_iter().collect()) + struct LintPluginsVisitor; + impl<'de> de::Visitor<'de> for LintPluginsVisitor { + type Value = LintPlugins; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a list of plugin names") + } + + fn visit_str(self, value: &str) -> Result { + Ok(LintPlugins::from(value)) + } + + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(LintPlugins::from(v.as_str())) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut plugins = LintPlugins::default(); + while let Some(plugin) = seq.next_element::<&str>()? { + plugins |= plugin.into(); + } + Ok(plugins) + } + } + + deserializer.deserialize_any(LintPluginsVisitor) } } diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index abd2b0b23a9c2..27f1db5285b38 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -100,6 +100,18 @@ impl RuleCategory { Self::Nursery => "New lints that are still under development.", } } + + pub fn as_str(self) -> &'static str { + match self { + Self::Correctness => "correctness", + Self::Suspicious => "suspicious", + Self::Pedantic => "pedantic", + Self::Perf => "perf", + Self::Style => "style", + Self::Restriction => "restriction", + Self::Nursery => "nursery", + } + } } impl TryFrom<&str> for RuleCategory { diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0b81fa730f127..b1fbdff96088a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -63,6 +63,7 @@ mod eslint { pub mod no_dupe_else_if; pub mod no_dupe_keys; pub mod no_duplicate_case; + pub mod no_else_return; pub mod no_empty; pub mod no_empty_character_class; pub mod no_empty_function; @@ -107,6 +108,7 @@ mod eslint { pub mod no_template_curly_in_string; pub mod no_ternary; pub mod no_this_before_super; + pub mod no_throw_literal; pub mod no_undef; pub mod no_undefined; pub mod no_unexpected_multiline; @@ -232,6 +234,7 @@ mod jest { mod react { pub mod button_has_type; pub mod checked_requires_onchange_or_readonly; + pub mod iframe_missing_sandbox; pub mod jsx_boolean_value; pub mod jsx_curly_brace_presence; pub mod jsx_key; @@ -537,6 +540,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_empty_function, eslint::no_empty_pattern, eslint::no_empty_static_block, + eslint::no_else_return, eslint::no_eq_null, eslint::no_eval, eslint::no_ex_assign, @@ -576,6 +580,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_template_curly_in_string, eslint::no_ternary, eslint::no_this_before_super, + eslint::no_throw_literal, eslint::no_undef, eslint::no_undefined, eslint::no_unexpected_multiline, @@ -769,6 +774,7 @@ oxc_macros::declare_all_lint_rules! { promise::valid_params, react::button_has_type, react::checked_requires_onchange_or_readonly, + react::iframe_missing_sandbox, react::jsx_boolean_value, react::jsx_curly_brace_presence, react::jsx_key, diff --git a/crates/oxc_linter/src/rules/eslint/array_callback_return/return_checker.rs b/crates/oxc_linter/src/rules/eslint/array_callback_return/return_checker.rs index e4dbbbf40399c..79f143bf38701 100644 --- a/crates/oxc_linter/src/rules/eslint/array_callback_return/return_checker.rs +++ b/crates/oxc_linter/src/rules/eslint/array_callback_return/return_checker.rs @@ -1,4 +1,5 @@ use oxc_ast::ast::{BlockStatement, FunctionBody, Statement, SwitchCase}; +use oxc_ecmascript::ToBoolean; /// `StatementReturnStatus` describes whether the CFG corresponding to /// the statement is termitated by return statement in all/some/nome of diff --git a/crates/oxc_linter/src/rules/eslint/default_case.rs b/crates/oxc_linter/src/rules/eslint/default_case.rs index e3e329dcf1929..59977313b665b 100644 --- a/crates/oxc_linter/src/rules/eslint/default_case.rs +++ b/crates/oxc_linter/src/rules/eslint/default_case.rs @@ -76,7 +76,6 @@ impl Rule for DefaultCase { let has_default_comment = ctx .semantic() - .trivias() .comments_range(last_case.span.start..switch.span.end) .last() .is_some_and(|comment| { diff --git a/crates/oxc_linter/src/rules/eslint/for_direction.rs b/crates/oxc_linter/src/rules/eslint/for_direction.rs index c95d5223d878e..5cfc65af09554 100644 --- a/crates/oxc_linter/src/rules/eslint/for_direction.rs +++ b/crates/oxc_linter/src/rules/eslint/for_direction.rs @@ -78,7 +78,7 @@ declare_oxc_lint!( /// ``` ForDirection, correctness, - dangerous_fix + fix_dangerous ); impl Rule for ForDirection { diff --git a/crates/oxc_linter/src/rules/eslint/getter_return.rs b/crates/oxc_linter/src/rules/eslint/getter_return.rs index f16d40969fe1a..4952d38dfd711 100644 --- a/crates/oxc_linter/src/rules/eslint/getter_return.rs +++ b/crates/oxc_linter/src/rules/eslint/getter_return.rs @@ -16,7 +16,11 @@ use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::Span; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::{ + context::{ContextHost, LintContext}, + rule::Rule, + AstNode, +}; fn getter_return_diagnostic(span: Span) -> OxcDiagnostic { OxcDiagnostic::warn("Expected to always return a value in getter.") @@ -38,18 +42,39 @@ const METHODS_TO_WATCH_FOR: [(&str, &str); 4] = [ declare_oxc_lint!( /// ### What it does - /// Requires all getters to have a return statement + /// + /// Requires all getters to have a `return` statement. /// /// ### Why is this bad? /// Getters should always return a value. If they don't, it's probably a mistake. /// + /// This rule does not run on TypeScript files, since type checking will + /// catch getters that do not return a value. + /// /// ### Example + /// + /// Examples of **incorrect** code for this rule: /// ```javascript - /// class Person{ - /// get name(){ + /// class Person { + /// get name() { /// // no return /// } /// } + /// + /// const obj = { + /// get foo() { + /// // object getter are also checked + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// class Person { + /// get name() { + /// return this._name; + /// } + /// } /// ``` GetterReturn, nursery @@ -57,10 +82,6 @@ declare_oxc_lint!( impl Rule for GetterReturn { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - // https://eslint.org/docs/latest/rules/getter-return#handled_by_typescript - if ctx.source_type().is_typescript() { - return; - } match node.kind() { AstKind::Function(func) if !func.is_typescript_syntax() => { self.run_diagnostic(node, ctx, func.span); @@ -81,6 +102,11 @@ impl Rule for GetterReturn { Self { allow_implicit } } + + fn should_run(&self, ctx: &ContextHost) -> bool { + // https://eslint.org/docs/latest/rules/getter-return#handled_by_typescript + !ctx.source_type().is_typescript() + } } impl GetterReturn { @@ -494,4 +520,20 @@ fn test() { Tester::new(GetterReturn::NAME, pass, fail) .change_rule_path_extension("js") .test_and_snapshot(); + + // TypeScript tests + let pass = vec![( + "var foo = { + get bar(): boolean | undefined { + if (Math.random() > 0.5) { + return true; + } + } + };", + None, + )]; + + let fail = vec![]; + + Tester::new(GetterReturn::NAME, pass, fail).test(); } diff --git a/crates/oxc_linter/src/rules/eslint/max_classes_per_file.rs b/crates/oxc_linter/src/rules/eslint/max_classes_per_file.rs index 662082a734b90..5bbc2cf906756 100644 --- a/crates/oxc_linter/src/rules/eslint/max_classes_per_file.rs +++ b/crates/oxc_linter/src/rules/eslint/max_classes_per_file.rs @@ -98,8 +98,8 @@ impl Rule for MaxClassesPerFile { return; } - let ast_node_id = ctx.semantic().classes().get_node_id(ClassId::from(self.max)); - let span = if let AstKind::Class(class) = ctx.nodes().kind(ast_node_id) { + let node_id = ctx.semantic().classes().get_node_id(ClassId::from(self.max)); + let span = if let AstKind::Class(class) = ctx.nodes().kind(node_id) { class.span } else { Span::new(0, 0) diff --git a/crates/oxc_linter/src/rules/eslint/max_lines.rs b/crates/oxc_linter/src/rules/eslint/max_lines.rs index 1f6a3955b0d44..2311b346a411b 100644 --- a/crates/oxc_linter/src/rules/eslint/max_lines.rs +++ b/crates/oxc_linter/src/rules/eslint/max_lines.rs @@ -82,7 +82,7 @@ impl Rule for MaxLines { fn run_once(&self, ctx: &LintContext) { let comment_lines = if self.skip_comments { let mut comment_lines: usize = 0; - for comment in ctx.semantic().trivias().comments() { + for comment in ctx.semantic().comments() { if comment.is_line() { let comment_line = ctx.source_text()[..comment.span.start as usize] .lines() diff --git a/crates/oxc_linter/src/rules/eslint/no_bitwise.rs b/crates/oxc_linter/src/rules/eslint/no_bitwise.rs index d92a5a5742d40..59db49cd73836 100644 --- a/crates/oxc_linter/src/rules/eslint/no_bitwise.rs +++ b/crates/oxc_linter/src/rules/eslint/no_bitwise.rs @@ -1,7 +1,7 @@ use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use oxc_syntax::operator::BinaryOperator; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -17,7 +17,7 @@ pub struct NoBitwise(Box); #[derive(Debug, Default, Clone)] pub struct NoBitwiseConfig { - allow: Vec, + allow: Vec, int32_hint: bool, } @@ -57,10 +57,7 @@ impl Rule for NoBitwise { .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(ToString::to_string) - .collect() + v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect() }) .unwrap_or_default(), int32_hint: obj @@ -107,7 +104,7 @@ impl Rule for NoBitwise { } } -fn allowed_operator(allow: &[String], operator: &str) -> bool { +fn allowed_operator(allow: &[CompactStr], operator: &str) -> bool { allow.iter().any(|s| s == operator) } diff --git a/crates/oxc_linter/src/rules/eslint/no_compare_neg_zero.rs b/crates/oxc_linter/src/rules/eslint/no_compare_neg_zero.rs index fb7f3d0a35987..7ec52bc797e8c 100644 --- a/crates/oxc_linter/src/rules/eslint/no_compare_neg_zero.rs +++ b/crates/oxc_linter/src/rules/eslint/no_compare_neg_zero.rs @@ -30,7 +30,7 @@ declare_oxc_lint!( /// ``` NoCompareNegZero, correctness, - conditional_suggestion_fix + conditional_fix_suggestion ); impl Rule for NoCompareNegZero { diff --git a/crates/oxc_linter/src/rules/eslint/no_console.rs b/crates/oxc_linter/src/rules/eslint/no_console.rs index 5bf03d311fa92..4a757fdcccba5 100644 --- a/crates/oxc_linter/src/rules/eslint/no_console.rs +++ b/crates/oxc_linter/src/rules/eslint/no_console.rs @@ -1,7 +1,7 @@ use oxc_ast::{ast::Expression, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -21,7 +21,7 @@ pub struct NoConsoleConfig { /// console.log('foo'); // will error /// console.info('bar'); // will not error /// ``` - pub allow: Vec, + pub allow: Vec, } impl std::ops::Deref for NoConsole { @@ -58,10 +58,7 @@ impl Rule for NoConsole { .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(ToString::to_string) - .collect() + v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect() }) .unwrap_or_default(), })) diff --git a/crates/oxc_linter/src/rules/eslint/no_control_regex.rs b/crates/oxc_linter/src/rules/eslint/no_control_regex.rs index dc4d6e95f1fec..7a678334d8ec0 100644 --- a/crates/oxc_linter/src/rules/eslint/no_control_regex.rs +++ b/crates/oxc_linter/src/rules/eslint/no_control_regex.rs @@ -1,23 +1,26 @@ +use itertools::Itertools as _; use oxc_allocator::Allocator; -use oxc_ast::{ - ast::{Argument, RegExpFlags}, - AstKind, -}; +use oxc_ast::{ast::Argument, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_regular_expression::{ - ast::{Character, Pattern}, - visit::Visit, + ast::{CapturingGroup, Character, Pattern}, + visit::{walk, Visit}, Parser, ParserOptions, }; use oxc_span::{GetSpan, Span}; use crate::{ast_util::extract_regex_flags, context::LintContext, rule::Rule, AstNode}; -fn no_control_regex_diagnostic(regex: &str, span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Unexpected control character(s)") - .with_help(format!("Unexpected control character(s) in regular expression: \"{regex}\"")) - .with_label(span) +fn no_control_regex_diagnostic(count: usize, regex: &str, span: Span) -> OxcDiagnostic { + debug_assert!(count > 0); + let (message, help) = if count == 1 { + ("Unexpected control character", format!("'{regex}' is not a valid control character.")) + } else { + ("Unexpected control characters", format!("'{regex}' are not valid control characters.")) + }; + + OxcDiagnostic::warn(message).with_help(help).with_label(span) } #[derive(Debug, Default, Clone)] @@ -91,30 +94,12 @@ impl Rule for NoControlRegex { if let Argument::StringLiteral(pattern) = &expr.arguments[0] { // get pattern from arguments. Missing or non-string arguments // will be runtime errors, but are not covered by this rule. - let alloc = Allocator::default(); - let pattern_with_slashes = format!("/{}/", &pattern.value); - let flags = extract_regex_flags(&expr.arguments); - let parser = Parser::new( - &alloc, - pattern_with_slashes.as_str(), - ParserOptions { - span_offset: expr - .arguments - .first() - .map_or(0, |arg| arg.span().start), - unicode_mode: flags - .is_some_and(|flags| flags.contains(RegExpFlags::U)), - unicode_sets_mode: flags - .is_some_and(|flags| flags.contains(RegExpFlags::V)), - }, + parse_and_check_regex( + context, + &pattern.value, + &expr.arguments, + pattern.span, ); - - let Some(pattern) = parser.parse().ok().map(|pattern| pattern.pattern) - else { - return; - }; - - check_pattern(context, &pattern, expr.span); } } } @@ -132,30 +117,12 @@ impl Rule for NoControlRegex { if let Argument::StringLiteral(pattern) = &expr.arguments[0] { // get pattern from arguments. Missing or non-string arguments // will be runtime errors, but are not covered by this rule. - let alloc = Allocator::default(); - let pattern_with_slashes = format!("/{}/", &pattern.value); - let flags = extract_regex_flags(&expr.arguments); - let parser = Parser::new( - &alloc, - pattern_with_slashes.as_str(), - ParserOptions { - span_offset: expr - .arguments - .first() - .map_or(0, |arg| arg.span().start), - unicode_mode: flags - .is_some_and(|flags| flags.contains(RegExpFlags::U)), - unicode_sets_mode: flags - .is_some_and(|flags| flags.contains(RegExpFlags::V)), - }, + parse_and_check_regex( + context, + &pattern.value, + &expr.arguments, + pattern.span, ); - - let Some(pattern) = parser.parse().ok().map(|pattern| pattern.pattern) - else { - return; - }; - - check_pattern(context, &pattern, expr.span); } } } @@ -164,21 +131,69 @@ impl Rule for NoControlRegex { } } +fn parse_and_check_regex<'a>( + ctx: &LintContext<'a>, + source_text: &'a str, + arguments: &oxc_allocator::Vec<'a, Argument<'a>>, + expr_span: Span, +) { + let allocator = Allocator::default(); + let flags = extract_regex_flags(arguments); + let flags_text = flags.map_or(String::new(), |f| f.to_string()); + let parser = Parser::new( + &allocator, + source_text, + ParserOptions::default() + .with_span_offset(arguments.first().map_or(0, |arg| arg.span().start)) + .with_flags(&flags_text), + ); + let Ok(pattern) = parser.parse() else { + return; + }; + check_pattern(ctx, &pattern, expr_span); +} + fn check_pattern(context: &LintContext, pattern: &Pattern, span: Span) { - let mut finder = ControlCharacterFinder { control_chars: Vec::new() }; + let mut finder = ControlCharacterFinder::default(); finder.visit_pattern(pattern); if !finder.control_chars.is_empty() { - let violations = finder.control_chars.join(", "); - context.diagnostic(no_control_regex_diagnostic(&violations, span)); + let num_control_chars = finder.control_chars.len(); + let violations = finder.control_chars.into_iter().map(|c| c.to_string()).join(", "); + context.diagnostic(no_control_regex_diagnostic(num_control_chars, &violations, span)); } } +#[derive(Default)] struct ControlCharacterFinder { - control_chars: Vec, + control_chars: Vec, + num_capture_groups: u32, } impl<'a> Visit<'a> for ControlCharacterFinder { + fn visit_pattern(&mut self, it: &Pattern<'a>) { + walk::walk_pattern(self, it); + // \1, \2, etc. are sometimes valid "control" characters as they can be + // used to reference values from capturing groups. Note in this case + // they're not actually control characters. However, if there's no + // corresponding capturing group, they _are_ invalid control characters. + // + // Some important notes: + // 1. Capture groups are 1-indexed. + // 2. Capture groups can be nested. + // 3. Capture groups may be referenced before they are defined. This is + // why we need to do this check here, instead of filtering inside of + // visit_character. + if self.num_capture_groups > 0 && !self.control_chars.is_empty() { + let control_chars = std::mem::take(&mut self.control_chars); + let control_chars = control_chars + .into_iter() + .filter(|c| !(c.value > 0x01 && c.value <= self.num_capture_groups)) + .collect::>(); + self.control_chars = control_chars; + } + } + fn visit_character(&mut self, ch: &Character) { // Control characters are in the range 0x00 to 0x1F if ch.value <= 0x1F && @@ -190,9 +205,14 @@ impl<'a> Visit<'a> for ControlCharacterFinder { ch.value != 0x0D { // TODO: check if starts with \x or \u when char spans work correctly - self.control_chars.push(ch.to_string()); + self.control_chars.push(*ch); } } + + fn visit_capturing_group(&mut self, group: &CapturingGroup<'a>) { + self.num_capture_groups += 1; + walk::walk_capturing_group(self, group); + } } #[cfg(test)] @@ -258,6 +278,8 @@ mod tests { ], vec![ r"let r = /\u{0}/u", + r"let r = new RegExp('\\u{0}', 'u');", + r"let r = new RegExp('\\u{0}', `u`);", r"let r = /\u{c}/u", r"let r = /\u{1F}/u", r"let r = new RegExp('\\u{1F}', 'u');", // flags are known & contain u @@ -266,6 +288,25 @@ mod tests { .test(); } + #[test] + fn test_capture_group_indexing() { + // https://github.com/oxc-project/oxc/issues/6525 + let pass = vec![ + r#"const filename = /filename[^;=\n]=((['"]).?\2|[^;\n]*)/;"#, + r"const r = /([a-z])\1/;", + r"const r = /\1([a-z])/;", + ]; + let fail = vec![ + r"const r = /\0/;", + r"const r = /[a-z]\1/;", + r"const r = /([a-z])\2/;", + r"const r = /([a-z])\0/;", + ]; + Tester::new(NoControlRegex::NAME, pass, fail) + .with_snapshot_suffix("capture-group-indexing") + .test_and_snapshot(); + } + #[test] fn test() { // test cases taken from eslint. See: diff --git a/crates/oxc_linter/src/rules/eslint/no_else_return.rs b/crates/oxc_linter/src/rules/eslint/no_else_return.rs new file mode 100644 index 0000000000000..7b199e8740505 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_else_return.rs @@ -0,0 +1,559 @@ +use crate::{context::LintContext, rule::Rule, AstNode}; +use oxc_ast::{ast::Statement, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::{ScopeId, ScopeTree}; +use oxc_span::{GetSpan, Span}; + +#[derive(Debug, Default, Clone)] +pub struct NoElseReturn { + allow_else_if: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// Disallow `else` blocks after `return` statements in `if` statements + /// + /// ### Why is this bad? + /// If an `if` block contains a `return` statement, the `else` block becomes + /// unnecessary. Its contents can be placed outside of the block. + /// + /// ```javascript + /// function foo() { + /// if (x) { + /// return y; + /// } else { + /// return z; + /// } + /// } + /// ``` + /// + /// This rule is aimed at highlighting an unnecessary block of code + /// following an `if` containing a return statement. As such, it will warn + /// when it encounters an `else` following a chain of `if`s, all of them + /// containing a `return` statement. + /// + /// Options + /// This rule has an object option: + /// + /// - `allowElseIf`: `true` _(default)_ allows `else if` blocks after a return + /// - `allowElseIf`: `false` disallows `else if` blocks after a return + /// + /// ### Examples + /// + /// #### `allowElseIf: true` + /// Examples of **incorrect** code for this rule: + /// ```javascript + /// function foo1() { + /// if (x) { + /// return y; + /// } else { + /// return z; + /// } + /// } + /// + /// function foo2() { + /// if (x) { + /// return y; + /// } else if (z) { + /// return w; + /// } else { + /// return t; + /// } + /// } + /// + /// function foo3() { + /// if (x) { + /// return y; + /// } else { + /// var t = "foo"; + /// } + /// + /// return t; + /// } + /// + /// function foo4() { + /// if (error) { + /// return 'It failed'; + /// } else { + /// if (loading) { + /// return "It's still loading"; + /// } + /// } + /// } + /// + /// // Two warnings for nested occurrences + /// function foo5() { + /// if (x) { + /// if (y) { + /// return y; + /// } else { + /// return x; + /// } + /// } else { + /// return z; + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// function foo1() { + /// if (x) { + /// return y; + /// } + /// + /// return z; + /// } + /// + /// function foo2() { + /// if (x) { + /// return y; + /// } else if (z) { + /// var t = "foo"; + /// } else { + /// return w; + /// } + /// } + /// + /// function foo3() { + /// if (x) { + /// if (z) { + /// return y; + /// } + /// } else { + /// return z; + /// } + /// } + /// + /// function foo4() { + /// if (error) { + /// return 'It failed'; + /// } else if (loading) { + /// return "It's still loading"; + /// } + /// } + /// ``` + /// + /// #### `allowElseIf: false` + /// + /// Examples of **incorrect** code for this rule: + /// ```javascript + /// function foo() { + /// if (error) { + /// return 'It failed'; + /// } else if (loading) { + /// return "It's still loading"; + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// function foo() { + /// if (error) { + /// return 'It failed'; + /// } + /// + /// if (loading) { + /// return "It's still loading"; + /// } + /// } + /// ``` + NoElseReturn, + pedantic, + conditional_fix +); + +fn no_else_return_diagnostic(else_keyword: Span, last_return: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Unnecessary 'else' after 'return'.") + .with_labels([ + last_return.label("This consequent block always returns,"), + else_keyword.label("Making this `else` block unnecessary."), + ]) + .with_help("Remove the `else` block, moving its contents outside of the `if` statement.") +} + +fn is_safe_from_name_collisions( + ctx: &LintContext, + stmt: &Statement, + parent_scope_id: ScopeId, +) -> bool { + let scopes: &ScopeTree = ctx.scopes(); + + match stmt { + Statement::BlockStatement(block) => { + let block_scope_id = block.scope_id.get().unwrap(); + let bindings = scopes.get_bindings(block_scope_id); + let parent_bindings = scopes.get_bindings(parent_scope_id); + + if bindings.iter().any(|(name, symbol_id)| { + let Some((parent_name, parent_symbol_id)) = parent_bindings.get_key_value(name) + else { + return false; + }; + parent_name == name && symbol_id != parent_symbol_id + }) { + return false; + } + + true + } + Statement::FunctionDeclaration(_) => false, + _ => true, + } +} + +fn no_else_return_diagnostic_fix( + ctx: &LintContext, + last_return_span: Span, + else_stmt_prev: &Statement, + else_stmt: &Statement, + if_block_node: &AstNode, +) { + let prev_span = else_stmt_prev.span(); + let else_content_span = else_stmt.span(); + let else_keyword_span = Span::new(prev_span.end, else_content_span.start); + let diagnostic = no_else_return_diagnostic(else_keyword_span, last_return_span); + let parent_scope_id = if_block_node.scope_id(); + + if !is_safe_from_name_collisions(ctx, else_stmt, parent_scope_id) { + ctx.diagnostic(diagnostic); + return; + } + ctx.diagnostic_with_fix(diagnostic, |fixer| { + let target_span = Span::new(prev_span.end, else_content_span.end); + + // Capture the contents of the `else` statement, removing curly braces + // for block statements + let mut replacement_span = if let Statement::BlockStatement(block) = else_stmt { + let first_stmt_start = block.body.first().map(|stmt| stmt.span().start); + let last_stmt_end = block.body.last().map(|stmt| stmt.span().end); + let (Some(start), Some(end)) = (first_stmt_start, last_stmt_end) else { + return fixer.noop(); + }; + Span::new(start, end) + } else { + else_content_span + }; + + // expand the span start leftwards to include any leading whitespace + replacement_span = + replacement_span.expand_left(left_offset_for_whitespace(ctx, replacement_span.start)); + + // Check if if statement's consequent block could introduce an ASI + // hazard when `else` is removed. + let needs_newline = match else_stmt_prev { + Statement::ExpressionStatement(s) => !ctx.source_range(s.span).ends_with(';'), + Statement::ReturnStatement(s) => !ctx.source_range(s.span).ends_with(';'), + _ => false, + }; + if needs_newline { + let replacement = ctx.source_range(replacement_span); + fixer.replace(target_span, "\n".to_string() + replacement) + } else { + fixer.replace_with(&target_span, &replacement_span) + } + }); +} + +#[allow(clippy::cast_possible_truncation)] +fn left_offset_for_whitespace(ctx: &LintContext, position: u32) -> u32 { + if position == 0 { + return position; + } + + let chars = ctx.source_text()[..(position as usize)].chars().rev(); + let offset = chars.take_while(|c| c.is_whitespace()).count(); + debug_assert!(offset < u32::MAX as usize); + offset as u32 +} + +fn naive_has_return(node: &Statement) -> Option { + match node { + Statement::BlockStatement(block) => { + let last_child = block.body.last()?; + if let Statement::ReturnStatement(r) = last_child { + Some(r.span) + } else { + None + } + } + Statement::ReturnStatement(r) => Some(r.span), + _ => None, + } +} + +fn check_for_return_or_if(node: &Statement) -> Option { + match node { + Statement::ReturnStatement(r) => Some(r.span), + Statement::IfStatement(if_stmt) => { + let alternate = if_stmt.alternate.as_ref()?; + if let (Some(_), Some(ret_span)) = + (naive_has_return(alternate), naive_has_return(&if_stmt.consequent)) + { + Some(ret_span) + } else { + None + } + } + _ => None, + } +} + +fn always_returns(stmt: &Statement) -> Option { + match stmt { + Statement::BlockStatement(block) => block.body.iter().find_map(check_for_return_or_if), + node => check_for_return_or_if(node), + } +} + +fn check_if_with_else(ctx: &LintContext, node: &AstNode) { + let AstKind::IfStatement(if_stmt) = node.kind() else { + return; + }; + let Some(alternate) = &if_stmt.alternate else { + return; + }; + + if let Some(last_return_span) = always_returns(&if_stmt.consequent) { + no_else_return_diagnostic_fix(ctx, last_return_span, &if_stmt.consequent, alternate, node); + } +} + +fn check_if_without_else(ctx: &LintContext, node: &AstNode) { + let AstKind::IfStatement(if_stmt) = node.kind() else { + return; + }; + let mut current_node = if_stmt; + let mut last_alternate; + let mut last_alternate_prev; + let mut last_return_span; + + loop { + let Some(alternate) = ¤t_node.alternate else { + return; + }; + let Some(ret_span) = always_returns(¤t_node.consequent) else { + return; + }; + last_alternate_prev = ¤t_node.consequent; + last_alternate = alternate; + last_return_span = ret_span; + match alternate { + Statement::IfStatement(if_stmt) => { + current_node = if_stmt; + } + _ => break, + } + } + + no_else_return_diagnostic_fix(ctx, last_return_span, last_alternate_prev, last_alternate, node); +} + +impl Rule for NoElseReturn { + fn from_configuration(value: serde_json::Value) -> Self { + let Some(value) = value.get(0) else { return Self { allow_else_if: true } }; + Self { + allow_else_if: value + .get("allowElseIf") + .and_then(serde_json::Value::as_bool) + .unwrap_or(true), + } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::IfStatement(_) = node.kind() else { + return; + }; + + let Some(parent_node) = ctx.nodes().parent_node(node.id()) else { + return; + }; + + if !matches!( + parent_node.kind(), + AstKind::Program(_) + | AstKind::BlockStatement(_) + | AstKind::StaticBlock(_) + | AstKind::SwitchCase(_) + | AstKind::FunctionBody(_) + ) { + return; + } + if self.allow_else_if { + check_if_without_else(ctx, node); + } else { + check_if_with_else(ctx, node); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("function foo() { if (true) { if (false) { return x; } } else { return y; } }", None), +("function foo() { if (true) { return x; } return y; }", None), +("function foo() { if (true) { for (;;) { return x; } } else { return y; } }", None), +("function foo() { var x = true; if (x) { return x; } else if (x === false) { return false; } }", None), +("function foo() { if (true) notAReturn(); else return y; }", None), +("function foo() {if (x) { notAReturn(); } else if (y) { return true; } else { notAReturn(); } }", None), +("function foo() {if (x) { return true; } else if (y) { notAReturn() } else { notAReturn(); } }", None), +("if (0) { if (0) {} else {} } else {}", None), +(" + function foo() { + if (foo) + if (bar) return; + else baz; + else qux; + } + ", None), +(" + function foo() { + while (foo) + if (bar) return; + else baz; + } + ", None), +("function foo19() { if (true) { return x; } else if (false) { return y; } }", Some(serde_json::json!([{ "allowElseIf": true }]))), +("function foo20() {if (x) { return true; } else if (y) { notAReturn() } else { notAReturn(); } }", Some(serde_json::json!([{ "allowElseIf": true }]))), +("function foo21() { var x = true; if (x) { return x; } else if (x === false) { return false; } }", Some(serde_json::json!([{ "allowElseIf": true }]))) + ]; + + let fail = vec![ + ("function foo1() { if (true) { return x; } else { return y; } }", None), +("function foo2() { if (true) { var x = bar; return x; } else { var y = baz; return y; } }", None), +("function foo3() { if (true) return x; else return y; }", None), +("function foo4() { if (true) { if (false) return x; else return y; } else { return z; } }", None), +("function foo5() { if (true) { if (false) { if (true) return x; else { w = y; } } else { w = x; } } else { return z; } }", None), +("function foo6() { if (true) { if (false) { if (true) return x; else return y; } } else { return z; } }", None), +("function foo7() { if (true) { if (false) { if (true) return x; else return y; } return w; } else { return z; } }", None), +("function foo8() { if (true) { if (false) { if (true) return x; else return y; } else { w = x; } } else { return z; } }", None), +("function foo9() {if (x) { return true; } else if (y) { return true; } else { notAReturn(); } }", None), +("function foo9a() {if (x) { return true; } else if (y) { return true; } else { notAReturn(); } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo9b() {if (x) { return true; } if (y) { return true; } else { notAReturn(); } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo10() { if (foo) return bar; else (foo).bar(); }", None), +("function foo11() { if (foo) return bar + else { [1, 2, 3].map(foo) } }", None), +("function foo12() { if (foo) return bar + else { baz() } + [1, 2, 3].map(foo) }", None), +("function foo13() { if (foo) return bar; + else { [1, 2, 3].map(foo) } }", None), +("function foo14() { if (foo) return bar + else { baz(); } + [1, 2, 3].map(foo) }", None), +("function foo15() { if (foo) return bar; else { baz() } qaz() }", None), +("function foo16() { if (foo) return bar + else { baz() } qaz() }", None), +("function foo17() { if (foo) return bar + else { baz() } + qaz() }", None), +("function foo18() { if (foo) return function() {} + else [1, 2, 3].map(bar) }", None), +("function foo19() { if (true) { return x; } else if (false) { return y; } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo20() {if (x) { return true; } else if (y) { notAReturn() } else { notAReturn(); } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo21() { var x = true; if (x) { return x; } else if (x === false) { return false; } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo() { var a; if (bar) { return true; } else { var a; } }", None), +("function foo() { if (bar) { var a; if (baz) { return true; } else { var a; } } }", None), +("function foo() { var a; if (bar) { return true; } else { var a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { var a; if (baz) { return true; } else { var a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { let a; if (bar) { return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("class foo { bar() { let a; if (baz) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { let a; if (baz) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() {let a; if (bar) { if (baz) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { const a = 1; if (bar) { return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { const a = 1; if (baz) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { let a; if (bar) { return true; } else { const a = 1 } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { let a; if (baz) { return true; } else { const a = 1; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { class a {}; if (bar) { return true; } else { const a = 1; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { class a {}; if (baz) { return true; } else { const a = 1; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { const a = 1; if (bar) { return true; } else { class a {} } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { const a = 1; if (baz) { return true; } else { class a {} } } }", None), // { "ecmaVersion": 6 }, +("function foo() { var a; if (bar) { return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { var a; return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let a; } while (baz) { var a; } }", None), // { "ecmaVersion": 6 }, +("function foo(a) { if (bar) { return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function foo(a = 1) { if (bar) { return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function foo(a, b = a) { if (bar) { return true; } else { let a; } if (bar) { return true; } else { let b; }}", None), // { "ecmaVersion": 6 }, +("function foo(...args) { if (bar) { return true; } else { let args; } }", None), // { "ecmaVersion": 6 }, +("function foo() { try {} catch (a) { if (bar) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { try {} catch (a) { if (bar) { if (baz) { return true; } else { let a; } } } }", None), // { "ecmaVersion": 6 }, +("function foo() { try {} catch ({bar, a = 1}) { if (baz) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let arguments; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let arguments; } return arguments[0]; }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let arguments; } if (baz) { return arguments[0]; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let arguments; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let a; } a; }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let a; } if (baz) { a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } a; }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } if (quux) { a; } } }", None), // { "ecmaVersion": 6 }, +("function a() { if (foo) { return true; } else { let a; } a(); }", None), // { "ecmaVersion": 6 }, +("function a() { if (a) { return true; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function a() { if (foo) { return a; } else { let a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let a; } function baz() { a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } (() => a) } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let a; } var a; }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } var a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } var { a } = {}; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } if (quux) { var a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } if (quux) { var a; } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (quux) { var a; } if (bar) { if (baz) { return true; } else { let a; } } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else { let a; } function a(){} }", None), // { "ecmaVersion": 6 }, +("function foo() { if (baz) { if (bar) { return true; } else { let a; } function a(){} } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } if (quux) { function a(){} } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } function a(){} }", None), // { "ecmaVersion": 6 }, +("function foo() { let a; if (bar) { return true; } else { function a(){} } }", None), // { "ecmaVersion": 6 }, +("function foo() { var a; if (bar) { return true; } else { function a(){} } }", None), // { "ecmaVersion": 6 }, +("function foo() { if (bar) { return true; } else function baz() {} };", None), +("if (foo) { return true; } else { let a; }", None), // { "ecmaVersion": 6, "sourceType": "commonjs" }, +("let a; if (foo) { return true; } else { let a; }", None), // { "ecmaVersion": 6, "sourceType": "commonjs" } + ]; + + let fix = vec![ + ("function foo1() { if (true) { return x; } else { return y; } }", "function foo1() { if (true) { return x; } return y; }", None), + ("function foo1() { if(true){ return x; }else{ return y; } }", "function foo1() { if(true){ return x; } return y; }", None), +("function foo2() { if (true) { var x = bar; return x; } else { var y = baz; return y; } }", "function foo2() { if (true) { var x = bar; return x; } var y = baz; return y; }", None), +("function foo3() { if (true) return x; else return y; }", "function foo3() { if (true) return x; return y; }", None), +("function foo4() { if (true) { if (false) return x; else return y; } else { return z; } }", "function foo4() { if (true) { if (false) return x; return y; } return z; }", None), +("function foo5() { if (true) { if (false) { if (true) return x; else { w = y; } } else { w = x; } } else { return z; } }", "function foo5() { if (true) { if (false) { if (true) return x; w = y; } else { w = x; } } else { return z; } }", None), +("function foo6() { if (true) { if (false) { if (true) return x; else return y; } } else { return z; } }", "function foo6() { if (true) { if (false) { if (true) return x; return y; } } else { return z; } }", None), +("function foo7() { if (true) { if (false) { if (true) return x; else return y; } return w; } else { return z; } }", "function foo7() { if (true) { if (false) { if (true) return x; return y; } return w; } return z; }", None), +("function foo8() { if (true) { if (false) { if (true) return x; else return y; } else { w = x; } } else { return z; } }", "function foo8() { if (true) { if (false) { if (true) return x; return y; } w = x; } else { return z; } }", None), +("function foo9() {if (x) { return true; } else if (y) { return true; } else { notAReturn(); } }", "function foo9() {if (x) { return true; } else if (y) { return true; } notAReturn(); }", None), +("function foo9a() {if (x) { return true; } else if (y) { return true; } else { notAReturn(); } }", "function foo9a() {if (x) { return true; } if (y) { return true; } else { notAReturn(); } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo9b() {if (x) { return true; } if (y) { return true; } else { notAReturn(); } }", "function foo9b() {if (x) { return true; } if (y) { return true; } notAReturn(); }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo10() { if (foo) return bar; else (foo).bar(); }", "function foo10() { if (foo) return bar; (foo).bar(); }", None), +("function foo13() { if (foo) return bar; + else { [1, 2, 3].map(foo) } }", "function foo13() { if (foo) return bar; [1, 2, 3].map(foo) }", None), +("function foo14() { if (foo) return bar + else { baz(); } + [1, 2, 3].map(foo) }", "function foo14() { if (foo) return bar\n baz(); + [1, 2, 3].map(foo) }", None), +("function foo17() { if (foo) return bar + else { baz() } + qaz() }", "function foo17() { if (foo) return bar\n baz() + qaz() }", None), +("function foo19() { if (true) { return x; } else if (false) { return y; } }", "function foo19() { if (true) { return x; } if (false) { return y; } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo20() {if (x) { return true; } else if (y) { notAReturn() } else { notAReturn(); } }", "function foo20() {if (x) { return true; } if (y) { notAReturn() } else { notAReturn(); } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo21() { var x = true; if (x) { return x; } else if (x === false) { return false; } }", "function foo21() { var x = true; if (x) { return x; } if (x === false) { return false; } }", Some(serde_json::json!([{ "allowElseIf": false }]))), +("function foo() { var a; if (bar) { return true; } else { var a; } }", "function foo() { var a; if (bar) { return true; } var a; }", None), +("function foo() { if (bar) { var a; if (baz) { return true; } else { var a; } } }", "function foo() { if (bar) { var a; if (baz) { return true; } var a; } }", None), +("function foo() { var a; if (bar) { return true; } else { var a; } }", "function foo() { var a; if (bar) { return true; } var a; }", None), +("function foo() { if (bar) { var a; if (baz) { return true; } else { var a; } } }", "function foo() { if (bar) { var a; if (baz) { return true; } var a; } }", None), +("function foo() {let a; if (bar) { if (baz) { return true; } else { let a; } } }", "function foo() {let a; if (bar) { if (baz) { return true; } let a; } }", None), +("function foo() { try {} catch (a) { if (bar) { if (baz) { return true; } else { let a; } } } }", "function foo() { try {} catch (a) { if (bar) { if (baz) { return true; } let a; } } }", None), +("function foo() { if (bar) { return true; } else { let arguments; } }", "function foo() { if (bar) { return true; } let arguments; }", None), +("function foo() { if (bar) { if (baz) { return true; } else { let arguments; } } }", "function foo() { if (bar) { if (baz) { return true; } let arguments; } }", None), +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } a; }", "function foo() { if (bar) { if (baz) { return true; } let a; } a; }", None), +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } if (quux) { var a; } }", "function foo() { if (bar) { if (baz) { return true; } let a; } if (quux) { var a; } }", None), +("function foo() { if (quux) { var a; } if (bar) { if (baz) { return true; } else { let a; } } }", "function foo() { if (quux) { var a; } if (bar) { if (baz) { return true; } let a; } }", None), +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } if (quux) { function a(){} } }", "function foo() { if (bar) { if (baz) { return true; } let a; } if (quux) { function a(){} } }", None), +("function foo() { if (bar) { if (baz) { return true; } else { let a; } } function a(){} }", "function foo() { if (bar) { if (baz) { return true; } let a; } function a(){} }", None), +("if (foo) { return true; } else { let a; }", "if (foo) { return true; } let a;", None) + ]; + Tester::new(NoElseReturn::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/eslint/no_empty.rs b/crates/oxc_linter/src/rules/eslint/no_empty.rs index 4949672d74907..5ee42bec6415b 100644 --- a/crates/oxc_linter/src/rules/eslint/no_empty.rs +++ b/crates/oxc_linter/src/rules/eslint/no_empty.rs @@ -54,7 +54,7 @@ impl Rule for NoEmpty { return; } - if ctx.semantic().trivias().has_comments_between(block.span) { + if ctx.semantic().has_comments_between(block.span) { return; } ctx.diagnostic_with_suggestion(no_empty_diagnostic("block", block.span), |fixer| { @@ -72,7 +72,7 @@ impl Rule for NoEmpty { // The visitor does not visit the `BlockStatement` inside the `FinallyClause`. // See `Visit::visit_finally_clause`. AstKind::FinallyClause(finally_clause) if finally_clause.body.is_empty() => { - if ctx.semantic().trivias().has_comments_between(finally_clause.span) { + if ctx.semantic().has_comments_between(finally_clause.span) { return; } ctx.diagnostic_with_suggestion( diff --git a/crates/oxc_linter/src/rules/eslint/no_empty_function.rs b/crates/oxc_linter/src/rules/eslint/no_empty_function.rs index 2eea58efe2b74..2a50b77550c81 100644 --- a/crates/oxc_linter/src/rules/eslint/no_empty_function.rs +++ b/crates/oxc_linter/src/rules/eslint/no_empty_function.rs @@ -65,7 +65,7 @@ impl Rule for NoEmptyFunction { let AstKind::FunctionBody(fb) = node.kind() else { return; }; - if fb.is_empty() && !ctx.semantic().trivias().has_comments_between(fb.span) { + if fb.is_empty() && !ctx.semantic().has_comments_between(fb.span) { let (kind, fn_name) = get_function_name_and_kind(node, ctx); ctx.diagnostic(no_empty_function_diagnostic(fb.span, kind, fn_name)); } diff --git a/crates/oxc_linter/src/rules/eslint/no_empty_static_block.rs b/crates/oxc_linter/src/rules/eslint/no_empty_static_block.rs index 2ac6e0d24d6fd..24de7dc61d01a 100644 --- a/crates/oxc_linter/src/rules/eslint/no_empty_static_block.rs +++ b/crates/oxc_linter/src/rules/eslint/no_empty_static_block.rs @@ -55,7 +55,7 @@ impl Rule for NoEmptyStaticBlock { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { if let AstKind::StaticBlock(static_block) = node.kind() { if static_block.body.is_empty() { - if ctx.semantic().trivias().has_comments_between(static_block.span) { + if ctx.semantic().has_comments_between(static_block.span) { return; } ctx.diagnostic(no_empty_static_block_diagnostic(static_block.span)); diff --git a/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs b/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs index 9927401f14610..4476fe38b9711 100644 --- a/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs +++ b/crates/oxc_linter/src/rules/eslint/no_fallthrough.rs @@ -11,7 +11,7 @@ use oxc_cfg::{ visit::{neighbors_filtered_by_edge_weight, EdgeRef}, Direction, }, - BasicBlockId, EdgeType, ErrorEdgeKind, InstructionKind, + BlockNodeId, EdgeType, ErrorEdgeKind, InstructionKind, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; @@ -274,7 +274,7 @@ impl Rule for NoFallthrough { return; }; - let fallthroughs: FxHashSet = neighbors_filtered_by_edge_weight( + let fallthroughs: FxHashSet = neighbors_filtered_by_edge_weight( graph, switch_id, &|e| match e { @@ -283,7 +283,7 @@ impl Rule for NoFallthrough { } _ => Some(None), }, - &mut |node, last_cond: Option| { + &mut |node, last_cond: Option| { let node = *node; if node == switch_id { @@ -295,7 +295,7 @@ impl Rule for NoFallthrough { if tests.contains_key(&node) { return (last_cond, true); } - if cfg.basic_block(node).unreachable { + if cfg.basic_block(node).is_unreachable() { return (None, false); } @@ -381,7 +381,6 @@ impl NoFallthrough { let semantic = ctx.semantic(); let is_fallthrough_comment_in_range = |range: Range| { let comment = semantic - .trivias() .comments_range(range) .map(|comment| { &semantic.source_text()[comment.span.start as usize..comment.span.end as usize] @@ -448,10 +447,10 @@ fn get_switch_semantic_cases( node: &AstNode, switch: &SwitchStatement, ) -> ( - Vec, - FxHashMap, - /* default */ Option, - /* exit */ Option, + Vec, + FxHashMap, + /* default */ Option, + /* exit */ Option, ) { let cfg = ctx.cfg(); let graph = cfg.graph(); diff --git a/crates/oxc_linter/src/rules/eslint/no_global_assign.rs b/crates/oxc_linter/src/rules/eslint/no_global_assign.rs index 974f0931b85d8..9169a3ff80e55 100644 --- a/crates/oxc_linter/src/rules/eslint/no_global_assign.rs +++ b/crates/oxc_linter/src/rules/eslint/no_global_assign.rs @@ -59,19 +59,17 @@ impl Rule for NoGlobalAssign { fn run_once(&self, ctx: &LintContext) { let symbol_table = ctx.symbols(); - for reference_id_list in ctx.scopes().root_unresolved_references_ids() { - for reference_id in reference_id_list { + for (name, reference_id_list) in ctx.scopes().root_unresolved_references() { + for &reference_id in reference_id_list { let reference = symbol_table.get_reference(reference_id); - if reference.is_write() { - let name = ctx.semantic().reference_name(reference); - if !self.excludes.contains(&CompactStr::from(name)) - && ctx.env_contains_var(name) - { - ctx.diagnostic(no_global_assign_diagnostic( - name, - ctx.semantic().reference_span(reference), - )); - } + if reference.is_write() + && !self.excludes.contains(name) + && ctx.env_contains_var(name) + { + ctx.diagnostic(no_global_assign_diagnostic( + name, + ctx.semantic().reference_span(reference), + )); } } } diff --git a/crates/oxc_linter/src/rules/eslint/no_invalid_regexp.rs b/crates/oxc_linter/src/rules/eslint/no_invalid_regexp.rs index 762fa3d529ac3..9b1a5341f0ec0 100644 --- a/crates/oxc_linter/src/rules/eslint/no_invalid_regexp.rs +++ b/crates/oxc_linter/src/rules/eslint/no_invalid_regexp.rs @@ -1,14 +1,28 @@ use oxc_allocator::Allocator; use oxc_ast::{ast::Argument, AstKind}; -use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; +use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_regular_expression::{FlagsParser, ParserOptions, PatternParser}; +use oxc_regular_expression::{Parser, ParserOptions}; use oxc_span::Span; use rustc_hash::FxHashSet; use serde::Deserialize; use crate::{context::LintContext, rule::Rule, AstNode}; +// Use the same prefix with `oxc_regular_expression` crate +fn duplicated_flag_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Invalid regular expression: Duplicated flag").with_label(span) +} + +fn unknown_flag_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Invalid regular expression: Unknown flag").with_label(span) +} + +fn invalid_unicode_flags_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Invalid regular expression: `u` and `v` flags should be used alone") + .with_label(span) +} + #[derive(Debug, Default, Clone)] pub struct NoInvalidRegexp(Box); @@ -72,75 +86,54 @@ impl Rule for NoInvalidRegexp { return; } - let allocator = Allocator::default(); - // Validate flags first if exists - let mut parsed_flags = None; if let Some((flags_span_start, flags_text)) = flags_arg { - // Check for duplicated flags - // For compatibility with ESLint, we need to check "user-defined duplicated" flags here - // "valid duplicated" flags are also checked + let (mut u_flag_found, mut v_flag_found) = (false, false); let mut unique_flags = FxHashSet::default(); - let mut violations = vec![]; for (idx, ch) in flags_text.char_indices() { - if !unique_flags.insert(ch) { - violations.push(idx); - } - } - if !violations.is_empty() { - return ctx.diagnostic( - // Use the same prefix with `oxc_regular_expression` - OxcDiagnostic::warn("Invalid regular expression: Duplicated flag").with_labels( - violations - .iter() - .map(|&start| { - #[allow(clippy::cast_possible_truncation)] - let start = flags_span_start + start as u32; - LabeledSpan::new_with_span(None, Span::new(start, start)) - }) - .collect::>(), - ), - ); - } + #[allow(clippy::cast_possible_truncation)] + let start = flags_span_start + idx as u32; - // Omit user defined invalid flags - for flag in &self.0.allow_constructor_flags { - match flag { - // Keep valid flags, even if they are defined - 'd' | 'g' | 'i' | 'm' | 's' | 'u' | 'v' | 'y' => continue, - _ => { - unique_flags.remove(flag); + // Invalid combination: u+v + if ch == 'u' { + if v_flag_found { + return ctx + .diagnostic(invalid_unicode_flags_diagnostic(Span::new(start, start))); } + u_flag_found = true; + } + if ch == 'v' { + if u_flag_found { + return ctx + .diagnostic(invalid_unicode_flags_diagnostic(Span::new(start, start))); + } + v_flag_found = true; + } + + // Duplicated: user defined, invalid or valid + if !unique_flags.insert(ch) { + return ctx.diagnostic(duplicated_flag_diagnostic(Span::new(start, start))); } - } - // Use parser to check: - // - Unknown invalid flags - // - Invalid flags combination: u+v - // - (Valid duplicated flags are already checked above) - // It can be done without `FlagsParser`, though - let flags_text = unique_flags.iter().collect::(); - let options = ParserOptions::default().with_span_offset(flags_span_start); - match FlagsParser::new(&allocator, flags_text.as_str(), options).parse() { - Ok(flags) => parsed_flags = Some(flags), - Err(diagnostic) => return ctx.diagnostic(diagnostic), + // Unknown: not valid, not user defined + if !(matches!(ch, 'd' | 'g' | 'i' | 'm' | 's' | 'u' | 'v' | 'y') + || self.0.allow_constructor_flags.contains(&ch)) + { + return ctx.diagnostic(unknown_flag_diagnostic(Span::new(start, start))); + } } } // Then, validate pattern if exists // Pattern check is skipped when 1st argument is NOT a `StringLiteral` // e.g. `new RegExp(var)`, `RegExp("str" + var)` + let allocator = Allocator::default(); if let Some((pattern_span_start, pattern_text)) = pattern_arg { - let mut options = ParserOptions::default().with_span_offset(pattern_span_start); - if let Some(flags) = parsed_flags { - if flags.unicode || flags.unicode_sets { - options = options.with_unicode_mode(); - } - if flags.unicode_sets { - options = options.with_unicode_sets_mode(); - } - } - match PatternParser::new(&allocator, pattern_text, options).parse() { + let options = ParserOptions::default() + .with_span_offset(pattern_span_start) + .with_flags(flags_arg.map_or("", |(_, flags_text)| flags_text)); + + match Parser::new(&allocator, pattern_text, options).parse() { Ok(_) => {} Err(diagnostic) => ctx.diagnostic(diagnostic), } diff --git a/crates/oxc_linter/src/rules/eslint/no_irregular_whitespace.rs b/crates/oxc_linter/src/rules/eslint/no_irregular_whitespace.rs index 7a32b33668fd5..5729c450ec1cb 100644 --- a/crates/oxc_linter/src/rules/eslint/no_irregular_whitespace.rs +++ b/crates/oxc_linter/src/rules/eslint/no_irregular_whitespace.rs @@ -33,7 +33,7 @@ declare_oxc_lint!( impl Rule for NoIrregularWhitespace { fn run_once(&self, ctx: &LintContext) { - let irregular_whitespaces = ctx.semantic().trivias().irregular_whitespaces(); + let irregular_whitespaces = ctx.semantic().irregular_whitespaces(); for irregular_whitespace in irregular_whitespaces { ctx.diagnostic(no_irregular_whitespace_diagnostic(*irregular_whitespace)); } diff --git a/crates/oxc_linter/src/rules/eslint/no_plusplus.rs b/crates/oxc_linter/src/rules/eslint/no_plusplus.rs index 60b66c94e3576..a2161e66f4366 100644 --- a/crates/oxc_linter/src/rules/eslint/no_plusplus.rs +++ b/crates/oxc_linter/src/rules/eslint/no_plusplus.rs @@ -78,7 +78,10 @@ declare_oxc_lint!( /// ``` NoPlusplus, restriction, - pending + // This is not guaranteed to rewrite the code in a way that is equivalent. + // For example, `++i` and `i++` will be rewritten as `i += 1` even though they are not the same. + // If the code depends on the order of evaluation, then this might break it. + conditional_suggestion, ); impl Rule for NoPlusplus { @@ -102,7 +105,20 @@ impl Rule for NoPlusplus { return; } - ctx.diagnostic(no_plusplus_diagnostic(expr.span, expr.operator)); + let ident = expr.argument.get_identifier(); + + if let Some(ident) = ident { + let operator = match expr.operator { + UpdateOperator::Increment => "+=", + UpdateOperator::Decrement => "-=", + }; + ctx.diagnostic_with_suggestion( + no_plusplus_diagnostic(expr.span, expr.operator), + |fixer| fixer.replace(expr.span, format!("{ident} {operator} 1")), + ); + } else { + ctx.diagnostic(no_plusplus_diagnostic(expr.span, expr.operator)); + } } } @@ -186,5 +202,69 @@ fn test() { ), ]; - Tester::new(NoPlusplus::NAME, pass, fail).test_and_snapshot(); + let fix = vec![ + ("var foo = 0; foo++;", "var foo = 0; foo += 1;", None), + ("var foo = 0; foo--;", "var foo = 0; foo -= 1;", None), + ("var foo = 0; --foo;", "var foo = 0; foo -= 1;", None), + ("var foo = 0; ++foo;", "var foo = 0; foo += 1;", None), + ( + "for (i = 0; i < l; i++) { console.log(i); }", + "for (i = 0; i < l; i += 1) { console.log(i); }", + None, + ), + ( + "for (i = 0; i < l; foo, i++) { console.log(i); }", + "for (i = 0; i < l; foo, i += 1) { console.log(i); }", + None, + ), + ( + "var foo = 0; foo++;", + "var foo = 0; foo += 1;", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + "for (i = 0; i < l; i++) { v++; }", + "for (i = 0; i < l; i++) { v += 1; }", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + "for (i++;;);", + "for (i += 1;;);", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + "for (;--i;);", + "for (;i -= 1;);", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + "for (;;) ++i;", + "for (;;) i += 1;", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + "for (;; i = j++);", + "for (;; i = j += 1);", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + // Do not fix if part of a function call like f(--j) + "for (;; i++, f(--j));", + "for (;; i++, f(j -= 1));", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + "for (;; foo + (i++, bar));", + "for (;; foo + (i += 1, bar));", + Some(serde_json::json!([{ "allowForLoopAfterthoughts": true }])), + ), + ( + // Do not fix if part of property definition + "let x = 0; let y = { foo: x++ };", + "let x = 0; let y = { foo: x += 1 };", + None, + ), + ]; + + Tester::new(NoPlusplus::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs b/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs index dabb4961f0395..650b6828e3b1b 100644 --- a/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs +++ b/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs @@ -105,12 +105,14 @@ impl NoRegexSpaces { } let alloc = Allocator::default(); - let pattern_with_slashes = format!("/{}/", &pattern.value); - let parser = Parser::new(&alloc, pattern_with_slashes.as_str(), ParserOptions::default()); - let regex = parser.parse().ok()?; + let parser = Parser::new( + &alloc, + pattern.value.as_str(), + ParserOptions::default().with_span_offset(pattern.span.start + 1), + ); + let parsed_pattern = parser.parse().ok()?; - find_consecutive_spaces(®ex.pattern) - .map(|span| Span::new(span.start + pattern.span.start, span.end + pattern.span.start)) + find_consecutive_spaces(&parsed_pattern) } fn is_regexp_new_expression(expr: &NewExpression<'_>) -> bool { diff --git a/crates/oxc_linter/src/rules/eslint/no_script_url.rs b/crates/oxc_linter/src/rules/eslint/no_script_url.rs index edbfee0c7f366..29220cc6c06cc 100644 --- a/crates/oxc_linter/src/rules/eslint/no_script_url.rs +++ b/crates/oxc_linter/src/rules/eslint/no_script_url.rs @@ -7,7 +7,7 @@ use oxc_span::Span; use crate::{context::LintContext, rule::Rule, AstNode}; fn no_script_url_diagnostic(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Unexpeced `javascript:` url") + OxcDiagnostic::warn("Unexpected `javascript:` url") .with_help("Execute the code directly instead.") .with_label(span) } diff --git a/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs b/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs index db4fb50439d5e..eb1b9e28059a2 100644 --- a/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs +++ b/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs @@ -4,7 +4,7 @@ use oxc_ast::{ }; use oxc_cfg::{ graph::visit::{neighbors_filtered_by_edge_weight, EdgeRef}, - BasicBlockId, ControlFlowGraph, EdgeType, ErrorEdgeKind, + BlockNodeId, ControlFlowGraph, EdgeType, ErrorEdgeKind, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; @@ -50,7 +50,7 @@ enum DefinitelyCallsThisBeforeSuper { #[default] No, Yes, - Maybe(BasicBlockId), + Maybe(BlockNodeId), } impl Rule for NoThisBeforeSuper { @@ -60,9 +60,9 @@ impl Rule for NoThisBeforeSuper { // first pass -> find super calls and local violations let mut wanted_nodes = Vec::new(); - let mut basic_blocks_with_super_called = FxHashSet::::default(); + let mut basic_blocks_with_super_called = FxHashSet::::default(); let mut basic_blocks_with_local_violations = - FxHashMap::>::default(); + FxHashMap::>::default(); for node in semantic.nodes() { match node.kind() { AstKind::Function(_) | AstKind::ArrowFunctionExpression(_) => { @@ -151,9 +151,9 @@ impl NoThisBeforeSuper { fn analyze( cfg: &ControlFlowGraph, - id: BasicBlockId, - basic_blocks_with_super_called: &FxHashSet, - basic_blocks_with_local_violations: &FxHashMap>, + id: BlockNodeId, + basic_blocks_with_super_called: &FxHashSet, + basic_blocks_with_local_violations: &FxHashMap>, follow_join: bool, ) -> Vec { neighbors_filtered_by_edge_weight( @@ -211,8 +211,8 @@ impl NoThisBeforeSuper { fn check_for_violation( cfg: &ControlFlowGraph, output: Vec, - basic_blocks_with_super_called: &FxHashSet, - basic_blocks_with_local_violations: &FxHashMap>, + basic_blocks_with_super_called: &FxHashSet, + basic_blocks_with_local_violations: &FxHashMap>, ) -> bool { // Deciding whether we definitely call this before super in all // codepaths is as simple as seeing if any individual codepath diff --git a/crates/oxc_linter/src/rules/eslint/no_throw_literal.rs b/crates/oxc_linter/src/rules/eslint/no_throw_literal.rs new file mode 100644 index 0000000000000..7cc515c263a0f --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_throw_literal.rs @@ -0,0 +1,277 @@ +use oxc_ast::{ + ast::{AssignmentOperator, Expression, LogicalOperator, TSType}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_throw_literal_diagnostic(span: Span, is_undef: bool) -> OxcDiagnostic { + let message = + if is_undef { "Do not throw undefined" } else { "Expected an error object to be thrown" }; + + OxcDiagnostic::warn(message).with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoThrowLiteral; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallows throwing literals or non-Error objects as exceptions. + /// + /// ### Why is this bad? + /// + /// It is considered good practice to only throw the Error object itself or an object using + /// the Error object as base objects for user-defined exceptions. The fundamental benefit of + /// Error objects is that they automatically keep track of where they were built and originated. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// throw "error"; + /// + /// throw 0; + /// + /// throw undefined; + /// + /// throw null; + /// + /// var err = new Error(); + /// throw "an " + err; + /// // err is recast to a string literal + /// + /// var err = new Error(); + /// throw `${err}` + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// throw new Error(); + /// + /// throw new Error("error"); + /// + /// var e = new Error("error"); + /// throw e; + /// + /// try { + /// throw new Error("error"); + /// } catch (e) { + /// throw e; + /// } + /// ``` + NoThrowLiteral, + pedantic, + conditional_suggestion, +); + +const SPECIAL_IDENTIFIERS: [&str; 3] = ["undefined", "Infinity", "NaN"]; +impl Rule for NoThrowLiteral { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::ThrowStatement(stmt) = node.kind() else { + return; + }; + + let expr = &stmt.argument; + + match expr.get_inner_expression() { + Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => { + let span = expr.span(); + ctx.diagnostic_with_suggestion(no_throw_literal_diagnostic(span, false), |fixer| { + fixer.replace( + span, + format!("new Error({})", span.source_text(ctx.source_text())), + ) + }); + } + Expression::Identifier(id) if SPECIAL_IDENTIFIERS.contains(&id.name.as_str()) => { + ctx.diagnostic(no_throw_literal_diagnostic(expr.span(), true)); + } + expr if !Self::could_be_error(ctx, expr) => { + ctx.diagnostic(no_throw_literal_diagnostic(expr.span(), false)); + } + _ => {} + } + } +} + +impl NoThrowLiteral { + fn could_be_error(ctx: &LintContext, expr: &Expression) -> bool { + match expr.get_inner_expression() { + Expression::NewExpression(_) + | Expression::AwaitExpression(_) + | Expression::CallExpression(_) + | Expression::ChainExpression(_) + | Expression::YieldExpression(_) + | Expression::PrivateFieldExpression(_) + | Expression::StaticMemberExpression(_) + | Expression::ComputedMemberExpression(_) + | Expression::TaggedTemplateExpression(_) => true, + Expression::AssignmentExpression(expr) => { + if matches!( + expr.operator, + AssignmentOperator::Assign | AssignmentOperator::LogicalAnd + ) { + return Self::could_be_error(ctx, &expr.right); + } + + if matches!( + expr.operator, + AssignmentOperator::LogicalOr | AssignmentOperator::LogicalNullish + ) { + return expr + .left + .get_expression() + .map_or(true, |expr| Self::could_be_error(ctx, expr)) + || Self::could_be_error(ctx, &expr.right); + } + + false + } + Expression::SequenceExpression(expr) => { + expr.expressions.last().is_some_and(|expr| Self::could_be_error(ctx, expr)) + } + Expression::LogicalExpression(expr) => { + if matches!(expr.operator, LogicalOperator::And) { + return Self::could_be_error(ctx, &expr.right); + } + + Self::could_be_error(ctx, &expr.left) || Self::could_be_error(ctx, &expr.right) + } + Expression::ConditionalExpression(expr) => { + Self::could_be_error(ctx, &expr.consequent) + || Self::could_be_error(ctx, &expr.alternate) + } + Expression::Identifier(ident) => { + let Some(ref_id) = ident.reference_id() else { + return true; + }; + let reference = ctx.symbols().get_reference(ref_id); + let Some(symbol_id) = reference.symbol_id() else { + return true; + }; + let decl = ctx.nodes().get_node(ctx.symbols().get_declaration(symbol_id)); + match decl.kind() { + AstKind::VariableDeclarator(decl) => { + if let Some(init) = &decl.init { + Self::could_be_error(ctx, init) + } else { + // TODO: warn about throwing undefined + false + } + } + AstKind::Function(_) + | AstKind::Class(_) + | AstKind::TSModuleDeclaration(_) + | AstKind::TSEnumDeclaration(_) => false, + AstKind::FormalParameter(param) => { + !param.pattern.type_annotation.as_ref().is_some_and(|annot| { + is_definitely_non_error_type(&annot.type_annotation) + }) + } + _ => true, + } + } + _ => false, + } + } +} + +fn is_definitely_non_error_type(ty: &TSType) -> bool { + match ty { + TSType::TSNumberKeyword(_) + | TSType::TSStringKeyword(_) + | TSType::TSBooleanKeyword(_) + | TSType::TSNullKeyword(_) + | TSType::TSUndefinedKeyword(_) => true, + TSType::TSUnionType(union) => union.types.iter().all(is_definitely_non_error_type), + TSType::TSIntersectionType(intersect) => { + intersect.types.iter().all(is_definitely_non_error_type) + } + _ => false, + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "throw new Error();", + "throw new Error('error');", + "throw Error('error');", + "var e = new Error(); throw e;", + "try {throw new Error();} catch (e) {throw e;};", + "throw a;", + "throw foo();", + "throw new foo();", + "throw foo.bar;", + "throw foo[bar];", + "class C { #field; foo() { throw foo.#field; } }", // { "ecmaVersion": 2022 }, + "throw foo = new Error();", + "throw foo.bar ||= 'literal'", // { "ecmaVersion": 2021 }, + "throw foo[bar] ??= 'literal'", // { "ecmaVersion": 2021 }, + "throw 1, 2, new Error();", + "throw 'literal' && new Error();", + "throw new Error() || 'literal';", + "throw foo ? new Error() : 'literal';", + "throw foo ? 'literal' : new Error();", + "throw tag `${foo}`;", // { "ecmaVersion": 6 }, + "function* foo() { var index = 0; throw yield index++; }", // { "ecmaVersion": 6 }, + "async function foo() { throw await bar; }", // { "ecmaVersion": 8 }, + "throw obj?.foo", // { "ecmaVersion": 2020 }, + "throw obj?.foo()", // { "ecmaVersion": 2020 } + "throw obj?.foo() as string", + "throw obj?.foo() satisfies Direction", + // local reference resolution + "const err = new Error(); throw err;", + "function main(x) { throw x; }", // cannot determine type of x + "function main(x: any) { throw x; }", + "function main(x: TypeError) { throw x; }", + ]; + + let fail = vec![ + "throw 'error';", + "throw 0;", + "throw false;", + "throw null;", + "throw {};", + "throw undefined;", + "throw Infinity;", + "throw NaN;", + "throw 'a' + 'b';", + "var b = new Error(); throw 'a' + b;", + "throw foo = 'error';", + "throw foo += new Error();", + "throw foo &= new Error();", + "throw foo &&= 'literal'", // { "ecmaVersion": 2021 }, + "throw new Error(), 1, 2, 3;", + "throw 'literal' && 'not an Error';", + "throw foo && 'literal'", + "throw foo ? 'not an Error' : 'literal';", + "throw `${err}`;", // { "ecmaVersion": 6 } + "throw 0 as number", + "throw 'error' satisfies Error", + // local reference resolution + "let foo = 'foo'; throw foo;", + "let foo = 'foo' as unknown as Error; throw foo;", + "function foo() {}; throw foo;", + "const foo = () => {}; throw foo;", + "class Foo {}\nthrow Foo;", + "function main(x: number) { throw x; }", + "function main(x: string) { throw x; }", + "function main(x: string | number) { throw x; }", + ]; + + let fix = vec![ + ("throw 'error';", "throw new Error('error');"), + ("throw `${err}`;", "throw new Error(`${err}`);"), + ("throw 'error' satisfies Error", "throw new Error('error' satisfies Error)"), + ]; + + Tester::new(NoThrowLiteral::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unexpected_multiline.rs b/crates/oxc_linter/src/rules/eslint/no_unexpected_multiline.rs index 573517aa5a197..424654c3a1920 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unexpected_multiline.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unexpected_multiline.rs @@ -387,5 +387,8 @@ fn test() { "class C { field1 = function() {}\n[field2]; }", // { "ecmaVersion": 2022 } ]; - Tester::new(NoUnexpectedMultiline::NAME, pass, fail).test_and_snapshot(); + // TODO: add more fixer tests + let fix = vec![("var a = b\n(x || y).doSomething()", "var a = b\n;(x || y).doSomething()")]; + + Tester::new(NoUnexpectedMultiline::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/eslint/no_unreachable.rs b/crates/oxc_linter/src/rules/eslint/no_unreachable.rs index 776a68828beac..abdfb89001da9 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unreachable.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unreachable.rs @@ -52,7 +52,7 @@ impl Rule for NoUnreachable { // prevent other reachable blocks from ever getting executed. let _: Control<()> = depth_first_search(graph, Some(root.cfg_id()), |event| { if let DfsEvent::Finish(node, _) = event { - let unreachable = cfg.basic_block(node).unreachable; + let unreachable = cfg.basic_block(node).is_unreachable(); unreachables[node.index()] = unreachable; if !unreachable { diff --git a/crates/oxc_linter/src/rules/eslint/no_unsafe_negation.rs b/crates/oxc_linter/src/rules/eslint/no_unsafe_negation.rs index d5fb528178eea..6eee9d10c9e96 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unsafe_negation.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unsafe_negation.rs @@ -83,15 +83,15 @@ impl NoUnsafeNegation { // modify `!a instance of B` to `!(a instanceof B)` let modified_code = { let mut codegen = fixer.codegen(); - codegen.print_char(b'!'); + codegen.print_ascii_byte(b'!'); let Expression::UnaryExpression(left) = &expr.left else { unreachable!() }; - codegen.print_char(b'('); + codegen.print_ascii_byte(b'('); codegen.print_expression(&left.argument); - codegen.print_char(b' '); + codegen.print_ascii_byte(b' '); codegen.print_str(expr.operator.as_str()); - codegen.print_char(b' '); + codegen.print_ascii_byte(b' '); codegen.print_expression(&expr.right); - codegen.print_char(b')'); + codegen.print_ascii_byte(b')'); codegen.into_source_text() }; fixer.replace(expr.span, modified_code) @@ -140,5 +140,22 @@ fn test() { ("! a <= b", Some(serde_json::json!([{ "enforceForOrderingRelations": true }]))), ]; - Tester::new(NoUnsafeNegation::NAME, pass, fail).test_and_snapshot(); + let fix = vec![ + ("!a in b", "!(a in b)"), + ("(!a in b)", "(!(a in b))"), + ("!(a) in b", "!(a in b)"), + ("!a instanceof b", "!(a instanceof b)"), + ("(!a instanceof b)", "(!(a instanceof b))"), + ("!(a) instanceof b", "!(a instanceof b)"), + // FIXME: I think these should be failing. I encountered these while + // making sure all fix-reporting rules have fix test cases. Debugging + + // fixing this is out of scope for this PR. + // ("if (! a < b) {}", "if (!(a < b)) {}"), + // ("while (! a > b) {}", "while (!(a > b)) {}"), + // ("foo = ! a <= b;", "foo = !(a <= b);"), + // ("foo = ! a >= b;", "foo = !(a >= b);"), + // ("!a <= b", "!(a <= b)"), + ]; + + Tester::new(NoUnsafeNegation::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } 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 dcc88d21a44e0..5f9be9e87c962 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 @@ -49,7 +49,7 @@ declare_oxc_lint!( /// const { bar } = obj?.foo; // TypeError /// ``` NoUnsafeOptionalChaining, - restriction // TypeScript checks optional chaining + correctness ); impl Rule for NoUnsafeOptionalChaining { 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 0461804773082..efc2441387dc1 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 @@ -24,9 +24,19 @@ impl<'s, 'a> Symbol<'s, 'a> { assert!(kind.is_function_like() || matches!(kind, AstKind::Class(_))); } - for parent in self.iter_parents() { + for parent in self.iter_relevant_parents() { match parent.kind() { - AstKind::MemberExpression(_) | AstKind::ParenthesizedExpression(_) => { + AstKind::MemberExpression(_) | AstKind::ParenthesizedExpression(_) + // e.g. `const x = [function foo() {}]` + // Only considered used if the array containing the symbol is used. + | AstKind::ArrayExpressionElement(_) + | AstKind::ExpressionArrayElement(_) + | 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 @@ -37,6 +47,9 @@ impl<'s, 'a> Symbol<'s, 'a> { // 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` diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs index e6c313af6b0dd..0a8f812fce6a8 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs @@ -377,4 +377,26 @@ mod test { assert!(rule.is_ignored_array_destructured(&Atom::from("_x"))); assert!(!rule.is_ignored_array_destructured("notIgnored")); } + + #[test] + fn test_ignored_catch_errors() { + let rule = NoUnusedVars::from_configuration(serde_json::json!([ + { + "caughtErrorsIgnorePattern": "^_", + "caughtErrors": "all", + } + ])); + assert!(rule.is_ignored_catch_err("_")); + assert!(rule.is_ignored_catch_err("_err")); + assert!(!rule.is_ignored_catch_err("err")); + + let rule = NoUnusedVars::from_configuration(serde_json::json!([ + { + "caughtErrors": "none", + } + ])); + assert!(rule.is_ignored_catch_err("_")); + assert!(rule.is_ignored_catch_err("_err")); + assert!(rule.is_ignored_catch_err("err")); + } } 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 974ff1a335b6f..fbcadd72167b7 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 @@ -201,7 +201,7 @@ impl Deref for NoUnusedVars { impl Rule for NoUnusedVars { fn from_configuration(value: serde_json::Value) -> Self { - Self(Box::new(NoUnusedVarsOptions::from(value))) + Self(Box::new(NoUnusedVarsOptions::try_from(value).unwrap())) } fn run_on_symbol(&self, symbol_id: SymbolId, ctx: &LintContext<'_>) { diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs index 0c4ac93e358f4..ddfe83ec71974 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs @@ -405,21 +405,22 @@ fn parse_unicode_rule(value: Option<&Value>, name: &str) -> Option { .map_err(|err| panic!("Invalid '{name}' option for no-unused-vars: {err}")) .unwrap() } -impl From for NoUnusedVarsOptions { - fn from(value: Value) -> Self { - let Some(config) = value.get(0) else { return Self::default() }; +impl TryFrom for NoUnusedVarsOptions { + type Error = OxcDiagnostic; + fn try_from(value: Value) -> Result { + let Some(config) = value.get(0) else { return Ok(Self::default()) }; match config { Value::String(vars) => { - let vars: VarsOption = vars.try_into().unwrap(); - Self { vars, ..Default::default() } + let vars: VarsOption = vars.try_into()?; + Ok(Self { vars, ..Default::default() }) } Value::Object(config) => { let vars = config .get("vars") .map(|vars| { - let vars: VarsOption = vars.try_into().unwrap(); - vars + vars.try_into() }) + .transpose()? .unwrap_or_default(); // NOTE: when a configuration object is provided, do not provide @@ -431,9 +432,9 @@ impl From for NoUnusedVarsOptions { let args: ArgsOption = config .get("args") .map(|args| { - let args: ArgsOption = args.try_into().unwrap(); - args + args.try_into() }) + .transpose()? .unwrap_or_default(); let args_ignore_pattern: Option = @@ -442,9 +443,9 @@ impl From for NoUnusedVarsOptions { let caught_errors: CaughtErrors = config .get("caughtErrors") .map(|caught_errors| { - let caught_errors: CaughtErrors = caught_errors.try_into().unwrap(); - caught_errors + caught_errors.try_into() }) + .transpose()? .unwrap_or_default(); let caught_errors_ignore_pattern = parse_unicode_rule( @@ -472,7 +473,7 @@ impl From for NoUnusedVarsOptions { .map_or(Some(false), Value::as_bool) .unwrap_or(false); - Self { + Ok(Self { vars, vars_ignore_pattern, args, @@ -483,12 +484,12 @@ impl From for NoUnusedVarsOptions { destructured_array_ignore_pattern, ignore_class_with_static_init_block, report_used_ignore_pattern, - } + }) } - Value::Null => Self::default(), - _ => panic!( + Value::Null => Ok(Self::default()), + _ => Err(OxcDiagnostic::error( "Invalid 'vars' option for no-unused-vars: Expected a string or an object, got {config}" - ), + )), } } } @@ -516,10 +517,10 @@ mod tests { #[test] fn test_options_from_string() { - let rule: NoUnusedVarsOptions = json!(["all"]).into(); + let rule: NoUnusedVarsOptions = json!(["all"]).try_into().unwrap(); assert_eq!(rule.vars, VarsOption::All); - let rule: NoUnusedVarsOptions = json!(["local"]).into(); + let rule: NoUnusedVarsOptions = json!(["local"]).try_into().unwrap(); assert_eq!(rule.vars, VarsOption::Local); } @@ -538,7 +539,8 @@ mod tests { "reportUsedIgnorePattern": true } ]) - .into(); + .try_into() + .unwrap(); assert_eq!(rule.vars, VarsOption::Local); assert_eq!(rule.vars_ignore_pattern.unwrap().as_str(), "^_"); @@ -559,7 +561,8 @@ mod tests { "argsIgnorePattern": "^_", } ]) - .into(); + .try_into() + .unwrap(); // option object provided, no default varsIgnorePattern assert!(rule.vars_ignore_pattern.is_none()); assert!(rule.args_ignore_pattern.unwrap().as_str() == "^_"); @@ -569,7 +572,8 @@ mod tests { "varsIgnorePattern": "^_", } ]) - .into(); + .try_into() + .unwrap(); // option object provided, no default argsIgnorePattern assert!(rule.vars_ignore_pattern.unwrap().as_str() == "^_"); @@ -583,7 +587,8 @@ mod tests { "ignoreRestSiblings": true, } ]) - .into(); + .try_into() + .unwrap(); assert!(rule.ignore_rest_siblings); // an options object is provided, so no default pattern is set. assert!(rule.vars_ignore_pattern.is_none()); @@ -591,19 +596,22 @@ mod tests { #[test] fn test_options_from_null() { - let opts = NoUnusedVarsOptions::from(json!(null)); - let default = NoUnusedVarsOptions::default(); - assert_eq!(opts.vars, default.vars); - assert!(default.vars_ignore_pattern.unwrap().as_str() == "^_"); + let option_values = [json!(null), json!([null])]; + for json in option_values { + let opts = NoUnusedVarsOptions::try_from(json).unwrap(); + let default = NoUnusedVarsOptions::default(); + assert_eq!(opts.vars, default.vars); + assert!(default.vars_ignore_pattern.unwrap().as_str() == "^_"); - assert_eq!(opts.args, default.args); - assert!(default.args_ignore_pattern.unwrap().as_str() == "^_"); + assert_eq!(opts.args, default.args); + assert!(default.args_ignore_pattern.unwrap().as_str() == "^_"); - assert_eq!(opts.caught_errors, default.caught_errors); - assert!(opts.caught_errors_ignore_pattern.is_none()); - assert!(default.caught_errors_ignore_pattern.is_none()); + assert_eq!(opts.caught_errors, default.caught_errors); + assert!(opts.caught_errors_ignore_pattern.is_none()); + assert!(default.caught_errors_ignore_pattern.is_none()); - assert_eq!(opts.ignore_rest_siblings, default.ignore_rest_siblings); + assert_eq!(opts.ignore_rest_siblings, default.ignore_rest_siblings); + } } #[test] @@ -612,4 +620,21 @@ mod tests { parse_unicode_rule(Some(&pat), "varsIgnorePattern") .expect("json strings should get parsed into a regex"); } + + #[test] + fn test_invalid() { + let invalid_options: Vec = vec![ + json!(["invalid"]), + json!([1]), + json!([true]), + json!([{ "caughtErrors": 0 }]), + json!([{ "caughtErrors": "invalid" }]), + json!([{ "vars": "invalid" }]), + json!([{ "args": "invalid" }]), + ]; + for options in invalid_options { + let result: Result = options.try_into(); + assert!(result.is_err()); + } + } } 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 ce4eac741f2d2..87b96e45fb059 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 @@ -105,7 +105,12 @@ impl<'s, 'a> Symbol<'s, 'a> { self.nodes().iter_parents(self.declaration_id()) } - pub fn iter_relevant_parents( + #[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, ) -> impl Iterator> + Clone + '_ { @@ -131,7 +136,15 @@ impl<'s, 'a> Symbol<'s, 'a> { #[inline] const fn is_relevant_kind(kind: AstKind<'a>) -> bool { - !matches!(kind, AstKind::ParenthesizedExpression(_)) + !matches!( + kind, + AstKind::ParenthesizedExpression(_) + | AstKind::TSAsExpression(_) + | AstKind::TSSatisfiesExpression(_) + | AstKind::TSInstantiationExpression(_) + | AstKind::TSNonNullExpression(_) + | AstKind::TSTypeAssertion(_) + ) } /// @@ -182,7 +195,16 @@ impl<'s, 'a> Symbol<'s, 'a> { AstKind::ModuleDeclaration(module) => { return module.is_export(); } - AstKind::VariableDeclaration(_) => { + AstKind::ExportDefaultDeclaration(_) => { + return true; + } + AstKind::VariableDeclaration(_) + | AstKind::ExpressionArrayElement(_) + | AstKind::ArrayExpressionElement(_) + | AstKind::ArrayExpression(_) + | AstKind::ParenthesizedExpression(_) + | AstKind::TSAsExpression(_) + | AstKind::TSSatisfiesExpression(_) => { continue; } _ => { 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 61302856e6dca..60fd30c20453a 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 @@ -13,8 +13,6 @@ use crate::{tester::Tester, RuleMeta as _}; #[test] fn fixme() { let pass = vec![ - // ESLint says this one should pass, but I disagree. foox could be - // safely removed here. ("function foo(cb) { cb = function(a) { return cb(1 + a); }(); } foo();", None), ("function foo(cb) { cb = (0, function(a) { cb(1 + a); }); } foo();", None), ( @@ -24,8 +22,9 @@ fn fixme() { ), // { "ecmaVersion": 2015 }, ]; let fail = vec![]; - Tester::new(NoUnusedVars::NAME, pass, fail).test(); + Tester::new(NoUnusedVars::NAME, pass, fail).intentionally_allow_no_fix_tests().test(); } + #[test] fn test() { let pass = vec![ @@ -33,8 +32,8 @@ fn test() { "var foo = 5; label: while (true) { - console.log(foo); - break label; + console.log(foo); + break label; }", None, ), @@ -42,21 +41,19 @@ fn test() { "var foo = 5; while (true) { - console.log(foo); - break; + console.log(foo); + break; }", None, ), ( - "for (let prop in box) { - box[prop] = parseInt(box[prop]); - }", + "for (let prop in box) { box[prop] = parseInt(box[prop]); }", None, ), // { "ecmaVersion": 6 }, ( "var box = {a: 2}; - for (var prop in box) { - box[prop] = parseInt(box[prop]); + for (var prop in box) { + box[prop] = parseInt(box[prop]); }", None, ), @@ -222,44 +219,44 @@ fn test() { ), // { "ecmaVersion": 6 }, ( " - let _a, b; - foo.forEach(item => { - [_a, b] = item; - doSomething(b); - }); - ", + let _a, b; + foo.forEach(item => { + [_a, b] = item; + doSomething(b); + }); + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 6 }, ( " - // doesn't report _x - let _x, y; - _x = 1; - [_x, y] = foo; - y; + // doesn't report _x + let _x, y; + _x = 1; + [_x, y] = foo; + y; - // doesn't report _a - let _a, b; - [_a, b] = foo; - _a = 1; - b; - ", + // doesn't report _a + let _a, b; + [_a, b] = foo; + _a = 1; + b; + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 2018 }, ( " - // doesn't report _x - let _x, y; - _x = 1; - [_x, y] = foo; - y; - - // doesn't report _a - let _a, b; - _a = 1; - ({_a, ...b } = foo); - b; - ", + // doesn't report _x + let _x, y; + _x = 1; + [_x, y] = foo; + y; + + // doesn't report _a + let _a, b; + _a = 1; + ({_a, ...b } = foo); + b; + ", Some( serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "ignoreRestSiblings": true }]), ), @@ -507,18 +504,8 @@ fn test() { Some(serde_json::json!([{}])), ), (r#"import x from "y";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, - ( - "export function fn2({ x, y }) { - console.log(x); - };", - None, - ), // { "ecmaVersion": 6, "sourceType": "module" }, - ( - "export function fn2( x, y ) { - console.log(x); - };", - None, - ), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export function fn2({ x, y }) { console.log(x); };", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export function fn2( x, y ) { console.log(x); };", None), // { "ecmaVersion": 6, "sourceType": "module" }, ("/*exported max*/ var max = 1, min = {min: 1}", None), ("/*exported x*/ var { x, y } = z", None), // { "ecmaVersion": 6 }, ("var _a; var b;", Some(serde_json::json!([{ "vars": "all", "varsIgnorePattern": "^_" }]))), @@ -544,56 +531,56 @@ fn test() { ), // { "ecmaVersion": 6 }, ( " - const array = ['a', 'b', 'c']; - const [a, _b, c] = array; - const newArray = [a, c]; - ", + const array = ['a', 'b', 'c']; + const [a, _b, c] = array; + const newArray = [a, c]; + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 2020 }, ( " - const array = ['a', 'b', 'c', 'd', 'e']; - const [a, _b, c] = array; - ", + const array = ['a', 'b', 'c', 'd', 'e']; + const [a, _b, c] = array; + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 2020 }, ( " - const array = ['a', 'b', 'c']; - const [a, _b, c] = array; - const fooArray = ['foo']; - const barArray = ['bar']; - const ignoreArray = ['ignore']; - ", + const array = ['a', 'b', 'c']; + const [a, _b, c] = array; + const fooArray = ['foo']; + const barArray = ['bar']; + const ignoreArray = ['ignore']; + ", Some( serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "varsIgnorePattern": "ignore" }]), ), ), // { "ecmaVersion": 2020 }, ( " - const array = [obj]; - const [{_a, foo}] = array; - console.log(foo); - ", + const array = [obj]; + const [{_a, foo}] = array; + console.log(foo); + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 2020 }, ( " - function foo([{_a, bar}]) { - bar; - } - foo(); - ", + function foo([{_a, bar}]) { + bar; + } + foo(); + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 2020 }, ( " - let _a, b; - - foo.forEach(item => { - [a, b] = item; - }); - ", + let _a, b; + + foo.forEach(item => { + [a, b] = item; + }); + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), ), // { "ecmaVersion": 2020 }, ("(function(obj) { var name; for ( name in obj ) { i(); return; } })({});", None), @@ -721,7 +708,7 @@ fn test() { ( "try{}catch(err){};", Some( - serde_json::json!([ { "vars": "all", "args": "all", "caughtErrors": "all", "argsIgnorePattern": "^er" } ]), + serde_json::json!([{ "vars": "all", "args": "all", "caughtErrors": "all","argsIgnorePattern": "^er" }]), ), ), ("var a = 0; a = a + 1;", None), @@ -800,9 +787,9 @@ fn test() { ("let x = 0; 0, x = x+1;", None), // { "ecmaVersion": 2020 }, ("let x = 0; x = x+1, 0;", None), // { "ecmaVersion": 2020 }, // https://github.com/oxc-project/oxc/issues/4437 - // ("let x = 0; foo = ((0, x = x + 1), 0);", None), // { "ecmaVersion": 2020 }, - // ("let x = 0; foo = (x = x+1, 0);", None), // { "ecmaVersion": 2020 }, - ("let x = 0; 0, (1, x=x+1);", None), // { "ecmaVersion": 2020 }, + ("let x = 0; foo = ((0, x = x + 1), 0);", None), // { "ecmaVersion": 2020 }, + ("let x = 0; foo = (x = x+1, 0);", None), // { "ecmaVersion": 2020 }, + ("let x = 0; 0, (1, x=x+1);", None), // { "ecmaVersion": 2020 }, ("(function ({ a, b }, { c } ) { return b; })();", None), // { "ecmaVersion": 2015 }, ("(function ([ a ], b ) { return b; })();", None), // { "ecmaVersion": 2015 }, ("(function ([ a ], [ b, c ] ) { return b; })();", None), // { "ecmaVersion": 2015 }, @@ -833,41 +820,38 @@ fn test() { // ), // { "ecmaVersion": 2015 }, ( "let a = 'a'; - a = 10; - function foo(){ - a = 11; - a = () => { - a = 13 - } - }", + a = 10; + function foo(){ + a = 11; + a = () => { + a = 13 + } + }", None, ), // { "ecmaVersion": 2020 }, ( "let foo; - init(); - foo = foo + 2; - function init() { - foo = 1; - }", + init(); + foo = foo + 2; + function init() { + foo = 1; + }", None, ), // { "ecmaVersion": 2020 }, ( "function foo(n) { - if (n < 2) return 1; - return n * foo(n - 1); - }", + if (n < 2) return 1; + return n * foo(n - 1); + }", None, ), // { "ecmaVersion": 2020 }, ( "let c = 'c' c = 10 function foo1() { - c = 11 - c = () => { - c = 13 - } + c = 11 + c = () => { c = 13 } } - c = foo1", None, ), // { "ecmaVersion": 2020 }, @@ -971,7 +955,7 @@ fn test() { } ", Some( - serde_json::json!([{ "caughtErrorsIgnorePattern": "ignored", "varsIgnorePattern": "_" }]), + serde_json::json!([{ "caughtErrorsIgnorePattern": "ignored", "varsIgnorePattern": "_" }]), ), ), ( @@ -986,11 +970,12 @@ fn test() { " _ => { _ = _ + 1 }; ", - Some( - serde_json::json!([{ "argsIgnorePattern": "ignored", "varsIgnorePattern": "_" }]), - ), + Some(serde_json::json!([{ "argsIgnorePattern": "ignored", "varsIgnorePattern": "_" }])), ), // { "ecmaVersion": 2015 } ]; - Tester::new(NoUnusedVars::NAME, pass, fail).with_snapshot_suffix("eslint").test_and_snapshot(); + Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() + .with_snapshot_suffix("eslint") + .test_and_snapshot(); } 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 332b2edd5718d..1c460c48fe9ad 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 @@ -5,6 +5,14 @@ use serde_json::json; use super::NoUnusedVars; use crate::{tester::Tester, FixKind, RuleMeta as _}; +// uncomment to only run a single test. useful for step-through debugging. +// #[test] +// fn test_debug() { +// let pass: Vec<&str> = vec![]; +// let fail = vec![]; +// Tester::new(NoUnusedVars::NAME, pass, fail).intentionally_allow_no_fix_tests().test(); +// } + #[test] fn test_vars_simple() { let pass = vec![ @@ -54,6 +62,9 @@ fn test_vars_simple() { ", None, ), + ("console.log(function a() {} ? b : c)", None), + ("console.log(a ? function b() {} : c)", None), + ("console.log(a ? b : function c() {})", None), ]; let fail = vec![ ("let a = 1", None), @@ -118,6 +129,12 @@ fn test_vars_simple() { ), // vars with references get renamed ("let x = 1; x = 2;", "let _x = 1; _x = 2;", None, FixKind::DangerousFix), + ( + "let a = 1; a = 2; a = 3;", + "let _a = 1; _a = 2; _a = 3;", + Some(json!([{ "varsIgnorePattern": "^_" }])), + FixKind::DangerousFix, + ), ( "let x = 1; x = 2;", "let x = 1; x = 2;", @@ -126,8 +143,6 @@ fn test_vars_simple() { ), // type annotations do not get clobbered ("let x: number = 1; x = 2;", "let _x: number = 1; _x = 2;", None, FixKind::DangerousFix), - ("const { a } = obj;", "", None, FixKind::DangerousSuggestion), - ("let [f,\u{a0}a]=p", "let [,a]=p", None, FixKind::DangerousSuggestion), ]; Tester::new(NoUnusedVars::NAME, pass, fail) @@ -161,6 +176,7 @@ fn test_vars_self_use() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-vars-self-use") .test_and_snapshot(); } @@ -201,6 +217,7 @@ fn test_vars_discarded_reads() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-vars-discarded-read") .test_and_snapshot(); } @@ -288,6 +305,7 @@ fn test_vars_reassignment() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-vars-reassignment") .test_and_snapshot(); } @@ -352,6 +370,7 @@ fn test_vars_destructure() { None, FixKind::DangerousSuggestion, ), + ("let [f,\u{a0}a]=p", "let [,a]=p", None, FixKind::DangerousSuggestion), ( "const [a, b, c, d, e] = arr; f(a, e)", "const [a, ,,,e] = arr; f(a, e)", @@ -379,13 +398,6 @@ fn test_vars_destructure() { ), // TODO: destructures in VariableDeclarations with more than one declarator (r#"const l="",{e}=r"#, r"const {e}=r", None, FixKind::All), - // renaming - // ( - // "let a = 1; a = 2;", - // "let _a = 1; _a = 2;", - // Some(json!([{ "varsIgnorePattern": "^_" }])), - // FixKind::DangerousSuggestion, - // ), ]; Tester::new(NoUnusedVars::NAME, pass, fail) @@ -397,17 +409,35 @@ fn test_vars_destructure() { #[test] fn test_vars_catch() { let pass = vec![ - // lb ("try {} catch (e) { throw e }", None), ("try {} catch (e) { }", Some(json!([{ "caughtErrors": "none" }]))), ("try {} catch { }", None), + ("try {} catch(_) { }", Some(json!([{ "caughtErrorsIgnorePattern": "^_" }]))), + ( + "try {} catch(_) { }", + Some(json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^_" }])), + ), + ( + "try {} catch(_e) { }", + Some(json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^_" }])), + ), ]; + let fail = vec![ - // lb ("try {} catch (e) { }", Some(json!([{ "caughtErrors": "all" }]))), + ("try {} catch(_) { }", None), + ( + "try {} catch(_) { }", + Some(json!([{ "caughtErrors": "all", "varsIgnorePattern": "^_" }])), + ), + ( + "try {} catch(foo) { }", + Some(json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^ignored" }])), + ), ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-vars-catch") .test_and_snapshot(); } @@ -419,6 +449,7 @@ fn test_vars_using() { let fail = vec![("using a = 1;", None)]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-vars-using") .test_and_snapshot(); } @@ -598,6 +629,60 @@ fn test_functions() { .test_and_snapshot(); } +#[test] +fn test_self_call() { + let pass = vec![ + "const _thunk = (function createThunk(count) { + if (count === 0) return () => count + return () => createThunk(count - 1)() + })()", + ]; + + let fail = vec![ + // Functions that call themselves are considered unused, even if that + // call happens within an inner function. + "function foo() { return function bar() { return foo() } }", + // Classes that construct themselves are considered unused + "class Foo { + static createFoo() { + return new Foo(); + } + }", + "class Foo { + static createFoo(): Foo { + return new Foo(); + } + }", + "class Point { + public x: number; + public y: number; + public add(other): Point { + const p = new Point(); + p.x = this.x + (other as Point).x; + p.y = this.y + (other as Point).y; + return p; + } + } + ", + // FIXME + // "class Foo { + // inner: any + // public foo(): Foo { + // if(this.inner?.constructor.name === Foo.name) { + // return this.inner; + // } else { + // return new Foo(); + // } + // } + // }", + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() + .with_snapshot_suffix("oxc-self-call") + .test_and_snapshot(); +} + #[test] fn test_imports() { let pass = vec![ @@ -684,6 +769,38 @@ fn test_imports() { .test_and_snapshot(); } +#[test] +fn test_used_declarations() { + let pass = vec![ + // function declarations passed as arguments, used in assignments, etc. are used, even if they are + // first put into an intermediate (e.g. an object or array) + "arr.reduce(function reducer (acc, el) { return acc + el }, 0)", + "console.log({ foo: function foo() {} })", + "console.log({ foo: function foo() {} as unknown as Function })", + "test.each([ function foo() {} ])('test some function', (fn) => { expect(fn(1)).toBe(1) })", + "export default { foo() {} }", + "const arr = [function foo() {}, function bar() {}]; console.log(arr[0]())", + "const foo = function foo() {}; console.log(foo())", + "const foo = function bar() {}; console.log(foo())", + // Class expressions behave similarly + "console.log([class Foo {}])", + "export default { foo: class Foo {} }", + "export const Foo = class Foo {}", + "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 {}]", + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() + .with_snapshot_suffix("oxc-used-declarations") + .test_and_snapshot(); +} + #[test] fn test_exports() { let pass = vec![ @@ -694,6 +811,15 @@ fn test_exports() { "export interface A {}", "export type A = string", "export enum E { }", + // default exports + "export default class Foo {}", + "export default [ class Foo {} ];", + "export default function foo() {}", + "export default { foo() {} };", + "export default { foo: function foo() {} };", + "export default { get foo() {} };", + "export default [ function foo() {} ];", + "export default (function foo() { return 1 })();", // "export enum E { A, B }", "const a = 1; export { a }", "const a = 1; export default a", @@ -706,7 +832,7 @@ fn test_exports() { let fail = vec!["import { a as b } from 'a'; export { a }"]; // these are mostly pass[] cases, so do not snapshot - Tester::new(NoUnusedVars::NAME, pass, fail).test(); + Tester::new(NoUnusedVars::NAME, pass, fail).intentionally_allow_no_fix_tests().test(); } #[test] @@ -752,7 +878,7 @@ fn test_react() { ", ]; - Tester::new(NoUnusedVars::NAME, pass, fail).test(); + Tester::new(NoUnusedVars::NAME, pass, fail).intentionally_allow_no_fix_tests().test(); } #[test] @@ -783,6 +909,7 @@ fn test_arguments() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-arguments") .test_and_snapshot(); } @@ -798,6 +925,7 @@ fn test_enums() { let fail = vec!["enum Foo { A }"]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-enums") .test_and_snapshot(); } @@ -863,6 +991,7 @@ fn test_classes() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-classes") .test_and_snapshot(); } @@ -915,6 +1044,7 @@ fn test_namespaces() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-namespaces") .test_and_snapshot(); } @@ -928,9 +1058,11 @@ fn test_type_aliases() { "type Foo = Foo", "type Foo = Array", "type Unbox = B extends Box ? Unbox : B", + "export type F = T extends infer R ? /* R not used */ string : never", ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-type-aliases") .test_and_snapshot(); } @@ -985,12 +1117,52 @@ fn test_type_references() { ]; let fail = vec![ + // Type aliases "type T = number; function foo(a: T): T { return a as T }; foo(1)", "type A = number; type B = A; console.log(3 as B<3>)", + "type T = { foo: T }", + "type T = { foo?: T | undefined }", + "type A = { foo: T extends Array ? A : T }", + "type T = { foo(): T }", + // Type references on class symbols within that classes' definition is + // not considered used + "class Foo { + private _inner: Foo | undefined; + }", + "class Foo { + _inner: any; + constructor(other: Foo); + constructor(somethingElse: any) { + this._inner = somethingElse; + } + }", + "class LinkedList { + #next?: LinkedList; + public append(other: LinkedList) { + this.#next = other; + } + }", + "class LinkedList { + #next?: LinkedList; + public nextUnchecked(): LinkedList { + return >this.#next!; + } + }", + // FIXME: ambient classes declared using `declare` are not bound by + // semantic's binder. + // https://github.com/oxc-project/oxc/blob/a9260cf6d1b83917c7a61b25cabd2d40858b0fff/crates/oxc_semantic/src/binder.rs#L105 + // "declare class LinkedList { + // next(): LinkedList | undefined; + // }" + + // Same is true for interfaces + "interface LinkedList { next: LinkedList | undefined }", ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("oxc-type-references") + .change_rule_path_extension("ts") .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs index bc3f15eb2eb68..46078aa089d47 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs @@ -1700,6 +1700,7 @@ fn test() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .change_rule_path_extension("ts") .with_snapshot_suffix("typescript-eslint") .test_and_snapshot(); @@ -1867,6 +1868,7 @@ fn test_tsx() { ]; Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() .with_snapshot_suffix("typescript-eslint-tsx") .test_and_snapshot(); } @@ -1906,5 +1908,8 @@ fn test_d_ts() { ]; let fail = vec![]; - Tester::new(NoUnusedVars::NAME, pass, fail).change_rule_path_extension("d.ts").test(); + Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() + .change_rule_path_extension("d.ts") + .test(); } 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 649bbf825c4cd..52aa4e656f7e9 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 @@ -60,8 +60,10 @@ impl<'s, 'a> Symbol<'s, 'a> { } #[inline] - const fn is_type_alias(&self) -> bool { - self.flags().contains(SymbolFlags::TypeAlias) + const fn could_have_type_reference_within_own_decl(&self) -> bool { + const TYPE_DECLS: SymbolFlags = + SymbolFlags::TypeAlias.union(SymbolFlags::Interface).union(SymbolFlags::Class); + self.flags().intersects(TYPE_DECLS) } /// Check if this [`Symbol`] has an [`Reference`]s that are considered a usage. @@ -69,7 +71,7 @@ impl<'s, 'a> Symbol<'s, 'a> { // 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(); - let do_type_self_usage_checks = self.is_type_alias(); + let do_type_self_usage_checks = self.could_have_type_reference_within_own_decl(); let do_self_call_check = self.is_maybe_callable(); let do_discarded_read_checks = self.is_definitely_reassignable_variable(); @@ -244,19 +246,19 @@ impl<'s, 'a> Symbol<'s, 'a> { /// type Foo = Array /// ``` fn is_type_self_usage(&self, reference: &Reference) -> bool { - for parent in self.iter_relevant_parents(reference.node_id()).map(AstNode::kind) { + for parent in self.iter_relevant_parents_of(reference.node_id()).map(AstNode::kind) { match parent { AstKind::TSTypeAliasDeclaration(decl) => { return self == &decl.id; } // definitely not within a type alias, we can be sure this isn't // a self-usage. Safe CPU cycles by breaking early. - AstKind::CallExpression(_) - | AstKind::BinaryExpression(_) - | AstKind::Function(_) - | AstKind::Class(_) - | AstKind::TSInterfaceDeclaration(_) - | AstKind::TSModuleDeclaration(_) + // NOTE: we cannot short-circuit on functions since they could + // be methods with annotations referencing the type they're in. + // e.g.: + // - `type Foo = { bar(): Foo }` + // - `class Foo { static factory(): Foo { return new Foo() } }` + AstKind::TSModuleDeclaration(_) | AstKind::VariableDeclaration(_) | AstKind::VariableDeclarator(_) | AstKind::ExportNamedDeclaration(_) @@ -266,6 +268,27 @@ impl<'s, 'a> Symbol<'s, 'a> { return false; } + AstKind::CallExpression(_) | AstKind::BinaryExpression(_) => { + // interfaces/type aliases cannot have value expressions + // within their declarations, so we know we're not in one. + // However, classes can. + if self.flags().is_class() { + continue; + } + return false; + } + + // `interface LinkedList { next?: LinkedList }` + AstKind::TSInterfaceDeclaration(iface) => { + return self.flags().is_interface() && self == &iface.id; + } + + // `class Foo { bar(): Foo }` + AstKind::Class(class) => { + return self.flags().is_class() + && class.id.as_ref().is_some_and(|id| self == id); + } + _ => continue, } } @@ -425,7 +448,7 @@ impl<'s, 'a> Symbol<'s, 'a> { /// Check if a [`AstNode`] is within a return statement or implicit return. fn is_in_return_statement(&self, node_id: NodeId) -> bool { - for parent in self.iter_relevant_parents(node_id).map(AstNode::kind) { + for parent in self.iter_relevant_parents_of(node_id).map(AstNode::kind) { match parent { AstKind::ReturnStatement(_) => return true, AstKind::ExpressionStatement(_) => continue, @@ -652,7 +675,7 @@ impl<'s, 'a> Symbol<'s, 'a> { /// 2. "relevant" nodes are non "transparent". For example, parenthesis are "transparent". #[inline] fn get_ref_relevant_node(&self, reference: &Reference) -> Option<&AstNode<'a>> { - self.iter_relevant_parents(reference.node_id()).next() + self.iter_relevant_parents_of(reference.node_id()).next() } /// Find the [`SymbolId`] for the nearest function declaration or expression @@ -662,7 +685,7 @@ impl<'s, 'a> Symbol<'s, 'a> { // name from the variable its assigned to. let mut needs_variable_identifier = false; - for parent in self.iter_relevant_parents(node_id) { + 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()); diff --git a/crates/oxc_linter/src/rules/eslint/no_useless_constructor.rs b/crates/oxc_linter/src/rules/eslint/no_useless_constructor.rs index 31f091792b2e4..f8f9796fe88f7 100644 --- a/crates/oxc_linter/src/rules/eslint/no_useless_constructor.rs +++ b/crates/oxc_linter/src/rules/eslint/no_useless_constructor.rs @@ -7,18 +7,34 @@ use oxc_ast::{ }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{GetSpan, Span}; use crate::{context::LintContext, rule::Rule, AstNode}; +/// ```js +/// class A { constructor(){} } +/// ``` fn no_empty_constructor(constructor_span: Span) -> OxcDiagnostic { OxcDiagnostic::warn("Empty constructors are unnecessary") .with_label(constructor_span) .with_help("Remove the constructor or add code to it.") } -fn no_redundant_super_call(constructor_span: Span) -> OxcDiagnostic { + +/// ```js +/// class A { } +/// class B extends A { +/// constructor() { +/// super(); +/// } +/// } +/// ``` +fn no_redundant_super_call(constructor_span: Span, super_span: Span) -> OxcDiagnostic { OxcDiagnostic::warn("Redundant super call in constructor") - .with_label(constructor_span).with_help("Constructors of subclasses invoke super() automatically if they are empty. Remove this constructor or add code to it.") + .with_labels([ + constructor_span.primary_label("This constructor is unnecessary,"), + super_span.label("because it only passes arguments through to the superclass"), + ]) + .with_help("Subclasses automatically use the constructor of their superclass, making this redundant.\nRemove this constructor or add code to it.") } #[derive(Debug, Default, Clone)] @@ -165,9 +181,10 @@ fn lint_redundant_super_call<'a>( && !is_overriding(params) && (is_spread_arguments(super_args) || is_passing_through(params, super_args)) { - ctx.diagnostic_with_fix(no_redundant_super_call(constructor.span), |fixer| { - fixer.delete_range(constructor.span) - }); + ctx.diagnostic_with_fix( + no_redundant_super_call(constructor.key.span(), super_call.span()), + |fixer| fixer.delete_range(constructor.span), + ); } } @@ -270,12 +287,58 @@ fn test() { "class A { constructor(readonly x: number) {} }", "class A { constructor(private readonly x: number) {} }", "class A extends B { constructor(override x: number) { super(x); } }", + " + class A { + protected foo: number | undefined; + constructor(foo?: number) { + this.foo = foo; + } + } + class B extends A { + protected foo: number; + constructor(foo: number = 0) { + super(foo); + } + } + ", + " + class A { + protected foo: number | undefined; + constructor(foo?: number) { + this.foo = foo; + } + } + class B extends A { + constructor(foo?: number) { + super(foo ?? 0); + } + } + ", + // TODO: type aware linting :( + // " + // class A { + // protected foo: string; + // constructor(foo: string) { + // this.foo = foo; + // } + // } + // class B extends A { + // constructor(foo: 'a' | 'b') { + // super(foo); + // } + // } + // ", ]; let fail = vec![ "class A { constructor(){} }", "class A { 'constructor'(){} }", "class A extends B { constructor() { super(); } }", + "class A extends B { + constructor() { + super(); + } +}", "class A extends B { constructor(foo){ super(foo); } }", "class A extends B { constructor(foo, bar){ super(foo, bar); } }", "class A extends B { constructor(...args){ super(...args); } }", diff --git a/crates/oxc_linter/src/rules/eslint/sort_imports.rs b/crates/oxc_linter/src/rules/eslint/sort_imports.rs index b5473af98bb80..7e8b4408f19bb 100644 --- a/crates/oxc_linter/src/rules/eslint/sort_imports.rs +++ b/crates/oxc_linter/src/rules/eslint/sort_imports.rs @@ -302,7 +302,7 @@ impl SortImports { // import { /* comment */ a, b, c, d } from 'foo.js' // ``` // I use ImportStatement's span to check if there are comments between the specifiers. - let is_fixable = !ctx.semantic().trivias().has_comments_between(current.span); + let is_fixable = !ctx.semantic().has_comments_between(current.span); if is_fixable { // Safe to index because we know that `specifiers` is at least 2 element long diff --git a/crates/oxc_linter/src/rules/eslint/sort_keys.rs b/crates/oxc_linter/src/rules/eslint/sort_keys.rs index a0fad92a0e5c6..76502d3198d3a 100644 --- a/crates/oxc_linter/src/rules/eslint/sort_keys.rs +++ b/crates/oxc_linter/src/rules/eslint/sort_keys.rs @@ -84,7 +84,7 @@ declare_oxc_lint!( /// }; /// ``` SortKeys, - pedantic, + style, pending ); diff --git a/crates/oxc_linter/src/rules/eslint/valid_typeof.rs b/crates/oxc_linter/src/rules/eslint/valid_typeof.rs index b1e768a2b8165..64957d6c994eb 100644 --- a/crates/oxc_linter/src/rules/eslint/valid_typeof.rs +++ b/crates/oxc_linter/src/rules/eslint/valid_typeof.rs @@ -222,5 +222,7 @@ fn test() { ), ]; - Tester::new(ValidTypeof::NAME, pass, fail).test_and_snapshot(); + let fix = vec![("typeof foo === undefined", r#"typeof foo === "undefined""#)]; + + Tester::new(ValidTypeof::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/jest/expect_expect.rs b/crates/oxc_linter/src/rules/jest/expect_expect.rs index f98d35b9a51b1..3721630b1b7e5 100644 --- a/crates/oxc_linter/src/rules/jest/expect_expect.rs +++ b/crates/oxc_linter/src/rules/jest/expect_expect.rs @@ -5,7 +5,7 @@ use oxc_ast::{ }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use regex::Regex; use rustc_hash::FxHashSet; @@ -30,8 +30,8 @@ pub struct ExpectExpect(Box); #[derive(Debug, Clone)] pub struct ExpectExpectConfig { - assert_function_names: Vec, - additional_test_block_functions: Vec, + assert_function_names: Vec, + additional_test_block_functions: Vec, } impl std::ops::Deref for ExpectExpect { @@ -45,7 +45,7 @@ impl std::ops::Deref for ExpectExpect { impl Default for ExpectExpectConfig { fn default() -> Self { Self { - assert_function_names: vec![String::from("expect")], + assert_function_names: vec!["expect".into()], additional_test_block_functions: vec![], } } @@ -85,7 +85,7 @@ declare_oxc_lint!( impl Rule for ExpectExpect { fn from_configuration(value: serde_json::Value) -> Self { - let default_assert_function_names = vec![String::from("expect")]; + let default_assert_function_names = vec!["expect".into()]; let config = value.get(0); let assert_function_names = config @@ -98,9 +98,7 @@ impl Rule for ExpectExpect { let additional_test_block_functions = config .and_then(|config| config.get("additionalTestBlockFunctions")) .and_then(serde_json::Value::as_array) - .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() - }) + .map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()) .unwrap_or_default(); Self(Box::new(ExpectExpectConfig { @@ -158,7 +156,7 @@ fn run<'a>( fn check_arguments<'a>( call_expr: &'a CallExpression<'a>, - assert_function_names: &[String], + assert_function_names: &[CompactStr], visited: &mut FxHashSet, ctx: &LintContext<'a>, ) -> bool { @@ -174,7 +172,7 @@ fn check_arguments<'a>( fn check_assert_function_used<'a>( expr: &'a Expression<'a>, - assert_function_names: &[String], + assert_function_names: &[CompactStr], visited: &mut FxHashSet, ctx: &LintContext<'a>, ) -> bool { @@ -245,7 +243,7 @@ fn check_assert_function_used<'a>( fn check_statements<'a>( statements: &'a oxc_allocator::Vec>, - assert_function_names: &[String], + assert_function_names: &[CompactStr], visited: &mut FxHashSet, ctx: &LintContext<'a>, ) -> bool { @@ -263,11 +261,11 @@ fn check_statements<'a>( } /// Checks if node names returned by getNodeName matches any of the given star patterns -fn matches_assert_function_name(name: &str, patterns: &[String]) -> bool { +fn matches_assert_function_name(name: &str, patterns: &[CompactStr]) -> bool { patterns.iter().any(|pattern| Regex::new(pattern).unwrap().is_match(name)) } -fn convert_pattern(pattern: &str) -> String { +fn convert_pattern(pattern: &str) -> CompactStr { // Pre-process pattern, e.g. // request.*.expect -> request.[a-z\\d]*.expect // request.**.expect -> request.[a-z\\d\\.]*.expect @@ -276,16 +274,16 @@ fn convert_pattern(pattern: &str) -> String { .split('.') .map(|p| { if p == "**" { - String::from("[a-z\\d\\.]*") + CompactStr::from("[a-z\\d\\.]*") } else { - p.cow_replace('*', "[a-z\\d]*").into_owned() + p.cow_replace('*', "[a-z\\d]*").into() } }) .collect::>() .join("\\."); // 'a.b.c' -> /^a\.b\.c(\.|$)/iu - format!("(?ui)^{pattern}(\\.|$)") + format!("(?ui)^{pattern}(\\.|$)").into() } #[test] diff --git a/crates/oxc_linter/src/rules/jest/no_commented_out_tests.rs b/crates/oxc_linter/src/rules/jest/no_commented_out_tests.rs index d4552c1ea7dee..089b017666a4e 100644 --- a/crates/oxc_linter/src/rules/jest/no_commented_out_tests.rs +++ b/crates/oxc_linter/src/rules/jest/no_commented_out_tests.rs @@ -59,9 +59,9 @@ impl Rule for NoCommentedOutTests { static ref RE: Regex = Regex::new(r#"(?mu)^\s*[xf]?(test|it|describe)(\.\w+|\[['"]\w+['"]\])?\s*\("#).unwrap(); } - let comments = ctx.semantic().trivias().comments(); + let comments = ctx.semantic().comments(); let source_text = ctx.semantic().source_text(); - let commented_tests = comments.filter_map(|comment| { + let commented_tests = comments.iter().filter_map(|comment| { let text = comment.span.source_text(source_text); if RE.is_match(text) { Some(comment.span) diff --git a/crates/oxc_linter/src/rules/jest/no_duplicate_hooks.rs b/crates/oxc_linter/src/rules/jest/no_duplicate_hooks.rs index 927f9d2f280a0..b576cda13be36 100644 --- a/crates/oxc_linter/src/rules/jest/no_duplicate_hooks.rs +++ b/crates/oxc_linter/src/rules/jest/no_duplicate_hooks.rs @@ -142,12 +142,12 @@ impl NoDuplicateHooks { } let hook_name = jest_fn_call.name.to_string(); - let parent_ast_node_id = + let parent_node_id = match ctx.nodes().ancestors(node.id()).find(|n| hook_contexts.contains_key(n)) { Some(n) => Some(n), _ => Some(root_node_id), }; - let Some(parent_id) = parent_ast_node_id else { + let Some(parent_id) = parent_node_id else { return; }; diff --git a/crates/oxc_linter/src/rules/jest/no_hooks.rs b/crates/oxc_linter/src/rules/jest/no_hooks.rs index 8517f64351994..b0ef6c3cae2fa 100644 --- a/crates/oxc_linter/src/rules/jest/no_hooks.rs +++ b/crates/oxc_linter/src/rules/jest/no_hooks.rs @@ -1,7 +1,7 @@ use oxc_ast::{ast::Expression, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use crate::{ context::LintContext, @@ -21,7 +21,7 @@ pub struct NoHooks(Box); #[derive(Debug, Default, Clone)] pub struct NoHooksConfig { - allow: Vec, + allow: Vec, } impl std::ops::Deref for NoHooks { @@ -90,9 +90,7 @@ impl Rule for NoHooks { .get(0) .and_then(|config| config.get("allow")) .and_then(serde_json::Value::as_array) - .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() - }) + .map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()) .unwrap_or_default(); Self(Box::new(NoHooksConfig { allow })) @@ -121,7 +119,8 @@ impl NoHooks { } if let Expression::Identifier(ident) = &call_expr.callee { - if !self.allow.contains(&ident.name.to_string()) { + let name = CompactStr::from(ident.name.as_str()); + if !self.allow.contains(&name) { ctx.diagnostic(unexpected_hook_diagonsitc(call_expr.callee.span())); } } diff --git a/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs b/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs index 8ea5f851d1434..60074a4471133 100644 --- a/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs +++ b/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs @@ -6,7 +6,7 @@ use oxc_ast::{ }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use regex::Regex; use rustc_hash::FxHashMap; @@ -38,7 +38,7 @@ pub struct NoLargeSnapshots(Box); pub struct NoLargeSnapshotsConfig { pub max_size: usize, pub inline_max_size: usize, - pub allowed_snapshots: FxHashMap>, + pub allowed_snapshots: FxHashMap>, } impl Deref for NoLargeSnapshots { @@ -281,21 +281,19 @@ impl NoLargeSnapshots { #[allow(clippy::unnecessary_wraps)] pub fn compile_allowed_snapshots( matchers: &serde_json::Map, - ) -> Option>> { + ) -> Option>> { Some( matchers .iter() .map(|(key, value)| { let serde_json::Value::Array(configs) = value else { - return (String::from(key), vec![]); + return (CompactStr::from(key.as_str()), vec![]); }; - let configs = configs - .iter() - .filter_map(|c| c.as_str().map(std::string::ToString::to_string)) - .collect(); + let configs = + configs.iter().filter_map(|c| c.as_str().map(CompactStr::from)).collect(); - (String::from(key), configs) + (CompactStr::from(key.as_str()), configs) }) .collect(), ) diff --git a/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs b/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs index 01b682b744f54..b9eebaf72a720 100644 --- a/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs +++ b/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs @@ -2,7 +2,7 @@ use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_semantic::NodeId; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use rustc_hash::FxHashMap; use crate::{ @@ -28,7 +28,7 @@ pub struct NoStandaloneExpect(Box); #[derive(Debug, Default, Clone)] pub struct NoStandaloneExpectConfig { - additional_test_block_functions: Vec, + additional_test_block_functions: Vec, } impl std::ops::Deref for NoStandaloneExpect { @@ -65,9 +65,7 @@ impl Rule for NoStandaloneExpect { .get(0) .and_then(|v| v.get("additionalTestBlockFunctions")) .and_then(serde_json::Value::as_array) - .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() - }) + .map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()) .unwrap_or_default(); Self(Box::new(NoStandaloneExpectConfig { additional_test_block_functions })) @@ -128,7 +126,7 @@ impl NoStandaloneExpect { fn is_correct_place_to_call_expect<'a>( node: &AstNode<'a>, - additional_test_block_functions: &[String], + additional_test_block_functions: &[CompactStr], id_nodes_mapping: &FxHashMap>, ctx: &LintContext<'a>, ) -> Option<()> { @@ -196,7 +194,7 @@ fn is_correct_place_to_call_expect<'a>( fn is_var_declarator_or_test_block<'a>( node: &AstNode<'a>, - additional_test_block_functions: &[String], + additional_test_block_functions: &[CompactStr], id_nodes_mapping: &FxHashMap>, ctx: &LintContext<'a>, ) -> bool { @@ -213,7 +211,7 @@ fn is_var_declarator_or_test_block<'a>( } let node_name = get_node_name(&call_expr.callee); - if additional_test_block_functions.iter().any(|fn_name| &node_name == fn_name) { + if additional_test_block_functions.iter().any(|fn_name| node_name == fn_name) { return true; } } diff --git a/crates/oxc_linter/src/rules/jest/no_test_prefixes.rs b/crates/oxc_linter/src/rules/jest/no_test_prefixes.rs index dcf49906ae113..9b13d8d819444 100644 --- a/crates/oxc_linter/src/rules/jest/no_test_prefixes.rs +++ b/crates/oxc_linter/src/rules/jest/no_test_prefixes.rs @@ -204,8 +204,19 @@ fn test() { pass.extend(pass_vitest); fail.extend(fail_vitest); + let fix = vec![ + ("xdescribe('foo', () => {})", "describe.skip('foo', () => {})"), + ("fdescribe('foo', () => {})", "describe.only('foo', () => {})"), + ("xtest('foo', () => {})", "test.skip('foo', () => {})"), + // NOTE(@DonIsaac): is this intentional? + // ("ftest('foo', () => {})", "test.only('foo', () => {})"), + ("xit('foo', () => {})", "it.skip('foo', () => {})"), + ("fit('foo', () => {})", "it.only('foo', () => {})"), + ]; + Tester::new(NoTestPrefixes::NAME, pass, fail) .with_jest_plugin(true) .with_vitest_plugin(true) + .expect_fix(fix) .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/jest/prefer_comparison_matcher.rs b/crates/oxc_linter/src/rules/jest/prefer_comparison_matcher.rs index 2d7b6fa90d753..844aef74f8c83 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_comparison_matcher.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_comparison_matcher.rs @@ -183,10 +183,10 @@ impl PreferComparisonMatcher { ) -> String { let mut content = fixer.codegen(); content.print_str(local_name); - content.print_char(b'('); + content.print_ascii_byte(b'('); content.print_expression(&binary_expr.left); content.print_str(call_span_end); - content.print_char(b'.'); + content.print_ascii_byte(b'.'); for modifier in modifiers { let Some(modifier_name) = modifier.name() else { continue; @@ -194,11 +194,11 @@ impl PreferComparisonMatcher { if !modifier_name.eq("not") { content.print_str(&modifier_name); - content.print_char(b'.'); + content.print_ascii_byte(b'.'); } } content.print_str(prefer_matcher_name); - content.print_char(b'('); + content.print_ascii_byte(b'('); content.print_expression(&binary_expr.right); content.print_str(arg_span_end); content.into_source_text() diff --git a/crates/oxc_linter/src/rules/jest/prefer_expect_resolves.rs b/crates/oxc_linter/src/rules/jest/prefer_expect_resolves.rs index 5a72f528e9119..23720301ec728 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_expect_resolves.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_expect_resolves.rs @@ -137,9 +137,9 @@ impl PreferExpectResolves { ); formatter.print_str("await"); - formatter.print_char(b' '); + formatter.print_ascii_byte(b' '); formatter.print_str(&jest_expect_fn_call.local); - formatter.print_char(b'('); + formatter.print_ascii_byte(b'('); formatter.print_str(fixer.source_range(arg_span)); formatter.print_str(".resolves"); fixer.replace(call_expr.span, formatter) diff --git a/crates/oxc_linter/src/rules/jest/prefer_lowercase_title.rs b/crates/oxc_linter/src/rules/jest/prefer_lowercase_title.rs index ebaeb00ea4616..fe60c03866188 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_lowercase_title.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_lowercase_title.rs @@ -1,7 +1,7 @@ use oxc_ast::{ast::Argument, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use crate::{ context::LintContext, @@ -20,8 +20,8 @@ fn unexpected_lowercase(x0: &str, span1: Span) -> OxcDiagnostic { #[derive(Debug, Default, Clone)] pub struct PreferLowercaseTitleConfig { - allowed_prefixes: Vec, - ignore: Vec, + allowed_prefixes: Vec, + ignore: Vec, ignore_top_level_describe: bool, } @@ -142,16 +142,12 @@ impl Rule for PreferLowercaseTitle { let ignore = obj .and_then(|config| config.get("ignore")) .and_then(serde_json::Value::as_array) - .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() - }) + .map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()) .unwrap_or_default(); let allowed_prefixes = obj .and_then(|config| config.get("allowedPrefixes")) .and_then(serde_json::Value::as_array) - .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() - }) + .map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()) .unwrap_or_default(); Self(Box::new(PreferLowercaseTitleConfig { @@ -209,7 +205,7 @@ impl PreferLowercaseTitle { } } - fn populate_ignores(ignore: &[String]) -> Vec<&str> { + fn populate_ignores(ignore: &[CompactStr]) -> Vec<&str> { let mut ignores: Vec<&str> = vec![]; let test_case_name = ["fit", "it", "xit", "test", "xtest"]; let describe_alias = ["describe", "fdescribe", "xdescribe"]; @@ -232,7 +228,8 @@ impl PreferLowercaseTitle { } fn lint_string<'a>(&self, ctx: &LintContext<'a>, literal: &'a str, span: Span) { - if literal.is_empty() || self.allowed_prefixes.iter().any(|name| literal.starts_with(name)) + if literal.is_empty() + || self.allowed_prefixes.iter().any(|name| literal.starts_with(name.as_str())) { return; } diff --git a/crates/oxc_linter/src/rules/jest/prefer_mock_promise_shorthand.rs b/crates/oxc_linter/src/rules/jest/prefer_mock_promise_shorthand.rs index 94036fbca6e37..153b3d7e784da 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_mock_promise_shorthand.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_mock_promise_shorthand.rs @@ -170,7 +170,7 @@ impl PreferMockPromiseShorthand { ) -> String { let mut content = fixer.codegen(); content.print_str(prefer_name); - content.print_char(b'('); + content.print_ascii_byte(b'('); if call_expr.arguments.is_empty() { content.print_str("undefined"); } else { diff --git a/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs b/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs index eb0b2aeaa2b66..756d238a32eb7 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs @@ -145,21 +145,21 @@ impl PreferSpyOn { match left_assign { MemberExpression::ComputedMemberExpression(cmp_mem_expr) => { formatter.print_expression(&cmp_mem_expr.object); - formatter.print_char(b','); - formatter.print_char(b' '); + formatter.print_ascii_byte(b','); + formatter.print_ascii_byte(b' '); formatter.print_expression(&cmp_mem_expr.expression); } MemberExpression::StaticMemberExpression(static_mem_expr) => { let name = &static_mem_expr.property.name; formatter.print_expression(&static_mem_expr.object); - formatter.print_char(b','); - formatter.print_char(b' '); + formatter.print_ascii_byte(b','); + formatter.print_ascii_byte(b' '); formatter.print_str(format!("\'{name}\'").as_str()); } MemberExpression::PrivateFieldExpression(_) => (), } - formatter.print_char(b')'); + formatter.print_ascii_byte(b')'); if has_mock_implementation { return formatter.into_source_text(); @@ -171,7 +171,7 @@ impl PreferSpyOn { formatter.print_expression(expr); } - formatter.print_char(b')'); + formatter.print_ascii_byte(b')'); formatter.into_source_text() } diff --git a/crates/oxc_linter/src/rules/jest/prefer_todo.rs b/crates/oxc_linter/src/rules/jest/prefer_todo.rs index 655593e6a5853..3b40293d412b2 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_todo.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_todo.rs @@ -163,17 +163,17 @@ fn build_code<'a>(fixer: RuleFixer<'_, 'a>, expr: &CallExpression<'a>) -> RuleFi if let Argument::StringLiteral(ident) = &expr.arguments[0] { // Todo: this punctuation should read from the config - formatter.print_char(b'\''); + formatter.print_ascii_byte(b'\''); formatter.print_str(ident.value.as_str()); - formatter.print_char(b'\''); - formatter.print_char(b')'); + formatter.print_ascii_byte(b'\''); + formatter.print_ascii_byte(b')'); } else if let Argument::TemplateLiteral(temp) = &expr.arguments[0] { - formatter.print_char(b'`'); + formatter.print_ascii_byte(b'`'); for q in &temp.quasis { formatter.print_str(q.value.raw.as_str()); } - formatter.print_char(b'`'); - formatter.print_char(b')'); + formatter.print_ascii_byte(b'`'); + formatter.print_ascii_byte(b')'); } fixer.replace(expr.span, formatter) diff --git a/crates/oxc_linter/src/rules/jest/require_hook.rs b/crates/oxc_linter/src/rules/jest/require_hook.rs index 5ca53d8a0dd70..c98acd9c20891 100644 --- a/crates/oxc_linter/src/rules/jest/require_hook.rs +++ b/crates/oxc_linter/src/rules/jest/require_hook.rs @@ -6,7 +6,7 @@ use oxc_ast::{ use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_semantic::AstNode; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use crate::{ context::LintContext, @@ -25,7 +25,7 @@ fn use_hook(span: Span) -> OxcDiagnostic { #[derive(Debug, Default, Clone)] pub struct RequireHookConfig { - allowed_function_calls: Vec, + allowed_function_calls: Vec, } #[derive(Debug, Default, Clone)] @@ -153,9 +153,7 @@ impl Rule for RequireHook { .get(0) .and_then(|config| config.get("allowedFunctionCalls")) .and_then(serde_json::Value::as_array) - .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() - }) + .map(|v| v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()) .unwrap_or_default(); Self(Box::new(RequireHookConfig { allowed_function_calls })) @@ -230,7 +228,7 @@ impl RequireHook { ctx: &LintContext<'a>, ) { if let Expression::CallExpression(call_expr) = expr { - let name: String = get_node_name(&call_expr.callee); + let name = get_node_name(&call_expr.callee); if !(parse_jest_fn_call(call_expr, &PossibleJestNode { node, original: None }, ctx) .is_some() diff --git a/crates/oxc_linter/src/rules/jest/valid_expect.rs b/crates/oxc_linter/src/rules/jest/valid_expect.rs index 711a6e7cab513..be2923a731d75 100644 --- a/crates/oxc_linter/src/rules/jest/valid_expect.rs +++ b/crates/oxc_linter/src/rules/jest/valid_expect.rs @@ -159,21 +159,21 @@ impl ValidExpect { return; }; - if call_expr.arguments.len() < self.min_args { + if call_expr.arguments.len() > self.max_args { let error = format!( "Expect takes at most {} argument{} ", - self.min_args, - if self.min_args > 1 { "s" } else { "" } + self.max_args, + if self.max_args > 1 { "s" } else { "" } ); let help = "Remove the extra arguments."; ctx.diagnostic(valid_expect_diagnostic(error, help, call_expr.span)); return; } - if call_expr.arguments.len() > self.max_args { + if call_expr.arguments.len() < self.min_args { let error = format!( "Expect requires at least {} argument{} ", - self.max_args, - if self.max_args > 1 { "s" } else { "" } + self.min_args, + if self.min_args > 1 { "s" } else { "" } ); let help = "Add the missing arguments."; ctx.diagnostic(valid_expect_diagnostic(error, help, call_expr.span)); diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs index 36fafee306807..d8205f8be4ba8 100644 --- a/crates/oxc_linter/src/rules/jest/valid_title.rs +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -7,7 +7,7 @@ use oxc_ast::{ }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use regex::Regex; use rustc_hash::FxHashMap; @@ -30,7 +30,7 @@ pub struct ValidTitle(Box); #[derive(Debug, Default, Clone)] pub struct ValidTitleConfig { ignore_type_of_describe_name: bool, - disallowed_words: Vec, + disallowed_words: Vec, ignore_space: bool, must_not_match_patterns: FxHashMap, must_match_patterns: FxHashMap, @@ -85,9 +85,7 @@ impl Rule for ValidTitle { let disallowed_words = config .and_then(|v| v.get("disallowedWords")) .and_then(|v| v.as_array()) - .map(|v| { - v.iter().filter_map(|v| v.as_str().map(std::string::ToString::to_string)).collect() - }) + .map(|v| v.iter().filter_map(|v| v.as_str().map(CompactStr::from)).collect()) .unwrap_or_default(); let must_not_match_patterns = config .and_then(|v| v.get("mustNotMatch")) @@ -179,7 +177,7 @@ impl ValidTitle { } } -type CompiledMatcherAndMessage = (Regex, Option); +type CompiledMatcherAndMessage = (Regex, Option); #[derive(Debug, Clone, Hash, PartialEq, Eq)] enum MatchKind { @@ -263,7 +261,7 @@ fn compile_matcher_pattern(pattern: MatcherPattern) -> Option { let reg_str = pattern.first().and_then(|v| v.as_str()).map(|v| format!("(?u){v}"))?; let reg = Regex::new(®_str).ok()?; - let message = pattern.get(1).map(std::string::ToString::to_string); + let message = pattern.get(1).and_then(serde_json::Value::as_str).map(CompactStr::from); Some((reg, message)) } } @@ -322,12 +320,12 @@ fn validate_title( if let Some((regex, message)) = valid_title.must_match_patterns.get(&jest_fn_name) { if !regex.is_match(title) { let raw_pattern = regex.as_str(); - let message = message.as_ref().map_or_else( - || format!("{un_prefixed_name} should match {raw_pattern}"), - std::clone::Clone::clone, - ); + let message = match message.as_ref() { + Some(message) => message.as_str(), + None => &format!("{un_prefixed_name} should match {raw_pattern}"), + }; ctx.diagnostic(valid_title_diagnostic( - &message, + message, "Make sure the title matches the `mustMatch` of your config file", span, )); @@ -337,13 +335,13 @@ fn validate_title( if let Some((regex, message)) = valid_title.must_not_match_patterns.get(&jest_fn_name) { if regex.is_match(title) { let raw_pattern = regex.as_str(); - let message = message.as_ref().map_or_else( - || format!("{un_prefixed_name} should not match {raw_pattern}"), - std::string::ToString::to_string, - ); + let message = match message.as_ref() { + Some(message) => message.as_str(), + None => &format!("{un_prefixed_name} should not match {raw_pattern}"), + }; ctx.diagnostic(valid_title_diagnostic( - &message, + message, "Make sure the title not matches the `mustNotMatch` of your config file", span, )); diff --git a/crates/oxc_linter/src/rules/jsx_a11y/alt_text.rs b/crates/oxc_linter/src/rules/jsx_a11y/alt_text.rs index 38a16fe2e37cc..21c09194cfabc 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/alt_text.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/alt_text.rs @@ -4,7 +4,7 @@ use oxc_ast::{ }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use crate::{ context::LintContext, @@ -71,10 +71,10 @@ pub struct AltText(Box); #[derive(Debug, Clone, PartialEq, Eq)] pub struct AltTextConfig { - img: Option>, - object: Option>, - area: Option>, - input_type_image: Option>, + img: Option>, + object: Option>, + area: Option>, + input_type_image: Option>, } impl std::ops::Deref for AltText { @@ -160,9 +160,7 @@ impl Rule for AltText { if let (Some(tags), Some(elements)) = (tags, config.get(field).and_then(|v| v.as_array())) { - tags.extend( - elements.iter().filter_map(|v| v.as_str().map(ToString::to_string)), - ); + tags.extend(elements.iter().filter_map(|v| v.as_str().map(CompactStr::from))); } } } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/heading_has_content.rs b/crates/oxc_linter/src/rules/jsx_a11y/heading_has_content.rs index ea843299810b5..04d6a4adc80fc 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/heading_has_content.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/heading_has_content.rs @@ -1,7 +1,7 @@ use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{CompactStr, Span}; use crate::{ context::LintContext, @@ -23,7 +23,7 @@ pub struct HeadingHasContent(Box); #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct HeadingHasContentConfig { - components: Option>, + components: Option>, } impl std::ops::Deref for HeadingHasContent { @@ -74,10 +74,7 @@ impl Rule for HeadingHasContent { .and_then(|v| v.get("components")) .and_then(serde_json::Value::as_array) .map(|v| { - v.iter() - .filter_map(serde_json::Value::as_str) - .map(ToString::to_string) - .collect() + v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect() }), })) } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/mouse_events_have_key_events.rs b/crates/oxc_linter/src/rules/jsx_a11y/mouse_events_have_key_events.rs index 4aec7a36ca9e9..a1b10afc7e233 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/mouse_events_have_key_events.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/mouse_events_have_key_events.rs @@ -1,7 +1,7 @@ use oxc_ast::{ast::JSXAttributeValue, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use crate::{ context::LintContext, @@ -28,15 +28,15 @@ pub struct MouseEventsHaveKeyEvents(Box); #[derive(Debug, Clone)] pub struct MouseEventsHaveKeyEventsConfig { - hover_in_handlers: Vec, - hover_out_handlers: Vec, + hover_in_handlers: Vec, + hover_out_handlers: Vec, } impl Default for MouseEventsHaveKeyEventsConfig { fn default() -> Self { Self { - hover_in_handlers: vec!["onMouseOver".to_string()], - hover_out_handlers: vec!["onMouseOut".to_string()], + hover_in_handlers: vec!["onMouseOver".into()], + hover_out_handlers: vec!["onMouseOut".into()], } } } @@ -78,7 +78,7 @@ impl Rule for MouseEventsHaveKeyEvents { config.hover_in_handlers = hover_in_handlers_config .iter() .filter_map(serde_json::Value::as_str) - .map(ToString::to_string) + .map(CompactStr::from) .collect(); } @@ -90,7 +90,7 @@ impl Rule for MouseEventsHaveKeyEvents { config.hover_out_handlers = hover_out_handlers_config .iter() .filter_map(serde_json::Value::as_str) - .map(ToString::to_string) + .map(CompactStr::from) .collect(); } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_redundant_roles.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_redundant_roles.rs index bf1183646167d..f4d53b514667e 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/no_redundant_roles.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_redundant_roles.rs @@ -67,11 +67,7 @@ impl Rule for NoRedundantRoles { if let Some(JSXAttributeItem::Attribute(attr)) = has_jsx_prop_ignore_case(jsx_el, "role") { if let Some(JSXAttributeValue::StringLiteral(role_values)) = &attr.value { - let roles: Vec = role_values - .value - .split_whitespace() - .map(std::string::ToString::to_string) - .collect(); + let roles = role_values.value.split_whitespace().collect::>(); for role in &roles { let exceptions = DEFAULT_ROLE_EXCEPTIONS.get(&component); if exceptions.map_or(false, |set| set.contains(role)) { diff --git a/crates/oxc_linter/src/rules/nextjs/no_typos.rs b/crates/oxc_linter/src/rules/nextjs/no_typos.rs index b380d668bd582..097f3d2b3e2b9 100644 --- a/crates/oxc_linter/src/rules/nextjs/no_typos.rs +++ b/crates/oxc_linter/src/rules/nextjs/no_typos.rs @@ -133,9 +133,9 @@ fn min_distance(a: &str, b: &str) -> usize { let mut previous_row: Vec = (0..=n).collect(); - for (i, s1) in a.chars().enumerate() { + for (i, s1) in a.char_indices() { let mut current_row = vec![i + 1]; - for (j, s2) in b.chars().enumerate() { + for (j, s2) in b.char_indices() { let insertions = previous_row[j + 1] + 1; let deletions = current_row[j] + 1; let substitutions = previous_row[j] + usize::from(s1 != s2); diff --git a/crates/oxc_linter/src/rules/oxc/double_comparisons.rs b/crates/oxc_linter/src/rules/oxc/double_comparisons.rs index 31522a7e89171..e2f7e6bd9b00c 100644 --- a/crates/oxc_linter/src/rules/oxc/double_comparisons.rs +++ b/crates/oxc_linter/src/rules/oxc/double_comparisons.rs @@ -94,9 +94,9 @@ impl Rule for DoubleComparisons { let modified_code = { let mut codegen = fixer.codegen(); codegen.print_expression(llhs); - codegen.print_char(b' '); + codegen.print_ascii_byte(b' '); codegen.print_str(new_op); - codegen.print_char(b' '); + codegen.print_ascii_byte(b' '); codegen.print_expression(lrhs); codegen.into_source_text() }; 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 a043346eb51db..92e35c9199bfe 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 @@ -1,9 +1,10 @@ use oxc_ast::{ - ast::{BindingIdentifier, BindingPatternKind, Expression}, + ast::{BindingIdentifier, BindingPatternKind, CallExpression, Expression, FormalParameters}, AstKind, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; +use oxc_semantic::SymbolId; use oxc_span::{GetSpan, Span}; use crate::{ @@ -96,139 +97,158 @@ impl Rule for OnlyUsedInRecursion { }; if is_argument_only_used_in_recursion(function_id, arg, arg_index, ctx) { - if arg_index == function_parameters.items.len() - 1 - && !ctx - .semantic() - .symbols() - .get_flags(function_id.symbol_id.get().expect("`symbol_id` should be set")) - .is_export() - { - ctx.diagnostic_with_dangerous_fix( - 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, - ); - fix.push(Fix::delete(arg.span())); - - for reference in ctx.semantic().symbol_references( - arg.symbol_id.get().expect("`symbol_id` should be set"), - ) { - 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"), - ) { - let node = ctx.nodes().get_node(reference.node_id()); - - if let Some(AstKind::CallExpression(call_expr)) = - ctx.nodes().parent_kind(node.id()) - { - // check if the number of arguments is the same - if call_expr.arguments.len() != function_parameters.items.len() - || function_span.contains_inclusive(call_expr.span) - { - continue; - } - - // remove the argument - let arg_to_delete = call_expr.arguments[arg_index].span(); - - fix.push(Fix::delete(Span::new( - arg_to_delete.start, - skip_to_next_char(ctx.source_text(), arg_to_delete.end), - ))); - } - } - - fix - }, - ); - } else { - ctx.diagnostic(only_used_in_recursion_diagnostic(arg.span, arg.name.as_str())); - } + create_diagnostic( + ctx, + function_id, + function_parameters, + arg, + arg_index, + function_span, + ); } } } } +fn create_diagnostic( + ctx: &LintContext, + function_id: &BindingIdentifier, + function_parameters: &FormalParameters, + arg: &BindingIdentifier, + arg_index: usize, + 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; + + if is_diagnostic_only { + return ctx.diagnostic(only_used_in_recursion_diagnostic(arg.span, arg.name.as_str())); + } + + ctx.diagnostic_with_dangerous_fix( + 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, + ); + fix.push(Fix::delete(arg.span())); + + for reference in ctx + .semantic() + .symbol_references(arg.symbol_id.get().expect("`symbol_id` should be set")) + { + 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")) + { + let node = ctx.nodes().get_node(reference.node_id()); + + if let Some(AstKind::CallExpression(call_expr)) = ctx.nodes().parent_kind(node.id()) + { + if call_expr.arguments.len() != function_parameters.items.len() + || function_span.contains_inclusive(call_expr.span) + { + continue; + } + + let arg_to_delete = call_expr.arguments[arg_index].span(); + fix.push(Fix::delete(Span::new( + arg_to_delete.start, + skip_to_next_char(ctx.source_text(), arg_to_delete.end), + ))); + } + } + + fix + }, + ); +} + fn is_argument_only_used_in_recursion<'a>( function_id: &'a BindingIdentifier, arg: &'a BindingIdentifier, arg_index: usize, ctx: &'a LintContext<'_>, ) -> bool { - let mut is_used_only_in_recursion = true; - let mut has_references = false; - - for reference in - ctx.semantic().symbol_references(arg.symbol_id.get().expect("`symbol_id` should be set")) - { - has_references = true; - if let Some(AstKind::Argument(argument)) = ctx.nodes().parent_kind(reference.node_id()) { - if let Some(AstKind::CallExpression(call_expr)) = - ctx.nodes().parent_kind(ctx.nodes().parent_node(reference.node_id()).unwrap().id()) - { - if !call_expr.arguments.iter().enumerate().any(|(index, arg)| { - index == arg_index - && arg.span() == argument.span() - && if let Expression::Identifier(identifier) = &call_expr.callee { - identifier - .reference_id() - .and_then(|id| ctx.symbols().get_reference(id).symbol_id()) - .is_some_and(|v| { - let function_symbol_id = function_id.symbol_id.get(); - debug_assert!(function_symbol_id.is_some()); - function_symbol_id - .is_some_and(|function_symbol_id| function_symbol_id == v) - }) - } else { - false - } - }) { - is_used_only_in_recursion = false; - break; - } - } else { - is_used_only_in_recursion = false; - break; - } - } else { - is_used_only_in_recursion = false; - break; + let mut references = ctx + .semantic() + .symbol_references(arg.symbol_id.get().expect("`symbol_id` should be set")) + .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(); + + for reference in references { + let Some(AstKind::Argument(argument)) = ctx.nodes().parent_kind(reference.node_id()) else { + return false; + }; + let Some(AstKind::CallExpression(call_expr)) = + ctx.nodes().parent_kind(ctx.nodes().parent_node(reference.node_id()).unwrap().id()) + else { + return false; + }; + + let Some(call_arg) = call_expr.arguments.get(arg_index) else { + return false; + }; + + if argument.span() != call_arg.span() { + return false; + } + + if !is_recursive_call(call_expr, function_symbol_id, ctx) { + return false; } } - has_references && is_used_only_in_recursion + true } -fn is_function_maybe_reassigned<'a>( - function_id: &'a BindingIdentifier, - ctx: &'a LintContext<'_>, +fn is_recursive_call( + call_expr: &CallExpression, + function_symbol_id: SymbolId, + ctx: &LintContext, ) -> bool { - let mut is_maybe_reassigned = false; - - for reference in ctx - .semantic() - .symbol_references(function_id.symbol_id.get().expect("`symbol_id` should be set")) - { - if let Some(AstKind::SimpleAssignmentTarget(_)) = - ctx.nodes().parent_kind(reference.node_id()) + if let Expression::Identifier(identifier) = &call_expr.callee { + if let Some(symbol_id) = + identifier.reference_id().and_then(|id| ctx.symbols().get_reference(id).symbol_id()) { - is_maybe_reassigned = true; + return symbol_id == function_symbol_id; } } + false +} - is_maybe_reassigned +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(_)) + ) + }) } // skipping whitespace, commas, finds the next character (exclusive) diff --git a/crates/oxc_linter/src/rules/promise/spec_only.rs b/crates/oxc_linter/src/rules/promise/spec_only.rs index e9ce43322d17b..081f92256f639 100644 --- a/crates/oxc_linter/src/rules/promise/spec_only.rs +++ b/crates/oxc_linter/src/rules/promise/spec_only.rs @@ -1,7 +1,7 @@ use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{GetSpan, Span}; +use oxc_span::{CompactStr, GetSpan, Span}; use rustc_hash::FxHashSet; use crate::{context::LintContext, rule::Rule, utils::PROMISE_STATIC_METHODS, AstNode}; @@ -16,7 +16,7 @@ pub struct SpecOnly(Box); #[derive(Debug, Default, Clone)] pub struct SpecOnlyConfig { - allowed_methods: Option>, + allowed_methods: Option>, } impl std::ops::Deref for SpecOnly { @@ -58,7 +58,7 @@ impl Rule for SpecOnly { .and_then(|v| v.get("allowedMethods")) .and_then(serde_json::Value::as_array) .map(|v| { - v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() + v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect() }); Self(Box::new(SpecOnlyConfig { allowed_methods })) diff --git a/crates/oxc_linter/src/rules/react/iframe_missing_sandbox.rs b/crates/oxc_linter/src/rules/react/iframe_missing_sandbox.rs new file mode 100644 index 0000000000000..0fcd3f4300beb --- /dev/null +++ b/crates/oxc_linter/src/rules/react/iframe_missing_sandbox.rs @@ -0,0 +1,233 @@ +use oxc_ast::ast::{ + Argument, Expression, JSXAttributeItem, JSXAttributeValue, JSXElementName, ObjectProperty, + ObjectPropertyKind, StringLiteral, +}; +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use phf::{phf_set, Set}; + +use crate::utils::{get_prop_value, has_jsx_prop_ignore_case, is_create_element_call}; +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn missing_sandbox_prop(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("An iframe element is missing a sandbox attribute") + .with_help("Add a `sandbox` attribute to the `iframe` element.") + .with_label(span) +} + +fn invalid_sandbox_prop(span: Span, value: &str) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("An iframe element defines a sandbox attribute with invalid value: {value}")) + .with_help("Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.") + .with_label(span) +} + +fn invalid_sandbox_combination_prop(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid") + .with_help("Remove `allow-scripts` or `allow-same-origin`.") + .with_label(span) +} + +const ALLOWED_VALUES: Set<&'static str> = phf_set! { + "", + "allow-downloads-without-user-activation", + "allow-downloads", + "allow-forms", + "allow-modals", + "allow-orientation-lock", + "allow-pointer-lock", + "allow-popups", + "allow-popups-to-escape-sandbox", + "allow-presentation", + "allow-same-origin", + "allow-scripts", + "allow-storage-access-by-user-activation", + "allow-top-navigation", + "allow-top-navigation-by-user-activation" +}; + +#[derive(Debug, Default, Clone)] +pub struct IframeMissingSandbox; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforce sandbox attribute on iframe elements + /// + /// ### Why is this bad? + /// + /// The sandbox attribute enables an extra set of restrictions for the content in the iframe. Using sandbox attribute is considered a good security practice. + /// To learn more about sandboxing, see [MDN's documentation on the `sandbox` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). + + /// + /// This rule checks all React `"#, + r#"React.createElement("iframe", { src: "foo.htm", sandbox: true })"#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#"React.createElement("iframe", { sandbox: "allow-forms" })"#, + r#"React.createElement("iframe", { sandbox: "allow-modals" })"#, + r#"React.createElement("iframe", { sandbox: "allow-orientation-lock" })"#, + r#"React.createElement("iframe", { sandbox: "allow-pointer-lock" })"#, + r#"React.createElement("iframe", { sandbox: "allow-popups" })"#, + r#"React.createElement("iframe", { sandbox: "allow-popups-to-escape-sandbox" })"#, + r#"React.createElement("iframe", { sandbox: "allow-presentation" })"#, + r#"React.createElement("iframe", { sandbox: "allow-same-origin" })"#, + r#"React.createElement("iframe", { sandbox: "allow-scripts" })"#, + r#"React.createElement("iframe", { sandbox: "allow-top-navigation" })"#, + r#"React.createElement("iframe", { sandbox: "allow-top-navigation-by-user-activation" })"#, + r#"React.createElement("iframe", { sandbox: "allow-forms allow-modals" })"#, + r#"React.createElement("iframe", { sandbox: "allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation" })"#, + ]; + + let fail = vec![ + ";", + ""#, + r#"React.createElement("iframe", { sandbox: "__unknown__" })"#, + r#";"#, + r#"; + · ────── + ╰──── + help: Add a `sandbox` attribute to the `iframe` element. + + ⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute + ╭─[iframe_missing_sandbox.tsx:1:2] + 1 │ + · ───────────── + ╰──── + help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox. + + ⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__ + ╭─[iframe_missing_sandbox.tsx:1:42] + 1 │ React.createElement("iframe", { sandbox: "__unknown__" }) + · ───────────── + ╰──── + help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox. + + ⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__ + ╭─[iframe_missing_sandbox.tsx:1:17] + 1 │ ; + · ───────────────────────────────── + ╰──── + help: Remove `allow-scripts` or `allow-same-origin`. + + ⚠ eslint-plugin-react(iframe-missing-sandbox): An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid + ╭─[iframe_missing_sandbox.tsx:1:17] + 1 │