├── .cargo └── config.toml ├── .github ├── dependabot.yml ├── foo.ts ├── verdaccio-config.yml └── workflows │ ├── build.yml │ ├── publish-dry-run.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── MAINTENANCE.md ├── README.md ├── crates ├── fta-wasm │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── lib_tests.rs └── fta │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ ├── config │ ├── mod.rs │ └── tests.rs │ ├── cyclo │ ├── mod.rs │ └── tests.rs │ ├── halstead │ ├── mod.rs │ └── tests.rs │ ├── lib.rs │ ├── main.rs │ ├── output │ ├── mod.rs │ └── tests.rs │ ├── parse │ ├── mod.rs │ └── tests.rs │ ├── structs │ └── mod.rs │ ├── utils │ ├── mod.rs │ └── tests.rs │ └── walk │ └── mod.rs ├── fta-logo.png ├── package.json ├── packages └── fta │ ├── .gitignore │ ├── .npmignore │ ├── @types │ └── fta-cli.d.ts │ ├── README.md │ ├── binaries │ └── README.md │ ├── check.js │ ├── index.js │ ├── package.json │ └── targets.js └── yarn.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | jobs = 8 3 | 4 | rustdocflags = ["--cfg", "docsrs"] 5 | rustflags = [] 6 | 7 | [target.aarch64-apple-darwin] 8 | rustflags = [] 9 | 10 | [target.aarch64-unknown-linux-musl] 11 | linker = "aarch64-linux-musl-gcc" 12 | rustflags = ["-C", "target-feature=-crt-static", "-C", "link-arg=-lgcc"] 13 | 14 | [target.aarch64-linux-android] 15 | rustflags = [] 16 | 17 | [target.x86_64-pc-windows-msvc] 18 | linker = "rust-lld" 19 | 20 | [target.aarch64-pc-windows-msvc] 21 | linker = "rust-lld" 22 | rustflags = [] 23 | 24 | [target.wasm32-unknown-unknown] 25 | rustflags = [] 26 | 27 | [target.arm-unknown-linux-musleabi] 28 | linker = "arm-linux-gnueabi-gcc" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/foo.ts: -------------------------------------------------------------------------------- 1 | export type SimpleGraph = Map; 2 | 3 | export const breadthFirstSearch = (graph: SimpleGraph, start: T): T[] => { 4 | let bfsOrder: T[] = []; 5 | 6 | const bfs = (queue: T[], visited: Set) => { 7 | if (queue.length === 0) return; 8 | 9 | const [node, ...rest] = queue; 10 | bfsOrder = [...bfsOrder, node]; 11 | 12 | const unvisitedNeighbors = 13 | graph.get(node)?.filter((neighbor) => !visited.has(neighbor)) || []; 14 | const newQueue = [...rest, ...unvisitedNeighbors]; 15 | const newVisited = new Set([...visited, ...unvisitedNeighbors]); 16 | 17 | bfs(newQueue, newVisited); 18 | }; 19 | 20 | bfs([start], new Set([start])); 21 | 22 | return bfsOrder; 23 | }; 24 | -------------------------------------------------------------------------------- /.github/verdaccio-config.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | auth-memory: 3 | users: 4 | foo: 5 | name: test 6 | password: test 7 | store: 8 | memory: 9 | limit: 1000 10 | ## we don't need any remote request 11 | uplinks: 12 | packages: 13 | '@*/*': 14 | access: $all 15 | publish: $all 16 | '**': 17 | access: $all 18 | publish: $all 19 | middlewares: 20 | audit: 21 | enabled: true 22 | max_body_size: 100mb 23 | log: 24 | - {type: stdout, format: pretty, level: trace} -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Binaries & Upload Artifacts 2 | 3 | on: workflow_call 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | CARGO_INCREMENTAL: 0 8 | 9 | jobs: 10 | upload_assets_macos: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Cache Cargo packages 16 | uses: actions/cache@v4 17 | continue-on-error: false 18 | with: 19 | path: | 20 | ~/.cargo/bin/ 21 | ~/.cargo/registry/index/ 22 | ~/.cargo/registry/cache/ 23 | ~/.cargo/git/db/ 24 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 25 | 26 | - name: Setup Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | components: rustfmt, clippy 31 | target: x86_64-apple-darwin 32 | default: true 33 | 34 | - name: Install dependencies 35 | run: brew install llvm 36 | 37 | - name: Build x86_64-apple-darwin 38 | run: cargo build --release --target=x86_64-apple-darwin 39 | 40 | - name: Setup Rust for aarch64-apple-darwin 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | toolchain: stable 44 | target: aarch64-apple-darwin 45 | default: true 46 | 47 | - name: Build aarch64-apple-darwin 48 | run: cargo build --release --target=aarch64-apple-darwin 49 | 50 | - name: Set permissions for macOS binaries 51 | run: | 52 | chmod +x target/x86_64-apple-darwin/release/fta 53 | chmod +x target/aarch64-apple-darwin/release/fta 54 | 55 | - name: Create tarballs and move binaries 56 | run: | 57 | tar czvf fta-x86_64-apple-darwin.tar.gz -C target/x86_64-apple-darwin/release fta 58 | tar czvf fta-aarch64-apple-darwin.tar.gz -C target/aarch64-apple-darwin/release fta 59 | 60 | - name: Upload binaries as artifacts 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: macos-binaries 64 | path: | 65 | *.tar.gz 66 | 67 | upload_assets_windows: 68 | runs-on: windows-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: Cache Cargo packages 73 | uses: actions/cache@v4 74 | continue-on-error: false 75 | with: 76 | path: | 77 | ~/.cargo/bin/ 78 | ~/.cargo/registry/index/ 79 | ~/.cargo/registry/cache/ 80 | ~/.cargo/git/db/ 81 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 82 | 83 | - name: Setup Rust for x86_64-pc-windows-msvc 84 | uses: actions-rs/toolchain@v1 85 | with: 86 | toolchain: stable 87 | components: rustfmt, clippy 88 | target: x86_64-pc-windows-msvc 89 | default: true 90 | override: true 91 | 92 | - name: Build x86_64-pc-windows-msvc 93 | run: cargo build --release --target=x86_64-pc-windows-msvc 94 | 95 | - name: Setup Rust for aarch64-pc-windows-msvc 96 | uses: actions-rs/toolchain@v1 97 | with: 98 | toolchain: stable 99 | target: aarch64-pc-windows-msvc 100 | default: true 101 | override: true 102 | 103 | - name: Build aarch64-pc-windows-msvc 104 | run: cargo build --release --target=aarch64-pc-windows-msvc 105 | 106 | - name: Create zipfiles and move binaries 107 | shell: pwsh 108 | run: | 109 | Compress-Archive -Path target/x86_64-pc-windows-msvc/release/fta.exe -DestinationPath fta-x86_64-pc-windows-msvc.zip 110 | Compress-Archive -Path target/aarch64-pc-windows-msvc/release/fta.exe -DestinationPath fta-aarch64-pc-windows-msvc.zip 111 | 112 | - name: Upload binaries as artifacts 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: windows-binaries 116 | path: | 117 | *.zip 118 | 119 | upload_assets_linux: 120 | runs-on: ubuntu-latest 121 | steps: 122 | - uses: actions/checkout@v4 123 | 124 | - name: Cache Cargo packages 125 | uses: actions/cache@v4 126 | continue-on-error: false 127 | with: 128 | path: | 129 | ~/.cargo/bin/ 130 | ~/.cargo/registry/index/ 131 | ~/.cargo/registry/cache/ 132 | ~/.cargo/git/db/ 133 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 134 | 135 | - name: Update packages 136 | run: sudo apt-get update 137 | 138 | - name: Install aarch64 dependencies 139 | run: sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross 140 | 141 | - name: Install aarch64-unknown-linux-musl dependencies 142 | run: | 143 | sudo apt-get install -y musl-tools 144 | sudo apt-get install gcc-arm-linux-gnueabi 145 | 146 | - name: Install Rust 147 | uses: actions-rs/toolchain@v1 148 | with: 149 | toolchain: stable 150 | components: rustfmt, clippy 151 | profile: minimal 152 | target: x86_64-unknown-linux-musl 153 | 154 | - name: Add Rust targets 155 | run: | 156 | rustup target add x86_64-unknown-linux-musl 157 | rustup target add aarch64-unknown-linux-musl 158 | rustup target add arm-unknown-linux-musleabi 159 | 160 | - name: Install MUSL toolchain for AArch64 161 | run: | 162 | wget -q https://musl.cc/aarch64-linux-musl-cross.tgz 163 | tar -xf aarch64-linux-musl-cross.tgz 164 | echo "$(pwd)/aarch64-linux-musl-cross/bin" >> $GITHUB_PATH 165 | 166 | - name: Build and tarball 167 | run: | 168 | TARGETS=( 169 | x86_64-unknown-linux-musl 170 | aarch64-unknown-linux-musl 171 | arm-unknown-linux-musleabi 172 | ) 173 | 174 | for TARGET in "${TARGETS[@]}"; do 175 | echo "Building for $TARGET" 176 | cargo build --release --target="$TARGET" 177 | chmod +x target/${TARGET}/release/fta 178 | tar czf "fta-${TARGET}.tar.gz" -C "./target/${TARGET}/release/" fta 179 | done 180 | 181 | - name: Upload binaries as artifacts 182 | uses: actions/upload-artifact@v4 183 | with: 184 | name: linux-binaries 185 | path: | 186 | *.tar.gz 187 | -------------------------------------------------------------------------------- /.github/workflows/publish-dry-run.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package (Dry Run) 2 | 3 | on: workflow_call 4 | 5 | jobs: 6 | publish_fta_cli_dry_run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Setup Node.js environment 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | 17 | - name: Download macOS artifacts 18 | uses: actions/download-artifact@v4 19 | with: 20 | name: macos-binaries 21 | path: artifact/ 22 | 23 | - name: Download linux artifacts 24 | uses: actions/download-artifact@v4 25 | with: 26 | name: linux-binaries 27 | path: artifact/ 28 | 29 | - name: Download windows artifacts 30 | uses: actions/download-artifact@v4 31 | with: 32 | name: windows-binaries 33 | path: artifact/ 34 | 35 | - name: Extract nix artifacts 36 | run: | 37 | for file in artifact/*.tar.gz; do 38 | base=$(basename -- "$file") 39 | dirname="${base%%.*}" 40 | mkdir -p packages/fta/binaries/"$dirname" 41 | tar -xzf "$file" -C packages/fta/binaries/"$dirname" 42 | done 43 | 44 | - name: Extract artifacts 45 | run: | 46 | for file in artifact/*.zip; do 47 | dir=$(basename "$file" .zip) 48 | mkdir -p "packages/fta/binaries/$dir" 49 | unzip -o "$file" -d "packages/fta/binaries/$dir" 50 | done 51 | 52 | # List out the binaries dir 53 | ls -R packages/fta/binaries/ 54 | 55 | - name: Install Verdaccio 56 | run: npm install -g verdaccio verdaccio-memory verdaccio-auth-memory 57 | 58 | - name: Setup Verdaccio Config 59 | run: | 60 | mkdir -p $HOME/.config/verdaccio 61 | cp .github/verdaccio-config.yml $HOME/.config/verdaccio/config.yml 62 | 63 | - name: Start Verdaccio 64 | run: | 65 | npx verdaccio --config $HOME/.config/verdaccio/config.yml --listen 4873 & 66 | sleep 10 67 | 68 | - name: Publish package 69 | run: | 70 | npm config set registry http://localhost:4873/ 71 | npm config set //localhost:4873/:_authToken "$(echo -n 'test:test' | base64)" 72 | cd packages/fta 73 | npm publish --registry http://localhost:4873 74 | cd ../ 75 | 76 | - name: Install and check package 77 | run: | 78 | # Install FTA via the CLI package 79 | npm install fta-cli --registry http://localhost:4873 80 | 81 | # Verify the output is what we expect 82 | sudo apt-get install -y jq 83 | EXPECTED_OUTPUT=$(cat <<'EOF' 84 | [{"file_name":"foo.ts","cyclo":3,"halstead":{"uniq_operators":13,"uniq_operands":21,"total_operators":39,"total_operands":44,"program_length":83,"vocabulary_size":34,"volume":422.25941582377817,"difficulty":12.571428571428571,"effort":5308.404084641783,"time":294.9113380356546,"bugs":0.14075313860792607},"line_count":16,"fta_score":36.502594866022214,"assessment":"OK"}] 85 | EOF 86 | ) 87 | OUTPUT=$(npx fta-cli .github --json) 88 | if [ "$(echo "$OUTPUT" | jq --sort-keys '.')" == "$(echo "$EXPECTED_OUTPUT" | jq --sort-keys '.')" ]; then 89 | echo "$OUTPUT" 90 | echo "Output matches expected" 91 | else 92 | echo "Output does not match expected." 93 | echo "Expected:" 94 | echo "$EXPECTED_OUTPUT" 95 | echo "Got:" 96 | echo "$OUTPUT" 97 | exit 1 98 | fi 99 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build Binaries & Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | CARGO_INCREMENTAL: 0 11 | 12 | jobs: 13 | call_build_binaries: 14 | uses: ./.github/workflows/build.yml 15 | 16 | create_github_release: 17 | needs: [call_build_binaries] 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Get version 23 | id: get_version 24 | run: | 25 | VERSION=$(grep '^version =' crates/fta/Cargo.toml | sed 's/^version = "\(.*\)"/\1/') 26 | echo "Version: $VERSION" 27 | echo "FTA_VERSION=$VERSION" >> $GITHUB_OUTPUT 28 | 29 | - name: Create GitHub release 30 | id: create_release 31 | uses: softprops/action-gh-release@v2 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | tag_name: ${{ github.ref }} 36 | name: v${{ steps.get_version.outputs.FTA_VERSION }} 37 | draft: true 38 | prerelease: false 39 | 40 | - name: Download macOS artifacts 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: macos-binaries 44 | 45 | - name: Download windows artifacts 46 | uses: actions/download-artifact@v4 47 | with: 48 | name: windows-binaries 49 | 50 | - name: Download linux artifacts 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: linux-binaries 54 | 55 | - name: Upload all assets 56 | uses: softprops/action-gh-release@v2 57 | with: 58 | files: | 59 | fta-x86_64-apple-darwin.tar.gz 60 | fta-aarch64-apple-darwin.tar.gz 61 | fta-x86_64-pc-windows-msvc.zip 62 | fta-aarch64-pc-windows-msvc.zip 63 | fta-aarch64-unknown-linux-musl.tar.gz 64 | fta-x86_64-unknown-linux-musl.tar.gz 65 | fta-arm-unknown-linux-musleabi.tar.gz 66 | 67 | publish_rust_crate: 68 | runs-on: ubuntu-latest 69 | needs: [create_github_release] 70 | steps: 71 | - uses: actions/checkout@v4 72 | - name: Update packages 73 | run: sudo apt-get update 74 | - name: Install Rust 75 | uses: actions-rs/toolchain@v1 76 | with: 77 | toolchain: stable 78 | components: rustfmt, clippy 79 | profile: minimal 80 | target: x86_64-unknown-linux-musl 81 | - name: Publish to crates.io 82 | env: 83 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 84 | run: | 85 | cargo publish --token $CARGO_REGISTRY_TOKEN 86 | working-directory: crates/fta 87 | 88 | publish_fta_cli: 89 | needs: [create_github_release] 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: Checkout code 93 | uses: actions/checkout@v4 94 | 95 | - name: Setup Node.js environment 96 | uses: actions/setup-node@v4 97 | with: 98 | node-version: "20.x" 99 | registry-url: "https://registry.npmjs.org" 100 | 101 | - name: Download macOS artifacts 102 | uses: actions/download-artifact@v4 103 | with: 104 | name: macos-binaries 105 | path: artifact/ 106 | 107 | - name: Download linux artifacts 108 | uses: actions/download-artifact@v4 109 | with: 110 | name: linux-binaries 111 | path: artifact/ 112 | 113 | - name: Download windows artifacts 114 | uses: actions/download-artifact@v4 115 | with: 116 | name: windows-binaries 117 | path: artifact/ 118 | 119 | - name: Extract .tar.gz artifacts 120 | run: | 121 | for file in artifact/*.tar.gz; do 122 | base=$(basename -- "$file") 123 | dirname="${base%%.*}" 124 | mkdir -p packages/fta/binaries/"$dirname" 125 | tar -xzf "$file" -C packages/fta/binaries/"$dirname" 126 | done 127 | 128 | - name: Extract .zip artifacts 129 | run: | 130 | for file in artifact/*.zip; do 131 | dir=$(basename "$file" .zip) 132 | mkdir -p "packages/fta/binaries/$dir" 133 | unzip -o "$file" -d "packages/fta/binaries/$dir" 134 | done 135 | 136 | # List out the binaries dir 137 | ls -R packages/fta/binaries/ 138 | 139 | - name: Publish to npm 140 | run: npm publish 141 | working-directory: packages/fta 142 | env: 143 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 144 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Rust package and built binaries 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | CARGO_INCREMENTAL: 0 12 | 13 | jobs: 14 | test_rust_crate: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Basic build validation 27 | run: | 28 | cargo build 29 | 30 | - name: Check program 31 | run: | 32 | cargo check 33 | 34 | - name: Check formatting 35 | uses: actions-rs/cargo@v1 36 | with: 37 | command: fmt 38 | args: --all -- --check 39 | 40 | - name: Run tests 41 | uses: actions-rs/cargo@v1 42 | with: 43 | command: test 44 | args: --all-features 45 | 46 | - name: Output test coverage 47 | run: | 48 | cargo install cargo-tarpaulin 49 | cargo tarpaulin --fail-under 75 50 | 51 | build_binaries: 52 | uses: ./.github/workflows/build.yml 53 | 54 | test_built_binaries: 55 | needs: build_binaries 56 | runs-on: ${{ matrix.os }} 57 | strategy: 58 | matrix: 59 | os: [macos-latest, windows-latest, ubuntu-latest] 60 | include: 61 | - os: macos-latest 62 | artifact: macos-binaries 63 | - os: windows-latest 64 | artifact: windows-binaries 65 | - os: ubuntu-latest 66 | artifact: linux-binaries 67 | steps: 68 | - uses: actions/checkout@v4 69 | 70 | - name: Download artifact 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: ${{ matrix.artifact }} 74 | path: artifact/ 75 | 76 | - name: Extract artifact (Windows) 77 | if: runner.os == 'Windows' 78 | shell: pwsh 79 | run: | 80 | $sourcePath = "artifact" 81 | 82 | # Get all .zip files from the sourcePath directory 83 | $files = Get-ChildItem -Path $sourcePath -Recurse -Include "*.zip" 84 | 85 | # Loop through each .zip file 86 | foreach ($file in $files) { 87 | # Extract the file to the current directory 88 | Expand-Archive -Path $file.FullName -DestinationPath "." -Force 89 | } 90 | 91 | - name: Extract artifact (Linux/MacOS) 92 | if: runner.os != 'Windows' 93 | shell: bash 94 | run: | 95 | for file in artifact/*; do 96 | tar -xf "$file" 97 | done 98 | 99 | - name: Create sample folder and copy foo.ts 100 | shell: bash 101 | run: | 102 | rm -rf sample 103 | mkdir sample 104 | cp ./.github/foo.ts sample/foo.ts 105 | 106 | - name: Test binary 107 | shell: bash 108 | run: | 109 | EXPECTED_OUTPUT=$(cat <<'EOF' 110 | [{"file_name":"foo.ts","cyclo":3,"halstead":{"uniq_operators":13,"uniq_operands":21,"total_operators":39,"total_operands":44,"program_length":83,"vocabulary_size":34,"volume":422.25941582377817,"difficulty":12.571428571428571,"effort":5308.404084641783,"time":294.9113380356546,"bugs":0.14075313860792607},"line_count":16,"fta_score":36.502594866022214,"assessment":"OK"}] 111 | EOF 112 | ) 113 | if [[ "${{ runner.os }}" == "Windows" ]]; then 114 | OUTPUT=$(./fta.exe sample --json) 115 | elif [[ "${{ runner.os }}" == "macOS" ]]; then 116 | brew install jq 117 | OUTPUT=$(./fta sample --json) 118 | else 119 | sudo apt-get install -y jq 120 | OUTPUT=$(./fta sample --json) 121 | fi 122 | if [ "$(echo "$OUTPUT" | jq --sort-keys '.')" == "$(echo "$EXPECTED_OUTPUT" | jq --sort-keys '.')" ]; then 123 | echo "Output matches expected" 124 | else 125 | echo "Output does not match expected." 126 | echo "Expected:" 127 | echo "$EXPECTED_OUTPUT" 128 | echo "Got:" 129 | echo "$OUTPUT" 130 | exit 1 131 | fi 132 | 133 | publish_dry_run_nix: 134 | needs: test_built_binaries 135 | uses: ./.github/workflows/publish-dry-run.yml 136 | 137 | # This is a "trick", a meta task which does not change, and we can use in 138 | # the protected branch rules as opposed to the tests one above which 139 | # may change regularly. 140 | validate-tests: 141 | name: Tests status 142 | runs-on: ubuntu-latest 143 | needs: 144 | - test_rust_crate 145 | - build_binaries 146 | - test_built_binaries 147 | - publish_dry_run_nix 148 | if: always() 149 | steps: 150 | - name: Successful run 151 | if: ${{ !(contains(needs.*.result, 'failure')) }} 152 | run: exit 0 153 | 154 | - name: Failing run 155 | if: ${{ contains(needs.*.result, 'failure') }} 156 | run: exit 1 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | debug.log 3 | release 4 | node_modules 5 | tarpaulin-report.html 6 | build_rs_cov.profraw 7 | .idea/ 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": ["./crates/fta-wasm/Cargo.toml"], 3 | "[rust]": { 4 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 5 | "editor.formatOnSave": true 6 | } 7 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.0.1 2 | 3 | - Added type definition to NPM package 4 | - Upgraded release actions to use up-to-date dependencies 5 | 6 | # v2.0.0 7 | 8 | Breaking changes 9 | 10 | - The halstead calculation was corrected which results in different halstead + FTA scores 11 | 12 | # v1.0.0 13 | 14 | Breaking changes 15 | 16 | - Added the `include_comments` option with a default value of `false`, which means that comments are no longer included in scoring by default 17 | - Added the `exclude_under` option with a default value of `6`, which means that files that are under _n_ lines of code are excluded from output. This option also takes into account the `include_comments` option. 18 | - Changed `output_limit` to (a) only affect the `table` format output and (b) work as expected. 19 | 20 | Other changes 21 | 22 | - Exposed `output_limit`, `score_cap`, `include_comments` and `exclude_under` as CLI options 23 | - Fixed an `ENOBUFS` crash that could occur when analyzing very large projects 24 | 25 | # v0.2.0 26 | 27 | - Potentially breaking: changed linux target platforms: we now target `musl` linux on `x86_64`, `arm` and `aarch64` 28 | - This change should result in a more portable and widely compatible `fta-cli` on Linux systems 29 | - Refactored Github Actions workflow so that the publishing of the npm packages is automatic and coupled with releasing the Rust crate 30 | 31 | # v0.1.11 32 | 33 | - Improved language detection, add retry mechanism ([#31](https://github.com/sgb-io/fta/pull/31)) 34 | 35 | # v0.1.10 36 | 37 | - Fix binaries for Ubuntu 38 | 39 | # v0.1.9 40 | 41 | - Set +x permissions on macOS + linux binaries during build 42 | 43 | # v0.1.8 44 | 45 | - Added WASM npm module 46 | - Refactored internals 47 | 48 | # v0.1.7 49 | 50 | - Internal fixes for the NPM module 51 | 52 | # v0.1.4 53 | 54 | - Added `--json` option 55 | 56 | # v0.1.3 57 | 58 | - Added npm package 59 | 60 | # v0.1.2 61 | 62 | - Initial release 63 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | dependencies = [ 11 | "lazy_static", 12 | "regex", 13 | ] 14 | 15 | [[package]] 16 | name = "aho-corasick" 17 | version = "1.1.3" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 20 | dependencies = [ 21 | "memchr", 22 | ] 23 | 24 | [[package]] 25 | name = "anstream" 26 | version = "0.6.15" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 29 | dependencies = [ 30 | "anstyle", 31 | "anstyle-parse", 32 | "anstyle-query", 33 | "anstyle-wincon", 34 | "colorchoice", 35 | "is_terminal_polyfill", 36 | "utf8parse", 37 | ] 38 | 39 | [[package]] 40 | name = "anstyle" 41 | version = "1.0.8" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 44 | 45 | [[package]] 46 | name = "anstyle-parse" 47 | version = "0.2.5" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 50 | dependencies = [ 51 | "utf8parse", 52 | ] 53 | 54 | [[package]] 55 | name = "anstyle-query" 56 | version = "1.1.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 59 | dependencies = [ 60 | "windows-sys 0.52.0", 61 | ] 62 | 63 | [[package]] 64 | name = "anstyle-wincon" 65 | version = "3.0.4" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 68 | dependencies = [ 69 | "anstyle", 70 | "windows-sys 0.52.0", 71 | ] 72 | 73 | [[package]] 74 | name = "ast_node" 75 | version = "0.9.9" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2" 78 | dependencies = [ 79 | "proc-macro2", 80 | "quote", 81 | "swc_macros_common", 82 | "syn", 83 | ] 84 | 85 | [[package]] 86 | name = "autocfg" 87 | version = "1.3.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 90 | 91 | [[package]] 92 | name = "better_scoped_tls" 93 | version = "0.1.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "794edcc9b3fb07bb4aecaa11f093fd45663b4feadb782d68303a2268bc2701de" 96 | dependencies = [ 97 | "scoped-tls", 98 | ] 99 | 100 | [[package]] 101 | name = "bitflags" 102 | version = "2.6.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 105 | 106 | [[package]] 107 | name = "bstr" 108 | version = "1.10.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 111 | dependencies = [ 112 | "memchr", 113 | "serde", 114 | ] 115 | 116 | [[package]] 117 | name = "bumpalo" 118 | version = "3.16.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 121 | 122 | [[package]] 123 | name = "byteorder" 124 | version = "1.5.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 127 | 128 | [[package]] 129 | name = "cc" 130 | version = "1.1.21" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" 133 | dependencies = [ 134 | "shlex", 135 | ] 136 | 137 | [[package]] 138 | name = "cfg-if" 139 | version = "1.0.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 142 | 143 | [[package]] 144 | name = "clap" 145 | version = "4.5.38" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 148 | dependencies = [ 149 | "clap_builder", 150 | "clap_derive", 151 | ] 152 | 153 | [[package]] 154 | name = "clap_builder" 155 | version = "4.5.38" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 158 | dependencies = [ 159 | "anstream", 160 | "anstyle", 161 | "clap_lex", 162 | "strsim", 163 | ] 164 | 165 | [[package]] 166 | name = "clap_derive" 167 | version = "4.5.32" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 170 | dependencies = [ 171 | "heck", 172 | "proc-macro2", 173 | "quote", 174 | "syn", 175 | ] 176 | 177 | [[package]] 178 | name = "clap_lex" 179 | version = "0.7.4" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 182 | 183 | [[package]] 184 | name = "colorchoice" 185 | version = "1.0.2" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 188 | 189 | [[package]] 190 | name = "comfy-table" 191 | version = "7.1.4" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" 194 | dependencies = [ 195 | "crossterm", 196 | "unicode-segmentation", 197 | "unicode-width 0.2.0", 198 | ] 199 | 200 | [[package]] 201 | name = "crossbeam-deque" 202 | version = "0.8.5" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 205 | dependencies = [ 206 | "crossbeam-epoch", 207 | "crossbeam-utils", 208 | ] 209 | 210 | [[package]] 211 | name = "crossbeam-epoch" 212 | version = "0.9.18" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 215 | dependencies = [ 216 | "crossbeam-utils", 217 | ] 218 | 219 | [[package]] 220 | name = "crossbeam-utils" 221 | version = "0.8.20" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 224 | 225 | [[package]] 226 | name = "crossterm" 227 | version = "0.28.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 230 | dependencies = [ 231 | "bitflags", 232 | "crossterm_winapi", 233 | "parking_lot", 234 | "rustix 0.38.40", 235 | "winapi", 236 | ] 237 | 238 | [[package]] 239 | name = "crossterm_winapi" 240 | version = "0.9.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 243 | dependencies = [ 244 | "winapi", 245 | ] 246 | 247 | [[package]] 248 | name = "either" 249 | version = "1.13.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 252 | 253 | [[package]] 254 | name = "env_filter" 255 | version = "0.1.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" 258 | dependencies = [ 259 | "log", 260 | "regex", 261 | ] 262 | 263 | [[package]] 264 | name = "env_logger" 265 | version = "0.11.6" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 268 | dependencies = [ 269 | "anstream", 270 | "anstyle", 271 | "env_filter", 272 | "humantime", 273 | "log", 274 | ] 275 | 276 | [[package]] 277 | name = "errno" 278 | version = "0.3.10" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 281 | dependencies = [ 282 | "libc", 283 | "windows-sys 0.59.0", 284 | ] 285 | 286 | [[package]] 287 | name = "fastrand" 288 | version = "2.1.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 291 | 292 | [[package]] 293 | name = "form_urlencoded" 294 | version = "1.2.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 297 | dependencies = [ 298 | "percent-encoding", 299 | ] 300 | 301 | [[package]] 302 | name = "from_variant" 303 | version = "0.1.9" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4" 306 | dependencies = [ 307 | "proc-macro2", 308 | "swc_macros_common", 309 | "syn", 310 | ] 311 | 312 | [[package]] 313 | name = "fta" 314 | version = "2.0.1" 315 | dependencies = [ 316 | "clap", 317 | "comfy-table", 318 | "env_logger", 319 | "globset", 320 | "ignore", 321 | "log", 322 | "serde", 323 | "serde_json", 324 | "swc_common", 325 | "swc_ecma_ast", 326 | "swc_ecma_parser", 327 | "swc_ecma_visit", 328 | "tempfile", 329 | ] 330 | 331 | [[package]] 332 | name = "fta-wasm" 333 | version = "1.0.0" 334 | dependencies = [ 335 | "fta", 336 | "serde_json", 337 | "wasm-bindgen", 338 | ] 339 | 340 | [[package]] 341 | name = "getrandom" 342 | version = "0.2.15" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 345 | dependencies = [ 346 | "cfg-if", 347 | "libc", 348 | "wasi 0.11.0+wasi-snapshot-preview1", 349 | ] 350 | 351 | [[package]] 352 | name = "getrandom" 353 | version = "0.3.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 356 | dependencies = [ 357 | "cfg-if", 358 | "libc", 359 | "wasi 0.13.3+wasi-0.2.2", 360 | "windows-targets", 361 | ] 362 | 363 | [[package]] 364 | name = "globset" 365 | version = "0.4.16" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 368 | dependencies = [ 369 | "aho-corasick", 370 | "bstr", 371 | "log", 372 | "regex-automata", 373 | "regex-syntax", 374 | ] 375 | 376 | [[package]] 377 | name = "heck" 378 | version = "0.5.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 381 | 382 | [[package]] 383 | name = "humantime" 384 | version = "2.1.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 387 | 388 | [[package]] 389 | name = "idna" 390 | version = "0.5.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 393 | dependencies = [ 394 | "unicode-bidi", 395 | "unicode-normalization", 396 | ] 397 | 398 | [[package]] 399 | name = "ignore" 400 | version = "0.4.23" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 403 | dependencies = [ 404 | "crossbeam-deque", 405 | "globset", 406 | "log", 407 | "memchr", 408 | "regex-automata", 409 | "same-file", 410 | "walkdir", 411 | "winapi-util", 412 | ] 413 | 414 | [[package]] 415 | name = "is-macro" 416 | version = "0.3.6" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "2069faacbe981460232f880d26bf3c7634e322d49053aa48c27e3ae642f728f1" 419 | dependencies = [ 420 | "Inflector", 421 | "proc-macro2", 422 | "quote", 423 | "syn", 424 | ] 425 | 426 | [[package]] 427 | name = "is_terminal_polyfill" 428 | version = "1.70.1" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 431 | 432 | [[package]] 433 | name = "itoa" 434 | version = "1.0.11" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 437 | 438 | [[package]] 439 | name = "lazy_static" 440 | version = "1.5.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 443 | 444 | [[package]] 445 | name = "lexical" 446 | version = "6.1.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" 449 | dependencies = [ 450 | "lexical-core", 451 | ] 452 | 453 | [[package]] 454 | name = "lexical-core" 455 | version = "0.8.5" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" 458 | dependencies = [ 459 | "lexical-parse-float", 460 | "lexical-parse-integer", 461 | "lexical-util", 462 | "lexical-write-float", 463 | "lexical-write-integer", 464 | ] 465 | 466 | [[package]] 467 | name = "lexical-parse-float" 468 | version = "0.8.5" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" 471 | dependencies = [ 472 | "lexical-parse-integer", 473 | "lexical-util", 474 | "static_assertions", 475 | ] 476 | 477 | [[package]] 478 | name = "lexical-parse-integer" 479 | version = "0.8.6" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" 482 | dependencies = [ 483 | "lexical-util", 484 | "static_assertions", 485 | ] 486 | 487 | [[package]] 488 | name = "lexical-util" 489 | version = "0.8.5" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" 492 | dependencies = [ 493 | "static_assertions", 494 | ] 495 | 496 | [[package]] 497 | name = "lexical-write-float" 498 | version = "0.8.5" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" 501 | dependencies = [ 502 | "lexical-util", 503 | "lexical-write-integer", 504 | "static_assertions", 505 | ] 506 | 507 | [[package]] 508 | name = "lexical-write-integer" 509 | version = "0.8.5" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" 512 | dependencies = [ 513 | "lexical-util", 514 | "static_assertions", 515 | ] 516 | 517 | [[package]] 518 | name = "libc" 519 | version = "0.2.170" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 522 | 523 | [[package]] 524 | name = "linux-raw-sys" 525 | version = "0.4.14" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 528 | 529 | [[package]] 530 | name = "linux-raw-sys" 531 | version = "0.9.2" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" 534 | 535 | [[package]] 536 | name = "lock_api" 537 | version = "0.4.12" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 540 | dependencies = [ 541 | "autocfg", 542 | "scopeguard", 543 | ] 544 | 545 | [[package]] 546 | name = "log" 547 | version = "0.4.27" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 550 | 551 | [[package]] 552 | name = "memchr" 553 | version = "2.7.4" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 556 | 557 | [[package]] 558 | name = "new_debug_unreachable" 559 | version = "1.0.6" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 562 | 563 | [[package]] 564 | name = "num-bigint" 565 | version = "0.4.6" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 568 | dependencies = [ 569 | "num-integer", 570 | "num-traits", 571 | "serde", 572 | ] 573 | 574 | [[package]] 575 | name = "num-integer" 576 | version = "0.1.46" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 579 | dependencies = [ 580 | "num-traits", 581 | ] 582 | 583 | [[package]] 584 | name = "num-traits" 585 | version = "0.2.19" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 588 | dependencies = [ 589 | "autocfg", 590 | ] 591 | 592 | [[package]] 593 | name = "once_cell" 594 | version = "1.19.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 597 | 598 | [[package]] 599 | name = "parking_lot" 600 | version = "0.12.3" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 603 | dependencies = [ 604 | "lock_api", 605 | "parking_lot_core", 606 | ] 607 | 608 | [[package]] 609 | name = "parking_lot_core" 610 | version = "0.9.10" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 613 | dependencies = [ 614 | "cfg-if", 615 | "libc", 616 | "redox_syscall", 617 | "smallvec", 618 | "windows-targets", 619 | ] 620 | 621 | [[package]] 622 | name = "percent-encoding" 623 | version = "2.3.1" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 626 | 627 | [[package]] 628 | name = "phf_generator" 629 | version = "0.10.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 632 | dependencies = [ 633 | "phf_shared", 634 | "rand", 635 | ] 636 | 637 | [[package]] 638 | name = "phf_shared" 639 | version = "0.10.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 642 | dependencies = [ 643 | "siphasher", 644 | ] 645 | 646 | [[package]] 647 | name = "pin-project-lite" 648 | version = "0.2.14" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 651 | 652 | [[package]] 653 | name = "ppv-lite86" 654 | version = "0.2.20" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 657 | dependencies = [ 658 | "zerocopy", 659 | ] 660 | 661 | [[package]] 662 | name = "precomputed-hash" 663 | version = "0.1.1" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 666 | 667 | [[package]] 668 | name = "proc-macro2" 669 | version = "1.0.86" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 672 | dependencies = [ 673 | "unicode-ident", 674 | ] 675 | 676 | [[package]] 677 | name = "psm" 678 | version = "0.1.23" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" 681 | dependencies = [ 682 | "cc", 683 | ] 684 | 685 | [[package]] 686 | name = "quote" 687 | version = "1.0.37" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 690 | dependencies = [ 691 | "proc-macro2", 692 | ] 693 | 694 | [[package]] 695 | name = "rand" 696 | version = "0.8.5" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 699 | dependencies = [ 700 | "libc", 701 | "rand_chacha", 702 | "rand_core", 703 | ] 704 | 705 | [[package]] 706 | name = "rand_chacha" 707 | version = "0.3.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 710 | dependencies = [ 711 | "ppv-lite86", 712 | "rand_core", 713 | ] 714 | 715 | [[package]] 716 | name = "rand_core" 717 | version = "0.6.4" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 720 | dependencies = [ 721 | "getrandom 0.2.15", 722 | ] 723 | 724 | [[package]] 725 | name = "redox_syscall" 726 | version = "0.5.4" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" 729 | dependencies = [ 730 | "bitflags", 731 | ] 732 | 733 | [[package]] 734 | name = "regex" 735 | version = "1.10.6" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 738 | dependencies = [ 739 | "aho-corasick", 740 | "memchr", 741 | "regex-automata", 742 | "regex-syntax", 743 | ] 744 | 745 | [[package]] 746 | name = "regex-automata" 747 | version = "0.4.7" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 750 | dependencies = [ 751 | "aho-corasick", 752 | "memchr", 753 | "regex-syntax", 754 | ] 755 | 756 | [[package]] 757 | name = "regex-syntax" 758 | version = "0.8.4" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 761 | 762 | [[package]] 763 | name = "rustc-hash" 764 | version = "1.1.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 767 | 768 | [[package]] 769 | name = "rustix" 770 | version = "0.38.40" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" 773 | dependencies = [ 774 | "bitflags", 775 | "errno", 776 | "libc", 777 | "linux-raw-sys 0.4.14", 778 | "windows-sys 0.52.0", 779 | ] 780 | 781 | [[package]] 782 | name = "rustix" 783 | version = "1.0.1" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" 786 | dependencies = [ 787 | "bitflags", 788 | "errno", 789 | "libc", 790 | "linux-raw-sys 0.9.2", 791 | "windows-sys 0.59.0", 792 | ] 793 | 794 | [[package]] 795 | name = "rustversion" 796 | version = "1.0.17" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 799 | 800 | [[package]] 801 | name = "ryu" 802 | version = "1.0.18" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 805 | 806 | [[package]] 807 | name = "same-file" 808 | version = "1.0.6" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 811 | dependencies = [ 812 | "winapi-util", 813 | ] 814 | 815 | [[package]] 816 | name = "scoped-tls" 817 | version = "1.0.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 820 | 821 | [[package]] 822 | name = "scopeguard" 823 | version = "1.2.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 826 | 827 | [[package]] 828 | name = "serde" 829 | version = "1.0.219" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 832 | dependencies = [ 833 | "serde_derive", 834 | ] 835 | 836 | [[package]] 837 | name = "serde_derive" 838 | version = "1.0.219" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 841 | dependencies = [ 842 | "proc-macro2", 843 | "quote", 844 | "syn", 845 | ] 846 | 847 | [[package]] 848 | name = "serde_json" 849 | version = "1.0.140" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 852 | dependencies = [ 853 | "itoa", 854 | "memchr", 855 | "ryu", 856 | "serde", 857 | ] 858 | 859 | [[package]] 860 | name = "shlex" 861 | version = "1.3.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 864 | 865 | [[package]] 866 | name = "siphasher" 867 | version = "0.3.11" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 870 | 871 | [[package]] 872 | name = "smallvec" 873 | version = "1.13.2" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 876 | 877 | [[package]] 878 | name = "smartstring" 879 | version = "1.0.1" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" 882 | dependencies = [ 883 | "autocfg", 884 | "static_assertions", 885 | "version_check", 886 | ] 887 | 888 | [[package]] 889 | name = "stable_deref_trait" 890 | version = "1.2.0" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 893 | 894 | [[package]] 895 | name = "stacker" 896 | version = "0.1.17" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" 899 | dependencies = [ 900 | "cc", 901 | "cfg-if", 902 | "libc", 903 | "psm", 904 | "windows-sys 0.59.0", 905 | ] 906 | 907 | [[package]] 908 | name = "static_assertions" 909 | version = "1.1.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 912 | 913 | [[package]] 914 | name = "string_cache" 915 | version = "0.8.7" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" 918 | dependencies = [ 919 | "new_debug_unreachable", 920 | "once_cell", 921 | "parking_lot", 922 | "phf_shared", 923 | "precomputed-hash", 924 | "serde", 925 | ] 926 | 927 | [[package]] 928 | name = "string_cache_codegen" 929 | version = "0.5.2" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" 932 | dependencies = [ 933 | "phf_generator", 934 | "phf_shared", 935 | "proc-macro2", 936 | "quote", 937 | ] 938 | 939 | [[package]] 940 | name = "string_enum" 941 | version = "0.4.4" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" 944 | dependencies = [ 945 | "proc-macro2", 946 | "quote", 947 | "swc_macros_common", 948 | "syn", 949 | ] 950 | 951 | [[package]] 952 | name = "strsim" 953 | version = "0.11.1" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 956 | 957 | [[package]] 958 | name = "swc_atoms" 959 | version = "0.5.9" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "9f54563d7dcba626d4acfe14ed12def7ecc28e004debe3ecd2c3ee07cc47e449" 962 | dependencies = [ 963 | "once_cell", 964 | "rustc-hash", 965 | "serde", 966 | "string_cache", 967 | "string_cache_codegen", 968 | "triomphe", 969 | ] 970 | 971 | [[package]] 972 | name = "swc_common" 973 | version = "0.31.22" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "88d00f960c667c59c133f30492f4d07f26242fcf988a066d3871e6d3d838d528" 976 | dependencies = [ 977 | "ast_node", 978 | "better_scoped_tls", 979 | "cfg-if", 980 | "either", 981 | "from_variant", 982 | "new_debug_unreachable", 983 | "num-bigint", 984 | "once_cell", 985 | "rustc-hash", 986 | "serde", 987 | "siphasher", 988 | "string_cache", 989 | "swc_atoms", 990 | "swc_eq_ignore_macros", 991 | "swc_visit", 992 | "tracing", 993 | "unicode-width 0.1.14", 994 | "url", 995 | ] 996 | 997 | [[package]] 998 | name = "swc_ecma_ast" 999 | version = "0.106.6" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "ebf4d6804b1da4146c4c0359d129e3dd43568d321f69d7953d9abbca4ded76ba" 1002 | dependencies = [ 1003 | "bitflags", 1004 | "is-macro", 1005 | "num-bigint", 1006 | "scoped-tls", 1007 | "string_enum", 1008 | "swc_atoms", 1009 | "swc_common", 1010 | "unicode-id", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "swc_ecma_parser" 1015 | version = "0.136.8" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "45d40421c607d7a48334f78a9b24a5cbde1f36250f9986746ec082208d68b39f" 1018 | dependencies = [ 1019 | "either", 1020 | "lexical", 1021 | "num-bigint", 1022 | "serde", 1023 | "smallvec", 1024 | "smartstring", 1025 | "stacker", 1026 | "swc_atoms", 1027 | "swc_common", 1028 | "swc_ecma_ast", 1029 | "tracing", 1030 | "typed-arena", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "swc_ecma_visit" 1035 | version = "0.92.5" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "0f61da6cac0ec3b7e62d367cfbd9e38e078a4601271891ad94f0dac5ff69f839" 1038 | dependencies = [ 1039 | "num-bigint", 1040 | "swc_atoms", 1041 | "swc_common", 1042 | "swc_ecma_ast", 1043 | "swc_visit", 1044 | "tracing", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "swc_eq_ignore_macros" 1049 | version = "0.1.4" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "63db0adcff29d220c3d151c5b25c0eabe7e32dd936212b84cdaa1392e3130497" 1052 | dependencies = [ 1053 | "proc-macro2", 1054 | "quote", 1055 | "syn", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "swc_macros_common" 1060 | version = "0.3.13" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "f486687bfb7b5c560868f69ed2d458b880cebc9babebcb67e49f31b55c5bf847" 1063 | dependencies = [ 1064 | "proc-macro2", 1065 | "quote", 1066 | "syn", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "swc_visit" 1071 | version = "0.5.14" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "043d11fe683dcb934583ead49405c0896a5af5face522e4682c16971ef7871b9" 1074 | dependencies = [ 1075 | "either", 1076 | "swc_visit_macros", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "swc_visit_macros" 1081 | version = "0.5.13" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "92807d840959f39c60ce8a774a3f83e8193c658068e6d270dbe0a05e40e90b41" 1084 | dependencies = [ 1085 | "Inflector", 1086 | "proc-macro2", 1087 | "quote", 1088 | "swc_macros_common", 1089 | "syn", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "syn" 1094 | version = "2.0.85" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 1097 | dependencies = [ 1098 | "proc-macro2", 1099 | "quote", 1100 | "unicode-ident", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "tempfile" 1105 | version = "3.20.0" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1108 | dependencies = [ 1109 | "fastrand", 1110 | "getrandom 0.3.1", 1111 | "once_cell", 1112 | "rustix 1.0.1", 1113 | "windows-sys 0.59.0", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "tinyvec" 1118 | version = "1.8.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1121 | dependencies = [ 1122 | "tinyvec_macros", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "tinyvec_macros" 1127 | version = "0.1.1" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1130 | 1131 | [[package]] 1132 | name = "tracing" 1133 | version = "0.1.40" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1136 | dependencies = [ 1137 | "pin-project-lite", 1138 | "tracing-attributes", 1139 | "tracing-core", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "tracing-attributes" 1144 | version = "0.1.27" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1147 | dependencies = [ 1148 | "proc-macro2", 1149 | "quote", 1150 | "syn", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "tracing-core" 1155 | version = "0.1.32" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1158 | dependencies = [ 1159 | "once_cell", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "triomphe" 1164 | version = "0.1.13" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" 1167 | dependencies = [ 1168 | "serde", 1169 | "stable_deref_trait", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "typed-arena" 1174 | version = "2.0.2" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 1177 | 1178 | [[package]] 1179 | name = "unicode-bidi" 1180 | version = "0.3.15" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1183 | 1184 | [[package]] 1185 | name = "unicode-id" 1186 | version = "0.3.5" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" 1189 | 1190 | [[package]] 1191 | name = "unicode-ident" 1192 | version = "1.0.13" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1195 | 1196 | [[package]] 1197 | name = "unicode-normalization" 1198 | version = "0.1.24" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 1201 | dependencies = [ 1202 | "tinyvec", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "unicode-segmentation" 1207 | version = "1.12.0" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1210 | 1211 | [[package]] 1212 | name = "unicode-width" 1213 | version = "0.1.14" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1216 | 1217 | [[package]] 1218 | name = "unicode-width" 1219 | version = "0.2.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1222 | 1223 | [[package]] 1224 | name = "url" 1225 | version = "2.5.2" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1228 | dependencies = [ 1229 | "form_urlencoded", 1230 | "idna", 1231 | "percent-encoding", 1232 | ] 1233 | 1234 | [[package]] 1235 | name = "utf8parse" 1236 | version = "0.2.2" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1239 | 1240 | [[package]] 1241 | name = "version_check" 1242 | version = "0.9.5" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1245 | 1246 | [[package]] 1247 | name = "walkdir" 1248 | version = "2.5.0" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1251 | dependencies = [ 1252 | "same-file", 1253 | "winapi-util", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "wasi" 1258 | version = "0.11.0+wasi-snapshot-preview1" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1261 | 1262 | [[package]] 1263 | name = "wasi" 1264 | version = "0.13.3+wasi-0.2.2" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1267 | dependencies = [ 1268 | "wit-bindgen-rt", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "wasm-bindgen" 1273 | version = "0.2.100" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1276 | dependencies = [ 1277 | "cfg-if", 1278 | "once_cell", 1279 | "rustversion", 1280 | "wasm-bindgen-macro", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "wasm-bindgen-backend" 1285 | version = "0.2.100" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1288 | dependencies = [ 1289 | "bumpalo", 1290 | "log", 1291 | "proc-macro2", 1292 | "quote", 1293 | "syn", 1294 | "wasm-bindgen-shared", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "wasm-bindgen-macro" 1299 | version = "0.2.100" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1302 | dependencies = [ 1303 | "quote", 1304 | "wasm-bindgen-macro-support", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "wasm-bindgen-macro-support" 1309 | version = "0.2.100" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1312 | dependencies = [ 1313 | "proc-macro2", 1314 | "quote", 1315 | "syn", 1316 | "wasm-bindgen-backend", 1317 | "wasm-bindgen-shared", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "wasm-bindgen-shared" 1322 | version = "0.2.100" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1325 | dependencies = [ 1326 | "unicode-ident", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "winapi" 1331 | version = "0.3.9" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1334 | dependencies = [ 1335 | "winapi-i686-pc-windows-gnu", 1336 | "winapi-x86_64-pc-windows-gnu", 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "winapi-i686-pc-windows-gnu" 1341 | version = "0.4.0" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1344 | 1345 | [[package]] 1346 | name = "winapi-util" 1347 | version = "0.1.9" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1350 | dependencies = [ 1351 | "windows-sys 0.59.0", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "winapi-x86_64-pc-windows-gnu" 1356 | version = "0.4.0" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1359 | 1360 | [[package]] 1361 | name = "windows-sys" 1362 | version = "0.52.0" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1365 | dependencies = [ 1366 | "windows-targets", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "windows-sys" 1371 | version = "0.59.0" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1374 | dependencies = [ 1375 | "windows-targets", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "windows-targets" 1380 | version = "0.52.6" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1383 | dependencies = [ 1384 | "windows_aarch64_gnullvm", 1385 | "windows_aarch64_msvc", 1386 | "windows_i686_gnu", 1387 | "windows_i686_gnullvm", 1388 | "windows_i686_msvc", 1389 | "windows_x86_64_gnu", 1390 | "windows_x86_64_gnullvm", 1391 | "windows_x86_64_msvc", 1392 | ] 1393 | 1394 | [[package]] 1395 | name = "windows_aarch64_gnullvm" 1396 | version = "0.52.6" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1399 | 1400 | [[package]] 1401 | name = "windows_aarch64_msvc" 1402 | version = "0.52.6" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1405 | 1406 | [[package]] 1407 | name = "windows_i686_gnu" 1408 | version = "0.52.6" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1411 | 1412 | [[package]] 1413 | name = "windows_i686_gnullvm" 1414 | version = "0.52.6" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1417 | 1418 | [[package]] 1419 | name = "windows_i686_msvc" 1420 | version = "0.52.6" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1423 | 1424 | [[package]] 1425 | name = "windows_x86_64_gnu" 1426 | version = "0.52.6" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1429 | 1430 | [[package]] 1431 | name = "windows_x86_64_gnullvm" 1432 | version = "0.52.6" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1435 | 1436 | [[package]] 1437 | name = "windows_x86_64_msvc" 1438 | version = "0.52.6" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1441 | 1442 | [[package]] 1443 | name = "wit-bindgen-rt" 1444 | version = "0.33.0" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1447 | dependencies = [ 1448 | "bitflags", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "zerocopy" 1453 | version = "0.7.35" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1456 | dependencies = [ 1457 | "byteorder", 1458 | "zerocopy-derive", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "zerocopy-derive" 1463 | version = "0.7.35" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1466 | dependencies = [ 1467 | "proc-macro2", 1468 | "quote", 1469 | "syn", 1470 | ] 1471 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/fta", 5 | "crates/fta-wasm" 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present Sam Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # Maintenance of FTA 2 | 3 | This project currently consists of 4 components: 4 | 5 | - The Rust `fta` crate, in `crates/fta` 6 | - The Rust `fta-wasm` crate, in `crates/fta-wasm` 7 | - The NPM `fta-cli` package, in `packages/fta` 8 | - The NPM `fta-wasm` package, an artefact of the `fta-wasm` Rust crate 9 | 10 | The NPM `fta-cli` package is a super thin layer that simply calls the relevant `fta` binary. For this to work, the NPM package is designed to contain pre-built binaries. 11 | 12 | ## Development 13 | 14 | Use PRs into `main`. GitHub Actions are set up to: 15 | 16 | - Compile the Rust crate & run Rust tests, output test coverage (Ubuntu) 17 | - Build binaries for all targets on windows/macos/linux 18 | - Smoke test all built binaries against a sample file 19 | - Construct the NPM package, i.e. install the compiled binaries into `packages/fta/binaries` 20 | - Publish the NPM package locally using Verdaccio 21 | - Smoke test the verdaccio-published NPM package via a sample file 22 | 23 | The NPM CLI package itself is plain JavaScript without any Node.js tests or build step, since those things aren't really warranted. 24 | 25 | ## Publishing and releasing (`fta` crate, `fta-cli` npm package) 26 | 27 | 1. Merge changes over time to `main`, with green builds to verify everything is healthy 28 | 2. Bump versions and update `CHANGELOG.md` 29 | 1. Set the version in `packages/fta/package.json` 30 | 2. Set the version in `crates/fta/Cargo.toml`, run `cargo update` so that the lockfile updates too. Do this in a PR and merge it to `main`. 31 | 3. When you're satisfied everything is ready on `main` (and the build is green), locally tag the repo with a new version e.g. `v1.0.0`. Push this tag to trigger the release. 32 | 33 | ## WASM npm package 34 | 35 | This should be published manually. From the `crates/fta-wasm` directory: 36 | 37 | 1. Ensure the crate version is in sync. Similar to the `fta-cli` package, it usually makes sense for the core `fta` crate to be published first. 38 | 2. If you already have the `crates/fta-wasm/pkg` dir, delete it / clear it out. 39 | 3. Run `wasm-pack build --target web`. This'll prep the files in `pkg`. 40 | 4. If you want to locally debug before publish, you can paste the contents of `pkg` to override an existing version in `node_modules.` 41 | 5. Run `wasm-pack publish pkg`. This directly publishes the output to NPM. 42 | 43 | ## Code Coverage 44 | 45 | Code coverage is reported during the `test` workflow. 46 | 47 | To check the coverage locally, install and run `tarpaulin`: 48 | 49 | ``` 50 | cargo install cargo-tarpaulin 51 | cargo tarpaulin 52 | ``` 53 | 54 | Note that `tarpaulin` is not installed as a build dependency, hence should be installed manually to generate coverage. 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/fta/README.md -------------------------------------------------------------------------------- /crates/fta-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fta-wasm" 3 | version = "1.0.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = "Fast TypeScript Analyzer" 7 | homepage = "https://ftaproject.dev" 8 | documentation = "https://github.com/sgb-io/fta" 9 | repository = "https://github.com/sgb-io/fta" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [dependencies] 15 | fta = { path = "../fta", default-features = false } 16 | serde_json = "1.0.140" 17 | wasm-bindgen = "0.2.100" -------------------------------------------------------------------------------- /crates/fta-wasm/README.md: -------------------------------------------------------------------------------- 1 | # fta-wasm 2 | 3 | WebAssembly build of [FTA](https://ftaproject.dev). 4 | 5 | View the full [docs](https://ftaproject.dev/docs/getting-started) for more information. 6 | -------------------------------------------------------------------------------- /crates/fta-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use fta::analyze_file; 2 | use fta::parse; 3 | use serde_json::{json, to_string, Value}; 4 | use std::collections::HashMap; 5 | use wasm_bindgen::prelude::*; 6 | 7 | #[cfg(test)] 8 | mod lib_tests; 9 | 10 | #[wasm_bindgen] 11 | pub fn analyze_file_wasm(source_code: &str, use_tsx: bool, include_comments: bool) -> String { 12 | let json_string; 13 | 14 | match parse::parse_module(source_code, use_tsx, include_comments) { 15 | (Ok(module), line_count) => { 16 | let (cyclo, halstead_metrics, fta_score) = analyze_file(&module, line_count); 17 | let mut analyzed: HashMap<&str, Value> = HashMap::new(); 18 | analyzed.insert("line_count", json!(line_count)); 19 | analyzed.insert("cyclo", json!(cyclo)); 20 | analyzed.insert("halstead_metrics", json!(halstead_metrics)); 21 | analyzed.insert("fta_score", json!(fta_score)); 22 | json_string = to_string(&analyzed).unwrap(); 23 | } 24 | (Err(_err), _) => { 25 | wasm_bindgen::throw_str("Unable to parse module"); 26 | } 27 | } 28 | 29 | json_string 30 | } 31 | -------------------------------------------------------------------------------- /crates/fta-wasm/src/lib_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::analyze_file_wasm; 4 | use serde_json::{from_str, Value}; 5 | 6 | #[test] 7 | fn test_analyze_project() { 8 | let input_code = r#" 9 | function add(a: number, b: number): number { 10 | return a + b; 11 | } 12 | 13 | const myResult = add(23, 56); 14 | console.log(myResult); // 79 15 | "#; 16 | 17 | let expected_output = r#" 18 | { 19 | "cyclo": 1, 20 | "fta_score": 8.159706499414824, 21 | "line_count": 5, 22 | "halstead_metrics": { 23 | "bugs": 0.027920602761755765, 24 | "difficulty": 4.5, 25 | "effort": 376.9281372837028, 26 | "program_length": 22, 27 | "time": 20.940452071316823, 28 | "total_operands": 12, 29 | "total_operators": 10, 30 | "uniq_operands": 8, 31 | "uniq_operators": 6, 32 | "vocabulary_size": 14, 33 | "volume": 83.76180828526729 34 | } 35 | } 36 | "#; 37 | 38 | let result = analyze_file_wasm(input_code, true, false); 39 | let expected_json: Value = from_str(expected_output).unwrap(); 40 | let actual_json: Value = from_str(&result).unwrap(); 41 | 42 | assert_eq!(expected_json, actual_json); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/fta/.gitignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /crates/fta/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | dependencies = [ 11 | "lazy_static", 12 | "regex", 13 | ] 14 | 15 | [[package]] 16 | name = "ahash" 17 | version = "0.7.6" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 20 | dependencies = [ 21 | "getrandom", 22 | "once_cell", 23 | "version_check", 24 | ] 25 | 26 | [[package]] 27 | name = "aho-corasick" 28 | version = "1.0.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 31 | dependencies = [ 32 | "memchr", 33 | ] 34 | 35 | [[package]] 36 | name = "ast_node" 37 | version = "0.9.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "c704e2f6ee1a98223f5a7629a6ef0f3decb3b552ed282889dc957edff98ce1e6" 40 | dependencies = [ 41 | "pmutil", 42 | "proc-macro2", 43 | "quote", 44 | "swc_macros_common", 45 | "syn 1.0.109", 46 | ] 47 | 48 | [[package]] 49 | name = "autocfg" 50 | version = "1.1.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 53 | 54 | [[package]] 55 | name = "better_scoped_tls" 56 | version = "0.1.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "b73e8ecdec39e98aa3b19e8cd0b8ed8f77ccb86a6b0b2dc7cd86d105438a2123" 59 | dependencies = [ 60 | "scoped-tls", 61 | ] 62 | 63 | [[package]] 64 | name = "bitflags" 65 | version = "1.3.2" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 68 | 69 | [[package]] 70 | name = "bitflags" 71 | version = "2.2.1" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" 74 | 75 | [[package]] 76 | name = "cc" 77 | version = "1.0.79" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 80 | 81 | [[package]] 82 | name = "cfg-if" 83 | version = "1.0.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 86 | 87 | [[package]] 88 | name = "either" 89 | version = "1.8.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 92 | 93 | [[package]] 94 | name = "form_urlencoded" 95 | version = "1.1.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 98 | dependencies = [ 99 | "percent-encoding", 100 | ] 101 | 102 | [[package]] 103 | name = "from_variant" 104 | version = "0.1.5" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "1d449976075322384507443937df2f1d5577afbf4282f12a5a66ef29fa3e6307" 107 | dependencies = [ 108 | "pmutil", 109 | "proc-macro2", 110 | "swc_macros_common", 111 | "syn 1.0.109", 112 | ] 113 | 114 | [[package]] 115 | name = "fta" 116 | version = "0.1.0" 117 | dependencies = [ 118 | "swc_common", 119 | "swc_ecma_ast", 120 | "swc_ecma_parser", 121 | "swc_ecma_visit", 122 | ] 123 | 124 | [[package]] 125 | name = "getrandom" 126 | version = "0.2.9" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 129 | dependencies = [ 130 | "cfg-if", 131 | "libc", 132 | "wasi", 133 | ] 134 | 135 | [[package]] 136 | name = "idna" 137 | version = "0.3.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 140 | dependencies = [ 141 | "unicode-bidi", 142 | "unicode-normalization", 143 | ] 144 | 145 | [[package]] 146 | name = "is-macro" 147 | version = "0.2.2" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "8a7d079e129b77477a49c5c4f1cfe9ce6c2c909ef52520693e8e811a714c7b20" 150 | dependencies = [ 151 | "Inflector", 152 | "pmutil", 153 | "proc-macro2", 154 | "quote", 155 | "syn 1.0.109", 156 | ] 157 | 158 | [[package]] 159 | name = "lazy_static" 160 | version = "1.4.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 163 | 164 | [[package]] 165 | name = "lexical" 166 | version = "6.1.1" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" 169 | dependencies = [ 170 | "lexical-core", 171 | ] 172 | 173 | [[package]] 174 | name = "lexical-core" 175 | version = "0.8.5" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" 178 | dependencies = [ 179 | "lexical-parse-float", 180 | "lexical-parse-integer", 181 | "lexical-util", 182 | "lexical-write-float", 183 | "lexical-write-integer", 184 | ] 185 | 186 | [[package]] 187 | name = "lexical-parse-float" 188 | version = "0.8.5" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" 191 | dependencies = [ 192 | "lexical-parse-integer", 193 | "lexical-util", 194 | "static_assertions", 195 | ] 196 | 197 | [[package]] 198 | name = "lexical-parse-integer" 199 | version = "0.8.6" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" 202 | dependencies = [ 203 | "lexical-util", 204 | "static_assertions", 205 | ] 206 | 207 | [[package]] 208 | name = "lexical-util" 209 | version = "0.8.5" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" 212 | dependencies = [ 213 | "static_assertions", 214 | ] 215 | 216 | [[package]] 217 | name = "lexical-write-float" 218 | version = "0.8.5" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" 221 | dependencies = [ 222 | "lexical-util", 223 | "lexical-write-integer", 224 | "static_assertions", 225 | ] 226 | 227 | [[package]] 228 | name = "lexical-write-integer" 229 | version = "0.8.5" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" 232 | dependencies = [ 233 | "lexical-util", 234 | "static_assertions", 235 | ] 236 | 237 | [[package]] 238 | name = "libc" 239 | version = "0.2.143" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024" 242 | 243 | [[package]] 244 | name = "lock_api" 245 | version = "0.4.9" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 248 | dependencies = [ 249 | "autocfg", 250 | "scopeguard", 251 | ] 252 | 253 | [[package]] 254 | name = "memchr" 255 | version = "2.5.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 258 | 259 | [[package]] 260 | name = "new_debug_unreachable" 261 | version = "1.0.4" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" 264 | 265 | [[package]] 266 | name = "num-bigint" 267 | version = "0.4.3" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" 270 | dependencies = [ 271 | "autocfg", 272 | "num-integer", 273 | "num-traits", 274 | "serde", 275 | ] 276 | 277 | [[package]] 278 | name = "num-integer" 279 | version = "0.1.45" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 282 | dependencies = [ 283 | "autocfg", 284 | "num-traits", 285 | ] 286 | 287 | [[package]] 288 | name = "num-traits" 289 | version = "0.2.15" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 292 | dependencies = [ 293 | "autocfg", 294 | ] 295 | 296 | [[package]] 297 | name = "once_cell" 298 | version = "1.17.1" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 301 | 302 | [[package]] 303 | name = "parking_lot" 304 | version = "0.12.1" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 307 | dependencies = [ 308 | "lock_api", 309 | "parking_lot_core", 310 | ] 311 | 312 | [[package]] 313 | name = "parking_lot_core" 314 | version = "0.9.7" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 317 | dependencies = [ 318 | "cfg-if", 319 | "libc", 320 | "redox_syscall", 321 | "smallvec", 322 | "windows-sys", 323 | ] 324 | 325 | [[package]] 326 | name = "percent-encoding" 327 | version = "2.2.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 330 | 331 | [[package]] 332 | name = "phf_generator" 333 | version = "0.10.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 336 | dependencies = [ 337 | "phf_shared", 338 | "rand", 339 | ] 340 | 341 | [[package]] 342 | name = "phf_shared" 343 | version = "0.10.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 346 | dependencies = [ 347 | "siphasher", 348 | ] 349 | 350 | [[package]] 351 | name = "pin-project-lite" 352 | version = "0.2.9" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 355 | 356 | [[package]] 357 | name = "pmutil" 358 | version = "0.5.3" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" 361 | dependencies = [ 362 | "proc-macro2", 363 | "quote", 364 | "syn 1.0.109", 365 | ] 366 | 367 | [[package]] 368 | name = "ppv-lite86" 369 | version = "0.2.17" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 372 | 373 | [[package]] 374 | name = "precomputed-hash" 375 | version = "0.1.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 378 | 379 | [[package]] 380 | name = "proc-macro2" 381 | version = "1.0.56" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" 384 | dependencies = [ 385 | "unicode-ident", 386 | ] 387 | 388 | [[package]] 389 | name = "psm" 390 | version = "0.1.21" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" 393 | dependencies = [ 394 | "cc", 395 | ] 396 | 397 | [[package]] 398 | name = "quote" 399 | version = "1.0.26" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 402 | dependencies = [ 403 | "proc-macro2", 404 | ] 405 | 406 | [[package]] 407 | name = "rand" 408 | version = "0.8.5" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 411 | dependencies = [ 412 | "libc", 413 | "rand_chacha", 414 | "rand_core", 415 | ] 416 | 417 | [[package]] 418 | name = "rand_chacha" 419 | version = "0.3.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 422 | dependencies = [ 423 | "ppv-lite86", 424 | "rand_core", 425 | ] 426 | 427 | [[package]] 428 | name = "rand_core" 429 | version = "0.6.4" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 432 | dependencies = [ 433 | "getrandom", 434 | ] 435 | 436 | [[package]] 437 | name = "redox_syscall" 438 | version = "0.2.16" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 441 | dependencies = [ 442 | "bitflags 1.3.2", 443 | ] 444 | 445 | [[package]] 446 | name = "regex" 447 | version = "1.8.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" 450 | dependencies = [ 451 | "aho-corasick", 452 | "memchr", 453 | "regex-syntax", 454 | ] 455 | 456 | [[package]] 457 | name = "regex-syntax" 458 | version = "0.7.1" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" 461 | 462 | [[package]] 463 | name = "rustc-hash" 464 | version = "1.1.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 467 | 468 | [[package]] 469 | name = "scoped-tls" 470 | version = "1.0.1" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 473 | 474 | [[package]] 475 | name = "scopeguard" 476 | version = "1.1.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 479 | 480 | [[package]] 481 | name = "serde" 482 | version = "1.0.162" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" 485 | dependencies = [ 486 | "serde_derive", 487 | ] 488 | 489 | [[package]] 490 | name = "serde_derive" 491 | version = "1.0.162" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "syn 2.0.15", 498 | ] 499 | 500 | [[package]] 501 | name = "siphasher" 502 | version = "0.3.10" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" 505 | 506 | [[package]] 507 | name = "smallvec" 508 | version = "1.10.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 511 | 512 | [[package]] 513 | name = "smartstring" 514 | version = "1.0.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" 517 | dependencies = [ 518 | "autocfg", 519 | "static_assertions", 520 | "version_check", 521 | ] 522 | 523 | [[package]] 524 | name = "stable_deref_trait" 525 | version = "1.2.0" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 528 | 529 | [[package]] 530 | name = "stacker" 531 | version = "0.1.15" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" 534 | dependencies = [ 535 | "cc", 536 | "cfg-if", 537 | "libc", 538 | "psm", 539 | "winapi", 540 | ] 541 | 542 | [[package]] 543 | name = "static_assertions" 544 | version = "1.1.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 547 | 548 | [[package]] 549 | name = "string_cache" 550 | version = "0.8.7" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" 553 | dependencies = [ 554 | "new_debug_unreachable", 555 | "once_cell", 556 | "parking_lot", 557 | "phf_shared", 558 | "precomputed-hash", 559 | "serde", 560 | ] 561 | 562 | [[package]] 563 | name = "string_cache_codegen" 564 | version = "0.5.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" 567 | dependencies = [ 568 | "phf_generator", 569 | "phf_shared", 570 | "proc-macro2", 571 | "quote", 572 | ] 573 | 574 | [[package]] 575 | name = "string_enum" 576 | version = "0.4.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "0090512bdfee4b56d82480d66c0fd8a6f53f0fe0f97e075e949b252acdd482e0" 579 | dependencies = [ 580 | "pmutil", 581 | "proc-macro2", 582 | "quote", 583 | "swc_macros_common", 584 | "syn 1.0.109", 585 | ] 586 | 587 | [[package]] 588 | name = "swc_atoms" 589 | version = "0.5.4" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "3e4a2900a3da67c8759564b4defcd47e35f14cee65952015a5122ba7cbba927f" 592 | dependencies = [ 593 | "once_cell", 594 | "rustc-hash", 595 | "serde", 596 | "string_cache", 597 | "string_cache_codegen", 598 | "triomphe", 599 | ] 600 | 601 | [[package]] 602 | name = "swc_common" 603 | version = "0.31.6" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "7efdfff3bc0297c74b7ce0489a15a49a49c17a99261e2a0c5cb70fc15dca2392" 606 | dependencies = [ 607 | "ahash", 608 | "ast_node", 609 | "better_scoped_tls", 610 | "cfg-if", 611 | "either", 612 | "from_variant", 613 | "new_debug_unreachable", 614 | "num-bigint", 615 | "once_cell", 616 | "rustc-hash", 617 | "serde", 618 | "siphasher", 619 | "string_cache", 620 | "swc_atoms", 621 | "swc_eq_ignore_macros", 622 | "swc_visit", 623 | "tracing", 624 | "unicode-width", 625 | "url", 626 | ] 627 | 628 | [[package]] 629 | name = "swc_ecma_ast" 630 | version = "0.103.7" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "2cf9a2bea4d019564781b64ad9038a859b829f58ddb6e3ba5571292f5b5e0145" 633 | dependencies = [ 634 | "bitflags 2.2.1", 635 | "is-macro", 636 | "num-bigint", 637 | "scoped-tls", 638 | "string_enum", 639 | "swc_atoms", 640 | "swc_common", 641 | "unicode-id", 642 | ] 643 | 644 | [[package]] 645 | name = "swc_ecma_parser" 646 | version = "0.133.14" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "1265e024029484ecc950bbb8ef347e0a874e3165c52ab64bfda1052d3e53c8e5" 649 | dependencies = [ 650 | "either", 651 | "lexical", 652 | "num-bigint", 653 | "serde", 654 | "smallvec", 655 | "smartstring", 656 | "stacker", 657 | "swc_atoms", 658 | "swc_common", 659 | "swc_ecma_ast", 660 | "tracing", 661 | "typed-arena", 662 | ] 663 | 664 | [[package]] 665 | name = "swc_ecma_visit" 666 | version = "0.89.7" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "d212dd6c58b496ecd880b0c1bd2de65a6c4004cb891423bfffa0068fcf5be9de" 669 | dependencies = [ 670 | "num-bigint", 671 | "swc_atoms", 672 | "swc_common", 673 | "swc_ecma_ast", 674 | "swc_visit", 675 | "tracing", 676 | ] 677 | 678 | [[package]] 679 | name = "swc_eq_ignore_macros" 680 | version = "0.1.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "0c20468634668c2bbab581947bb8c75c97158d5a6959f4ba33df20983b20b4f6" 683 | dependencies = [ 684 | "pmutil", 685 | "proc-macro2", 686 | "quote", 687 | "syn 1.0.109", 688 | ] 689 | 690 | [[package]] 691 | name = "swc_macros_common" 692 | version = "0.3.7" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "3e582c3e3c2269238524923781df5be49e011dbe29cf7683a2215d600a562ea6" 695 | dependencies = [ 696 | "pmutil", 697 | "proc-macro2", 698 | "quote", 699 | "syn 1.0.109", 700 | ] 701 | 702 | [[package]] 703 | name = "swc_visit" 704 | version = "0.5.5" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "d1d5999f23421c8e21a0f2bc53a0b9e8244f3b421de89471561af2fbe40b9cca" 707 | dependencies = [ 708 | "either", 709 | "swc_visit_macros", 710 | ] 711 | 712 | [[package]] 713 | name = "swc_visit_macros" 714 | version = "0.5.6" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "ebeed7eb0f545f48ad30f5aab314e5208b735bcea1d1464f26e20f06db904989" 717 | dependencies = [ 718 | "Inflector", 719 | "pmutil", 720 | "proc-macro2", 721 | "quote", 722 | "swc_macros_common", 723 | "syn 1.0.109", 724 | ] 725 | 726 | [[package]] 727 | name = "syn" 728 | version = "1.0.109" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 731 | dependencies = [ 732 | "proc-macro2", 733 | "quote", 734 | "unicode-ident", 735 | ] 736 | 737 | [[package]] 738 | name = "syn" 739 | version = "2.0.15" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" 742 | dependencies = [ 743 | "proc-macro2", 744 | "quote", 745 | "unicode-ident", 746 | ] 747 | 748 | [[package]] 749 | name = "tinyvec" 750 | version = "1.6.0" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 753 | dependencies = [ 754 | "tinyvec_macros", 755 | ] 756 | 757 | [[package]] 758 | name = "tinyvec_macros" 759 | version = "0.1.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 762 | 763 | [[package]] 764 | name = "tracing" 765 | version = "0.1.37" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 768 | dependencies = [ 769 | "cfg-if", 770 | "pin-project-lite", 771 | "tracing-attributes", 772 | "tracing-core", 773 | ] 774 | 775 | [[package]] 776 | name = "tracing-attributes" 777 | version = "0.1.24" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "syn 2.0.15", 784 | ] 785 | 786 | [[package]] 787 | name = "tracing-core" 788 | version = "0.1.30" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 791 | dependencies = [ 792 | "once_cell", 793 | ] 794 | 795 | [[package]] 796 | name = "triomphe" 797 | version = "0.1.8" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "f1ee9bd9239c339d714d657fac840c6d2a4f9c45f4f9ec7b0975113458be78db" 800 | dependencies = [ 801 | "serde", 802 | "stable_deref_trait", 803 | ] 804 | 805 | [[package]] 806 | name = "typed-arena" 807 | version = "2.0.2" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 810 | 811 | [[package]] 812 | name = "unicode-bidi" 813 | version = "0.3.13" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 816 | 817 | [[package]] 818 | name = "unicode-id" 819 | version = "0.3.3" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a" 822 | 823 | [[package]] 824 | name = "unicode-ident" 825 | version = "1.0.8" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 828 | 829 | [[package]] 830 | name = "unicode-normalization" 831 | version = "0.1.22" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 834 | dependencies = [ 835 | "tinyvec", 836 | ] 837 | 838 | [[package]] 839 | name = "unicode-width" 840 | version = "0.1.10" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 843 | 844 | [[package]] 845 | name = "url" 846 | version = "2.3.1" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 849 | dependencies = [ 850 | "form_urlencoded", 851 | "idna", 852 | "percent-encoding", 853 | ] 854 | 855 | [[package]] 856 | name = "version_check" 857 | version = "0.9.4" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 860 | 861 | [[package]] 862 | name = "wasi" 863 | version = "0.11.0+wasi-snapshot-preview1" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 866 | 867 | [[package]] 868 | name = "winapi" 869 | version = "0.3.9" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 872 | dependencies = [ 873 | "winapi-i686-pc-windows-gnu", 874 | "winapi-x86_64-pc-windows-gnu", 875 | ] 876 | 877 | [[package]] 878 | name = "winapi-i686-pc-windows-gnu" 879 | version = "0.4.0" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 882 | 883 | [[package]] 884 | name = "winapi-x86_64-pc-windows-gnu" 885 | version = "0.4.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 888 | 889 | [[package]] 890 | name = "windows-sys" 891 | version = "0.45.0" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 894 | dependencies = [ 895 | "windows-targets", 896 | ] 897 | 898 | [[package]] 899 | name = "windows-targets" 900 | version = "0.42.2" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 903 | dependencies = [ 904 | "windows_aarch64_gnullvm", 905 | "windows_aarch64_msvc", 906 | "windows_i686_gnu", 907 | "windows_i686_msvc", 908 | "windows_x86_64_gnu", 909 | "windows_x86_64_gnullvm", 910 | "windows_x86_64_msvc", 911 | ] 912 | 913 | [[package]] 914 | name = "windows_aarch64_gnullvm" 915 | version = "0.42.2" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 918 | 919 | [[package]] 920 | name = "windows_aarch64_msvc" 921 | version = "0.42.2" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 924 | 925 | [[package]] 926 | name = "windows_i686_gnu" 927 | version = "0.42.2" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 930 | 931 | [[package]] 932 | name = "windows_i686_msvc" 933 | version = "0.42.2" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 936 | 937 | [[package]] 938 | name = "windows_x86_64_gnu" 939 | version = "0.42.2" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 942 | 943 | [[package]] 944 | name = "windows_x86_64_gnullvm" 945 | version = "0.42.2" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 948 | 949 | [[package]] 950 | name = "windows_x86_64_msvc" 951 | version = "0.42.2" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 954 | -------------------------------------------------------------------------------- /crates/fta/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fta" 3 | version = "2.0.1" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = "Fast TypeScript Analyzer" 7 | homepage = "https://ftaproject.dev" 8 | documentation = "https://github.com/sgb-io/fta" 9 | repository = "https://github.com/sgb-io/fta" 10 | readme = "../../README.md" 11 | 12 | [dependencies] 13 | clap = { version = "4.5.38", features = ["derive"] } 14 | comfy-table = { version = "7.1.4", optional = true } 15 | env_logger = "0.11" 16 | globset = "0.4" 17 | ignore = "0.4" 18 | log = "0.4" 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | swc_common = "0.31.12" 22 | swc_ecma_ast = "0.106.0" 23 | swc_ecma_parser = "0.136.0" 24 | swc_ecma_visit = "0.92.0" 25 | tempfile = "3.20.0" 26 | 27 | [features] 28 | default = ["use_output"] 29 | use_output = ["comfy-table"] -------------------------------------------------------------------------------- /crates/fta/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::structs::{FtaConfigOptional, FtaConfigResolved}; 2 | use std::fmt; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::Path; 6 | 7 | mod tests; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct ConfigError { 11 | message: String, 12 | } 13 | 14 | impl fmt::Display for ConfigError { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | write!(f, "ConfigError! {}", self.message) 17 | } 18 | } 19 | 20 | impl From for FtaConfigResolved { 21 | fn from(opt_config: FtaConfigOptional) -> Self { 22 | let default_config = get_default_config(); 23 | FtaConfigResolved { 24 | extensions: opt_config.extensions.unwrap_or(default_config.extensions), 25 | exclude_filenames: opt_config 26 | .exclude_filenames 27 | .unwrap_or(default_config.exclude_filenames), 28 | exclude_directories: opt_config 29 | .exclude_directories 30 | .unwrap_or(default_config.exclude_directories), 31 | output_limit: opt_config 32 | .output_limit 33 | .unwrap_or(default_config.output_limit), 34 | score_cap: opt_config.score_cap.unwrap_or(default_config.score_cap), 35 | include_comments: opt_config 36 | .include_comments 37 | .unwrap_or(default_config.include_comments), 38 | exclude_under: opt_config 39 | .exclude_under 40 | .unwrap_or(default_config.exclude_under), 41 | } 42 | } 43 | } 44 | 45 | pub fn get_default_config() -> FtaConfigResolved { 46 | let default_config = FtaConfigResolved { 47 | extensions: vec![ 48 | ".js".to_string(), 49 | ".jsx".to_string(), 50 | ".ts".to_string(), 51 | ".tsx".to_string(), 52 | ], 53 | exclude_filenames: vec![ 54 | ".d.ts".to_string(), 55 | ".min.js".to_string(), 56 | ".bundle.js".to_string(), 57 | ], 58 | exclude_directories: vec![ 59 | "/dist".to_string(), 60 | "/bin".to_string(), 61 | "/build".to_string(), 62 | ], 63 | output_limit: 5000, 64 | score_cap: 1000, 65 | include_comments: false, 66 | exclude_under: 6, 67 | }; 68 | 69 | default_config 70 | } 71 | 72 | pub fn read_config( 73 | config_path: String, 74 | path_specified_by_user: bool, 75 | ) -> Result { 76 | let default_config = get_default_config(); 77 | if Path::new(&config_path).exists() { 78 | let mut file = File::open(config_path).unwrap(); 79 | let mut content = String::new(); 80 | file.read_to_string(&mut content).unwrap(); 81 | let provided_config: FtaConfigOptional = serde_json::from_str(&content).unwrap_or_default(); 82 | 83 | // For extensions, filenames and exclude_directories, 84 | // user-provided values are added to the defaults. 85 | return Result::Ok(FtaConfigResolved { 86 | extensions: { 87 | let mut extensions = default_config.extensions; 88 | if let Some(mut provided) = provided_config.extensions { 89 | extensions.append(&mut provided); 90 | } 91 | extensions 92 | }, 93 | exclude_filenames: { 94 | let mut exclude_filenames = default_config.exclude_filenames; 95 | if let Some(mut provided) = provided_config.exclude_filenames { 96 | exclude_filenames.append(&mut provided); 97 | } 98 | exclude_filenames 99 | }, 100 | exclude_directories: { 101 | let mut exclude_directories = default_config.exclude_directories; 102 | if let Some(mut provided) = provided_config.exclude_directories { 103 | exclude_directories.append(&mut provided); 104 | } 105 | exclude_directories 106 | }, 107 | output_limit: provided_config 108 | .output_limit 109 | .unwrap_or(default_config.output_limit), 110 | score_cap: provided_config 111 | .score_cap 112 | .unwrap_or(default_config.score_cap), 113 | exclude_under: provided_config 114 | .exclude_under 115 | .unwrap_or(default_config.exclude_under), 116 | include_comments: provided_config 117 | .include_comments 118 | .unwrap_or(default_config.include_comments), 119 | }); 120 | } 121 | 122 | if !path_specified_by_user { 123 | return Result::Ok(default_config); 124 | } 125 | 126 | Result::Err(ConfigError { 127 | message: format!("Config file not found at file path: {}", config_path), 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /crates/fta/src/config/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::config::read_config; 4 | use std::io::Write; 5 | use tempfile::NamedTempFile; 6 | 7 | fn create_temp_file(content: &str) -> NamedTempFile { 8 | let mut temp_file = NamedTempFile::new().unwrap(); 9 | write!(temp_file, "{}", content).unwrap(); 10 | temp_file 11 | } 12 | 13 | #[test] 14 | fn test_read_config_with_valid_json() { 15 | let valid_json = r#" 16 | { 17 | "extensions": [".foo.ts"], 18 | "exclude_filenames": [".bar.ts"], 19 | "exclude_directories": ["/baz"], 20 | "exclude_under": 10, 21 | "output_limit": 2500, 22 | "score_cap": 500, 23 | "include_comments": true 24 | } 25 | "#; 26 | 27 | let temp_file = create_temp_file(valid_json); 28 | let path = temp_file.path().to_str().unwrap(); 29 | let config = read_config(path.to_string(), false).unwrap(); 30 | 31 | assert_eq!( 32 | config.extensions, 33 | vec![ 34 | ".js".to_string(), 35 | ".jsx".to_string(), 36 | ".ts".to_string(), 37 | ".tsx".to_string(), 38 | ".foo.ts".to_string() 39 | ] 40 | ); 41 | assert_eq!( 42 | config.exclude_filenames, 43 | vec![ 44 | ".d.ts".to_string(), 45 | ".min.js".to_string(), 46 | ".bundle.js".to_string(), 47 | ".bar.ts".to_string() 48 | ] 49 | ); 50 | assert_eq!( 51 | config.exclude_directories, 52 | vec![ 53 | "/dist".to_string(), 54 | "/bin".to_string(), 55 | "/build".to_string(), 56 | "/baz".to_string(), 57 | ] 58 | ); 59 | assert_eq!(config.output_limit, 2500); 60 | assert_eq!(config.score_cap, 500); 61 | assert_eq!(config.include_comments, true); 62 | } 63 | 64 | #[test] 65 | fn test_read_config_with_partial_json() { 66 | let partial_json = r#" 67 | { 68 | "extensions": [".foo.ts"], 69 | "exclude_filenames": [".bar.ts"] 70 | } 71 | "#; 72 | 73 | let temp_file = create_temp_file(partial_json); 74 | let path = temp_file.path().to_str().unwrap(); 75 | let config = read_config(path.to_string(), false).unwrap(); 76 | 77 | assert_eq!( 78 | config.extensions, 79 | vec![ 80 | ".js".to_string(), 81 | ".jsx".to_string(), 82 | ".ts".to_string(), 83 | ".tsx".to_string(), 84 | ".foo.ts".to_string() 85 | ] 86 | ); 87 | assert_eq!( 88 | config.exclude_filenames, 89 | vec![ 90 | ".d.ts".to_string(), 91 | ".min.js".to_string(), 92 | ".bundle.js".to_string(), 93 | ".bar.ts".to_string() 94 | ] 95 | ); 96 | assert_eq!( 97 | config.exclude_directories, 98 | vec![ 99 | "/dist".to_string(), 100 | "/bin".to_string(), 101 | "/build".to_string(), 102 | ] 103 | ); 104 | assert_eq!(config.output_limit, 5000); 105 | assert_eq!(config.score_cap, 1000); 106 | assert_eq!(config.include_comments, false); 107 | } 108 | 109 | #[test] 110 | fn test_read_config_with_nonexistent_file() { 111 | let nonexistent_path = "nonexistent_file.json"; 112 | 113 | let config = read_config(nonexistent_path.to_string(), false).unwrap(); 114 | 115 | assert_eq!( 116 | config.extensions, 117 | vec![ 118 | ".js".to_string(), 119 | ".jsx".to_string(), 120 | ".ts".to_string(), 121 | ".tsx".to_string(), 122 | ] 123 | ); 124 | assert_eq!( 125 | config.exclude_filenames, 126 | vec![ 127 | ".d.ts".to_string(), 128 | ".min.js".to_string(), 129 | ".bundle.js".to_string(), 130 | ] 131 | ); 132 | assert_eq!( 133 | config.exclude_directories, 134 | vec![ 135 | "/dist".to_string(), 136 | "/bin".to_string(), 137 | "/build".to_string(), 138 | ] 139 | ); 140 | assert_eq!(config.output_limit, 5000); 141 | assert_eq!(config.score_cap, 1000); 142 | assert_eq!(config.include_comments, false); 143 | } 144 | 145 | #[test] 146 | fn test_read_config_with_user_specified_file_path() { 147 | let valid_json = r#" 148 | { 149 | "extensions": [".foo.ts"], 150 | "exclude_filenames": [".bar.ts"], 151 | "exclude_directories": ["/baz"], 152 | "output_limit": 2500, 153 | "score_cap": 500 154 | } 155 | "#; 156 | 157 | let temp_file = create_temp_file(valid_json); 158 | let path = temp_file.path().to_str().unwrap(); 159 | 160 | let config = read_config(path.to_string(), true).unwrap(); 161 | 162 | assert_eq!( 163 | config.extensions, 164 | vec![ 165 | ".js".to_string(), 166 | ".jsx".to_string(), 167 | ".ts".to_string(), 168 | ".tsx".to_string(), 169 | ".foo.ts".to_string(), 170 | ] 171 | ); 172 | assert_eq!( 173 | config.exclude_filenames, 174 | vec![ 175 | ".d.ts".to_string(), 176 | ".min.js".to_string(), 177 | ".bundle.js".to_string(), 178 | ".bar.ts".to_string(), 179 | ] 180 | ); 181 | assert_eq!( 182 | config.exclude_directories, 183 | vec![ 184 | "/dist".to_string(), 185 | "/bin".to_string(), 186 | "/build".to_string(), 187 | "/baz".to_string(), 188 | ] 189 | ); 190 | assert_eq!(config.output_limit, 2500); 191 | assert_eq!(config.score_cap, 500); 192 | assert_eq!(config.include_comments, false); 193 | } 194 | 195 | #[test] 196 | fn temp_read_config_with_nonexistent_file_and_user_specified_file_path() { 197 | let config = read_config(String::from("nonexistent_file.json"), true); 198 | 199 | assert!(config.is_err(), "Expected error, got {:?}", config); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /crates/fta/src/cyclo/mod.rs: -------------------------------------------------------------------------------- 1 | use swc_ecma_ast::*; 2 | use swc_ecma_visit::{Visit, VisitWith}; 3 | 4 | mod tests; 5 | 6 | struct ComplexityVisitor { 7 | complexity: usize, 8 | } 9 | 10 | impl ComplexityVisitor { 11 | fn new() -> Self { 12 | ComplexityVisitor { complexity: 1 } 13 | } 14 | } 15 | 16 | impl Visit for ComplexityVisitor { 17 | fn visit_bin_expr(&mut self, node: &BinExpr) { 18 | let op = node.op.as_str(); 19 | if op == "&&" || op == "||" { 20 | self.complexity += 1; 21 | } 22 | node.visit_children_with(self); 23 | } 24 | 25 | fn visit_if_stmt(&mut self, node: &IfStmt) { 26 | self.complexity += 1; 27 | node.visit_children_with(self); 28 | } 29 | 30 | fn visit_switch_stmt(&mut self, node: &SwitchStmt) { 31 | // Count each case as a decision point 32 | self.complexity += node.cases.len(); 33 | 34 | // Traverse the child nodes (cases and their statements) 35 | node.visit_children_with(self); 36 | } 37 | 38 | fn visit_for_stmt(&mut self, node: &ForStmt) { 39 | self.complexity += 1; 40 | node.visit_children_with(self); 41 | } 42 | 43 | fn visit_while_stmt(&mut self, node: &WhileStmt) { 44 | self.complexity += 1; 45 | node.visit_children_with(self); 46 | } 47 | 48 | fn visit_do_while_stmt(&mut self, node: &DoWhileStmt) { 49 | self.complexity += 1; 50 | node.visit_children_with(self); 51 | } 52 | 53 | fn visit_for_in_stmt(&mut self, node: &ForInStmt) { 54 | self.complexity += 1; 55 | node.visit_children_with(self); 56 | } 57 | 58 | fn visit_for_of_stmt(&mut self, node: &ForOfStmt) { 59 | self.complexity += 1; 60 | node.visit_children_with(self); 61 | } 62 | 63 | fn visit_catch_clause(&mut self, node: &CatchClause) { 64 | self.complexity += 1; 65 | node.visit_children_with(self); 66 | } 67 | 68 | fn visit_cond_expr(&mut self, node: &CondExpr) { 69 | self.complexity += 1; 70 | node.visit_children_with(self); 71 | } 72 | } 73 | 74 | pub fn cyclomatic_complexity(module: &Module) -> usize { 75 | let mut visitor = ComplexityVisitor::new(); 76 | visitor.visit_module(&module); 77 | visitor.complexity 78 | } 79 | -------------------------------------------------------------------------------- /crates/fta/src/cyclo/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::cyclo::cyclomatic_complexity; 4 | use crate::parse::parse_module; 5 | use swc_ecma_ast::Module; 6 | 7 | fn parse(src: &str) -> Module { 8 | match parse_module(src, false, false) { 9 | (Ok(module), _line_count) => module, 10 | (Err(_err), _) => { 11 | panic!("failed"); 12 | } 13 | } 14 | } 15 | 16 | #[test] 17 | fn test_empty_module() { 18 | let ts_code = r#" 19 | /* Empty TypeScript code */ 20 | "#; 21 | let module = parse(ts_code); 22 | assert_eq!(cyclomatic_complexity(&module), 1); 23 | } 24 | 25 | #[test] 26 | fn test_single_if() { 27 | let ts_code = r#" 28 | if (x > 0) { 29 | console.log("x is positive"); 30 | } 31 | "#; 32 | let module = parse(ts_code); 33 | assert_eq!(cyclomatic_complexity(&module), 2); 34 | } 35 | 36 | #[test] 37 | fn test_if_else() { 38 | let ts_code = r#" 39 | if (x > 0) { 40 | console.log("x is positive"); 41 | } else { 42 | console.log("x is not positive"); 43 | } 44 | "#; 45 | let module = parse(ts_code); 46 | assert_eq!(cyclomatic_complexity(&module), 2); 47 | } 48 | 49 | #[test] 50 | fn test_nested_ifs() { 51 | let ts_code = r#" 52 | if (x > 0) { 53 | if (x < 10) { 54 | console.log("x is between 0 and 10"); 55 | } 56 | } else { 57 | console.log("x is not positive"); 58 | } 59 | "#; 60 | let module = parse(ts_code); 61 | assert_eq!(cyclomatic_complexity(&module), 3); 62 | } 63 | 64 | #[test] 65 | fn test_switch_case() { 66 | let ts_code = r#" 67 | switch (x) { 68 | case 0: 69 | console.log("x is 0"); 70 | break; 71 | case 1: 72 | console.log("x is 1"); 73 | break; 74 | default: 75 | console.log("x is not 0 or 1"); 76 | } 77 | "#; 78 | let module = parse(ts_code); 79 | assert_eq!(cyclomatic_complexity(&module), 4); 80 | } 81 | 82 | #[test] 83 | fn test_for_loop() { 84 | let ts_code = r#" 85 | for (let i = 0; i < 10; i++) { 86 | console.log(i); 87 | } 88 | "#; 89 | let module = parse(ts_code); 90 | assert_eq!(cyclomatic_complexity(&module), 2); 91 | } 92 | 93 | #[test] 94 | fn test_while_loop() { 95 | let ts_code = r#" 96 | let i = 0; 97 | while (i < 10) { 98 | console.log(i); 99 | i++; 100 | } 101 | "#; 102 | let module = parse(ts_code); 103 | assert_eq!(cyclomatic_complexity(&module), 2); 104 | } 105 | 106 | #[test] 107 | fn test_do_while_loop() { 108 | let ts_code = r#" 109 | let i = 0; 110 | do { 111 | console.log(i); 112 | i++; 113 | } while (i < 10); 114 | "#; 115 | let module = parse(ts_code); 116 | assert_eq!(cyclomatic_complexity(&module), 2); 117 | } 118 | 119 | #[test] 120 | fn test_for_in_loop() { 121 | let ts_code = r#" 122 | let obj = { a: 1, b: 2, c: 3 }; 123 | for (let key in obj) { 124 | console.log(key, obj[key]); 125 | } 126 | "#; 127 | let module = parse(ts_code); 128 | assert_eq!(cyclomatic_complexity(&module), 2); 129 | } 130 | 131 | #[test] 132 | fn test_for_of_loop() { 133 | let ts_code = r#" 134 | let arr = [1, 2, 3]; 135 | for (let item of arr) { 136 | console.log(item); 137 | } 138 | "#; 139 | let module = parse(ts_code); 140 | assert_eq!(cyclomatic_complexity(&module), 2); 141 | } 142 | 143 | #[test] 144 | fn test_try_catch() { 145 | let ts_code = r#" 146 | try { 147 | throw new Error("An error occurred"); 148 | } catch (e) { 149 | console.log(e.message); 150 | } 151 | "#; 152 | let module = parse(ts_code); 153 | assert_eq!(cyclomatic_complexity(&module), 2); 154 | } 155 | 156 | #[test] 157 | fn test_conditional_expression() { 158 | let ts_code = r#" 159 | let result = x > 0 ? "positive" : "non-positive"; 160 | "#; 161 | let module = parse(ts_code); 162 | assert_eq!(cyclomatic_complexity(&module), 2); 163 | } 164 | 165 | #[test] 166 | fn comments_have_no_impact_on_complexity() { 167 | let uncommented_code = r##" 168 | let obj = { 169 | ['computed' + 'Property']: 'value' 170 | }; 171 | 172 | class MyClass { 173 | [Symbol.iterator]() {} 174 | } 175 | 176 | class MyClassTwo { 177 | #privateField = 'value'; 178 | 179 | getPrivateField() { 180 | return this.#privateField; 181 | } 182 | } 183 | "##; 184 | let commented_code = r##" 185 | // Define an object with a computed property 186 | let obj = { 187 | // The property name is the result of concatenating 'computed' and 'Property' 188 | ['computed' + 'Property']: 'value' // The value of the property is 'value' 189 | }; 190 | 191 | // Define a class named MyClass 192 | class MyClass { 193 | /* 194 | * Define a method with a computed name 195 | * In this case, the method name is Symbol.iterator, which is a built-in symbol 196 | */ 197 | [Symbol.iterator]() {} // The method is currently empty 198 | } 199 | 200 | // Define a class named MyClassTwo 201 | class MyClassTwo { 202 | // Define a private field named #privateField 203 | // The # syntax is used to denote private fields in JavaScript 204 | #privateField = 'value'; // The initial value of the field is 'value' 205 | 206 | // Define a method named getPrivateField 207 | getPrivateField() { 208 | // Return the value of the private field #privateField 209 | return this.#privateField; 210 | } 211 | } 212 | "##; 213 | let un_commented_module = parse(uncommented_code); 214 | let commented_module = parse(commented_code); 215 | assert_eq!( 216 | cyclomatic_complexity(&un_commented_module), 217 | cyclomatic_complexity(&commented_module) 218 | ); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /crates/fta/src/halstead/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::structs::HalsteadMetrics; 2 | use log::debug; 3 | use std::collections::HashSet; 4 | use swc_ecma_ast::*; 5 | use swc_ecma_visit::{Visit, VisitWith}; 6 | 7 | mod tests; 8 | 9 | #[derive(Debug)] 10 | struct AstAnalyzer { 11 | unique_operators: HashSet, 12 | unique_operands: HashSet, 13 | total_operators: usize, 14 | total_operands: usize, 15 | } 16 | 17 | impl AstAnalyzer { 18 | fn new() -> Self { 19 | AstAnalyzer { 20 | unique_operators: HashSet::new(), 21 | unique_operands: HashSet::new(), 22 | total_operators: 0, 23 | total_operands: 0, 24 | } 25 | } 26 | } 27 | 28 | impl Visit for AstAnalyzer { 29 | fn visit_expr(&mut self, expr: &Expr) { 30 | match expr { 31 | Expr::Bin(binary_expr) => { 32 | self.unique_operators.insert(binary_expr.op.to_string()); 33 | self.total_operators += 1; 34 | binary_expr.left.visit_with(self); 35 | binary_expr.right.visit_with(self); 36 | } 37 | Expr::Ident(ident) => { 38 | self.unique_operands.insert(ident.sym.to_string()); 39 | self.total_operands += 1; 40 | } 41 | Expr::Lit(lit) => match lit { 42 | Lit::Str(str_lit) => { 43 | let value = &str_lit.value; 44 | self.total_operands += 1; 45 | self.unique_operands.insert(value.to_string()); 46 | } 47 | Lit::Bool(bool_lit) => { 48 | let value = bool_lit.value.to_string(); 49 | self.total_operands += 1; 50 | self.unique_operands.insert(value); 51 | } 52 | Lit::Null(_) => { 53 | self.total_operands += 1; 54 | self.unique_operands.insert("null".to_string()); 55 | } 56 | Lit::Num(num_lit) => { 57 | let value = num_lit.value.to_string(); 58 | self.total_operands += 1; 59 | self.unique_operands.insert(value); 60 | } 61 | Lit::Regex(regex) => { 62 | let regex_literal = format!("/{}/{}", regex.exp, regex.flags); 63 | self.unique_operands.insert(regex_literal); 64 | self.total_operands += 1; 65 | } 66 | _ => { 67 | debug!( 68 | "visit_expr(Expr::Lit): Literal expression assumed to not count towards operators and operands: {:?}", 69 | lit 70 | ); 71 | } 72 | }, 73 | Expr::Array(array) => { 74 | for elem in &array.elems { 75 | if let Some(element) = elem { 76 | match element { 77 | ExprOrSpread { expr, spread } => { 78 | if spread.is_some() { 79 | self.unique_operators.insert("...".to_string()); 80 | self.total_operators += 1; 81 | } 82 | expr.visit_with(self); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | Expr::Arrow(arrow) => { 89 | self.unique_operators.insert("=>".to_string()); 90 | self.total_operators += 1; 91 | 92 | arrow.params.visit_with(self); 93 | arrow.body.visit_with(self); 94 | } 95 | Expr::Assign(assign) => { 96 | self.unique_operators.insert(assign.op.to_string()); 97 | self.total_operators += 1; 98 | 99 | assign.left.visit_with(self); 100 | assign.right.visit_with(self); 101 | } 102 | Expr::Call(call) => { 103 | self.unique_operators.insert("()".to_string()); 104 | self.total_operators += 1; 105 | 106 | call.callee.visit_with(self); 107 | for arg in &call.args { 108 | arg.visit_with(self); 109 | } 110 | } 111 | Expr::Cond(cond) => { 112 | self.unique_operators.insert("?".to_string()); 113 | self.unique_operators.insert(":".to_string()); 114 | self.total_operators += 2; 115 | 116 | cond.test.visit_with(self); 117 | cond.cons.visit_with(self); 118 | cond.alt.visit_with(self); 119 | } 120 | Expr::Member(member) => { 121 | self.unique_operators.insert(".".to_string()); 122 | self.total_operators += 1; 123 | member.obj.visit_with(self); 124 | member.prop.visit_with(self); 125 | } 126 | Expr::Object(object) => { 127 | for prop in &object.props { 128 | match prop { 129 | PropOrSpread::Prop(boxed_prop) => match &**boxed_prop { 130 | Prop::KeyValue(key_value) => { 131 | self.unique_operators.insert(":".to_string()); 132 | self.total_operators += 1; 133 | key_value.key.visit_with(self); 134 | key_value.value.visit_with(self); 135 | } 136 | Prop::Assign(assign) => { 137 | self.unique_operators.insert("=".to_string()); 138 | self.total_operators += 1; 139 | assign.key.visit_with(self); 140 | assign.value.visit_with(self); 141 | } 142 | Prop::Shorthand(ident) => { 143 | ident.visit_with(self); 144 | } 145 | Prop::Method(method_prop) => { 146 | method_prop.key.visit_with(self); 147 | method_prop.function.visit_with(self); 148 | } 149 | _ => { 150 | debug!( 151 | "visit_expr(Expr::Object): Object prop assumed to not count towards operators and operands: {:?}", 152 | boxed_prop 153 | ); 154 | } 155 | }, 156 | PropOrSpread::Spread(spread) => { 157 | spread.expr.visit_with(self); 158 | } 159 | } 160 | } 161 | } 162 | Expr::Tpl(tpl) => { 163 | self.unique_operators.insert("`".to_string()); // Template literal backticks 164 | self.total_operators += 2; // Opening and closing backticks 165 | 166 | for expr in &tpl.exprs { 167 | self.unique_operators.insert("${".to_string()); // Expression interpolation 168 | self.total_operators += 1; 169 | expr.visit_with(self); 170 | } 171 | } 172 | Expr::TsAs(ts_as) => { 173 | self.unique_operators.insert("TsAs".to_string()); 174 | self.total_operators += 1; 175 | ts_as.expr.visit_with(self); 176 | ts_as.type_ann.visit_with(self); 177 | // No need to visit the type_ann as it doesn't contribute to operands or operators. 178 | } 179 | Expr::TsNonNull(ts_non_null) => { 180 | self.unique_operators.insert("TsNonNull".to_string()); 181 | self.total_operators += 1; 182 | ts_non_null.expr.visit_with(self); 183 | } 184 | Expr::Unary(unary) => { 185 | self.unique_operators.insert(unary.op.to_string()); 186 | self.total_operators += 1; 187 | unary.arg.visit_with(self); 188 | } 189 | Expr::New(new_expr) => { 190 | self.unique_operators.insert("new".to_string()); 191 | self.total_operators += 1; 192 | 193 | new_expr.callee.visit_with(self); 194 | if let Some(args) = &new_expr.args { 195 | for arg in args { 196 | arg.visit_with(self); 197 | } 198 | } 199 | } 200 | Expr::Paren(paren_expr) => { 201 | self.unique_operators.insert("(".to_string()); 202 | self.unique_operators.insert(")".to_string()); 203 | self.total_operators += 2; 204 | 205 | paren_expr.expr.visit_with(self); 206 | } 207 | Expr::Update(update) => { 208 | self.unique_operators.insert(update.op.to_string()); 209 | self.total_operators += 1; 210 | update.arg.visit_with(self); 211 | } 212 | Expr::OptChain(opt_chain) => { 213 | self.unique_operators.insert("?.".to_string()); 214 | self.total_operators += 1; 215 | opt_chain.visit_with(self); 216 | } 217 | Expr::Seq(seq) => { 218 | self.unique_operators.insert("seq".to_string()); 219 | self.total_operators += 1; 220 | 221 | for expr in &seq.exprs { 222 | expr.visit_with(self); 223 | } 224 | } 225 | Expr::Await(await_expr) => { 226 | self.unique_operators.insert("await".to_string()); 227 | self.total_operators += 1; 228 | await_expr.arg.visit_with(self); 229 | } 230 | Expr::This(_) => { 231 | self.unique_operands.insert("this".to_string()); 232 | self.total_operands += 1; 233 | } 234 | 235 | // The below cases don't contribute to operators/operands, but their children could 236 | Expr::JSXElement(jsx_element) => { 237 | // Traverse the opening element. 238 | jsx_element.opening.visit_with(self); 239 | 240 | // Traverse the children. 241 | for child in &jsx_element.children { 242 | child.visit_with(self); 243 | } 244 | 245 | // Traverse the closing element, if present. 246 | if let Some(closing) = &jsx_element.closing { 247 | closing.visit_with(self); 248 | } 249 | } 250 | Expr::JSXFragment(fragment) => { 251 | for child in &fragment.children { 252 | child.visit_with(self); 253 | } 254 | } 255 | Expr::TaggedTpl(tagged_tpl) => { 256 | self.unique_operators.insert("TaggedTemplate".to_string()); 257 | self.total_operators += 1; // Implicit tagged template operator 258 | 259 | tagged_tpl.tag.visit_with(self); 260 | tagged_tpl.tpl.visit_with(self); 261 | } 262 | _ => { 263 | expr.visit_children_with(self); 264 | debug!( 265 | "visit_expr: Expression assumed to not count towards operators and operands: {:?}", 266 | expr 267 | ); 268 | } 269 | } 270 | } 271 | 272 | fn visit_export_decl(&mut self, export_decl: &ExportDecl) { 273 | self.total_operators += 1; 274 | self.unique_operators.insert("export".to_string()); 275 | 276 | // Continue visiting the declaration 277 | export_decl.visit_children_with(self); 278 | } 279 | 280 | fn visit_pat(&mut self, pat: &Pat) { 281 | match pat { 282 | Pat::Ident(ident) => { 283 | let ident_str = ident.sym.as_ref().to_string(); 284 | self.unique_operands.insert(ident_str); 285 | self.total_operands += 1; 286 | } 287 | _ => { 288 | // Handle other patterns if necessary or visit their children 289 | pat.visit_children_with(self); 290 | } 291 | } 292 | } 293 | 294 | fn visit_fn_decl(&mut self, fn_decl: &FnDecl) { 295 | self.total_operators += 1; 296 | self.unique_operators.insert("function".to_string()); 297 | 298 | fn_decl.visit_children_with(self); 299 | } 300 | 301 | fn visit_class_decl(&mut self, class_decl: &ClassDecl) { 302 | self.total_operators += 1; 303 | self.unique_operators.insert("class".to_string()); 304 | 305 | class_decl.visit_children_with(self); 306 | } 307 | 308 | fn visit_member_expr(&mut self, node: &MemberExpr) { 309 | if let MemberProp::Ident(_) = &node.prop { 310 | self.unique_operators.insert(".".to_string()); // Non-computed member access operator 311 | self.total_operators += 1; 312 | } 313 | 314 | node.obj.visit_with(self); 315 | } 316 | 317 | fn visit_ident(&mut self, node: &Ident) { 318 | self.unique_operands.insert(node.sym.to_string()); 319 | self.total_operands += 1; 320 | } 321 | 322 | fn visit_tpl(&mut self, node: &Tpl) { 323 | self.unique_operators.insert("Template String".to_string()); 324 | self.total_operators += 1; 325 | 326 | for element in &node.quasis { 327 | element.visit_with(self); 328 | } 329 | for expr in &node.exprs { 330 | expr.visit_with(self); 331 | } 332 | } 333 | 334 | fn visit_ts_type_operator(&mut self, node: &TsTypeOperator) { 335 | let operator = format!("{:?}", node.op); 336 | self.unique_operators.insert(operator); 337 | self.total_operators += 1; 338 | 339 | node.type_ann.visit_with(self); 340 | } 341 | 342 | fn visit_ts_mapped_type(&mut self, node: &TsMappedType) { 343 | self.unique_operators.insert("TsMappedType".to_string()); 344 | self.total_operators += 2; // Implicit key in keyof and value in mapping type operators 345 | 346 | node.type_param.visit_with(self); 347 | if let Some(type_ann) = &node.type_ann { 348 | type_ann.visit_with(self); 349 | } 350 | } 351 | 352 | fn visit_ts_indexed_access_type(&mut self, node: &TsIndexedAccessType) { 353 | self.unique_operators 354 | .insert("TsIndexedAccessType".to_string()); 355 | self.total_operators += 1; // Implicit indexed access operator 356 | 357 | node.obj_type.visit_with(self); 358 | node.index_type.visit_with(self); 359 | } 360 | 361 | fn visit_yield_expr(&mut self, node: &YieldExpr) { 362 | self.unique_operators.insert("yield".to_string()); 363 | self.total_operators += 1; 364 | if let Some(arg) = &node.arg { 365 | arg.visit_with(self); 366 | } 367 | } 368 | 369 | fn visit_meta_prop_expr(&mut self, _node: &MetaPropExpr) { 370 | self.unique_operators.insert("new.target".to_string()); 371 | self.total_operators += 1; 372 | // No children to visit 373 | } 374 | 375 | fn visit_return_stmt(&mut self, return_stmt: &ReturnStmt) { 376 | // Capture the return operator 377 | self.unique_operators.insert("return".to_string()); 378 | self.total_operators += 1; 379 | 380 | // Visit the expression within the return statement, if present 381 | if let Some(expr) = &return_stmt.arg { 382 | expr.visit_with(self); 383 | } 384 | } 385 | 386 | fn visit_import_decl(&mut self, node: &ImportDecl) { 387 | self.unique_operators.insert("import".to_string()); 388 | self.total_operators += 1; 389 | 390 | self.unique_operators.insert("from".to_string()); 391 | self.total_operators += 1; 392 | 393 | node.visit_children_with(self); 394 | } 395 | 396 | fn visit_stmt(&mut self, stmt: &Stmt) { 397 | match stmt { 398 | Stmt::Expr(_) | Stmt::Return(_) | Stmt::Throw(_) | Stmt::Decl(_) => { 399 | self.unique_operators.insert(";".to_string()); 400 | self.total_operators += 1; 401 | } 402 | _ => {} 403 | } 404 | stmt.visit_children_with(self); 405 | } 406 | } 407 | 408 | impl HalsteadMetrics { 409 | fn new( 410 | uniq_operators: usize, 411 | uniq_operands: usize, 412 | total_operators: usize, 413 | total_operands: usize, 414 | ) -> HalsteadMetrics { 415 | let program_length = total_operators + total_operands; 416 | let vocabulary_size = uniq_operators + uniq_operands; 417 | let volume = if vocabulary_size == 0 { 418 | 0.0 419 | } else { 420 | (program_length as f64) * (vocabulary_size as f64).log2() 421 | }; 422 | let difficulty = if total_operators == 0 || total_operands == 0 { 423 | 0.0 424 | } else { 425 | ((uniq_operators / 2) as f64) * (total_operands as f64) / (uniq_operands as f64) 426 | }; 427 | let effort = difficulty * volume; 428 | let time = effort / 18.0; 429 | let bugs = volume / 3000.0; 430 | 431 | HalsteadMetrics { 432 | uniq_operators, 433 | uniq_operands, 434 | total_operators, 435 | total_operands, 436 | program_length, 437 | vocabulary_size, 438 | volume, 439 | difficulty, 440 | effort, 441 | time, 442 | bugs, 443 | } 444 | } 445 | } 446 | 447 | pub fn analyze_module(module: &Module) -> HalsteadMetrics { 448 | let mut analyzer = AstAnalyzer::new(); 449 | module.visit_with(&mut analyzer); 450 | 451 | // Useful for debugging (but very verbose): 452 | // println!("unique operators: {:?}", analyzer.unique_operators); 453 | // println!("unique operands: {:?}", analyzer.unique_operands); 454 | 455 | HalsteadMetrics::new( 456 | analyzer.unique_operators.len(), 457 | analyzer.unique_operands.len(), 458 | analyzer.total_operators, 459 | analyzer.total_operands, 460 | ) 461 | } 462 | -------------------------------------------------------------------------------- /crates/fta/src/halstead/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::halstead::analyze_module; 4 | use crate::parse::parse_module; 5 | use crate::structs::HalsteadMetrics; 6 | use swc_ecma_ast::Module; 7 | 8 | fn parse(ts_code: &str) -> Module { 9 | let (parsed_module, _line_count) = parse_module(ts_code, true, false); 10 | 11 | if let Ok(parsed_module) = parsed_module { 12 | parsed_module 13 | } else { 14 | panic!("failed"); 15 | } 16 | } 17 | 18 | fn analyze(module: &Module) -> HalsteadMetrics { 19 | let metrics = analyze_module(module); 20 | metrics 21 | } 22 | 23 | #[test] 24 | fn test_empty_module() { 25 | let ts_code = r#" 26 | /* Empty TypeScript code */ 27 | "#; 28 | let module = parse(ts_code); 29 | let expected = HalsteadMetrics { 30 | uniq_operators: 0, 31 | uniq_operands: 0, 32 | total_operators: 0, 33 | total_operands: 0, 34 | program_length: 0, 35 | vocabulary_size: 0, 36 | volume: 0.0, 37 | difficulty: 0.0, 38 | effort: 0.0, 39 | time: 0.0, 40 | bugs: 0.0, 41 | }; 42 | assert_eq!(analyze(&module), expected); 43 | } 44 | 45 | #[test] 46 | fn test_switch_case() { 47 | let ts_code = r#" 48 | switch (x) { 49 | case 0: 50 | console.log("x is 0"); 51 | break; 52 | case 1: 53 | console.log("x is 1"); 54 | break; 55 | default: 56 | console.log("x is not 0 or 1"); 57 | } 58 | "#; 59 | let module = parse(ts_code); 60 | let expected = HalsteadMetrics { 61 | uniq_operators: 3, 62 | uniq_operands: 8, 63 | total_operators: 9, 64 | total_operands: 12, 65 | program_length: 21, 66 | vocabulary_size: 11, 67 | volume: 72.64806399138324, 68 | difficulty: 1.5, 69 | effort: 108.97209598707485, 70 | time: 6.05400533261527, 71 | bugs: 0.02421602133046108, 72 | }; 73 | assert_eq!(analyze(&module), expected); 74 | } 75 | 76 | #[test] 77 | fn test_complex_case_a() { 78 | let ts_code = r##" 79 | import { React, useState } from 'react'; 80 | import { asyncOperation } from './asyncOperation'; 81 | 82 | let staticFoo = true; 83 | 84 | function displayThing(thing: string) { 85 | return `thing: ${thing}`; 86 | } 87 | 88 | export default function DummyComponent() { 89 | const [thing, setThing] = useState(null); 90 | 91 | const thingForDisplay = displayThing(thing) as string; 92 | 93 | const interact = async () => { 94 | const result = await asyncOperation(); 95 | setThing(result); 96 | staticFoo = false; 97 | 98 | if (typeof thing === 'object' && thing?.foo?.bar) { 99 | console.log('This should not happen'); 100 | } 101 | } 102 | 103 | const baz = staticFoo ? 32 : 42; 104 | 105 | return ( 106 | <> 107 |
108 |

