diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b5d63e..fce2c0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,89 @@ -name: Build and Release Andromeda +name: CI on: - workflow_dispatch: push: - branches: [main] + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + +env: + CARGO_TERM_COLOR: always permissions: contents: write jobs: + typos: + name: Spellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Spell check + uses: crate-ci/typos@master + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + toolchain: nightly-2025-09-05 + components: rustfmt, clippy, llvm-tools-preview, rustc-dev + - name: Cache on ${{ github.ref_name }} + uses: Swatinem/rust-cache@v2 + with: + shared-key: warm + - name: Check formatting + run: cargo fmt --check + - name: Clippy + run: | + cargo +nightly-2025-09-05 clippy --all-targets -- -D warnings + cargo +nightly-2025-09-05 clippy --all-targets --all-features -- -D warnings + + test: + name: Build & Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + [ + "macos-14", + "macos-latest", + "ubuntu-24.04", + "ubuntu-latest", + "windows-latest", + ] + steps: + - uses: actions/checkout@v4 + - uses: oxc-project/setup-rust@cd82e1efec7fef815e2c23d296756f31c7cdc03d # v1.0.0 + with: + cache-key: warm + save-cache: ${{ github.ref_name == 'main' }} + - name: Check + run: cargo check --all-targets + - name: Build + run: cargo build --tests --bins --examples + - name: Test + run: cargo test + env: + RUST_BACKTRACE: 1 + build: name: Build ${{ matrix.asset-name }} runs-on: ${{ matrix.os }} - continue-on-error: true # Don't fail the entire workflow if one target fails + continue-on-error: true + if: github.ref == 'refs/heads/main' strategy: - fail-fast: false # Continue with other builds even if one fails + fail-fast: false matrix: include: # Linux (x86_64) @@ -24,20 +93,20 @@ jobs: installer-name: andromeda-installer-linux-amd64 # Linux (ARM64) - cross-compilation - # - os: ubuntu-24.04 - # rust-target: aarch64-unknown-linux-gnu - # asset-name: andromeda-linux-arm64 - # installer-name: andromeda-installer-linux-arm64 - # cross-compile: true + - os: ubuntu-24.04 + rust-target: aarch64-unknown-linux-gnu + asset-name: andromeda-linux-arm64 + installer-name: andromeda-installer-linux-arm64 + cross-compile: true # macOS (Intel) - - os: macos-13 + - os: macos-15-large rust-target: x86_64-apple-darwin asset-name: andromeda-macos-amd64 installer-name: andromeda-installer-macos-amd64 # macOS (Apple Silicon/ARM) - - os: macos-14 + - os: macos-latest rust-target: aarch64-apple-darwin asset-name: andromeda-macos-arm64 installer-name: andromeda-installer-macos-arm64 @@ -63,34 +132,28 @@ jobs: toolchain: "nightly-2025-09-05" targets: ${{ matrix.rust-target }} - # - name: Install cross-compilation dependencies - # if: matrix.cross-compile - # run: | - # sudo apt-get update - # sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross pkg-config + - name: Install cross-compilation dependencies + if: matrix.cross-compile + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross pkg-config - name: Build - continue-on-error: true # Allow individual builds to fail - run: cargo build --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - env: - # Cross-compilation environment variables - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - # PKG_CONFIG settings for cross-compilation - PKG_CONFIG_ALLOW_CROSS: 1 - - - name: Build installer - continue-on-error: true # Allow individual builds to fail - run: cargo build --bin andromeda-installer --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + id: build-main + continue-on-error: true + run: | + cargo build --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + cargo build --bin andromeda-installer --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml env: - # Cross-compilation environment variables CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - # PKG_CONFIG settings for cross-compilation PKG_CONFIG_ALLOW_CROSS: 1 + MACOSX_DEPLOYMENT_TARGET: ${{ runner.os == 'macOS' && '14.0' || '' }} + RUSTFLAGS: ${{ runner.os == 'macOS' && '-C link-arg=-mmacosx-version-min=14.0' || '' }} - name: Build satellites - continue-on-error: true # Allow individual builds to fail + id: build-satellites + continue-on-error: true run: | cargo build --bin andromeda-run --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml cargo build --bin andromeda-compile --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml @@ -99,37 +162,32 @@ jobs: cargo build --bin andromeda-check --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml cargo build --bin andromeda-bundle --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml env: - # Cross-compilation environment variables CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - # PKG_CONFIG settings for cross-compilation PKG_CONFIG_ALLOW_CROSS: 1 + MACOSX_DEPLOYMENT_TARGET: ${{ runner.os == 'macOS' && '14.0' || '' }} + RUSTFLAGS: ${{ runner.os == 'macOS' && '-C link-arg=-mmacosx-version-min=14.0' || '' }} - - name: Prepare binaries + - name: Prepare binary + if: steps.build-main.outcome == 'success' shell: bash run: | cd target/${{ matrix.rust-target }}/release/ - # Prepare main binary if [ -f "andromeda.exe" ]; then mv andromeda.exe ${{ matrix.asset-name }} elif [ -f "andromeda" ]; then mv andromeda ${{ matrix.asset-name }} - else - echo "Main binary not found, build may have failed" - exit 1 fi # Prepare installer binary if [ -f "andromeda-installer.exe" ]; then - cp "andromeda-installer.exe" "${{ matrix.installer-name }}" + mv andromeda-installer.exe ${{ matrix.installer-name }} elif [ -f "andromeda-installer" ]; then - cp "andromeda-installer" "${{ matrix.installer-name }}" - else - echo "Warning: Installer binary not found" + mv andromeda-installer ${{ matrix.installer-name }} fi - # Prepare satellite binaries + # Prepare satellite binaries (only if they exist) for satellite in run compile fmt lint check bundle; do if [ -f "andromeda-${satellite}.exe" ]; then # Windows binaries @@ -137,28 +195,26 @@ jobs: elif [ -f "andromeda-${satellite}" ]; then # Unix binaries cp "andromeda-${satellite}" "andromeda-${satellite}-${{ matrix.rust-target }}" - else - echo "Warning: andromeda-${satellite} binary not found" fi done - - name: Upload Main Binary as Artifact + - name: Upload Binary as Artifact uses: actions/upload-artifact@v4 - if: success() # Only upload if binary was prepared successfully + if: steps.build-main.outcome == 'success' with: name: ${{ matrix.asset-name }} path: target/${{ matrix.rust-target }}/release/${{ matrix.asset-name }} - - name: Upload Installer Binary as Artifact + - name: Upload Installer as Artifact uses: actions/upload-artifact@v4 - if: success() # Only upload if installer was prepared successfully + if: steps.build-main.outcome == 'success' && hashFiles(format('target/{0}/release/{1}', matrix.rust-target, matrix.installer-name)) != '' with: name: ${{ matrix.installer-name }} path: target/${{ matrix.rust-target }}/release/${{ matrix.installer-name }} - name: Upload Satellite Binaries as Artifacts uses: actions/upload-artifact@v4 - if: success() + if: steps.build-satellites.outcome == 'success' with: name: satellites-${{ matrix.rust-target }} path: | @@ -183,12 +239,6 @@ jobs: with: path: ./artifacts - - name: List all artifacts - run: | - echo "=== All artifacts ready for release ===" - find ./artifacts -type f -name "andromeda-*" | sort - echo "=======================================" - - name: Create Draft Release uses: svenstaro/upload-release-action@v2 with: @@ -198,31 +248,3 @@ jobs: draft: true tag: latest overwrite: true - body: | - ## Andromeda Release - - ### Installation - - **Main CLI:** - - Download `andromeda-{platform}-{arch}` for your platform - - Make executable and add to PATH - - **Installer:** - - Download `andromeda-installer-{platform}-{arch}` for your platform - - Run `andromeda-installer` to install main CLI - - Run `andromeda-installer satellite ` to install satellites - - **Satellites (specialized binaries):** - - `andromeda-run` - Execute JS/TS files - - `andromeda-compile` - Compile to executables - - `andromeda-fmt` - Format code - - `andromeda-lint` - Lint code - - `andromeda-check` - Type-check TypeScript - - `andromeda-bundle` - Bundle and minify - - ### Platforms - - Linux (x86_64, ARM64) - - macOS (Intel, Apple Silicon) - - Windows (x86_64, ARM64) - - See [SATELLITES.md](https://github.com/tryandromeda/andromeda/blob/main/cli/SATELLITES.md) for detailed satellite documentation. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d294057..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,187 +0,0 @@ -name: Build and Release Andromeda - -on: - workflow_dispatch: - push: - branches: [main] - -permissions: - contents: write - -jobs: - build: - name: Build ${{ matrix.asset-name }} - runs-on: ${{ matrix.os }} - continue-on-error: true # Don't fail the entire workflow if one target fails - strategy: - fail-fast: false # Continue with other builds even if one fails - matrix: - include: - # Linux (x86_64) - - os: ubuntu-24.04 - rust-target: x86_64-unknown-linux-gnu - asset-name: andromeda-linux-amd64 - installer-name: andromeda-installer-linux-amd64 - - # Linux (ARM64) - cross-compilation - - os: ubuntu-24.04 - rust-target: aarch64-unknown-linux-gnu - asset-name: andromeda-linux-arm64 - installer-name: andromeda-installer-linux-arm64 - cross-compile: true - - # macOS (Intel) - - os: macos-15-intel - rust-target: x86_64-apple-darwin - asset-name: andromeda-macos-amd64 - installer-name: andromeda-installer-macos-amd64 - - # macOS (Apple Silicon/ARM) - - os: macos-latest - rust-target: aarch64-apple-darwin - asset-name: andromeda-macos-arm64 - installer-name: andromeda-installer-macos-arm64 - - # Windows (x86_64) - - os: windows-latest - rust-target: x86_64-pc-windows-msvc - asset-name: andromeda-windows-amd64.exe - installer-name: andromeda-installer-windows-amd64.exe - - # Windows (ARM64) - - os: windows-latest - rust-target: aarch64-pc-windows-msvc - asset-name: andromeda-windows-arm64.exe - installer-name: andromeda-installer-windows-arm64.exe - - steps: - - uses: actions/checkout@v4 - - - name: Install the rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: "nightly-2025-09-05" - targets: ${{ matrix.rust-target }} - - - name: Install cross-compilation dependencies - if: matrix.cross-compile - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross pkg-config - - - name: Build - id: build-main - continue-on-error: true # Allow individual builds to fail - run: | - cargo build --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - cargo build --bin andromeda-installer --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - env: - # Cross-compilation environment variables - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - # PKG_CONFIG settings for cross-compilation - PKG_CONFIG_ALLOW_CROSS: 1 - # Set minimum macOS version to 14.0 via RUSTFLAGS to ensure it reaches the linker - MACOSX_DEPLOYMENT_TARGET: ${{ runner.os == 'macOS' && '14.0' || '' }} - RUSTFLAGS: ${{ runner.os == 'macOS' && '-C link-arg=-mmacosx-version-min=14.0' || '' }} - - - name: Build satellites - id: build-satellites - continue-on-error: true # Allow individual builds to fail - run: | - cargo build --bin andromeda-run --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - cargo build --bin andromeda-compile --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - cargo build --bin andromeda-fmt --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - cargo build --bin andromeda-lint --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - cargo build --bin andromeda-check --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - cargo build --bin andromeda-bundle --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml - env: - # Cross-compilation environment variables - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - # PKG_CONFIG settings for cross-compilation - PKG_CONFIG_ALLOW_CROSS: 1 - # Set minimum macOS version to 14.0 via RUSTFLAGS to ensure it reaches the linker - MACOSX_DEPLOYMENT_TARGET: ${{ runner.os == 'macOS' && '14.0' || '' }} - RUSTFLAGS: ${{ runner.os == 'macOS' && '-C link-arg=-mmacosx-version-min=14.0' || '' }} - - - name: Prepare binary - if: steps.build-main.outcome == 'success' - shell: bash - run: | - cd target/${{ matrix.rust-target }}/release/ - # Prepare main binary - if [ -f "andromeda.exe" ]; then - mv andromeda.exe ${{ matrix.asset-name }} - elif [ -f "andromeda" ]; then - mv andromeda ${{ matrix.asset-name }} - fi - - # Prepare installer binary - if [ -f "andromeda-installer.exe" ]; then - mv andromeda-installer.exe ${{ matrix.installer-name }} - elif [ -f "andromeda-installer" ]; then - mv andromeda-installer ${{ matrix.installer-name }} - fi - - # Prepare satellite binaries (only if they exist) - for satellite in run compile fmt lint check bundle; do - if [ -f "andromeda-${satellite}.exe" ]; then - # Windows binaries - cp "andromeda-${satellite}.exe" "andromeda-${satellite}-${{ matrix.rust-target }}.exe" - elif [ -f "andromeda-${satellite}" ]; then - # Unix binaries - cp "andromeda-${satellite}" "andromeda-${satellite}-${{ matrix.rust-target }}" - fi - done - - - name: Upload Binary as Artifact - uses: actions/upload-artifact@v4 - if: steps.build-main.outcome == 'success' - with: - name: ${{ matrix.asset-name }} - path: target/${{ matrix.rust-target }}/release/${{ matrix.asset-name }} - - - name: Upload Installer as Artifact - uses: actions/upload-artifact@v4 - if: steps.build-main.outcome == 'success' && hashFiles(format('target/{0}/release/{1}', matrix.rust-target, matrix.installer-name)) != '' - with: - name: ${{ matrix.installer-name }} - path: target/${{ matrix.rust-target }}/release/${{ matrix.installer-name }} - - - name: Upload Satellite Binaries as Artifacts - uses: actions/upload-artifact@v4 - if: steps.build-satellites.outcome == 'success' - with: - name: satellites-${{ matrix.rust-target }} - path: | - target/${{ matrix.rust-target }}/release/andromeda-run-${{ matrix.rust-target }}* - target/${{ matrix.rust-target }}/release/andromeda-compile-${{ matrix.rust-target }}* - target/${{ matrix.rust-target }}/release/andromeda-fmt-${{ matrix.rust-target }}* - target/${{ matrix.rust-target }}/release/andromeda-lint-${{ matrix.rust-target }}* - target/${{ matrix.rust-target }}/release/andromeda-check-${{ matrix.rust-target }}* - target/${{ matrix.rust-target }}/release/andromeda-bundle-${{ matrix.rust-target }}* - - release: - name: Create Release - needs: build - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - - steps: - - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: ./artifacts - - - name: Create Draft Release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ./artifacts/*/andromeda-* - file_glob: true - draft: true - tag: latest - overwrite: true diff --git a/Cargo.lock b/Cargo.lock index 72f8186..8744456 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ [[package]] name = "andromeda" -version = "0.1.0" +version = "0.1.1" dependencies = [ "andromeda-core", "andromeda-runtime", @@ -126,7 +126,7 @@ dependencies = [ [[package]] name = "andromeda-core" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "anymap", @@ -149,7 +149,7 @@ dependencies = [ [[package]] name = "andromeda-runtime" -version = "0.1.0" +version = "0.1.1" dependencies = [ "andromeda-core", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 17d6566..0dec300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = ["the Andromeda team"] edition = "2024" license = "Mozilla Public License 2.0" repository = "https://github.com/tryandromeda/andromeda" -version = "0.1.0" +version = "0.1.1" [workspace.dependencies] andromeda-core = { path = "core" } diff --git a/cli/src/lint.rs b/cli/src/lint.rs index 333f491..c7ecb1a 100644 --- a/cli/src/lint.rs +++ b/cli/src/lint.rs @@ -1,6 +1,61 @@ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +//! # Andromeda Linter +//! +//! A comprehensive JavaScript/TypeScript linter with 27+ rules inspired by ESLint, Deno, and oxc_linter. +//! +//! ## Implemented Rules +//! +//! ### Code Quality Rules +//! - **no-empty** - Disallow empty statements +//! - **no-var** - Require let or const instead of var +//! - **no-unused-vars** - Disallow unused variables +//! - **prefer-const** - Require const declarations for variables that are never reassigned +//! - **camelcase** - Enforce camelCase naming convention +//! - **no-eval** - Disallow use of eval() +//! +//! ### Error Prevention Rules +//! - **no-debugger** - Disallow debugger statements +//! - **no-console** - Disallow console statements +//! - **no-unreachable** - Disallow unreachable code after return, throw, break, or continue +//! - **no-duplicate-case** - Disallow duplicate case labels in switch statements +//! - **no-constant-condition** - Disallow constant expressions in conditions +//! - **no-dupe-keys** - Disallow duplicate keys in object literals +//! - **no-const-assign** - Disallow reassigning const variables +//! - **no-func-assign** - Disallow reassigning function declarations +//! - **no-ex-assign** - Disallow reassigning exception parameters in catch clauses +//! +//! ### Best Practices Rules +//! - **eqeqeq** - Require === and !== instead of == and != +//! - **no-compare-neg-zero** - Disallow comparing against -0 +//! - **no-cond-assign** - Disallow assignment operators in conditional expressions +//! - **use-isnan** - Require calls to isNaN() when checking for NaN +//! - **no-fallthrough** - Disallow fallthrough of case statements +//! - **no-unsafe-negation** - Disallow negating the left operand of relational operators +//! - **no-boolean-literal-for-arguments** - Disallow boolean literals as arguments +//! +//! ### TypeScript Rules +//! - **no-explicit-any** - Disallow the any type +//! +//! ### Async/Await Rules +//! - **require-await** - Disallow async functions which have no await expression +//! - **no-async-promise-executor** - Disallow async functions as Promise executors +//! +//! ### Advanced Rules +//! - **no-sparse-arrays** - Disallow sparse array literals +//! - **no-unsafe-finally** - Disallow control flow statements in finally blocks +//! +//! ## Usage +//! +//! Rules can be configured in `.andromeda.toml`: +//! ```toml +//! [lint] +//! rules = ["no-var", "no-debugger", "eqeqeq"] +//! disabled_rules = ["no-console"] +//! max_warnings = 10 +//! ``` + use crate::config::{AndromedaConfig, ConfigManager, LintConfig}; use crate::error::{AndromedaError, Result}; use console::Style; @@ -196,7 +251,7 @@ pub enum LintError { #[diagnostic( code(andromeda::lint::no_boolean_literal_for_arguments), help( - "๐Ÿ” Avoid passing boolean literals as arguments.\n๏ฟฝ Boolean arguments make code harder to understand.\n๐Ÿ“– Consider using named objects or enums instead." + "๐Ÿ” Avoid passing boolean literals as arguments.\n๐Ÿ’ก Boolean arguments make code harder to understand.\n๐Ÿ“– Consider using named objects or enums instead." ), url("https://docs.deno.com/lint/rules/no-boolean-literal-for-arguments") )] @@ -207,6 +262,234 @@ pub enum LintError { source_code: NamedSource, value: bool, }, + + /// Unreachable code (no-unreachable) + #[diagnostic( + code(andromeda::lint::no_unreachable), + help( + "๐Ÿ” Remove unreachable code after return, throw, break, or continue.\n๐Ÿ’ก Code after these statements will never execute.\n๐Ÿงน This usually indicates a logical error or dead code." + ), + url("https://eslint.org/docs/latest/rules/no-unreachable") + )] + NoUnreachable { + #[label("Unreachable code detected")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Duplicate case label (no-duplicate-case) + #[diagnostic( + code(andromeda::lint::no_duplicate_case), + help( + "๐Ÿ” Remove duplicate case labels in switch statements.\n๐Ÿ’ก Duplicate cases will never be reached.\n๐Ÿ› This is likely a copy-paste error." + ), + url("https://eslint.org/docs/latest/rules/no-duplicate-case") + )] + NoDuplicateCase { + #[label("Duplicate case label")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Constant condition (no-constant-condition) + #[diagnostic( + code(andromeda::lint::no_constant_condition), + help( + "๐Ÿ” Avoid using constant expressions in conditions.\n๐Ÿ’ก Constant conditions make branches unreachable.\n๐Ÿ“– Use meaningful boolean expressions instead." + ), + url("https://eslint.org/docs/latest/rules/no-constant-condition") + )] + NoConstantCondition { + #[label("Constant condition detected")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Duplicate keys in object literals (no-dupe-keys) + #[diagnostic( + code(andromeda::lint::no_dupe_keys), + help( + "๐Ÿ” Remove duplicate keys in object literals.\n๐Ÿ’ก Later keys overwrite earlier ones silently.\n๐Ÿ› This often indicates a typo or logical error." + ), + url("https://eslint.org/docs/latest/rules/no-dupe-keys") + )] + NoDupeKeys { + #[label("Duplicate key '{key}'")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + key: String, + }, + + /// Comparing against -0 (no-compare-neg-zero) + #[diagnostic( + code(andromeda::lint::no_compare_neg_zero), + help( + "๐Ÿ” Use Object.is(x, -0) to check for negative zero.\n๐Ÿ’ก Regular equality doesn't distinguish between 0 and -0.\n๐Ÿ“– This can lead to unexpected behavior in some cases." + ), + url("https://eslint.org/docs/latest/rules/no-compare-neg-zero") + )] + NoCompareNegZero { + #[label("Comparing against -0")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Assignment in conditional (no-cond-assign) + #[diagnostic( + code(andromeda::lint::no_cond_assign), + help( + "๐Ÿ” Avoid assignments in conditional expressions.\n๐Ÿ’ก This is often a typo where == was intended instead of =.\n๐Ÿ“– If intentional, wrap the assignment in parentheses." + ), + url("https://eslint.org/docs/latest/rules/no-cond-assign") + )] + NoCondAssign { + #[label("Assignment in conditional expression")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Const reassignment (no-const-assign) + #[diagnostic( + code(andromeda::lint::no_const_assign), + help( + "๐Ÿ” Cannot reassign const variable '{variable_name}'.\n๐Ÿ’ก Const variables cannot be reassigned after declaration.\n๐Ÿ› Use 'let' if you need to reassign the variable." + ), + url("https://eslint.org/docs/latest/rules/no-const-assign") + )] + NoConstAssign { + #[label("Reassignment to const variable '{variable_name}'")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + variable_name: String, + }, + + /// Use isNaN for NaN checks (use-isnan) + #[diagnostic( + code(andromeda::lint::use_isnan), + help( + "๐Ÿ” Use Number.isNaN() or isNaN() to check for NaN.\n๐Ÿ’ก NaN is never equal to itself, so comparisons will always be false.\n๐Ÿ“– Use isNaN(x) or Number.isNaN(x) instead." + ), + url("https://eslint.org/docs/latest/rules/use-isnan") + )] + UseIsNan { + #[label("Use isNaN() instead of comparing to NaN")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Missing break in switch case (no-fallthrough) + #[diagnostic( + code(andromeda::lint::no_fallthrough), + help( + "๐Ÿ” Add break, return, or throw at the end of this case.\n๐Ÿ’ก Fallthrough cases can lead to unexpected behavior.\n๐Ÿ“– Add a comment '// fallthrough' if intentional." + ), + url("https://eslint.org/docs/latest/rules/no-fallthrough") + )] + NoFallthrough { + #[label("Case falls through without break/return/throw")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Function reassignment (no-func-assign) + #[diagnostic( + code(andromeda::lint::no_func_assign), + help( + "๐Ÿ” Avoid reassigning function declarations.\n๐Ÿ’ก Reassigning functions can lead to confusing code.\n๐Ÿ› This may indicate a logical error." + ), + url("https://eslint.org/docs/latest/rules/no-func-assign") + )] + NoFuncAssign { + #[label("Reassignment to function '{function_name}'")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + function_name: String, + }, + + /// Unsafe negation (no-unsafe-negation) + #[diagnostic( + code(andromeda::lint::no_unsafe_negation), + help( + "๐Ÿ” Use parentheses to clarify negation intent.\n๐Ÿ’ก Negating the left operand of relational operators is often a mistake.\n๐Ÿ“– Did you mean !(a in b) instead of !a in b?" + ), + url("https://eslint.org/docs/latest/rules/no-unsafe-negation") + )] + NoUnsafeNegation { + #[label("Unsafe negation of left operand")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Sparse arrays (no-sparse-arrays) + #[diagnostic( + code(andromeda::lint::no_sparse_arrays), + help( + "๐Ÿ” Remove extra commas in array literals.\n๐Ÿ’ก Sparse arrays have undefined 'holes' which can cause bugs.\n๐Ÿ“– Use explicit undefined values if needed." + ), + url("https://eslint.org/docs/latest/rules/no-sparse-arrays") + )] + NoSparseArrays { + #[label("Sparse array detected")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Exception parameter reassignment (no-ex-assign) + #[diagnostic( + code(andromeda::lint::no_ex_assign), + help( + "๐Ÿ” Avoid reassigning exception parameters in catch clauses.\n๐Ÿ’ก This can lead to confusing code and lost error information.\n๐Ÿ“– Use a different variable if you need to modify the value." + ), + url("https://eslint.org/docs/latest/rules/no-ex-assign") + )] + NoExAssign { + #[label("Reassignment to exception parameter")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Async Promise executor (no-async-promise-executor) + #[diagnostic( + code(andromeda::lint::no_async_promise_executor), + help( + "๐Ÿ” Don't use async functions as Promise executors.\n๐Ÿ’ก Async executors can hide errors and lead to unhandled rejections.\n๐Ÿ“– Use regular functions and return promises explicitly." + ), + url("https://eslint.org/docs/latest/rules/no-async-promise-executor") + )] + NoAsyncPromiseExecutor { + #[label("Async function used as Promise executor")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, + + /// Unsafe finally (no-unsafe-finally) + #[diagnostic( + code(andromeda::lint::no_unsafe_finally), + help( + "๐Ÿ” Avoid return, throw, break, or continue in finally blocks.\n๐Ÿ’ก Control flow statements in finally can override earlier returns/throws.\n๐Ÿ› This can mask errors and lead to unexpected behavior." + ), + url("https://eslint.org/docs/latest/rules/no-unsafe-finally") + )] + NoUnsafeFinally { + #[label("Unsafe control flow in finally block")] + span: SourceSpan, + #[source_code] + source_code: NamedSource, + }, } impl std::fmt::Display for LintError { @@ -240,6 +523,27 @@ impl std::fmt::Display for LintError { LintError::NoBooleanLiteralForArguments { value, .. } => { write!(f, "Boolean literal '{value}' passed as argument") } + LintError::NoUnreachable { .. } => write!(f, "Unreachable code detected"), + LintError::NoDuplicateCase { .. } => write!(f, "Duplicate case label in switch"), + LintError::NoConstantCondition { .. } => write!(f, "Constant condition in expression"), + LintError::NoDupeKeys { key, .. } => write!(f, "Duplicate key '{key}' in object"), + LintError::NoCompareNegZero { .. } => write!(f, "Do not compare against -0"), + LintError::NoCondAssign { .. } => write!(f, "Assignment in conditional expression"), + LintError::NoConstAssign { variable_name, .. } => { + write!(f, "Assignment to const variable '{variable_name}'") + } + LintError::UseIsNan { .. } => write!(f, "Use isNaN() for NaN comparisons"), + LintError::NoFallthrough { .. } => write!(f, "Case falls through without break"), + LintError::NoFuncAssign { function_name, .. } => { + write!(f, "Reassignment to function '{function_name}'") + } + LintError::NoUnsafeNegation { .. } => write!(f, "Unsafe negation of left operand"), + LintError::NoSparseArrays { .. } => write!(f, "Sparse array detected"), + LintError::NoExAssign { .. } => write!(f, "Reassignment to exception parameter"), + LintError::NoAsyncPromiseExecutor { .. } => { + write!(f, "Async function used as Promise executor") + } + LintError::NoUnsafeFinally { .. } => write!(f, "Unsafe control flow in finally block"), } } } @@ -275,6 +579,21 @@ fn is_rule_enabled(rule_name: &str, lint_config: &LintConfig) -> bool { "require-await", "no-eval", "no-empty", + "no-unreachable", + "no-duplicate-case", + "no-constant-condition", + "no-dupe-keys", + "no-compare-neg-zero", + "no-cond-assign", + "no-const-assign", + "use-isnan", + "no-fallthrough", + "no-func-assign", + "no-unsafe-negation", + "no-sparse-arrays", + "no-ex-assign", + "no-async-promise-executor", + "no-unsafe-finally", ]; default_rules.contains(&rule_name) @@ -289,6 +608,44 @@ fn check_expression_for_issues( lint_config: &LintConfig, ) { match expr { + Expression::ObjectExpression(obj_expr) => { + check_dupe_keys(obj_expr, named_source, lint_errors, lint_config); + } + Expression::ArrayExpression(array_expr) => { + check_sparse_arrays(array_expr, named_source, lint_errors, lint_config); + } + Expression::NewExpression(new_expr) => { + // Check for async Promise executor + if is_rule_enabled("no-async-promise-executor", lint_config) + && let Expression::Identifier(ident) = &new_expr.callee + && ident.name == "Promise" + && let Some(first_arg) = new_expr.arguments.first() + { + if let Some(Expression::ArrowFunctionExpression(arrow)) = first_arg.as_expression() + && arrow.r#async + { + let span = SourceSpan::new( + (arrow.span.start as usize).into(), + arrow.span.size() as usize, + ); + lint_errors.push(LintError::NoAsyncPromiseExecutor { + span, + source_code: named_source.clone(), + }); + } else if let Some(Expression::FunctionExpression(func)) = first_arg.as_expression() + && func.r#async + { + let span = SourceSpan::new( + (func.span.start as usize).into(), + func.span.size() as usize, + ); + lint_errors.push(LintError::NoAsyncPromiseExecutor { + span, + source_code: named_source.clone(), + }); + } + } + } Expression::CallExpression(call) => { // Check for console usage (no-console) if is_rule_enabled("no-console", lint_config) @@ -377,6 +734,15 @@ fn check_expression_for_issues( _ => {} } } + + // Check for comparing against -0 + check_compare_neg_zero(bin_expr, named_source, lint_errors, lint_config); + + // Check for NaN comparisons + check_nan_comparison(bin_expr, named_source, lint_errors, lint_config); + + // Check for unsafe negation (e.g., !x in y) + check_unsafe_negation(bin_expr, named_source, lint_errors, lint_config); check_expression_for_issues( &bin_expr.left, _source_code, @@ -521,6 +887,107 @@ fn check_statement_for_expressions( use oxc_ast::ast::Statement; match stmt { + Statement::TryStatement(try_stmt) => { + // Check for exception parameter reassignment + if let Some(handler) = &try_stmt.handler { + if let Some(param) = &handler.param + && let oxc_ast::ast::BindingPatternKind::BindingIdentifier(ident) = + ¶m.pattern.kind + { + let exception_name = ident.name.to_string(); + check_ex_assign_in_catch( + &handler.body, + &exception_name, + named_source, + lint_errors, + lint_config, + ); + } + + for stmt in &handler.body.body { + check_statement_for_expressions( + stmt, + source_code, + named_source, + lint_errors, + lint_config, + ); + } + } + + for stmt in &try_stmt.block.body { + check_statement_for_expressions( + stmt, + source_code, + named_source, + lint_errors, + lint_config, + ); + } + + if let Some(finalizer) = &try_stmt.finalizer { + check_unsafe_finally(finalizer, named_source, lint_errors, lint_config); + for stmt in &finalizer.body { + check_statement_for_expressions( + stmt, + source_code, + named_source, + lint_errors, + lint_config, + ); + } + } + } + Statement::SwitchStatement(switch_stmt) => { + check_duplicate_cases(switch_stmt, named_source, lint_errors, lint_config); + + // Check for fallthrough + if is_rule_enabled("no-fallthrough", lint_config) { + for (i, case) in switch_stmt.cases.iter().enumerate() { + if i < switch_stmt.cases.len() - 1 && !case.consequent.is_empty() { + let has_break = case + .consequent + .iter() + .any(|s| matches!(s, Statement::BreakStatement(_))); + let has_return = case + .consequent + .iter() + .any(|s| matches!(s, Statement::ReturnStatement(_))); + let has_throw = case + .consequent + .iter() + .any(|s| matches!(s, Statement::ThrowStatement(_))); + + if !has_break + && !has_return + && !has_throw + && let Some(last_stmt) = case.consequent.last() + { + let span = SourceSpan::new( + (last_stmt.span().start as usize).into(), + last_stmt.span().size() as usize, + ); + lint_errors.push(LintError::NoFallthrough { + span, + source_code: named_source.clone(), + }); + } + } + } + } + + for case in &switch_stmt.cases { + for stmt in &case.consequent { + check_statement_for_expressions( + stmt, + source_code, + named_source, + lint_errors, + lint_config, + ); + } + } + } Statement::ExpressionStatement(expr_stmt) => { check_expression_for_issues( &expr_stmt.expression, @@ -529,6 +996,13 @@ fn check_statement_for_expressions( lint_errors, lint_config, ); + + // Check for assignment in condition (this catches top-level assignments that might be mistakes) + if is_rule_enabled("no-cond-assign", lint_config) + && let Expression::AssignmentExpression(_) = &expr_stmt.expression + { + // This is OK at top level + } } Statement::VariableDeclaration(var_decl) => { for declarator in &var_decl.declarations { @@ -544,6 +1018,23 @@ fn check_statement_for_expressions( } } Statement::IfStatement(if_stmt) => { + // Check for constant condition + check_constant_condition(&if_stmt.test, named_source, lint_errors, lint_config); + + // Check for assignment in condition + if is_rule_enabled("no-cond-assign", lint_config) + && let Expression::AssignmentExpression(_) = &if_stmt.test + { + let span = SourceSpan::new( + (if_stmt.test.span().start as usize).into(), + if_stmt.test.span().size() as usize, + ); + lint_errors.push(LintError::NoCondAssign { + span, + source_code: named_source.clone(), + }); + } + check_expression_for_issues( &if_stmt.test, source_code, @@ -569,6 +1060,8 @@ fn check_statement_for_expressions( } } Statement::BlockStatement(block) => { + check_unreachable_code(&block.body, named_source, lint_errors, lint_config); + for stmt in &block.body { check_statement_for_expressions( stmt, @@ -595,28 +1088,644 @@ fn check_statement_for_expressions( } } -/// Helper function to check for variables that could be const -fn check_prefer_const( +/// Check for unreachable code after return/throw/break/continue +fn check_unreachable_code( statements: &[Statement], - source_code: &str, named_source: &NamedSource, lint_errors: &mut Vec, lint_config: &LintConfig, ) { - let mut let_variables = std::collections::HashSet::new(); - for stmt in statements { - collect_let_variables(stmt, &mut let_variables); + if !is_rule_enabled("no-unreachable", lint_config) { + return; } - let mut reassigned_variables = HashSet::new(); + let mut found_terminal = false; for stmt in statements { - check_for_reassignments(stmt, &mut reassigned_variables); + if found_terminal { + let span = SourceSpan::new( + (stmt.span().start as usize).into(), + stmt.span().size() as usize, + ); + lint_errors.push(LintError::NoUnreachable { + span, + source_code: named_source.clone(), + }); + break; + } + + match stmt { + Statement::ReturnStatement(_) + | Statement::ThrowStatement(_) + | Statement::BreakStatement(_) + | Statement::ContinueStatement(_) => { + found_terminal = true; + } + _ => {} + } } +} - for stmt in statements { - report_prefer_const_violations( - stmt, - &let_variables, +/// Check for duplicate case labels in switch statements +fn check_duplicate_cases( + switch_stmt: &oxc_ast::ast::SwitchStatement, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-duplicate-case", lint_config) { + return; + } + + use std::collections::HashSet; + let mut seen_cases = HashSet::new(); + + for case in &switch_stmt.cases { + if let Some(test) = &case.test { + let case_str = format!("{:?}", test); + if !seen_cases.insert(case_str) { + let span = SourceSpan::new( + (test.span().start as usize).into(), + test.span().size() as usize, + ); + lint_errors.push(LintError::NoDuplicateCase { + span, + source_code: named_source.clone(), + }); + } + } + } +} + +/// Check for constant conditions +fn check_constant_condition( + test_expr: &Expression, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-constant-condition", lint_config) { + return; + } + + match test_expr { + Expression::BooleanLiteral(_) + | Expression::NumericLiteral(_) + | Expression::StringLiteral(_) => { + let span = SourceSpan::new( + (test_expr.span().start as usize).into(), + test_expr.span().size() as usize, + ); + lint_errors.push(LintError::NoConstantCondition { + span, + source_code: named_source.clone(), + }); + } + _ => {} + } +} + +/// Check for duplicate keys in object literals +fn check_dupe_keys( + obj_expr: &oxc_ast::ast::ObjectExpression, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-dupe-keys", lint_config) { + return; + } + + use std::collections::HashMap; + let mut seen_keys: HashMap = HashMap::new(); + + for prop in &obj_expr.properties { + if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(obj_prop) = prop + && let oxc_ast::ast::PropertyKey::StaticIdentifier(ident) = &obj_prop.key + { + let key_name = ident.name.to_string(); + if seen_keys.insert(key_name.clone(), ident.span).is_some() { + let span = SourceSpan::new( + (ident.span.start as usize).into(), + ident.span.size() as usize, + ); + lint_errors.push(LintError::NoDupeKeys { + span, + source_code: named_source.clone(), + key: key_name, + }); + } + } + } +} + +/// Check for comparisons against -0 +fn check_compare_neg_zero( + bin_expr: &oxc_ast::ast::BinaryExpression, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-compare-neg-zero", lint_config) { + return; + } + + let is_neg_zero = |expr: &Expression| -> bool { + if let Expression::UnaryExpression(unary) = expr + && unary.operator == oxc_ast::ast::UnaryOperator::UnaryNegation + && let Expression::NumericLiteral(num) = &unary.argument + { + return num.value == 0.0; + } + false + }; + + if is_neg_zero(&bin_expr.left) || is_neg_zero(&bin_expr.right) { + let span = SourceSpan::new( + (bin_expr.span.start as usize).into(), + bin_expr.span.size() as usize, + ); + lint_errors.push(LintError::NoCompareNegZero { + span, + source_code: named_source.clone(), + }); + } +} + +/// Check for NaN comparisons +fn check_nan_comparison( + bin_expr: &oxc_ast::ast::BinaryExpression, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("use-isnan", lint_config) { + return; + } + + let is_nan = |expr: &Expression| -> bool { + if let Expression::Identifier(ident) = expr { + ident.name == "NaN" + } else { + false + } + }; + + if is_nan(&bin_expr.left) || is_nan(&bin_expr.right) { + let span = SourceSpan::new( + (bin_expr.span.start as usize).into(), + bin_expr.span.size() as usize, + ); + lint_errors.push(LintError::UseIsNan { + span, + source_code: named_source.clone(), + }); + } +} + +/// Check for sparse arrays +fn check_sparse_arrays( + array_expr: &oxc_ast::ast::ArrayExpression, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-sparse-arrays", lint_config) { + return; + } + + for element in &array_expr.elements { + if element.is_elision() { + let span = SourceSpan::new( + (array_expr.span.start as usize).into(), + array_expr.span.size() as usize, + ); + lint_errors.push(LintError::NoSparseArrays { + span, + source_code: named_source.clone(), + }); + break; + } + } +} + +/// Check for unsafe control flow in finally blocks +fn check_unsafe_finally( + finally_block: &oxc_ast::ast::BlockStatement, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-unsafe-finally", lint_config) { + return; + } + + fn check_statements_recursive( + stmts: &[Statement], + named_source: &NamedSource, + lint_errors: &mut Vec, + ) { + for stmt in stmts { + match stmt { + Statement::ReturnStatement(_) + | Statement::ThrowStatement(_) + | Statement::BreakStatement(_) + | Statement::ContinueStatement(_) => { + let span = SourceSpan::new( + (stmt.span().start as usize).into(), + stmt.span().size() as usize, + ); + lint_errors.push(LintError::NoUnsafeFinally { + span, + source_code: named_source.clone(), + }); + } + Statement::BlockStatement(block) => { + check_statements_recursive(&block.body, named_source, lint_errors); + } + Statement::IfStatement(if_stmt) => { + check_statements_recursive( + std::slice::from_ref(&if_stmt.consequent), + named_source, + lint_errors, + ); + if let Some(alt) = &if_stmt.alternate { + check_statements_recursive( + std::slice::from_ref(alt), + named_source, + lint_errors, + ); + } + } + _ => {} + } + } + } + + check_statements_recursive(&finally_block.body, named_source, lint_errors); +} + +/// Check for exception parameter reassignment in catch clause +fn check_ex_assign_in_catch( + catch_body: &oxc_ast::ast::BlockStatement, + exception_name: &str, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-ex-assign", lint_config) { + return; + } + + fn check_expr_for_ex_assign( + expr: &Expression, + exception_name: &str, + named_source: &NamedSource, + lint_errors: &mut Vec, + ) { + use oxc_ast::ast::{AssignmentTarget, Expression}; + + match expr { + Expression::AssignmentExpression(assign) => { + if let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign.left + && id.name == exception_name + { + let span = + SourceSpan::new((id.span.start as usize).into(), id.span.size() as usize); + lint_errors.push(LintError::NoExAssign { + span, + source_code: named_source.clone(), + }); + } + } + Expression::UpdateExpression(update) => { + if let oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(id) = + &update.argument + && id.name == exception_name + { + let span = + SourceSpan::new((id.span.start as usize).into(), id.span.size() as usize); + lint_errors.push(LintError::NoExAssign { + span, + source_code: named_source.clone(), + }); + } + } + _ => {} + } + } + + fn check_stmt_recursive( + stmt: &Statement, + exception_name: &str, + named_source: &NamedSource, + lint_errors: &mut Vec, + ) { + match stmt { + Statement::ExpressionStatement(expr_stmt) => { + check_expr_for_ex_assign( + &expr_stmt.expression, + exception_name, + named_source, + lint_errors, + ); + } + Statement::BlockStatement(block) => { + for s in &block.body { + check_stmt_recursive(s, exception_name, named_source, lint_errors); + } + } + Statement::IfStatement(if_stmt) => { + check_stmt_recursive( + &if_stmt.consequent, + exception_name, + named_source, + lint_errors, + ); + if let Some(alt) = &if_stmt.alternate { + check_stmt_recursive(alt, exception_name, named_source, lint_errors); + } + } + _ => {} + } + } + + for stmt in &catch_body.body { + check_stmt_recursive(stmt, exception_name, named_source, lint_errors); + } +} + +/// Check for unsafe negation in relational expressions +fn check_unsafe_negation( + bin_expr: &oxc_ast::ast::BinaryExpression, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + if !is_rule_enabled("no-unsafe-negation", lint_config) { + return; + } + + // Check if this is a relational operator (in, instanceof) + let is_relational = matches!( + bin_expr.operator, + oxc_ast::ast::BinaryOperator::In | oxc_ast::ast::BinaryOperator::Instanceof + ); + + if is_relational { + // Check if left operand is negation + if let Expression::UnaryExpression(unary) = &bin_expr.left + && unary.operator == oxc_ast::ast::UnaryOperator::LogicalNot + { + let span = SourceSpan::new( + (bin_expr.span.start as usize).into(), + bin_expr.span.size() as usize, + ); + lint_errors.push(LintError::NoUnsafeNegation { + span, + source_code: named_source.clone(), + }); + } + } +} + +/// Check for const and function reassignments +fn check_const_and_func_reassignments( + statements: &[Statement], + const_variables: &HashSet, + function_declarations: &HashSet, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + fn check_expression_for_const_reassign( + expr: &Expression, + const_variables: &HashSet, + function_declarations: &HashSet, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, + ) { + use oxc_ast::ast::{AssignmentTarget, Expression}; + + match expr { + Expression::AssignmentExpression(assign) => { + if let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign.left { + let var_name = id.name.to_string(); + + // Check for const reassignment + if const_variables.contains(&var_name) + && is_rule_enabled("no-const-assign", lint_config) + { + let span = SourceSpan::new( + (id.span.start as usize).into(), + id.span.size() as usize, + ); + lint_errors.push(LintError::NoConstAssign { + span, + source_code: named_source.clone(), + variable_name: var_name.clone(), + }); + } + + // Check for function reassignment + if function_declarations.contains(&var_name) + && is_rule_enabled("no-func-assign", lint_config) + { + let span = SourceSpan::new( + (id.span.start as usize).into(), + id.span.size() as usize, + ); + lint_errors.push(LintError::NoFuncAssign { + span, + source_code: named_source.clone(), + function_name: var_name, + }); + } + } + + check_expression_for_const_reassign( + &assign.right, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + Expression::UpdateExpression(update) => { + if let oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(id) = + &update.argument + { + let var_name = id.name.to_string(); + + if const_variables.contains(&var_name) + && is_rule_enabled("no-const-assign", lint_config) + { + let span = SourceSpan::new( + (id.span.start as usize).into(), + id.span.size() as usize, + ); + lint_errors.push(LintError::NoConstAssign { + span, + source_code: named_source.clone(), + variable_name: var_name, + }); + } + } + } + Expression::CallExpression(call) => { + check_expression_for_const_reassign( + &call.callee, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + for arg in &call.arguments { + if let Some(expr) = arg.as_expression() { + check_expression_for_const_reassign( + expr, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + } + } + Expression::BinaryExpression(bin) => { + check_expression_for_const_reassign( + &bin.left, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + check_expression_for_const_reassign( + &bin.right, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + _ => {} + } + } + + fn check_statement_recursive( + stmt: &Statement, + const_variables: &HashSet, + function_declarations: &HashSet, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, + ) { + match stmt { + Statement::ExpressionStatement(expr_stmt) => { + check_expression_for_const_reassign( + &expr_stmt.expression, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + Statement::BlockStatement(block) => { + for stmt in &block.body { + check_statement_recursive( + stmt, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + } + Statement::IfStatement(if_stmt) => { + check_statement_recursive( + &if_stmt.consequent, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + if let Some(alt) = &if_stmt.alternate { + check_statement_recursive( + alt, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + } + Statement::ForStatement(for_stmt) => { + check_statement_recursive( + &for_stmt.body, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + Statement::WhileStatement(while_stmt) => { + check_statement_recursive( + &while_stmt.body, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } + _ => {} + } + } + + for stmt in statements { + check_statement_recursive( + stmt, + const_variables, + function_declarations, + named_source, + lint_errors, + lint_config, + ); + } +} + +/// Helper function to check for variables that could be const +fn check_prefer_const( + statements: &[Statement], + source_code: &str, + named_source: &NamedSource, + lint_errors: &mut Vec, + lint_config: &LintConfig, +) { + let mut let_variables = std::collections::HashSet::new(); + for stmt in statements { + collect_let_variables(stmt, &mut let_variables); + } + + let mut reassigned_variables = HashSet::new(); + for stmt in statements { + check_for_reassignments(stmt, &mut reassigned_variables); + } + + for stmt in statements { + report_prefer_const_violations( + stmt, + &let_variables, &reassigned_variables, source_code, named_source, @@ -904,6 +2013,9 @@ pub fn lint_file_content_with_config( let source_name = path.display().to_string(); let named_source = NamedSource::new(source_name.clone(), content.to_string()); + // Check for unreachable code at program level + check_unreachable_code(&program.body, &named_source, &mut lint_errors, lint_config); + for stmt in &program.body { check_statement_for_expressions( stmt, @@ -1031,11 +2143,26 @@ pub fn lint_file_content_with_config( let semantic = SemanticBuilder::new().build(program); let scoping = semantic.semantic.scoping(); + + // Collect const and function declarations + let mut const_variables = HashSet::new(); + let mut function_declarations = HashSet::new(); + for symbol_id in scoping.symbol_ids() { let flags = scoping.symbol_flags(symbol_id); let name = scoping.symbol_name(symbol_id); let symbol_span = scoping.symbol_span(symbol_id); + // Track const variables + if flags.contains(SymbolFlags::ConstVariable) { + const_variables.insert(name.to_string()); + } + + // Track function declarations + if flags.contains(SymbolFlags::Function) { + function_declarations.insert(name.to_string()); + } + if flags.intersects( SymbolFlags::BlockScopedVariable | SymbolFlags::ConstVariable @@ -1057,6 +2184,16 @@ pub fn lint_file_content_with_config( } } + // Check for const reassignments and function reassignments + check_const_and_func_reassignments( + &program.body, + &const_variables, + &function_declarations, + &named_source, + &mut lint_errors, + lint_config, + ); + Ok(lint_errors) } diff --git a/cli/src/lsp/diagnostic_converter.rs b/cli/src/lsp/diagnostic_converter.rs index 6a1c4e8..d8b20bb 100644 --- a/cli/src/lsp/diagnostic_converter.rs +++ b/cli/src/lsp/diagnostic_converter.rs @@ -105,6 +105,126 @@ pub fn lint_error_to_diagnostic(lint_error: &LintError, source_code: &str) -> Di )), Some("andromeda".to_string()), ), + LintError::NoUnreachable { .. } => ( + "Unreachable code detected".to_string(), + DiagnosticSeverity::WARNING, + Some(NumberOrString::String( + "andromeda::lint::no-unreachable".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoDuplicateCase { .. } => ( + "Duplicate case label in switch".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-duplicate-case".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoConstantCondition { .. } => ( + "Constant condition in expression".to_string(), + DiagnosticSeverity::WARNING, + Some(NumberOrString::String( + "andromeda::lint::no-constant-condition".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoDupeKeys { key, .. } => ( + format!("Duplicate key '{key}' in object"), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-dupe-keys".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoCompareNegZero { .. } => ( + "Do not compare against -0".to_string(), + DiagnosticSeverity::WARNING, + Some(NumberOrString::String( + "andromeda::lint::no-compare-neg-zero".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoCondAssign { .. } => ( + "Assignment in conditional expression".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-cond-assign".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoConstAssign { variable_name, .. } => ( + format!("Assignment to const variable '{variable_name}'"), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-const-assign".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::UseIsNan { .. } => ( + "Use isNaN() for NaN comparisons".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::use-isnan".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoFallthrough { .. } => ( + "Case falls through without break".to_string(), + DiagnosticSeverity::WARNING, + Some(NumberOrString::String( + "andromeda::lint::no-fallthrough".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoFuncAssign { function_name, .. } => ( + format!("Reassignment to function '{function_name}'"), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-func-assign".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoUnsafeNegation { .. } => ( + "Unsafe negation of left operand".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-unsafe-negation".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoSparseArrays { .. } => ( + "Sparse array detected".to_string(), + DiagnosticSeverity::WARNING, + Some(NumberOrString::String( + "andromeda::lint::no-sparse-arrays".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoExAssign { .. } => ( + "Reassignment to exception parameter".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-ex-assign".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoAsyncPromiseExecutor { .. } => ( + "Async function used as Promise executor".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-async-promise-executor".to_string(), + )), + Some("andromeda".to_string()), + ), + LintError::NoUnsafeFinally { .. } => ( + "Unsafe control flow in finally block".to_string(), + DiagnosticSeverity::ERROR, + Some(NumberOrString::String( + "andromeda::lint::no-unsafe-finally".to_string(), + )), + Some("andromeda".to_string()), + ), }; let span = get_lint_error_span(lint_error); @@ -138,6 +258,21 @@ fn get_lint_error_span(lint_error: &LintError) -> SourceSpan { LintError::Eqeqeq { span, .. } => *span, LintError::Camelcase { span, .. } => *span, LintError::NoBooleanLiteralForArguments { span, .. } => *span, + LintError::NoUnreachable { span, .. } => *span, + LintError::NoDuplicateCase { span, .. } => *span, + LintError::NoConstantCondition { span, .. } => *span, + LintError::NoDupeKeys { span, .. } => *span, + LintError::NoCompareNegZero { span, .. } => *span, + LintError::NoCondAssign { span, .. } => *span, + LintError::NoConstAssign { span, .. } => *span, + LintError::UseIsNan { span, .. } => *span, + LintError::NoFallthrough { span, .. } => *span, + LintError::NoFuncAssign { span, .. } => *span, + LintError::NoUnsafeNegation { span, .. } => *span, + LintError::NoSparseArrays { span, .. } => *span, + LintError::NoExAssign { span, .. } => *span, + LintError::NoAsyncPromiseExecutor { span, .. } => *span, + LintError::NoUnsafeFinally { span, .. } => *span, } }