Hello World

109 |
110 |
111 |

This is a test. {thingForDisplay} {baz}

112 | 113 |
114 | 115 | ) 116 | } 117 | "##; 118 | let module = parse(ts_code); 119 | let expected = HalsteadMetrics { 120 | uniq_operators: 21, 121 | uniq_operands: 26, 122 | total_operators: 43, 123 | total_operands: 47, 124 | program_length: 90, 125 | vocabulary_size: 47, 126 | volume: 499.9129966509874, 127 | difficulty: 18.076923076923077, 128 | effort: 9036.888785614003, 129 | time: 502.0493769785557, 130 | bugs: 0.16663766555032913, 131 | }; 132 | assert_eq!(analyze(&module), expected); 133 | } 134 | 135 | #[test] 136 | fn test_complex_case_c() { 137 | let ts_code = r##" 138 | let a, b, c = 3; 139 | a = 1; 140 | b = 2; 141 | let myArray = [a, b, c]; 142 | 143 | myArray = [...myArray, ...myArray, 8, 9, 10]; 144 | 145 | const myObject = { 146 | foo: 'bar' 147 | } 148 | 149 | const myOtherObject = { 150 | ...myObject, 151 | bar: 'baz' 152 | } 153 | 154 | class Foo { 155 | constructor() { 156 | this.foo = 'some value'; 157 | } 158 | 159 | getFoo() { 160 | return this.foo!; 161 | } 162 | 163 | isFooCool() { 164 | const myRegex = /cool/; 165 | return myRegex.test(this.foo); 166 | } 167 | } 168 | 169 | const myFoo = new Foo(); 170 | 171 | export { myFoo, myOtherObject }; 172 | "##; 173 | 174 | let module = parse(ts_code); 175 | let expected = HalsteadMetrics { 176 | uniq_operators: 10, 177 | uniq_operands: 25, 178 | total_operators: 31, 179 | total_operands: 44, 180 | program_length: 75, 181 | vocabulary_size: 35, 182 | volume: 384.6962262708725, 183 | difficulty: 8.8, 184 | effort: 3385.3267911836783, 185 | time: 188.07371062131546, 186 | bugs: 0.12823207542362416, 187 | }; 188 | assert_eq!(analyze(&module), expected); 189 | } 190 | 191 | #[test] 192 | fn test_complex_case_d() { 193 | let ts_code = r##" 194 | // Covers 'visit_export_decl' 195 | export declare const foo = 42; 196 | 197 | // Covers 'visit_tpl' 198 | const tpl = `result is ${binResult}`; 199 | 200 | // Covers 'visit_ts_mapped_type' 201 | type MappedType = { [P in keyof any]: P }; 202 | 203 | // Covers 'visit_ts_indexed_access_type' 204 | type AccessType = MappedType["key"]; 205 | 206 | // Covers 'visit_ts_type_operator' 207 | type NewType = keyof any; 208 | 209 | // Covers 'visit_tpl' 210 | const person = "Mike"; 211 | const age = 28; 212 | function myTag(strings, personExp, ageExp) { 213 | const str0 = strings[0]; // "That " 214 | const str1 = strings[1]; // " is a " 215 | const str2 = strings[2]; // "." 216 | 217 | const ageStr = ageExp > 99 ? "centenarian" : "youngster"; 218 | 219 | // We can even return a string built using a template literal 220 | return `${str0}${personExp}${str1}${ageStr}${str2}`; 221 | } 222 | const output = myTag`That ${person} is a ${age}.`; 223 | "##; 224 | let module = parse(ts_code); 225 | let expected = HalsteadMetrics { 226 | uniq_operators: 15, 227 | uniq_operands: 27, 228 | total_operators: 39, 229 | total_operands: 41, 230 | program_length: 80, 231 | vocabulary_size: 42, 232 | volume: 431.38539382230084, 233 | difficulty: 10.62962962962963, 234 | effort: 4585.466963962976, 235 | time: 254.74816466460976, 236 | bugs: 0.1437951312741003, 237 | }; 238 | assert_eq!(analyze(&module), expected); 239 | } 240 | 241 | #[test] 242 | fn test_complex_case_e() { 243 | let ts_code = r##" 244 | // visit_bin_expr 245 | let a = 5 + 3; 246 | 247 | // visit_unary_expr 248 | let b = !true; 249 | 250 | // visit_assign_expr 251 | let c = 10; 252 | c += a; 253 | 254 | // visit_update_expr 255 | c++; 256 | 257 | // visit_call_expr 258 | console.log(c); 259 | 260 | // visit_new_expr 261 | let obj = new Date(); 262 | 263 | // visit_lit 264 | let str = "test"; 265 | let num = 1; 266 | let bool = true; 267 | let reg = /ab+c/; 268 | let nullLit = null; 269 | 270 | // visit_arrow_expr 271 | let add = (x: number, y: number) => x + y; 272 | 273 | // visit_tagged_tpl 274 | let person = "John"; 275 | let greeting = `Hello ${person}`; 276 | 277 | // visit_spread_element 278 | let arr1 = [1, 2, 3]; 279 | let arr2 = [...arr1, 4, 5]; 280 | 281 | // visit_ts_non_null_expr 282 | let maybeString: string | null = "Hello"; 283 | let str2 = maybeString!; 284 | 285 | // visit_ts_type_assertion 286 | let someValue: unknown = "this is a string"; 287 | let strLength: number = (someValue as string).length; 288 | 289 | // visit_ts_as_expr 290 | let anotherValue: unknown = "this is another string"; 291 | 292 | // visit_ts_qualified_name 293 | namespace A { 294 | export namespace B { 295 | export const message = "Hello, TypeScript!"; 296 | } 297 | } 298 | console.log(A.B.message); 299 | 300 | // visit_cond_expr 301 | let condition = true ? "truthy" : "falsy"; 302 | 303 | // visit_await_expr 304 | async function foo() { 305 | let result = await Promise.resolve("Hello, world!"); 306 | console.log(result); 307 | } 308 | foo(); 309 | 310 | // visit_yield_expr 311 | function* generator() { 312 | yield 'yielding a value'; 313 | } 314 | 315 | // visit_meta_prop_expr 316 | function check() { 317 | if (new.target) { 318 | console.log('Function was called with "new" keyword'); 319 | } else { 320 | console.log('Function was not called with "new" keyword'); 321 | } 322 | } 323 | check(); 324 | 325 | // visit_seq_expr 326 | let seq = (console.log('first'), console.log('second'), 'third'); 327 | 328 | let a = 5; // visit_assign_expr 329 | let b = -a; // visit_unary_expr 330 | let c = a + b; // visit_bin_expr 331 | let d = ++c; // visit_update_expr 332 | let e = Math.sqrt(d); // visit_call_expr 333 | let f = new String(e); // visit_new_expr 334 | let g = "hello"; // visit_lit 335 | let h = (x: number) => x * 2; // visit_arrow_expr 336 | let arr = [...h]; // visit_spread_element 337 | let j: number! = 5; // visit_ts_non_null_expr 338 | let cond = (a > b) ? a : b; // visit_cond_expr 339 | async function asyncFunc() { 340 | let result = await Promise.resolve(true); // visit_await_expr 341 | return result; 342 | } 343 | function* generatorFunc() { 344 | yield 'hello'; // visit_yield_expr 345 | yield* arr; // visit_yield_expr 346 | } 347 | const meta = new.target; // visit_meta_prop_expr 348 | const seq = (1, 2, 3, 4, 5); // visit_seq_expr 349 | "##; 350 | let module = parse(ts_code); 351 | let expected = HalsteadMetrics { 352 | uniq_operators: 28, 353 | uniq_operands: 75, 354 | total_operators: 130, 355 | total_operands: 139, 356 | program_length: 269, 357 | vocabulary_size: 103, 358 | volume: 1798.6686418122858, 359 | difficulty: 25.946666666666665, 360 | effort: 46669.45569288944, 361 | time: 2592.7475384938575, 362 | bugs: 0.5995562139374286, 363 | }; 364 | assert_eq!(analyze(&module), expected); 365 | } 366 | 367 | #[test] 368 | fn test_complex_case_f() { 369 | let ts_code = r##" 370 | const obj = { 371 | prop1: 123, 372 | prop2: "hello", 373 | prop3: () => { 374 | console.log("Method prop"); 375 | }, 376 | }; 377 | 378 | const fn: () => void = obj.prop3; 379 | 380 | const jsxElement = ( 381 |
382 |

Hello

383 |
384 | ); 385 | "##; 386 | let module = parse(ts_code); 387 | let expected = HalsteadMetrics { 388 | uniq_operators: 7, 389 | uniq_operands: 13, 390 | total_operators: 13, 391 | total_operands: 17, 392 | program_length: 30, 393 | vocabulary_size: 20, 394 | volume: 129.65784284662087, 395 | difficulty: 3.923076923076923, 396 | effort: 508.6576911675126, 397 | time: 28.258760620417366, 398 | bugs: 0.043219280948873624, 399 | }; 400 | assert_eq!(analyze(&module), expected); 401 | } 402 | 403 | #[test] 404 | fn test_complex_case_g() { 405 | let ts_code = r##" 406 | const value: any = "123"; 407 | const result = value as number; 408 | const obj: MyNamespace.MyClass = new MyNamespace.MyClass(); 409 | 410 | const obj = { 411 | prop1: { 412 | nested: { 413 | value: 42, 414 | }, 415 | }, 416 | prop2: [1, 2, 3], 417 | }; 418 | console.log(obj.prop1.nested.value); 419 | console.log(obj.prop2[0]); 420 | "##; 421 | let module = parse(ts_code); 422 | let expected = HalsteadMetrics { 423 | uniq_operators: 6, 424 | uniq_operands: 16, 425 | total_operators: 22, 426 | total_operands: 27, 427 | program_length: 49, 428 | vocabulary_size: 22, 429 | volume: 218.51214931322758, 430 | difficulty: 5.0625, 431 | effort: 1106.2177558982146, 432 | time: 61.45654199434526, 433 | bugs: 0.0728373831044092, 434 | }; 435 | assert_eq!(analyze(&module), expected); 436 | } 437 | 438 | #[test] 439 | fn test_complex_case_h() { 440 | let ts_code = r##" 441 | const obj = { 442 | prop1: "value1", 443 | prop2: { 444 | nested: "value2", 445 | }, 446 | prop3() { 447 | return "value3"; 448 | }, 449 | prop4: 42, 450 | prop5, 451 | prop6: { 452 | nestedMethod() { 453 | return "nestedValue"; 454 | }, 455 | }, 456 | prop7: "value7", 457 | prop8 = "value8" 458 | }; 459 | 460 | const prop5 = "value5"; 461 | 462 | console.log(obj.prop1); 463 | console.log(obj.prop2.nested); 464 | console.log(obj.prop3()); 465 | console.log(obj.prop4); 466 | console.log(obj.prop5); 467 | console.log(obj.prop6.nestedMethod()); 468 | "##; 469 | let module = parse(ts_code); 470 | let expected = HalsteadMetrics { 471 | uniq_operators: 6, 472 | uniq_operands: 21, 473 | total_operators: 41, 474 | total_operands: 46, 475 | program_length: 87, 476 | vocabulary_size: 27, 477 | volume: 413.67521268822173, 478 | difficulty: 6.571428571428571, 479 | effort: 2718.437111951171, 480 | time: 151.02428399728728, 481 | bugs: 0.1378917375627406, 482 | }; 483 | assert_eq!(analyze(&module), expected); 484 | } 485 | 486 | #[test] 487 | fn test_complex_case_i() { 488 | let ts_code = r##" 489 | let obj = { 490 | ['computed' + 'Property']: 'value' 491 | }; 492 | 493 | class MyClass { 494 | [Symbol.iterator]() {} 495 | } 496 | 497 | class MyClassTwo { 498 | #privateField = 'value'; 499 | 500 | getPrivateField() { 501 | return this.#privateField; 502 | } 503 | } 504 | "##; 505 | let module = parse(ts_code); 506 | let expected = HalsteadMetrics { 507 | uniq_operators: 6, 508 | uniq_operands: 11, 509 | total_operators: 11, 510 | total_operands: 13, 511 | program_length: 24, 512 | vocabulary_size: 17, 513 | volume: 98.09910819000814, 514 | difficulty: 3.5454545454545454, 515 | effort: 347.80592903730155, 516 | time: 19.32255161318342, 517 | bugs: 0.032699702730002715, 518 | }; 519 | assert_eq!(analyze(&module), expected); 520 | } 521 | 522 | #[test] 523 | fn comments_have_no_impact_on_metrics() { 524 | let uncommented_code = r##" 525 | let obj = { 526 | ['computed' + 'Property']: 'value' 527 | }; 528 | 529 | class MyClass { 530 | [Symbol.iterator]() {} 531 | } 532 | 533 | class MyClassTwo { 534 | #privateField = 'value'; 535 | 536 | getPrivateField() { 537 | return this.#privateField; 538 | } 539 | } 540 | "##; 541 | let commented_code = r##" 542 | // Define an object with a computed property 543 | let obj = { 544 | // The property name is the result of concatenating 'computed' and 'Property' 545 | ['computed' + 'Property']: 'value' // The value of the property is 'value' 546 | }; 547 | 548 | // Define a class named MyClass 549 | class MyClass { 550 | /* 551 | * Define a method with a computed name 552 | * In this case, the method name is Symbol.iterator, which is a built-in symbol 553 | */ 554 | [Symbol.iterator]() {} // The method is currently empty 555 | } 556 | 557 | // Define a class named MyClassTwo 558 | class MyClassTwo { 559 | // Define a private field named #privateField 560 | // The # syntax is used to denote private fields in JavaScript 561 | #privateField = 'value'; // The initial value of the field is 'value' 562 | 563 | // Define a method named getPrivateField 564 | getPrivateField() { 565 | // Return the value of the private field #privateField 566 | return this.#privateField; 567 | } 568 | } 569 | "##; 570 | let commented_code_module = parse(commented_code); 571 | let uncommented_code_module = parse(uncommented_code); 572 | assert_eq!( 573 | analyze(&commented_code_module), 574 | analyze(&uncommented_code_module) 575 | ); 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /crates/fta/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod cyclo; 3 | mod halstead; 4 | pub mod parse; 5 | mod structs; 6 | mod utils; 7 | mod walk; 8 | 9 | #[cfg(feature = "use_output")] 10 | pub mod output; 11 | 12 | use ignore::DirEntry; 13 | use ignore::WalkBuilder; 14 | use log::debug; 15 | use log::warn; 16 | use std::env; 17 | use std::fs; 18 | use structs::{FileData, FtaConfigResolved, HalsteadMetrics}; 19 | use swc_ecma_ast::Module; 20 | use swc_ecma_parser::error::Error; 21 | use utils::{check_score_cap_breach, get_assessment, is_valid_file, warn_about_language}; 22 | use walk::walk_and_analyze_files; 23 | 24 | pub fn analyze_file(module: &Module, line_count: usize) -> (usize, HalsteadMetrics, f64) { 25 | let cyclo = cyclo::cyclomatic_complexity(module); 26 | let halstead_metrics = halstead::analyze_module(module); 27 | 28 | let line_count_float = line_count as f64; 29 | let cyclo_float = cyclo as f64; 30 | let vocab_float = halstead_metrics.vocabulary_size as f64; 31 | 32 | const MINIMUM_CYCLO: f64 = 1.0; 33 | 34 | let factor = if cyclo_float.ln() < MINIMUM_CYCLO { 35 | MINIMUM_CYCLO 36 | } else { 37 | line_count_float / cyclo_float.ln() 38 | }; 39 | 40 | // Normalization formula based on original research 41 | // Originates from codehawk-cli 42 | let absolute_fta_score = 43 | 171.0 - 5.2 * vocab_float.ln() - 0.23 * cyclo_float - 16.2 * factor.ln(); 44 | let mut fta_score = 100.0 - ((absolute_fta_score * 100.0) / 171.0); 45 | 46 | if fta_score < 0.0 { 47 | fta_score = 0.0; 48 | } 49 | 50 | (cyclo, halstead_metrics, fta_score) 51 | } 52 | 53 | fn analyze_parsed_code(file_name: String, module: Module, line_count: usize) -> FileData { 54 | let (cyclo, halstead, fta_score) = analyze_file(&module, line_count); 55 | debug!("{} cyclo: {}, halstead: {:?}", file_name, cyclo, halstead); 56 | 57 | FileData { 58 | file_name, 59 | cyclo, 60 | halstead, 61 | fta_score, 62 | line_count, 63 | assessment: get_assessment(fta_score), 64 | } 65 | } 66 | 67 | fn collect_results( 68 | entry: &DirEntry, 69 | repo_path: &str, 70 | module: Module, 71 | line_count: usize, 72 | score_cap: usize, 73 | ) -> FileData { 74 | // Parse the source code and run the analysis 75 | let file_name = entry 76 | .path() 77 | .strip_prefix(repo_path) 78 | .unwrap() 79 | .display() 80 | .to_string(); 81 | let file_name_cloned = file_name.clone(); 82 | let file_data = analyze_parsed_code(file_name, module, line_count); 83 | 84 | // Keep a record of the fta_score before moving the FileData 85 | let fta_score = file_data.fta_score; 86 | 87 | // Check if the score cap is breached 88 | check_score_cap_breach(file_name_cloned.clone(), fta_score, score_cap); 89 | 90 | file_data 91 | } 92 | 93 | fn do_analysis( 94 | entry: &DirEntry, 95 | repo_path: &str, 96 | config: &FtaConfigResolved, 97 | source_code: &str, 98 | use_tsx: bool, 99 | ) -> Result { 100 | let (result, line_count) = parse::parse_module(source_code, use_tsx, config.include_comments); 101 | 102 | match result { 103 | Ok(module) => Ok(collect_results( 104 | entry, 105 | repo_path, 106 | module, 107 | line_count, 108 | config.score_cap, 109 | )), 110 | Err(err) => Err(err), 111 | } 112 | } 113 | 114 | fn process_entry( 115 | entry: DirEntry, 116 | repo_path: &String, 117 | config: &FtaConfigResolved, 118 | ) -> Option> { 119 | let file_name = entry.path().display(); 120 | let source_code = match fs::read_to_string(file_name.to_string()) { 121 | Ok(code) => code, 122 | Err(_) => return None, 123 | }; 124 | 125 | let file_extension = entry 126 | .path() 127 | .extension() 128 | .and_then(std::ffi::OsStr::to_str) 129 | .unwrap_or_default() 130 | .to_string(); 131 | let use_tsx = file_extension == "tsx" || file_extension == "jsx"; 132 | 133 | let mut file_data_result = do_analysis(&entry, repo_path, &config, &source_code, use_tsx); 134 | 135 | if file_data_result.is_err() { 136 | warn_about_language(&file_name.to_string(), use_tsx); 137 | file_data_result = do_analysis(&entry, repo_path, &config, &source_code, !use_tsx); 138 | } 139 | 140 | if file_data_result.is_err() { 141 | warn!( 142 | "Failed to analyze {}: {:?}", 143 | file_name, 144 | file_data_result.unwrap_err() 145 | ); 146 | return None; 147 | } 148 | 149 | let mut file_data_list: Vec = Vec::new(); 150 | 151 | // Only include files that are equal to or greater than the `exclude_under` option 152 | match file_data_result { 153 | Ok(data) if data.line_count > config.exclude_under => file_data_list.push(data), 154 | _ => {} 155 | } 156 | 157 | Some(file_data_list) 158 | } 159 | 160 | pub fn analyze(repo_path: &String, config: &FtaConfigResolved) -> Vec { 161 | // Initialize the logger 162 | let mut builder = env_logger::Builder::new(); 163 | 164 | // Check if debug mode is enabled using an environment variable 165 | if env::var("DEBUG").is_ok() { 166 | builder.filter_level(log::LevelFilter::Debug); 167 | } else { 168 | builder.filter_level(log::LevelFilter::Info); 169 | } 170 | builder.init(); 171 | 172 | let walk = WalkBuilder::new(repo_path) 173 | .git_ignore(true) 174 | .git_exclude(true) 175 | .standard_filters(true) 176 | .build(); 177 | 178 | walk_and_analyze_files(walk, repo_path, config, process_entry, is_valid_file) 179 | } 180 | -------------------------------------------------------------------------------- /crates/fta/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use fta::analyze; 3 | use fta::config::read_config; 4 | use std::time::Instant; 5 | 6 | #[cfg(feature = "use_output")] 7 | use fta::output::generate_output; 8 | 9 | #[derive(Parser, Debug)] 10 | #[command(author, version, about, long_about = None)] 11 | struct Cli { 12 | #[arg(required = true, help = "Path to the project to analyze")] 13 | project: String, 14 | 15 | #[arg(long, short, help = "Path to config file")] 16 | config_path: Option, 17 | 18 | #[arg( 19 | long, 20 | short, 21 | default_value = "table", 22 | value_parser(["table", "csv", "json"]), 23 | help = "Output format (default: table)", 24 | conflicts_with = "json" 25 | )] 26 | format: String, 27 | 28 | #[arg(long, help = "Output as JSON.", conflicts_with = "format")] 29 | json: bool, 30 | 31 | #[arg( 32 | long, 33 | short, 34 | help = "Maximum number of files to include in the table output (only applies when using table output) (default: 5000)" 35 | )] 36 | output_limit: Option, 37 | 38 | #[arg( 39 | long, 40 | short, 41 | help = "Maximum FTA score which will cause FTA to throw (default: 1000)" 42 | )] 43 | score_cap: Option, 44 | 45 | #[arg( 46 | long, 47 | short, 48 | help = "Whether to include code comments when analysing (default: false)" 49 | )] 50 | include_comments: Option, 51 | 52 | #[arg( 53 | long, 54 | short, 55 | help = "Minimum number of lines of code for files to be included in output (default: 6)" 56 | )] 57 | exclude_under: Option, 58 | } 59 | 60 | pub fn main() { 61 | // Start tracking execution time 62 | let start = Instant::now(); 63 | 64 | let cli = Cli::parse(); 65 | 66 | // Resolve the fta.json path, which can optionally be used-supplied 67 | let (config_path, path_specified_by_user) = match cli.config_path { 68 | Some(config_path_arg) => (config_path_arg, true), 69 | None => (format!("{}/fta.json", cli.project), false), 70 | }; 71 | 72 | // Resolve the input config. Optionally adds fta.json values to the default config. 73 | let mut config = match read_config(config_path, path_specified_by_user) { 74 | Ok(config) => config, 75 | Err(err) => { 76 | eprintln!("{}", err); 77 | std::process::exit(1); 78 | } 79 | }; 80 | 81 | // Override config with CLI args where allowed + values are provided 82 | if let Some(value) = cli.output_limit { 83 | config.output_limit = value; 84 | } 85 | if let Some(value) = cli.score_cap { 86 | config.score_cap = value; 87 | } 88 | if let Some(value) = cli.include_comments { 89 | config.include_comments = value; 90 | } 91 | if let Some(value) = cli.exclude_under { 92 | config.exclude_under = value; 93 | } 94 | 95 | // Execute the analysis 96 | let mut findings = analyze(&cli.project, &config); 97 | 98 | // Sort the result for display 99 | findings.sort_unstable_by(|a, b| b.fta_score.partial_cmp(&a.fta_score).unwrap()); 100 | 101 | // Execution finished, capture elapsed time 102 | let elapsed = start.elapsed().as_secs_f64(); 103 | #[cfg(feature = "use_output")] 104 | { 105 | // Format and display the results 106 | let output = generate_output( 107 | &findings, 108 | if cli.json { 109 | "json".to_string() 110 | } else { 111 | cli.format 112 | }, 113 | &elapsed, 114 | config.output_limit, 115 | ); 116 | 117 | println!("{}", output); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/fta/src/output/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::structs::FileData; 2 | use comfy_table::{presets::UTF8_FULL, Table}; 3 | 4 | mod tests; 5 | 6 | pub fn truncate_string(input: &str, max_length: usize) -> String { 7 | if input.len() <= max_length { 8 | input.to_string() 9 | } else { 10 | format!("...{}", &input[input.len() - max_length + 3..]) 11 | } 12 | } 13 | 14 | pub fn generate_output( 15 | file_data_list: &Vec, 16 | format: String, 17 | elapsed: &f64, 18 | output_limit: usize, 19 | ) -> String { 20 | let mut output = String::new(); 21 | 22 | match Some(format.as_str()) { 23 | Some("json") => { 24 | output = serde_json::to_string(file_data_list).unwrap(); 25 | } 26 | Some("csv") => { 27 | output.push_str("File,Num. lines,FTA Score (Lower is better),Assessment"); 28 | for file_data in file_data_list { 29 | output.push_str(&format!( 30 | "\n{},{},{:.2},{}", 31 | file_data.file_name, 32 | file_data.line_count, 33 | file_data.fta_score, 34 | file_data.assessment 35 | )); 36 | } 37 | } 38 | Some("table") => { 39 | let mut table = Table::new(); 40 | table.load_preset(UTF8_FULL); 41 | table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 42 | table.set_header(vec![ 43 | "File", 44 | "Num. lines", 45 | "FTA Score (Lower is better)", 46 | "Assessment", 47 | ]); 48 | 49 | for file_data in file_data_list { 50 | if table.row_iter().count() >= output_limit { 51 | continue; 52 | } 53 | table.add_row(vec![ 54 | truncate_string(&file_data.file_name, 50), 55 | file_data.line_count.to_string(), 56 | format!("{:.2}", file_data.fta_score), 57 | file_data.assessment.clone().to_string(), 58 | ]); 59 | } 60 | 61 | output = format!( 62 | "{}\n{} files analyzed in {}s.", 63 | table.to_string(), 64 | file_data_list.len(), 65 | (elapsed * 10000.0).round() / 10000.0 66 | ); 67 | } 68 | _ => output.push_str("No output format specified."), 69 | } 70 | 71 | output 72 | } 73 | -------------------------------------------------------------------------------- /crates/fta/src/output/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::output::{generate_output, truncate_string}; 4 | use crate::structs::{FileData, HalsteadMetrics}; 5 | 6 | fn get_test_data() -> Vec { 7 | vec![ 8 | FileData { 9 | file_name: "test.js".to_string(), 10 | cyclo: 1, 11 | halstead: HalsteadMetrics { 12 | uniq_operators: 1, 13 | uniq_operands: 2, 14 | total_operators: 3, 15 | total_operands: 4, 16 | program_length: 5, 17 | vocabulary_size: 6, 18 | volume: 7.0, 19 | difficulty: 8.0, 20 | effort: 9.0, 21 | time: 10.0, 22 | bugs: 11.0, 23 | }, 24 | line_count: 1, 25 | fta_score: 45.00, 26 | assessment: "OK".to_string(), 27 | }, 28 | FileData { 29 | file_name: "foo.tsx".to_string(), 30 | cyclo: 1, 31 | halstead: HalsteadMetrics { 32 | uniq_operators: 1, 33 | uniq_operands: 2, 34 | total_operators: 3, 35 | total_operands: 4, 36 | program_length: 5, 37 | vocabulary_size: 6, 38 | volume: 7.0, 39 | difficulty: 8.0, 40 | effort: 9.0, 41 | time: 10.0, 42 | bugs: 11.0, 43 | }, 44 | line_count: 25, 45 | fta_score: 95.00, 46 | assessment: "OK".to_string(), 47 | }, 48 | FileData { 49 | file_name: "bar.jsx".to_string(), 50 | cyclo: 1, 51 | halstead: HalsteadMetrics { 52 | uniq_operators: 1, 53 | uniq_operands: 2, 54 | total_operators: 3, 55 | total_operands: 4, 56 | program_length: 5, 57 | vocabulary_size: 6, 58 | volume: 7.0, 59 | difficulty: 8.0, 60 | effort: 9.0, 61 | time: 10.0, 62 | bugs: 11.0, 63 | }, 64 | line_count: 50, 65 | fta_score: 145.00, 66 | assessment: "OK".to_string(), 67 | }, 68 | ] 69 | } 70 | 71 | // Mostly eliminate whitespace from table/csv output to make comparison easier 72 | fn format_expected_output(expected: &str) -> String { 73 | let formatted = expected 74 | .lines() 75 | .map(|line| line.trim()) 76 | .collect::>() 77 | .join("\n"); 78 | 79 | formatted 80 | } 81 | 82 | // Eliminate whitespace from json output to make comparison easier 83 | fn format_json_output(json: &str) -> String { 84 | json.chars().filter(|&c| !c.is_whitespace()).collect() 85 | } 86 | 87 | #[test] 88 | fn test_truncate_string() { 89 | assert_eq!( 90 | truncate_string("extremely-long-file-name-that-will-be-hard-to-display", 25), 91 | "...ill-be-hard-to-display" 92 | ); 93 | assert_eq!(truncate_string("abcdef", 7), "abcdef"); 94 | assert_eq!(truncate_string("abcdef", 6), "abcdef"); 95 | assert_eq!(truncate_string("abcdef", 5), "...ef"); 96 | assert_eq!(truncate_string("abcdef", 4), "...f"); 97 | assert_eq!(truncate_string("abcdef", 3), "..."); 98 | } 99 | 100 | #[test] 101 | fn test_output_csv_format() { 102 | let file_data_list = get_test_data(); 103 | let output_str = format!( 104 | "\n{}\n", 105 | generate_output(&file_data_list, "csv".to_string(), &0.1_f64, 100) 106 | ); 107 | let expected_output_raw = r##" 108 | File,Num. lines,FTA Score (Lower is better),Assessment 109 | test.js,1,45.00,OK 110 | foo.tsx,25,95.00,OK 111 | bar.jsx,50,145.00,OK 112 | "##; 113 | let expected_output = format_expected_output(expected_output_raw); 114 | assert_eq!(output_str, expected_output); 115 | } 116 | 117 | #[test] 118 | fn test_output_csv_format_is_not_limited_by_output_limit() { 119 | let file_data_list = get_test_data(); 120 | let output_limit = 1; 121 | let output_str = format!( 122 | "\n{}\n", 123 | generate_output(&file_data_list, "csv".to_string(), &0.1_f64, output_limit) 124 | ); 125 | let expected_output_raw = r##" 126 | File,Num. lines,FTA Score (Lower is better),Assessment 127 | test.js,1,45.00,OK 128 | foo.tsx,25,95.00,OK 129 | bar.jsx,50,145.00,OK 130 | "##; 131 | let expected_output = format_expected_output(expected_output_raw); 132 | assert_eq!(output_str, expected_output); 133 | } 134 | 135 | #[test] 136 | fn test_output_table_format() { 137 | let file_data_list = get_test_data(); 138 | let output_str = generate_output(&file_data_list, "table".to_string(), &0.1_f64, 100); 139 | let expected_output_raw = r##" 140 | ┌─────────┬────────────┬─────────────────────────────┬────────────┐ 141 | │ File ┆ Num. lines ┆ FTA Score (Lower is better) ┆ Assessment │ 142 | ╞═════════╪════════════╪═════════════════════════════╪════════════╡ 143 | │ test.js ┆ 1 ┆ 45.00 ┆ OK │ 144 | ├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ 145 | │ foo.tsx ┆ 25 ┆ 95.00 ┆ OK │ 146 | ├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤ 147 | │ bar.jsx ┆ 50 ┆ 145.00 ┆ OK │ 148 | └─────────┴────────────┴─────────────────────────────┴────────────┘ 149 | 3 files analyzed in 0.1s. 150 | "##; 151 | 152 | let expected_output = format_expected_output(expected_output_raw); 153 | let expected_output = expected_output 154 | .trim_start_matches('\n') 155 | .trim_end_matches('\n'); 156 | assert_eq!(output_str, expected_output); 157 | } 158 | 159 | #[test] 160 | fn test_output_table_can_be_limited() { 161 | let file_data_list = get_test_data(); 162 | let output_limit = 1; 163 | let output_str = 164 | generate_output(&file_data_list, "table".to_string(), &0.1_f64, output_limit); 165 | let expected_output_raw = r##" 166 | ┌─────────┬────────────┬─────────────────────────────┬────────────┐ 167 | │ File ┆ Num. lines ┆ FTA Score (Lower is better) ┆ Assessment │ 168 | ╞═════════╪════════════╪═════════════════════════════╪════════════╡ 169 | │ test.js ┆ 1 ┆ 45.00 ┆ OK │ 170 | └─────────┴────────────┴─────────────────────────────┴────────────┘ 171 | 3 files analyzed in 0.1s. 172 | "##; 173 | 174 | let expected_output = format_expected_output(expected_output_raw); 175 | let expected_output = expected_output 176 | .trim_start_matches('\n') 177 | .trim_end_matches('\n'); 178 | assert_eq!(output_str, expected_output); 179 | } 180 | 181 | #[test] 182 | fn test_output_unspecified_format() { 183 | let file_data_list = get_test_data(); 184 | let output_str = generate_output(&file_data_list, "unspecified".to_string(), &0.1_f64, 100); 185 | let expected_output = "No output format specified."; 186 | assert_eq!(output_str, expected_output); 187 | } 188 | 189 | #[test] 190 | fn test_output_json_format() { 191 | let file_data_list = get_test_data(); 192 | let output_str = generate_output(&file_data_list, "json".to_string(), &0.1_f64, 100); 193 | 194 | let expected_output = r##"[ 195 | { 196 | "file_name": "test.js", 197 | "cyclo": 1, 198 | "halstead": 199 | { 200 | "uniq_operators": 1, 201 | "uniq_operands": 2, 202 | "total_operators": 3, 203 | "total_operands": 4, 204 | "program_length": 5, 205 | "vocabulary_size": 6, 206 | "volume": 7.0, 207 | "difficulty": 8.0, 208 | "effort": 9.0, 209 | "time": 10.0, 210 | "bugs": 11.0 211 | }, 212 | "line_count": 1, 213 | "fta_score": 45.0, 214 | "assessment": "OK" 215 | }, 216 | { 217 | "file_name": "foo.tsx", 218 | "cyclo": 1, 219 | "halstead": 220 | { 221 | "uniq_operators": 1, 222 | "uniq_operands": 2, 223 | "total_operators": 3, 224 | "total_operands": 4, 225 | "program_length": 5, 226 | "vocabulary_size": 6, 227 | "volume": 7.0, 228 | "difficulty": 8.0, 229 | "effort": 9.0, 230 | "time": 10.0, 231 | "bugs": 11.0 232 | }, 233 | "line_count": 25, 234 | "fta_score": 95.0, 235 | "assessment": "OK" 236 | }, 237 | { 238 | "file_name": "bar.jsx", 239 | "cyclo": 1, 240 | "halstead": 241 | { 242 | "uniq_operators": 1, 243 | "uniq_operands": 2, 244 | "total_operators": 3, 245 | "total_operands": 4, 246 | "program_length": 5, 247 | "vocabulary_size": 6, 248 | "volume": 7.0, 249 | "difficulty": 8.0, 250 | "effort": 9.0, 251 | "time": 10.0, 252 | "bugs": 11.0 253 | }, 254 | "line_count": 50, 255 | "fta_score": 145.0, 256 | "assessment": "OK" 257 | } 258 | ]"##; 259 | 260 | assert_eq!( 261 | format_json_output(&output_str), 262 | format_json_output(expected_output) 263 | ); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /crates/fta/src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use swc_common::comments::Comment; 4 | use swc_common::sync::Lrc; 5 | use swc_common::{comments::Comments, input::SourceFileInput}; 6 | use swc_common::{BytePos, SourceMap}; 7 | use swc_ecma_ast::{EsVersion, Module}; 8 | use swc_ecma_parser::{error::Error, lexer::Lexer, Parser, Syntax, TsConfig}; 9 | 10 | mod tests; 11 | 12 | pub fn parse_module( 13 | source: &str, 14 | use_tsx: bool, 15 | include_comments: bool, 16 | ) -> (Result, usize) { 17 | let cm: Lrc = Default::default(); 18 | let comments = CountingComments::new(); 19 | let code: String = source 20 | .lines() 21 | .filter(|line| !line.trim().is_empty()) // Remove lines that are empty or contain only whitespace 22 | .collect::>() 23 | .join("\n"); 24 | 25 | let fm = cm.new_source_file( 26 | swc_common::FileName::Custom("input.ts".to_string()), 27 | code.clone(), 28 | ); 29 | 30 | let ts_config = TsConfig { 31 | tsx: use_tsx, 32 | decorators: false, 33 | dts: false, 34 | no_early_errors: false, 35 | disallow_ambiguous_jsx_like: true, 36 | }; 37 | 38 | let lexer = Lexer::new( 39 | Syntax::Typescript(ts_config), 40 | EsVersion::Es2020, 41 | SourceFileInput::from(&*fm), 42 | Some(&comments), 43 | ); 44 | 45 | let mut parser = Parser::new_from(lexer); 46 | let parsed = parser.parse_module(); 47 | 48 | let mut line_count = code.lines().count(); 49 | if include_comments == false { 50 | line_count -= comments.count() 51 | }; 52 | 53 | (parsed, line_count) 54 | } 55 | 56 | struct CountingComments { 57 | count: Cell, 58 | } 59 | 60 | impl Comments for CountingComments { 61 | fn add_leading(self: &CountingComments, _pos: BytePos, _comment: Comment) { 62 | self.count 63 | .set(self.count.get() + 1 + _comment.text.matches('\n').count()); 64 | } 65 | 66 | fn add_leading_comments(self: &CountingComments, _pos: BytePos, _comments: Vec) { 67 | let comment_count: usize = _comments 68 | .iter() 69 | .map(|comment| comment.text.matches('\n').count()) 70 | .sum(); 71 | self.count.set(self.count.get() + 1 + comment_count); 72 | } 73 | 74 | fn add_trailing(self: &CountingComments, _pos: BytePos, _comment: Comment) {} 75 | 76 | fn add_trailing_comments(self: &CountingComments, _pos: BytePos, _comments: Vec) {} 77 | 78 | fn has_leading(&self, _pos: BytePos) -> bool { 79 | false 80 | } 81 | 82 | fn has_trailing(&self, _pos: BytePos) -> bool { 83 | false 84 | } 85 | 86 | fn take_leading(self: &CountingComments, _pos: BytePos) -> Option> { 87 | None 88 | } 89 | 90 | fn take_trailing(self: &CountingComments, _pos: BytePos) -> Option> { 91 | None 92 | } 93 | 94 | fn move_leading(&self, _from: swc_common::BytePos, _to: swc_common::BytePos) { 95 | () 96 | } 97 | 98 | fn get_leading(&self, _pos: swc_common::BytePos) -> Option> { 99 | None 100 | } 101 | 102 | fn move_trailing(&self, _from: swc_common::BytePos, _to: swc_common::BytePos) {} 103 | 104 | fn get_trailing( 105 | &self, 106 | _pos: swc_common::BytePos, 107 | ) -> Option> { 108 | None 109 | } 110 | 111 | fn add_pure_comment(&self, _pos: swc_common::BytePos) {} 112 | } 113 | 114 | impl CountingComments { 115 | fn new() -> Self { 116 | Self { 117 | count: Cell::new(0), 118 | } 119 | } 120 | 121 | fn count(&self) -> usize { 122 | self.count.get() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crates/fta/src/parse/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::parse::parse_module; 4 | 5 | #[test] 6 | fn test_parse_module() { 7 | let ts_code = r#" 8 | function add(a: number, b: number): number { 9 | return a + b; 10 | } 11 | 12 | const myResult = add(23, 56); 13 | console.log(myResult); // 79 14 | "#; 15 | 16 | let (parsed_module, line_count) = parse_module(ts_code, true, false); 17 | 18 | assert!(parsed_module.is_ok(), "Failed to parse TypeScript code"); 19 | assert_eq!(line_count, 5, "Incorrect line count"); 20 | } 21 | 22 | #[test] 23 | fn it_ignores_comments() { 24 | let ts_code = r#" 25 | /* 26 | Block comment with multiple lines. 27 | */ 28 | function add(a: number, b: number): number { 29 | return a + b; 30 | } 31 | 32 | // line comment 33 | const myResult = add(23, 56); 34 | /* block comment with single line */ 35 | console.log(myResult); // Trailing comments don't count towards the comment count. 36 | "#; 37 | 38 | let (parsed_module, line_count) = parse_module(ts_code, true, false); 39 | 40 | assert!(parsed_module.is_ok(), "Failed to parse TypeScript code"); 41 | assert_eq!(line_count, 5, "Incorrect line count"); 42 | } 43 | 44 | #[test] 45 | fn it_can_be_configured_to_include_comments_in_the_line_count() { 46 | /* 47 | The below code includes 10 lines of code, but 12 lines in total due to a leading \n and the \n on like 7. 48 | These are filtered regardless of the include_comments flag. 49 | */ 50 | let ts_code = r#" 51 | /* 52 | Block comment with multiple lines. 53 | */ 54 | function add(a: number, b: number): number { 55 | return a + b; 56 | } 57 | 58 | // line comment 59 | const myResult = add(23, 56); 60 | /* block comment with single line */ 61 | console.log(myResult); // Trailing comments don't count towards the comment count. 62 | "#; 63 | 64 | let (parsed_module, line_count) = parse_module(ts_code, true, true); 65 | 66 | assert!(parsed_module.is_ok(), "Failed to parse TypeScript code"); 67 | assert_eq!(line_count, 10, "Incorrect line count"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/fta/src/structs/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Deserialize, Default)] 4 | pub struct FtaConfigOptional { 5 | pub extensions: Option>, 6 | pub exclude_filenames: Option>, 7 | pub exclude_directories: Option>, 8 | pub output_limit: Option, 9 | pub score_cap: Option, 10 | pub include_comments: Option, 11 | pub exclude_under: Option, 12 | } 13 | 14 | #[derive(Debug, Deserialize, Default)] 15 | pub struct FtaConfigResolved { 16 | pub extensions: Vec, 17 | pub exclude_filenames: Vec, 18 | pub exclude_directories: Vec, 19 | pub output_limit: usize, 20 | pub score_cap: usize, 21 | pub include_comments: bool, 22 | pub exclude_under: usize, 23 | } 24 | 25 | #[derive(Debug, Serialize, PartialEq)] 26 | pub struct HalsteadMetrics { 27 | pub uniq_operators: usize, // number of unique operators 28 | pub uniq_operands: usize, // number of unique operands 29 | pub total_operators: usize, // total number of operators 30 | pub total_operands: usize, // total number of operands 31 | pub program_length: usize, 32 | pub vocabulary_size: usize, 33 | pub volume: f64, 34 | pub difficulty: f64, 35 | pub effort: f64, 36 | pub time: f64, 37 | pub bugs: f64, 38 | } 39 | 40 | #[derive(Debug, Serialize)] 41 | pub struct FileData { 42 | pub file_name: String, 43 | pub cyclo: usize, 44 | pub halstead: HalsteadMetrics, 45 | pub line_count: usize, 46 | pub fta_score: f64, 47 | pub assessment: String, 48 | } 49 | -------------------------------------------------------------------------------- /crates/fta/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::structs::FtaConfigResolved; 2 | use globset::{Glob, GlobSetBuilder}; 3 | use ignore::DirEntry; 4 | use log::warn; 5 | 6 | mod tests; 7 | 8 | pub fn is_excluded_filename(file_name: &str, patterns: &[String]) -> bool { 9 | let mut builder = GlobSetBuilder::new(); 10 | 11 | for pattern in patterns { 12 | let glob = Glob::new(pattern).unwrap(); 13 | builder.add(glob); 14 | } 15 | 16 | let glob_set = builder.build().unwrap(); 17 | 18 | glob_set.is_match(file_name) 19 | } 20 | 21 | pub fn is_valid_file(repo_path: &String, entry: &DirEntry, config: &FtaConfigResolved) -> bool { 22 | let file_name = entry.path().file_name().unwrap().to_str().unwrap(); 23 | let relative_path = entry 24 | .path() 25 | .strip_prefix(repo_path) 26 | .unwrap() 27 | .to_str() 28 | .unwrap(); 29 | 30 | let valid_extension = config.extensions.iter().any(|ext| file_name.ends_with(ext)); 31 | let is_excluded_filename = is_excluded_filename(file_name, &config.exclude_filenames); 32 | let is_excluded_directory = config 33 | .exclude_directories 34 | .iter() 35 | .any(|dir| relative_path.starts_with(dir)); 36 | 37 | valid_extension && !is_excluded_filename && !is_excluded_directory 38 | } 39 | 40 | pub fn warn_about_language(file_name: &str, use_tsx: bool) { 41 | let tsx_name = if use_tsx { "j/tsx" } else { "non-j/tsx" }; 42 | let opposite_tsx_name = if use_tsx { "non-j/tsx" } else { "j/tsx" }; 43 | 44 | warn!( 45 | "File {} was interpreted as {} but seems to actually be {}. The file extension may be incorrect.", 46 | file_name, 47 | tsx_name, 48 | opposite_tsx_name 49 | ); 50 | } 51 | 52 | pub fn check_score_cap_breach(file_name: String, fta_score: f64, score_cap: usize) { 53 | // Exit 1 if score_cap breached 54 | if fta_score > score_cap as f64 { 55 | eprintln!( 56 | "File {} has a score of {}, which is beyond the score cap of {}, exiting.", 57 | file_name, fta_score, score_cap 58 | ); 59 | std::process::exit(1); 60 | } 61 | } 62 | 63 | pub fn get_assessment(score: f64) -> String { 64 | if score > 60.0 { 65 | "Needs improvement".to_string() 66 | } else if score > 50.0 { 67 | "Could be better".to_string() 68 | } else { 69 | "OK".to_string() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/fta/src/utils/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::utils::{get_assessment, is_excluded_filename}; 4 | 5 | #[test] 6 | fn test_get_assessment_ok() { 7 | let assessment = get_assessment(45.0); 8 | assert_eq!(assessment, "OK"); 9 | } 10 | 11 | #[test] 12 | fn test_get_assessment_could_be_better() { 13 | let assessment = get_assessment(60.0); 14 | assert_eq!(assessment, "Could be better"); 15 | } 16 | 17 | #[test] 18 | fn test_get_assessment_needs_improvement() { 19 | let assessment = get_assessment(75.0); 20 | assert_eq!(assessment, "Needs improvement"); 21 | } 22 | 23 | #[test] 24 | fn test_is_excluded_filename_a() { 25 | let pattern = String::from("*/naughty/*.ts"); 26 | let mut patterns = Vec::new(); 27 | patterns.push(pattern); 28 | let result = is_excluded_filename("path/to/naughty/file.ts", &patterns); 29 | assert_eq!(result, true); 30 | } 31 | 32 | #[test] 33 | fn test_is_excluded_filename_b() { 34 | let pattern = String::from("*/naughty/*.ts"); 35 | let mut patterns = Vec::new(); 36 | patterns.push(pattern); 37 | let result = is_excluded_filename("path/to/sensible/file.ts", &patterns); 38 | assert_eq!(result, false); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/fta/src/walk/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::structs::{FileData, FtaConfigResolved}; 2 | use ignore::DirEntry; 3 | 4 | pub fn walk_and_analyze_files( 5 | entries: I, 6 | repo_path: &String, 7 | config: &FtaConfigResolved, 8 | process_entry: P, 9 | is_valid: V, 10 | ) -> Vec 11 | where 12 | I: Iterator>, 13 | P: Fn(DirEntry, &String, &FtaConfigResolved) -> Option>, 14 | V: Fn(&String, &DirEntry, &FtaConfigResolved) -> bool, 15 | { 16 | let mut file_data_list: Vec = Vec::new(); 17 | 18 | entries 19 | // 1. Were we able to successfully read the DirEntry & is it a file? 20 | .filter(|entry| entry.is_ok()) 21 | .map(|entry| entry.unwrap()) 22 | .filter(|entry| entry.file_type().unwrap().is_file()) 23 | // 2. Is the file considered valid according to our basic requirements plus user configuration? 24 | .filter(|entry| is_valid(repo_path, &entry, config)) 25 | // 3. Analyze each file 26 | .filter_map(|entry| process_entry(entry, repo_path, config)) 27 | // 4. Return a list of analyzed files 28 | .for_each(|data_vec| file_data_list.extend(data_vec)); 29 | 30 | file_data_list 31 | } 32 | -------------------------------------------------------------------------------- /fta-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgb-io/fta/fb8c6ad82238529f1cc59f1be2fdd82ffae56ef9/fta-logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fta-workspace", 3 | "version": "1.0.0", 4 | "description": "FTA (Fast TypeScript Analyzer) is a super-fast TypeScript static analysis tool written in Rust", 5 | "repository": "https://github.com/sgb-io/fta.git", 6 | "author": "sgb-io ", 7 | "license": "MIT", 8 | "private": true, 9 | "workspaces": [ 10 | "packages" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/fta/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | binaries/**/fta.exe 3 | binaries/**/fta 4 | -------------------------------------------------------------------------------- /packages/fta/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgb-io/fta/fb8c6ad82238529f1cc59f1be2fdd82ffae56ef9/packages/fta/.npmignore -------------------------------------------------------------------------------- /packages/fta/@types/fta-cli.d.ts: -------------------------------------------------------------------------------- 1 | declare module "fta-cli" { 2 | /** 3 | * Represents an analyzed file with all its metrics. 4 | * 5 | * @property {string} file_name - The name of the file. 6 | * @property {number} cyclo - The cyclomatic complexity of the file. 7 | * @property {Object} halstead - The Halstead metrics of the file, a complexity measure. 8 | * @property {number} halstead.uniq_operators - The number of unique operators. 9 | * @property {number} halstead.uniq_operands - The number of unique operands. 10 | * @property {number} halstead.total_operators - The total number of operators. 11 | * @property {number} halstead.total_operands - The total number of operands. 12 | * @property {number} halstead.program_length - The total count of operators and operands. (N) 13 | * @property {number} halstead.vocabulary_size - The total count of unique operators and operands. (n) 14 | * @property {number} halstead.volume - A measure of the size of the program. V = N * log2(n). (V) 15 | * @property {number} halstead.difficulty - Quantifies how difficult a program is to write or understand. D = (n1/2) * (N2/n2). (D) 16 | * @property {number} halstead.effort - An estimation of the amount of work required to write a program. E = D * V. 17 | * @property {number} halstead.time - An estimation of the time required to write the program. T = E / 18 (seconds). 18 | * @property {number} halstead.bugs - An estimation of the number of bugs in the program. B = V / 3000. 19 | * @property {number} line_count - The number of lines in the file. 20 | * @property {number} fta_score - The FTA score of the file. 21 | * @property {string} assessment - The assessment of the file. 22 | */ 23 | export type AnalyzedFile = { 24 | /** 25 | * The name of the file. 26 | * 27 | * @type {string} 28 | */ 29 | file_name: string; 30 | /** 31 | * The cyclomatic complexity of the file. 32 | * 33 | * @type {number} 34 | */ 35 | cyclo: number; 36 | /** 37 | * The Halstead metrics of the file, a complexity measure. 38 | * For further information see the [docs](https://ftaproject.dev/docs/scoring) 39 | * 40 | * @type {Object} 41 | */ 42 | halstead: { 43 | /** 44 | * The number of unique operators. 45 | * 46 | * @type {number} 47 | */ 48 | uniq_operators: number; 49 | /** 50 | * The number of unique operands. 51 | * 52 | * @type {number} 53 | */ 54 | uniq_operands: number; 55 | /** 56 | * The total number of operators. 57 | * 58 | * @type {number} 59 | */ 60 | total_operators: number; 61 | /** 62 | * The total number of operands. 63 | * 64 | * @type {number} 65 | */ 66 | total_operands: number; 67 | /** 68 | * The total count of operators and operands. 69 | * 70 | * @type {number} 71 | */ 72 | program_length: number; 73 | /** 74 | * The total count of unique operators and operands. 75 | * 76 | * @type {number} 77 | */ 78 | vocabulary_size: number; 79 | /** 80 | * A measure of the size of the program. V = N * log2(n). 81 | * 82 | * @type {number} 83 | */ 84 | volume: number; 85 | /** 86 | * Quantifies how difficult a program is to write or understand. D = (n1/2) * (N2/n2). 87 | * 88 | * @type {number} 89 | */ 90 | difficulty: number; 91 | /** 92 | * An estimation of the amount of work required to write a program. E = D * V. 93 | * 94 | * @type {number} 95 | */ 96 | effort: number; 97 | /** 98 | * An estimation of the time required to write the program. T = E / 18 (seconds). 99 | * 100 | * @type {number} 101 | */ 102 | time: number; 103 | /** 104 | * An estimation of the number of bugs in the program. B = V / 3000. 105 | * 106 | * @type {number} 107 | */ 108 | bugs: number; 109 | }; 110 | /** 111 | * The number of lines in the file. 112 | * 113 | * @type {number} 114 | */ 115 | line_count: number; 116 | /** 117 | * The FTA score of the file. 118 | * 119 | * @type {number} 120 | */ 121 | fta_score: number; 122 | /** 123 | * The assessment of the file. 124 | * 125 | * @type {string} 126 | */ 127 | assessment: string; 128 | }; 129 | 130 | /** 131 | * Represents the possible options for the FTA-Analysis. 132 | * 133 | * @property {boolean} json 134 | */ 135 | export type FtaAnalysisOptions = { 136 | /** 137 | * Wether the result should be returned in a json or a pretty printed table. 138 | * 139 | * @type {boolean} 140 | */ 141 | json: boolean; 142 | }; 143 | 144 | /** 145 | * Runs the FTA-Analysis for the given project. 146 | * 147 | * @param projectPath - The path to the root of the project to analyze 148 | * @param options - The options for the analysis 149 | */ 150 | export function runFta( 151 | projectPath: string, 152 | options: FtaAnalysisOptions 153 | ): string; 154 | } 155 | -------------------------------------------------------------------------------- /packages/fta/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | FTA 4 | 5 |

6 | 7 |

8 | Fast TypeScript Analyzer 9 |

10 | 11 | FTA (Fast TypeScript Analyzer) is a super-fast TypeScript static analysis tool written in Rust. It captures static information about TypeScript code and generates easy-to-understand analytics that tell you about complexity and maintainability issues that you may want to address. 12 | 13 | FTA uses [swc](https://github.com/swc-project/swc) to parse your code then runs various analytical routines against it to understand how complex and maintainable it is likely to be. JavaScript code is also supported. 14 | 15 | **FTA is fast**: on typical hardware, it can analyze up to **1600 files per second**. 16 | 17 | The full docs can be viewed on the [ftaproject.dev website](https://ftaproject.dev/). 18 | 19 | ## Quickstart 20 | 21 | There are several ways to use `fta`. The simplest is to use `fta-cli`: 22 | 23 | ``` 24 | npx fta-cli path/to/project 25 | ``` 26 | 27 | Example output against the Redux project: 28 | 29 | ``` 30 | ┌─────────────────────────────────────────┬────────────┬─────────────────────────────┬───────────────────┐ 31 | │ File ┆ Num. lines ┆ FTA Score (Lower is better) ┆ Assessment │ 32 | ╞═════════════════════════════════════════╪════════════╪═════════════════════════════╪═══════════════════╡ 33 | │ website\src\pages\index.js ┆ 212 ┆ 64.43 ┆ Needs improvement │ 34 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 35 | │ src\createStore.ts ┆ 255 ┆ 64.17 ┆ Needs improvement │ 36 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 37 | │ src\combineReducers.ts ┆ 162 ┆ 59.51 ┆ Could be better │ 38 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 39 | │ src\compose.ts ┆ 36 ┆ 47.53 ┆ OK │ 40 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 41 | │ src\bindActionCreators.ts ┆ 51 ┆ 47.14 ┆ OK │ 42 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 43 | │ src\utils\kindOf.ts ┆ 58 ┆ 46.88 ┆ OK │ 44 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 45 | │ src\utils\isPlainObject.ts ┆ 8 ┆ 28.36 ┆ OK │ 46 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 47 | │ src\utils\symbol-observable.ts ┆ 7 ┆ 27.61 ┆ OK │ 48 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 49 | │ src\utils\warning.ts ┆ 8 ┆ 26.81 ┆ OK │ 50 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 51 | │ website\docusaurus.config.js ┆ 205 ┆ 18.19 ┆ OK │ 52 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 53 | │ website\sidebars.js ┆ 148 ┆ 15.82 ┆ OK │ 54 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 55 | │ rollup.config.js ┆ 71 ┆ 15.79 ┆ OK │ 56 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 57 | │ tsup.config.ts ┆ 63 ┆ 15.59 ┆ OK │ 58 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 59 | │ src\types\store.ts ┆ 63 ┆ 15.47 ┆ OK │ 60 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 61 | │ src\applyMiddleware.ts ┆ 55 ┆ 15.45 ┆ OK │ 62 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 63 | │ website\src\pages\errors.js ┆ 58 ┆ 15.07 ┆ OK │ 64 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 65 | │ src\types\reducers.ts ┆ 49 ┆ 14.46 ┆ OK │ 66 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 67 | │ website\src\js\monokaiTheme.js ┆ 62 ┆ 14.32 ┆ OK │ 68 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 69 | │ src\utils\actionTypes.ts ┆ 8 ┆ 11.91 ┆ OK │ 70 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 71 | │ src\index.ts ┆ 37 ┆ 11.91 ┆ OK │ 72 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 73 | │ src\types\actions.ts ┆ 15 ┆ 10.27 ┆ OK │ 74 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 75 | │ src\types\middleware.ts ┆ 14 ┆ 10.16 ┆ OK │ 76 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 77 | │ vitest.config.ts ┆ 14 ┆ 9.92 ┆ OK │ 78 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 79 | │ docs\components\DetailedExplanation.jsx ┆ 14 ┆ 9.53 ┆ OK │ 80 | └─────────────────────────────────────────┴────────────┴─────────────────────────────┴───────────────────┘ 81 | 24 files analyzed in 0.0372s. 82 | ``` 83 | 84 | For convenience, FTA generates a single `FTA Score` that serves as a general, overall indication of the quality of a particular TypeScript file. 85 | 86 | That said, all metrics are exposed, and it is up to users to decide how it's metrics can enhance productivity for your team. 87 | 88 | The full metrics available for each file: 89 | 90 | ```json 91 | { 92 | "file_name": "combineReducers.ts", 93 | "cyclo": 28, 94 | "halstead": { 95 | "uniq_operators": 28, 96 | "uniq_operands": 67, 97 | "total_operators": 271, 98 | "total_operands": 239, 99 | "program_length": 95, 100 | "vocabulary_size": 510, 101 | "volume": 854.4635765015915, 102 | "difficulty": 37.84518828451883, 103 | "effort": 32337.33493496609, 104 | "time": 1796.5186074981161, 105 | "bugs": 0.2848211921671972 106 | }, 107 | "line_count": 202, 108 | "fta_score": 61.61052634575169, 109 | "assessment": "(Needs improvement)" 110 | } 111 | ``` 112 | 113 | For more information about scoring, what is happening under the hood and interpreting results, view the [Scoring docs](https://ftaproject.dev/docs/scoring). 114 | 115 | ## Call FTA from a script 116 | 117 | 1. To call FTA from a script, install `fta-cli` as a dependency and call it: 118 | 119 | ```bash 120 | yarn add fta-cli 121 | # or 122 | npm install fta-cli 123 | # or 124 | pnpm install fta-cli 125 | ``` 126 | 127 | 2. Call `fta` from a `package.json` script: 128 | 129 | ```json 130 | "scripts": { 131 | "fta": "fta src" 132 | } 133 | ``` 134 | 135 | ## Call FTA from code 136 | 137 | You can also call `fta-cli` from code: 138 | 139 | ```javascript 140 | import { runFta } from "fta-cli"; 141 | // CommonJS alternative: 142 | // const { runFta } = require("fta-cli"); 143 | 144 | // Print the standard ascii table output 145 | const standardOutput = runFta("path/to/project"); 146 | 147 | // Alternatively, get the full output as JSON so that you can interact with it 148 | const output = runFta("path/to/project", { json: true }); 149 | ``` 150 | 151 | ## Output 152 | 153 | By default, `fta` outputs a table of output that summarizes the result. You can optionally supply the `json` argument to get the full output as JSON. 154 | 155 | You can also get the JSON output in a scripting context: 156 | 157 | ``` 158 | fta /path/to/project --json 159 | ``` 160 | 161 | For more information on using FTA, be sure to check out the [docs](https://ftaproject.dev). 162 | 163 | ## Configuring FTA 164 | 165 | Various configuration options are available, including the ability to cause CI to fail if a certain score threshold is breached. See the full Configuration options on the [docs](https://ftaproject.dev/docs/configuration). 166 | 167 | ## Docs 168 | 169 | Read the full documentation on the [docs](https://ftaproject.dev). 170 | 171 | ## License 172 | 173 | [MIT](LICENSE.md) 174 | -------------------------------------------------------------------------------- /packages/fta/binaries/README.md: -------------------------------------------------------------------------------- 1 | This directory must contain the fta crate binaries at publish time 2 | -------------------------------------------------------------------------------- /packages/fta/check.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("node:fs"); 4 | const path = require("node:path"); 5 | 6 | const { exeTargets, plainTargets } = require("./targets"); 7 | 8 | let missingBinaries = []; 9 | 10 | function checkBinaryFile(target, path) { 11 | try { 12 | fs.readFileSync(path); 13 | } catch (e) { 14 | missingBinaries.push(target); 15 | } 16 | } 17 | 18 | for (let i = 0; i < exeTargets.length; i += 1) { 19 | const bin = path.join(__dirname, "binaries", exeTargets[i], "fta.exe"); 20 | checkBinaryFile(exeTargets[i], bin); 21 | } 22 | 23 | for (let i = 0; i < plainTargets.length; i += 1) { 24 | const bin = path.join(__dirname, "binaries", plainTargets[i], "fta"); 25 | checkBinaryFile(plainTargets[i], bin); 26 | } 27 | 28 | if (missingBinaries.length > 0) { 29 | console.log("The following binaries are missing: \n"); 30 | missingBinaries.forEach((target) => { 31 | console.log("- " + target); 32 | }); 33 | console.log("\n"); 34 | throw new Error("Check failed"); 35 | } 36 | 37 | console.log("All binaries were located"); 38 | -------------------------------------------------------------------------------- /packages/fta/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { execSync, spawn } = require("node:child_process"); 4 | const path = require("node:path"); 5 | const fs = require("node:fs"); 6 | 7 | const platform = process.platform; 8 | const architecture = process.arch; 9 | 10 | function getBinaryPath() { 11 | const targetDirectory = path.join(__dirname, "binaries"); 12 | 13 | switch (platform) { 14 | case "win32": 15 | if (architecture === "x64") { 16 | return path.join( 17 | targetDirectory, 18 | "fta-x86_64-pc-windows-msvc", 19 | "fta.exe" 20 | ); 21 | } else if (architecture === "arm64") { 22 | return path.join( 23 | targetDirectory, 24 | "fta-aarch64-pc-windows-msvc", 25 | "fta.exe" 26 | ); 27 | } 28 | case "darwin": 29 | if (architecture === "x64") { 30 | return path.join(targetDirectory, "fta-x86_64-apple-darwin", "fta"); 31 | } else if (architecture === "arm64") { 32 | return path.join(targetDirectory, "fta-aarch64-apple-darwin", "fta"); 33 | } 34 | case "linux": 35 | if (architecture === "x64") { 36 | return path.join( 37 | targetDirectory, 38 | "fta-x86_64-unknown-linux-musl", 39 | "fta" 40 | ); 41 | } else if (architecture === "arm64") { 42 | return path.join( 43 | targetDirectory, 44 | "fta-aarch64-unknown-linux-musl", 45 | "fta" 46 | ); 47 | } else if (architecture === "arm") { 48 | return path.join( 49 | targetDirectory, 50 | "fta-arm-unknown-linux-musleabi", 51 | "fta" 52 | ); 53 | } 54 | break; 55 | default: 56 | throw new Error("Unsupported platform: " + platform); 57 | } 58 | 59 | throw new Error("Binary not found for the current platform"); 60 | } 61 | 62 | function setUnixPerms(binaryPath) { 63 | if (platform === "darwin" || platform === "linux") { 64 | try { 65 | fs.chmodSync(binaryPath, "755"); 66 | } catch (e) { 67 | console.warn("Could not chmod fta binary: ", e); 68 | } 69 | } 70 | } 71 | 72 | // Run the binary from code 73 | // We build arguments that get sent to the binary 74 | function runFta(project, options) { 75 | const binaryPath = getBinaryPath(); 76 | const binaryArgs = options.json ? "--json" : ""; 77 | setUnixPerms(binaryPath); 78 | const result = execSync(`${binaryPath} ${project} ${binaryArgs}`); 79 | return result.toString(); 80 | } 81 | 82 | // Run the binary directly if executed as a standalone script 83 | // Arguments are directly forwarded to the binary 84 | if (require.main === module) { 85 | const args = process.argv.slice(2); // Exclude the first two arguments (node binary and project path) 86 | const binaryPath = getBinaryPath(); 87 | const binaryArgs = args.join(" "); 88 | setUnixPerms(binaryPath); 89 | 90 | // Standard output will be printed due to use of `inherit`, i.e, no need to `console.log` anything 91 | execSync(`${binaryPath} ${binaryArgs}`, { stdio: "inherit" }); 92 | } 93 | 94 | module.exports.runFta = runFta; 95 | -------------------------------------------------------------------------------- /packages/fta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fta-cli", 3 | "version": "2.0.1", 4 | "description": "FTA (Fast TypeScript Analyzer) is a super-fast TypeScript static analysis tool written in Rust", 5 | "repository": "https://github.com/sgb-io/fta.git", 6 | "author": "sgb-io ", 7 | "license": "MIT", 8 | "main": "index.js", 9 | "bin": { 10 | "fta": "index.js" 11 | }, 12 | "types": "@types/fta-cli.d.ts", 13 | "scripts": { 14 | "prepublishOnly": "node check.js" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/fta/targets.js: -------------------------------------------------------------------------------- 1 | // Windows 2 | const exeTargets = [ 3 | "fta-aarch64-pc-windows-msvc", 4 | "fta-x86_64-pc-windows-msvc", 5 | ]; 6 | 7 | const plainTargets = [ 8 | // macOS 9 | "fta-x86_64-apple-darwin", 10 | "fta-aarch64-apple-darwin", 11 | // Linux 12 | "fta-x86_64-unknown-linux-musl", 13 | "fta-aarch64-unknown-linux-musl", 14 | "fta-arm-unknown-linux-musleabi", 15 | ]; 16 | 17 | module.exports.exeTargets = exeTargets; 18 | module.exports.plainTargets = plainTargets; 19 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------