├── .clippy.toml ├── .github └── workflows │ ├── ci.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── deno.json ├── rust-toolchain.toml ├── scripts ├── 01_setup.ts ├── 02_build.ts ├── 03_test.ts ├── 04_confirm.ts ├── README.md ├── repos.ts ├── reset.ts └── update_swc_deps.ts └── src ├── cjs_parse.rs ├── comments.rs ├── diagnostics.rs ├── emit.rs ├── exports.rs ├── lexing.rs ├── lib.rs ├── parsed_source.rs ├── parsing.rs ├── scopes.rs ├── source_map.rs ├── text_changes.rs ├── transpiling ├── jsx_precompile.rs ├── mod.rs ├── testdata │ └── tc39_decorator_proposal_output.txt └── transforms.rs ├── type_strip.rs └── types.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | # Prefer using `SourcePos` from because it abstracts 2 | # away swc's non-zero-indexed based positioning 3 | disallowed-methods = [ 4 | "swc_common::Spanned::span", 5 | ] 6 | disallowed-types = [ 7 | "swc_common::BytePos", 8 | "swc_common::Span", 9 | "swc_common::Spanned", 10 | ] 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | rust: 12 | name: deno_ast-ubuntu-latest-release 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | 16 | env: 17 | CARGO_INCREMENTAL: 0 18 | GH_ACTIONS: 1 19 | RUST_BACKTRACE: full 20 | RUSTFLAGS: -D warnings 21 | 22 | steps: 23 | - name: Clone repository 24 | uses: actions/checkout@v5 25 | 26 | - uses: denoland/setup-deno@v1 27 | - uses: dsherret/rust-toolchain-file@v1 28 | 29 | - uses: Swatinem/rust-cache@v2 30 | with: 31 | save-if: ${{ github.ref == 'refs/heads/main' }} 32 | 33 | - name: Format 34 | run: | 35 | cargo fmt --all -- --check 36 | deno fmt --check 37 | 38 | - name: Lint 39 | run: | 40 | cargo clippy --all-targets --all-features --release 41 | deno lint 42 | 43 | - name: Build 44 | run: cargo build --all-targets --all-features --release 45 | - name: Test 46 | run: cargo test --all-targets --all-features --release 47 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v5 17 | - uses: rust-lang/crates-io-auth-action@v1 18 | id: auth 19 | - run: cargo publish 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: "Kind of release" 8 | type: choice 9 | options: 10 | - patch 11 | - minor 12 | required: true 13 | 14 | jobs: 15 | rust: 16 | name: release 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 30 19 | 20 | steps: 21 | - name: Clone repository 22 | uses: actions/checkout@v5 23 | with: 24 | token: ${{ secrets.DENOBOT_PAT }} 25 | 26 | - uses: denoland/setup-deno@v1 27 | - uses: dsherret/rust-toolchain-file@v1 28 | 29 | - name: Tag and release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 32 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 33 | run: | 34 | git config user.email "denobot@users.noreply.github.com" 35 | git config user.name "denobot" 36 | deno run -A https://raw.githubusercontent.com/denoland/automation/0.20.0/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .vs/ 3 | /target 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2021" 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deno_ast" 3 | version = "0.52.0" 4 | authors = ["the Deno authors"] 5 | documentation = "https://docs.rs/deno_ast" 6 | edition = "2024" 7 | homepage = "https://deno.land/" 8 | license = "MIT" 9 | repository = "https://github.com/denoland/deno_ast" 10 | description = "Source text parsing, lexing, and AST related functionality for Deno" 11 | 12 | [package.metadata.docs.rs] 13 | all-features = true 14 | 15 | [features] 16 | bundler = ["swc_bundler", "swc_ecma_transforms_optimization", "swc_graph_analyzer"] 17 | concurrent = ["swc_common/concurrent"] 18 | cjs = ["utils", "visit"] 19 | codegen = ["swc_ecma_codegen", "swc_ecma_codegen_macros", "swc_macros_common"] 20 | compat = ["transforms", "swc_ecma_transforms_compat", "swc_trace_macro", "swc_config", "swc_config_macro"] 21 | proposal = ["transforms", "swc_ecma_transforms_proposal", "swc_ecma_transforms_classes", "swc_ecma_transforms_macros", "swc_macros_common"] 22 | react = ["transforms", "swc_ecma_transforms_react", "swc_ecma_transforms_macros", "swc_config", "swc_config_macro", "swc_macros_common"] 23 | scopes = ["view", "utils", "visit"] 24 | sourcemap = ["dprint-swc-ext/sourcemap", "swc_sourcemap"] 25 | transforms = ["swc_ecma_loader", "swc_ecma_transforms_base"] 26 | emit = ["base64", "codegen", "sourcemap"] 27 | type_strip = [ "swc_ts_fast_strip" ] 28 | transpiling = ["emit", "proposal", "react", "transforms", "typescript", "utils", "visit"] 29 | typescript = ["transforms", "swc_ecma_transforms_typescript"] 30 | utils = ["swc_ecma_utils"] 31 | view = ["dprint-swc-ext/view"] 32 | visit = ["swc_ecma_visit", "swc_visit", "swc_macros_common"] 33 | 34 | [dependencies] 35 | base64 = { version = "0.22.1", optional = true } 36 | capacity_builder = "0.5.0" 37 | deno_media_type = "0.3.0" 38 | deno_terminal = "0.2.2" 39 | deno_error = "0.7.0" 40 | 41 | dprint-swc-ext = "0.26.0" 42 | percent-encoding = "2.3.1" 43 | serde = { version = "1.0.219", features = ["derive"] } 44 | text_lines = { version = "0.6.0", features = ["serialization"] } 45 | url = { version = "2.5.4", features = ["serde"] } 46 | unicode-width = "0.2.0" 47 | 48 | # swc's version bumping is very buggy and there will often be patch versions 49 | # published that break our build, so we pin all swc versions to prevent 50 | # pulling in new versions of swc crates 51 | # 52 | # NOTE: You can automatically update these dependencies by running ./scripts/update_swc_deps.ts 53 | swc_atoms = "=9.0.0" 54 | swc_common = "=17.0.1" 55 | swc_config = { version = "=3.1.2", optional = true } 56 | swc_config_macro = { version = "=1.0.1", optional = true } 57 | swc_ecma_ast = { version = "=18.0.0", features = ["serde-impl"] } 58 | swc_ecma_codegen = { version = "=20.0.2", optional = true } 59 | swc_ecma_codegen_macros = { version = "=2.0.2", optional = true } 60 | swc_ecma_loader = { version = "=17.0.0", optional = true } 61 | swc_ecma_lexer = "=26.0.0" 62 | swc_ecma_parser = "=27.0.7" 63 | swc_ecma_transforms_base = { version = "=30.0.1", features = ["inline-helpers"], optional = true } 64 | swc_ecma_transforms_classes = { version = "=30.0.0", optional = true } 65 | swc_ecma_transforms_compat = { version = "=35.0.0", optional = true } 66 | swc_ecma_transforms_macros = { version = "=1.0.1", optional = true } 67 | swc_ecma_transforms_optimization = { version = "=32.0.0", optional = true } 68 | swc_ecma_transforms_proposal = { version = "=30.0.0", optional = true } 69 | swc_ecma_transforms_react = { version = "=33.0.0", optional = true } 70 | swc_ecma_transforms_typescript = { version = "=33.0.0", optional = true } 71 | swc_ecma_utils = { version = "=24.0.0", optional = true } 72 | swc_ecma_visit = { version = "=18.0.1", optional = true } 73 | swc_eq_ignore_macros = "=1.0.1" 74 | swc_bundler = { version = "=35.0.0", optional = true } 75 | swc_graph_analyzer = { version = "=14.0.1", optional = true } 76 | swc_macros_common = { version = "=1.0.1", optional = true } 77 | swc_sourcemap = { version = "=9.3.4", optional = true } 78 | swc_ts_fast_strip = { version = "=36.0.0", optional = true } 79 | swc_trace_macro = { version = "=2.0.2", optional = true } 80 | swc_visit = { version = "=2.0.1", optional = true } 81 | thiserror = "2.0.12" 82 | 83 | [dev-dependencies] 84 | pretty_assertions = "1.4.1" 85 | serde_json = { version = "1.0.140", features = ["preserve_order"] } 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 the Deno authors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno_ast 2 | 3 | [![](https://img.shields.io/crates/v/deno_ast.svg)](https://crates.io/crates/deno_ast) 4 | [![Discord Chat](https://img.shields.io/discord/684898665143206084?logo=discord&style=social)](https://discord.gg/deno) 5 | 6 | Source text parsing, lexing, and AST related functionality for 7 | [Deno](https://deno.land). 8 | 9 | ```rust 10 | use deno_ast::parse_module; 11 | use deno_ast::MediaType; 12 | use deno_ast::ParseParams; 13 | use deno_ast::SourceTextInfo; 14 | 15 | let source_text = "class MyClass {}"; 16 | let text_info = SourceTextInfo::new(source_text.into()); 17 | let parsed_source = parse_module(ParseParams { 18 | specifier: deno_ast::ModuleSpecifier::parse("file:///my_file.ts").unwrap(), 19 | media_type: MediaType::TypeScript, 20 | text_info, 21 | capture_tokens: true, 22 | maybe_syntax: None, 23 | scope_analysis: false, 24 | }).expect("should parse"); 25 | 26 | // returns the comments 27 | parsed_source.comments(); 28 | // returns the tokens if captured 29 | parsed_source.tokens(); 30 | // returns the module (AST) 31 | parsed_source.module(); 32 | // returns the `SourceTextInfo` 33 | parsed_source.text_info(); 34 | ``` 35 | 36 | ## Versioning Strategy 37 | 38 | This crate does not follow semver so make sure to pin it to a patch version. 39 | Instead a versioning strategy that optimizes for more efficient maintenance is 40 | used: 41 | 42 | - Does [deno_graph](https://github.com/denoland/deno_graph), 43 | [deno_doc](https://github.com/denoland/deno_doc), 44 | [deno_lint](https://github.com/denoland/deno_lint), 45 | [eszip](https://github.com/denoland/eszip), and 46 | [dprint-plugin-typescript](https://github.com/dprint/dprint-plugin-typescript) 47 | still compile in the [Deno](https://github.com/denoland/deno) repo? 48 | - If yes, is this a change that would break something at runtime? 49 | - If yes, it's recommended to do a minor release. 50 | - If no, it's a patch release. 51 | - If no, it's a minor release. 52 | 53 | ## swc upgrades 54 | 55 | We upgrade swc about once a month. Upgrading swc is a very involved process that 56 | often requires many changes in downstream Deno crates. We also test the new 57 | version of swc in all downstream crates before merging a PR into deno_ast that 58 | updates swc. 59 | 60 | **Please do not open a PR for upgrading swc** unless you have stated you are 61 | going to work on it in the issue tracker and have run the tests on all 62 | downstream crates. 63 | 64 | To upgrade swc: 65 | 66 | 1. Checkout the following repositories in sibling directories to this repository 67 | (ex. `/home/david/dev/deno_graph`, `/home/david/dev/deno_ast`): 68 | - https://github.com/denoland/deno_graph 69 | - https://github.com/denoland/deno_lint 70 | - https://github.com/denoland/eszip 71 | - https://github.com/denoland/deno_doc 72 | - https://github.com/dprint/dprint-plugin-typescript 73 | - https://github.com/denoland/deno 74 | 1. Ensure they all have `upstream` remotes set for the urls specified above. For 75 | example, your `deno_graph` repo should have 76 | `git remote add upstream https://github.com/denoland/deno_graph`. 77 | 1. Run `./scripts/update_swc_deps.ts` 78 | 1. Run `./scripts/01_setup.ts` 79 | 1. Run `./scripts/02_build.ts` and fix any build errors. 80 | 1. Run `./scripts/03_test.ts` and fix any failing tests. 81 | - At this point, all the tests should be passing in all the repositories. 82 | 1. Open a PR for upgrading deno_ast. 83 | 1. Merge the PR and publish a new version of deno_ast using the 84 | [release workflow](https://github.com/denoland/deno_ast/actions/workflows/release.yml). 85 | 1. At this point, bump the version of deno_ast and open PRs for deno_graph, 86 | deno_lint, and dprint-plugin-typescript (note: `./scripts/04_confirm.ts` 87 | might be helpful to automate some of this. Read its source code to understand 88 | it so that you can deal with any problems that may arise). 89 | 1. Merge the PR deno_graph and publish using its release workflow. 90 | 1. Open PRs to deno_doc and eszip. 91 | 1. Merge those PRs and do releases. Also merge and release deno_lint and 92 | dprint-plugin-typescript (Bartek and David have access to publish 93 | dprint-plugin-typescript). 94 | 1. Open a PR to Deno with the versions bumped and merge the PR. 95 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "exclude": [ 4 | "target" 5 | ], 6 | "imports": { 7 | "@deno/rust-automation": "jsr:@deno/rust-automation@^0.21.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.89.0" 3 | components = ["clippy", "rustfmt"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /scripts/01_setup.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 3 | 4 | import { Repos } from "./repos.ts"; 5 | import { $ } from "@deno/rust-automation"; 6 | 7 | const repos = await Repos.load(); 8 | 9 | // Ensure repos are latest main 10 | for (const repo of repos.nonDenoAstRepos()) { 11 | $.logStep("Setting up", `${repo.name}...`); 12 | if (await repo.hasLocalChanges()) { 13 | throw new Error( 14 | `Repo ${repo.name} had local changes. Please resolve this.`, 15 | ); 16 | } 17 | $.logGroup(); 18 | $.logStep("Switching to main..."); 19 | await repo.command("git switch main"); 20 | $.logStep("Pulling upstream main..."); 21 | await repo.command("git pull upstream main"); 22 | $.logGroupEnd(); 23 | } 24 | 25 | // Update the repos to refer to local versions of each other 26 | await repos.toLocalSource(); 27 | -------------------------------------------------------------------------------- /scripts/02_build.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 3 | 4 | import { Repos } from "./repos.ts"; 5 | import { $ } from "@deno/rust-automation"; 6 | 7 | const repos = await Repos.load(); 8 | 9 | for (const crate of repos.getCrates()) { 10 | $.logStep(`Building ${crate.name}...`); 11 | await crate.build({ allFeatures: true }); 12 | } 13 | -------------------------------------------------------------------------------- /scripts/03_test.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 3 | 4 | import { $ } from "@deno/rust-automation"; 5 | import { Repos } from "./repos.ts"; 6 | 7 | const repos = await Repos.load(); 8 | let hadConfirmed = false; 9 | 10 | for (const crate of repos.getCrates()) { 11 | if (hadConfirmed || confirm(`Do you want to run tests for ${crate.name}?`)) { 12 | hadConfirmed = true; 13 | $.logStep("Running tests", `for ${crate.name}...`); 14 | await crate.test({ allFeatures: true }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/04_confirm.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 3 | 4 | import { $, Repo } from "@deno/rust-automation"; 5 | import { Repos } from "./repos.ts"; 6 | 7 | const repos = await Repos.load(); 8 | const denoRepo = repos.get("deno"); 9 | const deno_ast = repos.getCrate("deno_ast"); 10 | const nonDenoRepos = repos.getRepos().filter((c) => c.name !== "deno"); 11 | 12 | // create a branch, commit, push for the non-deno repos 13 | for (const repo of nonDenoRepos) { 14 | if (!await repo.hasLocalChanges()) { 15 | continue; 16 | } 17 | const currentBranch = await repo.gitCurrentBranch(); 18 | $.logStep("Analyzing", repo.name); 19 | $.logLight("Branch:", currentBranch); 20 | if ( 21 | confirm( 22 | `Bump deps? (Note: do this after the dependency crates have PUBLISHED)`, 23 | ) 24 | ) { 25 | await bumpDeps(repo); 26 | for (const crate of repo.crates) { 27 | await crate.cargoCheck(); 28 | } 29 | 30 | if ( 31 | currentBranch === "main" && 32 | confirm(`Branch for ${repo.name}?`) 33 | ) { 34 | await repo.gitBranch("deno_ast_" + deno_ast.version); 35 | } 36 | if ( 37 | await repo.hasLocalChanges() && 38 | confirm(`Commit and push for ${repo.name}?`) 39 | ) { 40 | await repo.gitAdd(); 41 | await repo.gitCommit(`feat: upgrade deno_ast to ${deno_ast.version}`); 42 | await repo.gitPush(); 43 | } 44 | } 45 | } 46 | 47 | // now branch, commit, and push for the deno repo 48 | $.logStep("Analyzing Deno"); 49 | const currentBranch = await denoRepo.gitCurrentBranch(); 50 | $.logLight("Branch:", currentBranch); 51 | if (confirm(`Bump deps for deno?`)) { 52 | await bumpDeps(denoRepo); 53 | for (const crate of denoRepo.crates) { 54 | await crate.cargoCheck(); 55 | } 56 | if ( 57 | currentBranch === "main" && 58 | confirm(`Branch for deno?`) 59 | ) { 60 | await denoRepo.gitBranch("deno_ast_" + deno_ast.version); 61 | } 62 | if ( 63 | await denoRepo.hasLocalChanges() && confirm(`Commit and push for deno?`) 64 | ) { 65 | await denoRepo.gitAdd(); 66 | await denoRepo.gitCommit( 67 | `chore: upgrade to deno_ast ${deno_ast.version}`, 68 | ); 69 | await denoRepo.gitPush(); 70 | } 71 | } 72 | 73 | async function bumpDeps(repo: Repo) { 74 | for (const crate of repo.crates) { 75 | for (const depCrate of repos.getCrateLocalSourceCrates(crate)) { 76 | await crate.revertLocalSource(depCrate); 77 | const version = await depCrate.getLatestVersion(); 78 | if (version == null) { 79 | throw new Error(`Did not find version for ${crate.name}`); 80 | } 81 | await crate.setDependencyVersion(depCrate.name, version); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | These scripts provide a way to help upgrade, test, and open PRs in downstream 4 | crates on an swc version bump. 5 | 6 | ## Setup 7 | 8 | 1. Ensure all repos are cloned into sibling directories: 9 | 10 | - `./deno` 11 | - `./deno_ast` 12 | - `./deno_doc` 13 | - `./deno_graph` 14 | - `./deno_lint` 15 | - `./dprint-plugin-typescript` 16 | 17 | 2. Ensure all repos have an `upstream` remote defined as the original repo. 18 | 19 | ## Overview 20 | 21 | - `01_setup.ts` - Ensures all downstream crates are on the latest main, then 22 | points them at local copies of each other. 23 | - `02_build.ts` - Builds each crate. If you encounter any build errors, fix them 24 | and keep running this until everything builds. 25 | - `03_test.ts` - Tests each crate. If you encounter test failures, fix them and 26 | keep running this until all the tests pass. 27 | - `04_confirm.ts` - Updates the dependency versions, creates a branch, commits, 28 | and pushes a branch for every selected repo. 29 | -------------------------------------------------------------------------------- /scripts/repos.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { $, Crate, Repo } from "@deno/rust-automation"; 4 | 5 | export const rootDir = $.path(import.meta.url).join("../../../").resolve(); 6 | 7 | const repoNames = [ 8 | "deno_ast", 9 | "deno_graph", 10 | "deno_lint", 11 | "dprint-plugin-typescript", 12 | "deno_doc", 13 | "eszip", 14 | "deno", 15 | ]; 16 | 17 | export class Repos { 18 | #repos: readonly Repo[]; 19 | 20 | private constructor(repos: readonly Repo[]) { 21 | this.#repos = repos; 22 | } 23 | 24 | static createWithoutLoading() { 25 | return; 26 | } 27 | 28 | static async load({ skipLoadingCrates = false } = {}) { 29 | if (!skipLoadingCrates) { 30 | $.logStep("Loading repos..."); 31 | } 32 | const repos = []; 33 | for (const repoName of repoNames) { 34 | $.logStep("Loading", repoName); 35 | repos.push(await loadRepo(repoName)); 36 | } 37 | return new Repos(repos); 38 | 39 | function loadRepo(name: string) { 40 | return Repo.load({ 41 | name, 42 | path: rootDir.join(name), 43 | skipLoadingCrates, 44 | }).catch((err) => { 45 | console.error(`Error loading: ${name}`); 46 | throw err; 47 | }); 48 | } 49 | } 50 | 51 | getRepos() { 52 | return [...this.#repos]; 53 | } 54 | 55 | getCrates() { 56 | const crates = []; 57 | for (const repo of this.#repos) { 58 | if (repo.name === "deno") { 59 | crates.push(repo.getCrate("deno")); 60 | } else { 61 | crates.push( 62 | ...repo.crates.filter((c) => !c.name.endsWith("_wasm")), 63 | ); 64 | } 65 | } 66 | return crates; 67 | } 68 | 69 | nonDenoAstRepos() { 70 | return this.#repos.filter((c) => c.name !== "deno_ast"); 71 | } 72 | 73 | get(name: string) { 74 | const repo = this.#repos.find((c) => c.name === name); 75 | if (repo == null) { 76 | throw new Error(`Could not find repo with name ${name}.`); 77 | } 78 | return repo; 79 | } 80 | 81 | getCrate(name: string) { 82 | for (const repo of this.#repos) { 83 | for (const crate of repo.crates) { 84 | if (crate.name === name) { 85 | return crate; 86 | } 87 | } 88 | } 89 | 90 | throw new Error(`Could not find crate: ${name}`); 91 | } 92 | 93 | async toLocalSource() { 94 | for ( 95 | const [workingCrate, otherCrate] of this.#getLocalSourceRelationships() 96 | ) { 97 | await workingCrate.toLocalSource(otherCrate); 98 | } 99 | } 100 | 101 | async revertLocalSource() { 102 | for ( 103 | const [workingCrate, depCrate] of this.#getLocalSourceRelationships() 104 | ) { 105 | await workingCrate.revertLocalSource(depCrate); 106 | } 107 | } 108 | 109 | getCrateLocalSourceCrates(crate: Crate) { 110 | return this.#getLocalSourceRelationships() 111 | .filter(([workingCrate]) => workingCrate === crate) 112 | .map(([_workingCrate, depCrate]) => depCrate); 113 | } 114 | 115 | #getLocalSourceRelationships() { 116 | const deno_ast = this.getCrate("deno_ast"); 117 | const deno_graph = this.getCrate("deno_graph"); 118 | const deno_doc = this.getCrate("deno_doc"); 119 | const deno_lint = this.getCrate("deno_lint"); 120 | const dprint_plugin_typescript = this.getCrate("dprint-plugin-typescript"); 121 | const deno_cli = this.getCrate("deno"); 122 | const eszip = this.getCrate("eszip"); 123 | 124 | return [ 125 | [deno_graph, deno_ast], 126 | [deno_doc, deno_ast], 127 | [deno_doc, deno_graph], 128 | [eszip, deno_ast], 129 | [eszip, deno_graph], 130 | [deno_lint, deno_ast], 131 | [dprint_plugin_typescript, deno_ast], 132 | [deno_cli, deno_ast], 133 | [deno_cli, deno_graph], 134 | [deno_cli, deno_doc], 135 | [deno_cli, deno_lint], 136 | [deno_cli, eszip], 137 | [deno_cli, dprint_plugin_typescript], 138 | ] as [Crate, Crate][]; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /scripts/reset.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 3 | 4 | import { Repos } from "./repos.ts"; 5 | 6 | const repos = await Repos.load({ skipLoadingCrates: true }); 7 | 8 | if (confirm("Are you sure you want to git reset --hard all the repos?")) { 9 | await Promise.all(repos.nonDenoAstRepos().map((c) => c.gitResetHard())); 10 | } 11 | -------------------------------------------------------------------------------- /scripts/update_swc_deps.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 3 | 4 | import { $, Crate, Repo } from "@deno/rust-automation"; 5 | 6 | const repo = await Repo.load({ 7 | name: "deno_ast", 8 | path: $.path(import.meta.url) 9 | .parentOrThrow() 10 | .parentOrThrow() 11 | .resolve(), 12 | }); 13 | 14 | const crate = repo.getCrate("deno_ast"); 15 | const swcDeps = crate.dependencies.filter( 16 | (dep) => dep.name.startsWith("swc_") || dep.name === "dprint-swc-ext", 17 | ); 18 | for (const dep of swcDeps) { 19 | const version = await Crate.getLatestVersion(dep.name); 20 | if (version == null) { 21 | throw new Error(`Could not find latest version for ${dep.name}`); 22 | } 23 | 24 | const newReq = dep.name === "dprint-swc-ext" ? "^" + version : "=" + version; 25 | if (newReq !== dep.req) { 26 | $.logStep("Updating", dep.name, "from", dep.req, "to", newReq); 27 | crate.setDependencyVersion(dep.name, newReq.replace(/^\^/, "")); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/comments.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | // Need to enable this for this file in order to 4 | // implement swc's `Comments` trait 5 | #![allow(clippy::disallowed_types)] 6 | 7 | use crate::SourcePos; 8 | use crate::swc::common::BytePos as SwcBytePos; 9 | use crate::swc::common::comments::Comment; 10 | use crate::swc::common::comments::Comments as SwcComments; 11 | use crate::swc::common::comments::SingleThreadedComments; 12 | use crate::swc::common::comments::SingleThreadedCommentsMapInner; 13 | 14 | use std::cell::RefCell; 15 | use std::rc::Rc; 16 | use std::sync::Arc; 17 | 18 | #[derive(Debug)] 19 | struct MultiThreadedCommentsInner { 20 | leading: SingleThreadedCommentsMapInner, 21 | trailing: SingleThreadedCommentsMapInner, 22 | } 23 | 24 | /// An implementation of swc's `Comments` that implements `Sync` 25 | /// to support being used in multi-threaded code. This implementation 26 | /// is immutable and should you need mutability you may create a copy 27 | /// by converting it to an swc `SingleThreadedComments`. 28 | #[derive(Clone, Debug)] 29 | pub struct MultiThreadedComments { 30 | inner: Arc, 31 | } 32 | 33 | impl MultiThreadedComments { 34 | /// Creates a new `MultiThreadedComments` from an swc `SingleThreadedComments`. 35 | pub fn from_single_threaded(comments: SingleThreadedComments) -> Self { 36 | let (leading, trailing) = comments.take_all(); 37 | let leading = Rc::try_unwrap(leading).unwrap().into_inner(); 38 | let trailing = Rc::try_unwrap(trailing).unwrap().into_inner(); 39 | Self::from_leading_and_trailing(leading, trailing) 40 | } 41 | 42 | pub fn from_leading_and_trailing( 43 | leading: SingleThreadedCommentsMapInner, 44 | trailing: SingleThreadedCommentsMapInner, 45 | ) -> Self { 46 | Self { 47 | inner: Arc::new(MultiThreadedCommentsInner { leading, trailing }), 48 | } 49 | } 50 | 51 | /// Gets a clone of the underlying data as `SingleThreadedComments`. 52 | /// 53 | /// This may be useful for getting a mutable data structure for use 54 | /// when transpiling. 55 | pub fn as_single_threaded(&self) -> SingleThreadedComments { 56 | let inner = &self.inner; 57 | let leading = Rc::new(RefCell::new(inner.leading.to_owned())); 58 | let trailing = Rc::new(RefCell::new(inner.trailing.to_owned())); 59 | SingleThreadedComments::from_leading_and_trailing(leading, trailing) 60 | } 61 | 62 | pub fn into_single_threaded(self) -> SingleThreadedComments { 63 | let inner = match Arc::try_unwrap(self.inner) { 64 | Ok(inner) => inner, 65 | Err(inner) => MultiThreadedCommentsInner { 66 | leading: inner.leading.clone(), 67 | trailing: inner.trailing.clone(), 68 | }, 69 | }; 70 | let leading = Rc::new(RefCell::new(inner.leading)); 71 | let trailing = Rc::new(RefCell::new(inner.trailing)); 72 | SingleThreadedComments::from_leading_and_trailing(leading, trailing) 73 | } 74 | 75 | /// Gets a reference to the leading comment map. 76 | pub fn leading_map(&self) -> &SingleThreadedCommentsMapInner { 77 | &self.inner.leading 78 | } 79 | 80 | /// Gets a reference to the trailing comment map. 81 | pub fn trailing_map(&self) -> &SingleThreadedCommentsMapInner { 82 | &self.inner.trailing 83 | } 84 | 85 | /// Gets a vector of all the comments sorted by position. 86 | pub fn get_vec(&self) -> Vec { 87 | let mut comments = self.iter_unstable().cloned().collect::>(); 88 | comments.sort_by_key(|comment| comment.span.lo); 89 | comments 90 | } 91 | 92 | /// Iterates through all the comments in an unstable order. 93 | pub fn iter_unstable(&self) -> impl Iterator { 94 | self 95 | .inner 96 | .leading 97 | .values() 98 | .chain(self.inner.trailing.values()) 99 | .flatten() 100 | } 101 | 102 | pub fn has_leading(&self, pos: SourcePos) -> bool { 103 | self.inner.leading.contains_key(&pos.as_byte_pos()) 104 | } 105 | 106 | pub fn get_leading(&self, pos: SourcePos) -> Option<&Vec> { 107 | self.inner.leading.get(&pos.as_byte_pos()) 108 | } 109 | 110 | pub fn has_trailing(&self, pos: SourcePos) -> bool { 111 | self.inner.trailing.contains_key(&pos.as_byte_pos()) 112 | } 113 | 114 | pub fn get_trailing(&self, pos: SourcePos) -> Option<&Vec> { 115 | self.inner.trailing.get(&pos.as_byte_pos()) 116 | } 117 | 118 | /// Gets the comments as an `SwcComments` trait. 119 | /// 120 | /// Calling this is fast because it uses a shared reference. 121 | pub fn as_swc_comments(&self) -> Box { 122 | Box::new(SwcMultiThreadedComments(self.clone())) 123 | } 124 | } 125 | 126 | // Don't want to expose this API easily, so someone should 127 | // use the `.as_swc_comments()` above to access it. 128 | struct SwcMultiThreadedComments(MultiThreadedComments); 129 | 130 | impl SwcComments for SwcMultiThreadedComments { 131 | fn has_leading(&self, pos: SwcBytePos) -> bool { 132 | // It's ok to convert these byte positions to source 133 | // positions because we received them from swc and 134 | // didn't create them on their own. 135 | self.0.has_leading(SourcePos::unsafely_from_byte_pos(pos)) 136 | } 137 | 138 | fn get_leading(&self, pos: SwcBytePos) -> Option> { 139 | self 140 | .0 141 | .get_leading(SourcePos::unsafely_from_byte_pos(pos)) 142 | .cloned() 143 | } 144 | 145 | fn has_trailing(&self, pos: SwcBytePos) -> bool { 146 | self.0.has_trailing(SourcePos::unsafely_from_byte_pos(pos)) 147 | } 148 | 149 | fn get_trailing(&self, pos: SwcBytePos) -> Option> { 150 | self 151 | .0 152 | .get_trailing(SourcePos::unsafely_from_byte_pos(pos)) 153 | .cloned() 154 | } 155 | 156 | fn add_leading(&self, _pos: SwcBytePos, _cmt: Comment) { 157 | panic_readonly(); 158 | } 159 | 160 | fn add_leading_comments(&self, _pos: SwcBytePos, _comments: Vec) { 161 | panic_readonly(); 162 | } 163 | 164 | fn move_leading(&self, _from: SwcBytePos, _to: SwcBytePos) { 165 | panic_readonly(); 166 | } 167 | 168 | fn take_leading(&self, _pos: SwcBytePos) -> Option> { 169 | panic_readonly(); 170 | } 171 | 172 | fn add_trailing(&self, _pos: SwcBytePos, _cmt: Comment) { 173 | panic_readonly(); 174 | } 175 | 176 | fn add_trailing_comments(&self, _pos: SwcBytePos, _comments: Vec) { 177 | panic_readonly(); 178 | } 179 | 180 | fn move_trailing(&self, _from: SwcBytePos, _to: SwcBytePos) { 181 | panic_readonly(); 182 | } 183 | 184 | fn take_trailing(&self, _pos: SwcBytePos) -> Option> { 185 | panic_readonly(); 186 | } 187 | 188 | fn add_pure_comment(&self, _pos: SwcBytePos) { 189 | panic_readonly(); 190 | } 191 | } 192 | 193 | fn panic_readonly() -> ! { 194 | panic!("MultiThreadedComments do not support write operations") 195 | } 196 | 197 | #[cfg(test)] 198 | mod test { 199 | use crate::MediaType; 200 | use crate::ModuleSpecifier; 201 | use crate::MultiThreadedComments; 202 | use crate::ParseParams; 203 | use crate::StartSourcePos; 204 | use crate::parse_module; 205 | use crate::swc::common::comments::SingleThreadedComments; 206 | 207 | #[test] 208 | fn general_use() { 209 | let (comments, start_pos) = get_single_threaded_comments("// 1\nt;/* 2 */"); 210 | let comments = MultiThreadedComments::from_single_threaded(comments); 211 | 212 | // maps 213 | assert_eq!(comments.leading_map().len(), 1); 214 | assert_eq!( 215 | comments 216 | .leading_map() 217 | .get(&(start_pos + 5).as_byte_pos()) 218 | .unwrap()[0] 219 | .text, 220 | " 1" 221 | ); 222 | assert_eq!(comments.trailing_map().len(), 1); 223 | assert_eq!( 224 | comments 225 | .trailing_map() 226 | .get(&(start_pos + 7).as_byte_pos()) 227 | .unwrap()[0] 228 | .text, 229 | " 2 " 230 | ); 231 | 232 | // comment vector 233 | let comments_vec = comments.get_vec(); 234 | assert_eq!(comments_vec.len(), 2); 235 | assert_eq!(comments_vec[0].text, " 1"); 236 | assert_eq!(comments_vec[1].text, " 2 "); 237 | 238 | // comments trait 239 | assert!(comments.has_leading(start_pos + 5)); 240 | assert!(!comments.has_leading(start_pos + 7)); 241 | 242 | assert_eq!(comments.get_leading(start_pos + 5).unwrap()[0].text, " 1"); 243 | assert!(comments.get_leading(start_pos + 7).is_none()); 244 | 245 | assert!(!comments.has_trailing(start_pos + 5)); 246 | assert!(comments.has_trailing(start_pos + 7)); 247 | 248 | assert!(comments.get_trailing(start_pos + 5).is_none()); 249 | assert_eq!(comments.get_trailing(start_pos + 7).unwrap()[0].text, " 2 "); 250 | } 251 | 252 | fn get_single_threaded_comments( 253 | text: &str, 254 | ) -> (SingleThreadedComments, StartSourcePos) { 255 | let module = parse_module(ParseParams { 256 | specifier: ModuleSpecifier::parse("file:///file.ts").unwrap(), 257 | text: text.into(), 258 | media_type: MediaType::TypeScript, 259 | capture_tokens: false, 260 | maybe_syntax: None, 261 | scope_analysis: false, 262 | }) 263 | .expect("expects a module"); 264 | (module.comments().as_single_threaded(), module.range().start) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::borrow::Cow; 4 | use std::collections::HashMap; 5 | use std::fmt; 6 | use std::fmt::Display; 7 | use std::fmt::Write as _; 8 | use std::path::PathBuf; 9 | 10 | use deno_error::JsError; 11 | use deno_terminal::colors; 12 | use unicode_width::UnicodeWidthStr; 13 | 14 | use crate::ModuleSpecifier; 15 | use crate::SourcePos; 16 | use crate::SourceRange; 17 | use crate::SourceRanged; 18 | use crate::SourceTextInfo; 19 | 20 | use crate::swc::common::errors::Diagnostic as SwcDiagnostic; 21 | 22 | pub enum DiagnosticLevel { 23 | Error, 24 | Warning, 25 | } 26 | 27 | #[derive(Clone, Copy, Debug)] 28 | pub struct DiagnosticSourceRange { 29 | pub start: DiagnosticSourcePos, 30 | pub end: DiagnosticSourcePos, 31 | } 32 | 33 | #[derive(Clone, Copy, Debug)] 34 | pub enum DiagnosticSourcePos { 35 | SourcePos(SourcePos), 36 | ByteIndex(usize), 37 | LineAndCol { 38 | // 0-indexed line number in bytes 39 | line: usize, 40 | // 0-indexed column number in bytes 41 | column: usize, 42 | }, 43 | } 44 | 45 | impl DiagnosticSourcePos { 46 | fn pos(&self, source: &SourceTextInfo) -> SourcePos { 47 | match self { 48 | DiagnosticSourcePos::SourcePos(pos) => *pos, 49 | DiagnosticSourcePos::ByteIndex(index) => source.range().start() + *index, 50 | DiagnosticSourcePos::LineAndCol { line, column } => { 51 | source.line_start(*line) + *column 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone, Debug)] 58 | pub enum DiagnosticLocation<'a> { 59 | /// The diagnostic is relevant to a specific path. 60 | Path { path: PathBuf }, 61 | /// The diagnostic is relevant to an entire module. 62 | Module { 63 | /// The specifier of the module that contains the diagnostic. 64 | specifier: Cow<'a, ModuleSpecifier>, 65 | }, 66 | /// The diagnostic is relevant to a specific position in a module. 67 | /// 68 | /// This variant will get the relevant `SouceTextInfo` from the cache using 69 | /// the given specifier, and will then calculate the line and column numbers 70 | /// from the given `SourcePos`. 71 | ModulePosition { 72 | /// The specifier of the module that contains the diagnostic. 73 | specifier: Cow<'a, ModuleSpecifier>, 74 | /// The source position of the diagnostic. 75 | source_pos: DiagnosticSourcePos, 76 | text_info: Cow<'a, SourceTextInfo>, 77 | }, 78 | } 79 | 80 | impl DiagnosticLocation<'_> { 81 | /// Return the line and column number of the diagnostic. 82 | /// 83 | /// The line number is 1-indexed. 84 | /// 85 | /// The column number is 1-indexed. This is the number of UTF-16 code units 86 | /// from the start of the line to the diagnostic. 87 | /// Why UTF-16 code units? Because that's what VS Code understands, and 88 | /// everyone uses VS Code. :) 89 | fn position(&self) -> Option<(usize, usize)> { 90 | match self { 91 | DiagnosticLocation::Path { .. } => None, 92 | DiagnosticLocation::Module { .. } => None, 93 | DiagnosticLocation::ModulePosition { 94 | specifier: _specifier, 95 | source_pos, 96 | text_info, 97 | } => { 98 | let pos = source_pos.pos(text_info); 99 | let line_index = text_info.line_index(pos); 100 | let line_start_pos = text_info.line_start(line_index); 101 | // todo(dsherret): fix in text_lines 102 | let content = 103 | text_info.range_text(&SourceRange::new(line_start_pos, pos)); 104 | let line = line_index + 1; 105 | let column = content.encode_utf16().count() + 1; 106 | Some((line, column)) 107 | } 108 | } 109 | } 110 | } 111 | 112 | pub struct DiagnosticSnippet<'a> { 113 | /// The source text for this snippet. The 114 | pub source: Cow<'a, crate::SourceTextInfo>, 115 | /// The piece of the snippet that should be highlighted. For best results, the 116 | /// highlights should not overlap and be ordered by their start position. 117 | pub highlights: Vec>, 118 | } 119 | 120 | #[derive(Clone)] 121 | pub struct DiagnosticSnippetHighlight<'a> { 122 | /// The range of the snippet that should be highlighted. 123 | pub range: DiagnosticSourceRange, 124 | /// The style of the highlight. 125 | pub style: DiagnosticSnippetHighlightStyle, 126 | /// An optional inline description of the highlight. 127 | pub description: Option>, 128 | } 129 | 130 | #[derive(Clone, Copy)] 131 | pub enum DiagnosticSnippetHighlightStyle { 132 | /// The highlight is an error. This will place red carets under the highlight. 133 | Error, 134 | #[allow(dead_code)] 135 | /// The highlight is a warning. This will place yellow carets under the 136 | /// highlight. 137 | Warning, 138 | #[allow(dead_code)] 139 | /// The highlight shows a hint. This will place blue dashes under the 140 | /// highlight. 141 | Hint, 142 | } 143 | 144 | impl DiagnosticSnippetHighlightStyle { 145 | fn style_underline( 146 | &self, 147 | s: impl std::fmt::Display, 148 | ) -> impl std::fmt::Display { 149 | match self { 150 | DiagnosticSnippetHighlightStyle::Error => colors::red_bold(s), 151 | DiagnosticSnippetHighlightStyle::Warning => colors::yellow_bold(s), 152 | DiagnosticSnippetHighlightStyle::Hint => colors::intense_blue(s), 153 | } 154 | } 155 | 156 | fn underline_char(&self) -> char { 157 | match self { 158 | DiagnosticSnippetHighlightStyle::Error => '^', 159 | DiagnosticSnippetHighlightStyle::Warning => '^', 160 | DiagnosticSnippetHighlightStyle::Hint => '-', 161 | } 162 | } 163 | } 164 | 165 | /// Returns the text of the line with the given number. 166 | fn line_text(source: &SourceTextInfo, line_number: usize) -> &str { 167 | source.line_text(line_number - 1) 168 | } 169 | 170 | /// Returns the line number (1 indexed) of the line that contains the given 171 | /// position. 172 | fn line_number(source: &SourceTextInfo, pos: DiagnosticSourcePos) -> usize { 173 | source.line_index(pos.pos(source)) + 1 174 | } 175 | 176 | pub trait Diagnostic { 177 | /// The level of the diagnostic. 178 | fn level(&self) -> DiagnosticLevel; 179 | 180 | /// The diagnostic code, like `no-explicit-any` or `ban-untagged-ignore`. 181 | fn code(&self) -> Cow<'_, str>; 182 | 183 | /// The human-readable diagnostic message. 184 | fn message(&self) -> Cow<'_, str>; 185 | 186 | /// The location this diagnostic is associated with. 187 | fn location(&self) -> DiagnosticLocation<'_>; 188 | 189 | /// A snippet showing the source code associated with the diagnostic. 190 | fn snippet(&self) -> Option>; 191 | 192 | /// A hint for fixing the diagnostic. 193 | fn hint(&self) -> Option>; 194 | 195 | /// A snippet showing how the diagnostic can be fixed. 196 | fn snippet_fixed(&self) -> Option>; 197 | 198 | fn info(&self) -> Cow<'_, [Cow<'_, str>]>; 199 | 200 | /// An optional URL to the documentation for the diagnostic. 201 | fn docs_url(&self) -> Option>; 202 | 203 | fn display(&self) -> DiagnosticDisplay<'_, Self> { 204 | DiagnosticDisplay { diagnostic: self } 205 | } 206 | } 207 | 208 | struct RepeatingCharFmt(char, usize); 209 | impl fmt::Display for RepeatingCharFmt { 210 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 211 | for _ in 0..self.1 { 212 | f.write_char(self.0)?; 213 | } 214 | Ok(()) 215 | } 216 | } 217 | 218 | /// How many spaces a tab should be displayed as. 2 is the default used for 219 | /// `deno fmt`, so we'll use that here. 220 | const TAB_WIDTH: usize = 2; 221 | 222 | struct ReplaceTab<'a>(&'a str); 223 | impl fmt::Display for ReplaceTab<'_> { 224 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 225 | let mut written = 0; 226 | for (i, c) in self.0.char_indices() { 227 | if c == '\t' { 228 | self.0[written..i].fmt(f)?; 229 | RepeatingCharFmt(' ', TAB_WIDTH).fmt(f)?; 230 | written = i + 1; 231 | } 232 | } 233 | self.0[written..].fmt(f)?; 234 | Ok(()) 235 | } 236 | } 237 | 238 | /// The width of the string as displayed, assuming tabs are 2 spaces wide. 239 | /// 240 | /// This display width assumes that zero-width-joined characters are the width 241 | /// of their consituent characters. This means that "Person: Red Hair" (which is 242 | /// represented as "Person" + "ZWJ" + "Red Hair") will have a width of 4. 243 | /// 244 | /// Whether this is correct is unfortunately dependent on the font / terminal 245 | /// being used. Here is a list of what terminals consider the length of 246 | /// "Person: Red Hair" to be: 247 | /// 248 | /// | Terminal | Rendered Width | 249 | /// | ---------------- | -------------- | 250 | /// | Windows Terminal | 5 chars | 251 | /// | iTerm (macOS) | 2 chars | 252 | /// | Terminal (macOS) | 2 chars | 253 | /// | VS Code terminal | 4 chars | 254 | /// | GNOME Terminal | 4 chars | 255 | /// 256 | /// If we really wanted to, we could try and detect the terminal being used and 257 | /// adjust the width accordingly. However, this is probably not worth the 258 | /// effort. 259 | fn display_width(str: &str) -> usize { 260 | let num_tabs = str.chars().filter(|c| *c == '\t').count(); 261 | str.width_cjk() + num_tabs * TAB_WIDTH - num_tabs 262 | } 263 | 264 | pub struct DiagnosticDisplay<'a, T: Diagnostic + ?Sized> { 265 | diagnostic: &'a T, 266 | } 267 | 268 | impl Display for DiagnosticDisplay<'_, T> { 269 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 270 | print_diagnostic(f, self.diagnostic) 271 | } 272 | } 273 | 274 | // error[missing-return-type]: missing explicit return type on public function 275 | // at /mnt/artemis/Projects/github.com/denoland/deno/test.ts:1:16 276 | // | 277 | // 1 | export function test() { 278 | // | ^^^^ 279 | // = hint: add an explicit return type to the function 280 | // | 281 | // 1 | export function test(): string { 282 | // | ^^^^^^^^ 283 | // 284 | // info: all functions that are exported from a module must have an explicit return type to support fast check and documentation generation. 285 | // docs: https://jsr.io/d/missing-return-type 286 | fn print_diagnostic( 287 | io: &mut dyn std::fmt::Write, 288 | diagnostic: &(impl Diagnostic + ?Sized), 289 | ) -> Result<(), std::fmt::Error> { 290 | match diagnostic.level() { 291 | DiagnosticLevel::Error => { 292 | write!( 293 | io, 294 | "{}", 295 | colors::red_bold(format_args!("error[{}]", diagnostic.code())) 296 | )?; 297 | } 298 | DiagnosticLevel::Warning => { 299 | write!( 300 | io, 301 | "{}", 302 | colors::yellow_bold(format_args!("warning[{}]", diagnostic.code())) 303 | )?; 304 | } 305 | } 306 | 307 | writeln!(io, ": {}", colors::bold(diagnostic.message()))?; 308 | 309 | let mut max_line_number_digits = 1; 310 | if let Some(snippet) = diagnostic.snippet() { 311 | for highlight in snippet.highlights.iter() { 312 | let last_line = line_number(&snippet.source, highlight.range.end); 313 | max_line_number_digits = 314 | max_line_number_digits.max(last_line.ilog10() + 1); 315 | } 316 | } 317 | 318 | if let Some(snippet) = diagnostic.snippet_fixed() { 319 | for highlight in snippet.highlights.iter() { 320 | let last_line = line_number(&snippet.source, highlight.range.end); 321 | max_line_number_digits = 322 | max_line_number_digits.max(last_line.ilog10() + 1); 323 | } 324 | } 325 | 326 | let location = diagnostic.location(); 327 | write!( 328 | io, 329 | "{}{}", 330 | RepeatingCharFmt(' ', max_line_number_digits as usize), 331 | colors::intense_blue("-->"), 332 | )?; 333 | match &location { 334 | DiagnosticLocation::Path { path } => { 335 | write!(io, " {}", colors::cyan(path.display()))?; 336 | } 337 | DiagnosticLocation::Module { specifier } 338 | | DiagnosticLocation::ModulePosition { specifier, .. } => { 339 | if let Some(path) = specifier_to_file_path(specifier) { 340 | write!(io, " {}", colors::cyan(path.display()))?; 341 | } else { 342 | write!(io, " {}", colors::cyan(specifier.as_str()))?; 343 | } 344 | } 345 | } 346 | if let Some((line, column)) = location.position() { 347 | write!( 348 | io, 349 | "{}", 350 | colors::yellow(format_args!(":{}:{}", line, column)) 351 | )?; 352 | } 353 | 354 | if diagnostic.snippet().is_some() 355 | || diagnostic.hint().is_some() 356 | || diagnostic.snippet_fixed().is_some() 357 | || !diagnostic.info().is_empty() 358 | || diagnostic.docs_url().is_some() 359 | { 360 | writeln!(io)?; 361 | } 362 | 363 | if let Some(snippet) = diagnostic.snippet() { 364 | print_snippet(io, &snippet, max_line_number_digits)?; 365 | }; 366 | 367 | if let Some(hint) = diagnostic.hint() { 368 | write!( 369 | io, 370 | "{} {} ", 371 | RepeatingCharFmt(' ', max_line_number_digits as usize), 372 | colors::intense_blue("=") 373 | )?; 374 | writeln!(io, "{}: {}", colors::bold("hint"), hint)?; 375 | } 376 | 377 | if let Some(snippet) = diagnostic.snippet_fixed() { 378 | print_snippet(io, &snippet, max_line_number_digits)?; 379 | } 380 | 381 | if !diagnostic.info().is_empty() || diagnostic.docs_url().is_some() { 382 | writeln!(io)?; 383 | } 384 | 385 | for info in diagnostic.info().iter() { 386 | writeln!(io, " {}: {}", colors::intense_blue("info"), info)?; 387 | } 388 | if let Some(docs_url) = diagnostic.docs_url() { 389 | writeln!(io, " {}: {}", colors::intense_blue("docs"), docs_url)?; 390 | } 391 | 392 | Ok(()) 393 | } 394 | 395 | /// Prints a snippet to the given writer and returns the line number indent. 396 | fn print_snippet( 397 | io: &mut dyn std::fmt::Write, 398 | snippet: &DiagnosticSnippet<'_>, 399 | max_line_number_digits: u32, 400 | ) -> Result<(), std::fmt::Error> { 401 | let DiagnosticSnippet { source, highlights } = snippet; 402 | 403 | fn print_padded( 404 | io: &mut dyn std::fmt::Write, 405 | text: impl std::fmt::Display, 406 | padding: u32, 407 | ) -> Result<(), std::fmt::Error> { 408 | for _ in 0..padding { 409 | write!(io, " ")?; 410 | } 411 | write!(io, "{}", text)?; 412 | Ok(()) 413 | } 414 | 415 | let mut lines_to_show = HashMap::>::new(); 416 | let mut highlights_info = Vec::new(); 417 | for (i, highlight) in highlights.iter().enumerate() { 418 | let start_line_number = line_number(source, highlight.range.start); 419 | let end_line_number = line_number(source, highlight.range.end); 420 | highlights_info.push((start_line_number, end_line_number)); 421 | for line_number in start_line_number..=end_line_number { 422 | lines_to_show.entry(line_number).or_default().push(i); 423 | } 424 | } 425 | 426 | let mut lines_to_show = lines_to_show.into_iter().collect::>(); 427 | lines_to_show.sort(); 428 | 429 | print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?; 430 | writeln!(io)?; 431 | let mut previous_line_number = None; 432 | let mut previous_line_empty = false; 433 | for (line_number, highlight_indexes) in lines_to_show { 434 | if previous_line_number.is_some() 435 | && previous_line_number == Some(line_number - 1) 436 | && !previous_line_empty 437 | { 438 | print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?; 439 | writeln!(io)?; 440 | } 441 | 442 | print_padded( 443 | io, 444 | colors::intense_blue(format_args!("{} | ", line_number)), 445 | max_line_number_digits - line_number.ilog10() - 1, 446 | )?; 447 | 448 | let line_start_pos = source.line_start(line_number - 1); 449 | let line_end_pos = source.line_end(line_number - 1); 450 | let line_text = line_text(source, line_number); 451 | writeln!(io, "{}", ReplaceTab(line_text))?; 452 | previous_line_empty = false; 453 | 454 | let mut wrote_description = false; 455 | for highlight_index in highlight_indexes { 456 | let highlight = &highlights[highlight_index]; 457 | let (start_line_number, end_line_number) = 458 | highlights_info[highlight_index]; 459 | 460 | let padding_width; 461 | let highlight_width; 462 | if start_line_number == end_line_number { 463 | padding_width = display_width(source.range_text(&SourceRange::new( 464 | line_start_pos, 465 | highlight.range.start.pos(source), 466 | ))); 467 | highlight_width = display_width(source.range_text(&SourceRange::new( 468 | highlight.range.start.pos(source), 469 | highlight.range.end.pos(source), 470 | ))); 471 | } else if start_line_number == line_number { 472 | padding_width = display_width(source.range_text(&SourceRange::new( 473 | line_start_pos, 474 | highlight.range.start.pos(source), 475 | ))); 476 | highlight_width = display_width(source.range_text(&SourceRange::new( 477 | highlight.range.start.pos(source), 478 | line_end_pos, 479 | ))); 480 | } else if end_line_number == line_number { 481 | padding_width = 0; 482 | highlight_width = display_width(source.range_text(&SourceRange::new( 483 | line_start_pos, 484 | highlight.range.end.pos(source), 485 | ))); 486 | } else { 487 | padding_width = 0; 488 | highlight_width = display_width(line_text); 489 | } 490 | 491 | let underline = 492 | RepeatingCharFmt(highlight.style.underline_char(), highlight_width); 493 | print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?; 494 | write!(io, "{}", RepeatingCharFmt(' ', padding_width))?; 495 | write!(io, "{}", highlight.style.style_underline(underline))?; 496 | 497 | if line_number == end_line_number 498 | && let Some(description) = &highlight.description 499 | { 500 | write!(io, " {}", highlight.style.style_underline(description))?; 501 | wrote_description = true; 502 | } 503 | 504 | writeln!(io)?; 505 | } 506 | 507 | if wrote_description { 508 | print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?; 509 | writeln!(io)?; 510 | previous_line_empty = true; 511 | } 512 | 513 | previous_line_number = Some(line_number); 514 | } 515 | 516 | Ok(()) 517 | } 518 | 519 | /// Attempts to convert a specifier to a file path. By default, uses the Url 520 | /// crate's `to_file_path()` method, but falls back to try and resolve unix-style 521 | /// paths on Windows. 522 | fn specifier_to_file_path(specifier: &ModuleSpecifier) -> Option { 523 | fn to_file_path_if_not_wasm(_specifier: &ModuleSpecifier) -> Option { 524 | #[cfg(target_arch = "wasm32")] 525 | { 526 | None 527 | } 528 | #[cfg(not(target_arch = "wasm32"))] 529 | { 530 | // not available in Wasm 531 | _specifier.to_file_path().ok() 532 | } 533 | } 534 | 535 | if specifier.scheme() != "file" { 536 | None 537 | } else if cfg!(windows) { 538 | match to_file_path_if_not_wasm(specifier) { 539 | Some(path) => Some(path), 540 | None => { 541 | // This might be a unix-style path which is used in the tests even on Windows. 542 | // Attempt to see if we can convert it to a `PathBuf`. This code should be removed 543 | // once/if https://github.com/servo/rust-url/issues/730 is implemented. 544 | if specifier.scheme() == "file" 545 | && specifier.host().is_none() 546 | && specifier.port().is_none() 547 | && specifier.path_segments().is_some() 548 | { 549 | let path_str = specifier.path(); 550 | match String::from_utf8( 551 | percent_encoding::percent_decode(path_str.as_bytes()).collect(), 552 | ) { 553 | Ok(path_str) => Some(PathBuf::from(path_str)), 554 | Err(_) => None, 555 | } 556 | } else { 557 | None 558 | } 559 | } 560 | } 561 | } else { 562 | to_file_path_if_not_wasm(specifier) 563 | } 564 | } 565 | 566 | #[cfg(any(feature = "transpiling", feature = "type_strip"))] 567 | pub(crate) type DiagnosticsCell = crate::swc::common::sync::Lrc< 568 | crate::swc::common::sync::Lock>, 569 | >; 570 | 571 | #[cfg(any(feature = "transpiling", feature = "type_strip"))] 572 | #[derive(Default, Clone)] 573 | pub(crate) struct DiagnosticCollector { 574 | diagnostics: DiagnosticsCell, 575 | } 576 | 577 | #[cfg(any(feature = "transpiling", feature = "type_strip"))] 578 | impl DiagnosticCollector { 579 | pub fn into_handler_and_cell( 580 | self, 581 | ) -> (crate::swc::common::errors::Handler, DiagnosticsCell) { 582 | let cell = self.diagnostics.clone(); 583 | ( 584 | crate::swc::common::errors::Handler::with_emitter( 585 | true, 586 | false, 587 | Box::new(self), 588 | ), 589 | cell, 590 | ) 591 | } 592 | } 593 | 594 | #[cfg(any(feature = "transpiling", feature = "type_strip"))] 595 | impl crate::swc::common::errors::Emitter for DiagnosticCollector { 596 | fn emit( 597 | &mut self, 598 | db: &mut crate::swc::common::errors::DiagnosticBuilder<'_>, 599 | ) { 600 | let mut diagnostics = self.diagnostics.lock(); 601 | diagnostics.push(db.take()); 602 | } 603 | } 604 | 605 | #[derive(Debug, JsError)] 606 | #[class(syntax)] 607 | pub struct SwcFoldDiagnosticsError(Vec); 608 | 609 | impl std::error::Error for SwcFoldDiagnosticsError {} 610 | 611 | impl std::fmt::Display for SwcFoldDiagnosticsError { 612 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 613 | for (i, diagnostic) in self.0.iter().enumerate() { 614 | if i > 0 { 615 | write!(f, "\n\n")?; 616 | } 617 | 618 | write!(f, "{}", diagnostic)? 619 | } 620 | 621 | Ok(()) 622 | } 623 | } 624 | 625 | pub fn ensure_no_fatal_swc_diagnostics( 626 | source_map: &swc_common::SourceMap, 627 | diagnostics: impl Iterator, 628 | ) -> Result<(), SwcFoldDiagnosticsError> { 629 | let fatal_diagnostics = diagnostics 630 | .filter(is_fatal_swc_diagnostic) 631 | .collect::>(); 632 | if !fatal_diagnostics.is_empty() { 633 | Err(SwcFoldDiagnosticsError( 634 | fatal_diagnostics 635 | .iter() 636 | .map(|d| format_swc_diagnostic(source_map, d)) 637 | .collect::>(), 638 | )) 639 | } else { 640 | Ok(()) 641 | } 642 | } 643 | 644 | fn is_fatal_swc_diagnostic(diagnostic: &SwcDiagnostic) -> bool { 645 | use crate::swc::common::errors::Level; 646 | match diagnostic.level { 647 | Level::Bug 648 | | Level::Cancelled 649 | | Level::FailureNote 650 | | Level::Fatal 651 | | Level::PhaseFatal 652 | | Level::Error => true, 653 | Level::Help | Level::Note | Level::Warning => false, 654 | } 655 | } 656 | 657 | fn format_swc_diagnostic( 658 | source_map: &swc_common::SourceMap, 659 | diagnostic: &SwcDiagnostic, 660 | ) -> String { 661 | if let Some(span) = &diagnostic.span.primary_span() { 662 | let file_name = source_map.span_to_filename(*span); 663 | let loc = source_map.lookup_char_pos(span.lo); 664 | format!( 665 | "{} at {}:{}:{}", 666 | diagnostic.message(), 667 | file_name, 668 | loc.line, 669 | loc.col_display + 1, 670 | ) 671 | } else { 672 | diagnostic.message() 673 | } 674 | } 675 | 676 | #[cfg(test)] 677 | mod tests { 678 | use std::borrow::Cow; 679 | 680 | use super::*; 681 | use crate::ModuleSpecifier; 682 | use crate::SourceTextInfo; 683 | 684 | #[test] 685 | fn test_display_width() { 686 | assert_eq!(display_width("abc"), 3); 687 | assert_eq!(display_width("\t"), 2); 688 | assert_eq!(display_width("\t\t123"), 7); 689 | assert_eq!(display_width("🎄"), 2); 690 | assert_eq!(display_width("🎄🎄"), 4); 691 | assert_eq!(display_width("🧑‍🦰"), 2); 692 | } 693 | 694 | #[test] 695 | fn test_position_in_file_from_text_info_simple() { 696 | let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap(); 697 | let text_info = SourceTextInfo::new("foo\nbar\nbaz".into()); 698 | let pos = text_info.line_start(1); 699 | let location = DiagnosticLocation::ModulePosition { 700 | specifier: Cow::Borrowed(&specifier), 701 | source_pos: DiagnosticSourcePos::SourcePos(pos), 702 | text_info: Cow::Owned(text_info), 703 | }; 704 | let position = location.position().unwrap(); 705 | assert_eq!(position, (2, 1)) 706 | } 707 | 708 | #[test] 709 | fn test_position_in_file_from_text_info_emoji() { 710 | let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap(); 711 | let text_info = SourceTextInfo::new("🧑‍🦰text".into()); 712 | let pos = text_info.line_start(0) + 11; // the end of the emoji 713 | let location = DiagnosticLocation::ModulePosition { 714 | specifier: Cow::Borrowed(&specifier), 715 | source_pos: DiagnosticSourcePos::SourcePos(pos), 716 | text_info: Cow::Owned(text_info), 717 | }; 718 | let position = location.position().unwrap(); 719 | assert_eq!(position, (1, 6)) 720 | } 721 | 722 | #[test] 723 | fn test_specifier_to_file_path() { 724 | run_success_test("file:///", "/"); 725 | run_success_test("file:///test", "/test"); 726 | run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt"); 727 | run_success_test( 728 | "file:///dir/test%20test/test.txt", 729 | "/dir/test test/test.txt", 730 | ); 731 | 732 | fn run_success_test(specifier: &str, expected_path: &str) { 733 | let result = 734 | specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap()) 735 | .unwrap(); 736 | assert_eq!(result, PathBuf::from(expected_path)); 737 | } 738 | } 739 | } 740 | -------------------------------------------------------------------------------- /src/emit.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use base64::Engine; 4 | use thiserror::Error; 5 | 6 | use crate::ModuleSpecifier; 7 | use crate::ProgramRef; 8 | use crate::SourceMap; 9 | use crate::swc::codegen::Node; 10 | use crate::swc::codegen::text_writer::JsWriter; 11 | use crate::swc::common::FileName; 12 | 13 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] 14 | pub enum SourceMapOption { 15 | /// Source map should be inlined into the source (default) 16 | #[default] 17 | Inline, 18 | /// Source map should be generated as a separate file. 19 | Separate, 20 | /// Source map should not be generated at all. 21 | None, 22 | } 23 | 24 | #[derive(Debug, Clone, Hash)] 25 | pub struct EmitOptions { 26 | /// How and if source maps should be generated. 27 | pub source_map: SourceMapOption, 28 | /// Base url to use for source maps. 29 | /// 30 | /// When a base is provided, when mapping source names in the source map, the 31 | /// name will be relative to the base. 32 | pub source_map_base: Option, 33 | /// The `"file"` field of the generated source map. 34 | pub source_map_file: Option, 35 | /// Whether to inline the source contents in the source map. Defaults to `true`. 36 | pub inline_sources: bool, 37 | /// Whether to remove comments in the output. Defaults to `false`. 38 | pub remove_comments: bool, 39 | } 40 | 41 | impl Default for EmitOptions { 42 | fn default() -> Self { 43 | EmitOptions { 44 | source_map: SourceMapOption::default(), 45 | source_map_base: None, 46 | source_map_file: None, 47 | inline_sources: true, 48 | remove_comments: false, 49 | } 50 | } 51 | } 52 | 53 | /// Source emitted based on the emit options. 54 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 55 | pub struct EmittedSourceText { 56 | /// Emitted text as utf8 bytes. 57 | pub text: String, 58 | /// Source map back to the original file. 59 | pub source_map: Option, 60 | } 61 | 62 | #[derive(Debug, Error, deno_error::JsError)] 63 | pub enum EmitError { 64 | #[class(inherit)] 65 | #[error(transparent)] 66 | SwcEmit(std::io::Error), 67 | #[class(type)] 68 | #[error(transparent)] 69 | SourceMap(crate::swc::sourcemap::Error), 70 | #[class(type)] 71 | #[error(transparent)] 72 | SourceMapEncode(base64::EncodeSliceError), 73 | } 74 | 75 | /// Emits the program as a string of JavaScript code, possibly with the passed 76 | /// comments, and optionally also a source map. 77 | pub fn emit( 78 | program: ProgramRef, 79 | comments: &dyn crate::swc::common::comments::Comments, 80 | source_map: &SourceMap, 81 | emit_options: &EmitOptions, 82 | ) -> Result { 83 | let source_map = source_map.inner(); 84 | let mut src_map_buf = vec![]; 85 | let mut src_buf = vec![]; 86 | { 87 | let mut writer = Box::new(JsWriter::new( 88 | source_map.clone(), 89 | "\n", 90 | &mut src_buf, 91 | Some(&mut src_map_buf), 92 | )); 93 | writer.set_indent_str(" "); // two spaces 94 | 95 | let mut emitter = crate::swc::codegen::Emitter { 96 | cfg: swc_codegen_config(), 97 | comments: if emit_options.remove_comments { 98 | None 99 | } else { 100 | Some(&comments) 101 | }, 102 | cm: source_map.clone(), 103 | wr: writer, 104 | }; 105 | match program { 106 | ProgramRef::Module(n) => { 107 | n.emit_with(&mut emitter).map_err(EmitError::SwcEmit)?; 108 | } 109 | ProgramRef::Script(n) => { 110 | n.emit_with(&mut emitter).map_err(EmitError::SwcEmit)?; 111 | } 112 | } 113 | } 114 | 115 | let mut map: Option> = None; 116 | 117 | if emit_options.source_map != SourceMapOption::None { 118 | let mut map_buf = Vec::new(); 119 | let source_map_config = SourceMapConfig { 120 | inline_sources: emit_options.inline_sources, 121 | maybe_base: emit_options.source_map_base.as_ref(), 122 | }; 123 | let mut source_map = 124 | source_map.build_source_map(&src_map_buf, None, source_map_config); 125 | if let Some(file) = &emit_options.source_map_file { 126 | source_map.set_file(Some(file.to_string())); 127 | } 128 | source_map 129 | .to_writer(&mut map_buf) 130 | .map_err(EmitError::SourceMap)?; 131 | 132 | if emit_options.source_map == SourceMapOption::Inline { 133 | // length is from the base64 crate examples 134 | let mut inline_buf = vec![0; map_buf.len() * 4 / 3 + 4]; 135 | let size = base64::prelude::BASE64_STANDARD 136 | .encode_slice(map_buf, &mut inline_buf) 137 | .map_err(EmitError::SourceMapEncode)?; 138 | let inline_buf = &inline_buf[..size]; 139 | let prelude_text = "//# sourceMappingURL=data:application/json;base64,"; 140 | let src_has_trailing_newline = src_buf.ends_with(b"\n"); 141 | let additional_capacity = if src_has_trailing_newline { 0 } else { 1 } 142 | + prelude_text.len() 143 | + inline_buf.len(); 144 | let expected_final_capacity = src_buf.len() + additional_capacity; 145 | src_buf.reserve(additional_capacity); 146 | if !src_has_trailing_newline { 147 | src_buf.push(b'\n'); 148 | } 149 | src_buf.extend(prelude_text.as_bytes()); 150 | src_buf.extend(inline_buf); 151 | debug_assert_eq!(src_buf.len(), expected_final_capacity); 152 | } else { 153 | map = Some(map_buf); 154 | } 155 | } 156 | 157 | debug_assert!(std::str::from_utf8(&src_buf).is_ok(), "valid utf-8"); 158 | if let Some(map) = &map { 159 | debug_assert!(std::str::from_utf8(map).is_ok(), "valid utf-8"); 160 | } 161 | 162 | // It's better to return a string here because then we can pass this to deno_core/v8 163 | // as a known string, so it doesn't need to spend any time analyzing it. 164 | Ok(EmittedSourceText { 165 | // SAFETY: swc appends UTF-8 bytes to the JsWriter, so we can safely assume 166 | // that the final string is UTF-8 (unchecked for performance reasons) 167 | text: unsafe { String::from_utf8_unchecked(src_buf) }, 168 | // SAFETY: see above comment 169 | source_map: map.map(|b| unsafe { String::from_utf8_unchecked(b) }), 170 | }) 171 | } 172 | 173 | /// Implements a configuration trait for source maps that reflects the logic 174 | /// to embed sources in the source map or not. 175 | #[derive(Debug)] 176 | pub struct SourceMapConfig<'a> { 177 | pub inline_sources: bool, 178 | pub maybe_base: Option<&'a ModuleSpecifier>, 179 | } 180 | 181 | impl crate::swc::common::source_map::SourceMapGenConfig 182 | for SourceMapConfig<'_> 183 | { 184 | fn file_name_to_source(&self, f: &FileName) -> String { 185 | match f { 186 | FileName::Url(specifier) => self 187 | .maybe_base 188 | .and_then(|base| { 189 | debug_assert!( 190 | base.as_str().ends_with('/'), 191 | "source map base should end with a slash" 192 | ); 193 | base.make_relative(specifier) 194 | }) 195 | .filter(|relative| !relative.is_empty()) 196 | .unwrap_or_else(|| f.to_string()), 197 | _ => f.to_string(), 198 | } 199 | } 200 | 201 | fn inline_sources_content(&self, f: &FileName) -> bool { 202 | match f { 203 | FileName::Real(..) | FileName::Custom(..) => false, 204 | FileName::Url(..) => self.inline_sources, 205 | _ => true, 206 | } 207 | } 208 | } 209 | 210 | pub fn swc_codegen_config() -> crate::swc::codegen::Config { 211 | // NOTICE ON UPGRADE: This struct has #[non_exhaustive] on it, 212 | // which prevents creating a struct expr here. For that reason, 213 | // inspect the struct on swc upgrade and explicitly specify any 214 | // new options here in order to ensure we maintain these settings. 215 | let mut config = crate::swc::codegen::Config::default(); 216 | config.target = crate::ES_VERSION; 217 | config.ascii_only = false; 218 | config.minify = false; 219 | config.omit_last_semi = false; 220 | config.emit_assert_for_import_attributes = false; 221 | config.inline_script = false; 222 | config 223 | } 224 | -------------------------------------------------------------------------------- /src/exports.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | 6 | use crate::ParsedSource; 7 | use crate::ProgramRef; 8 | use crate::swc::ast::ExportSpecifier; 9 | use crate::swc::ast::ModuleDecl; 10 | use crate::swc::ast::ModuleItem; 11 | use crate::swc::atoms::Atom; 12 | use crate::swc::utils::find_pat_ids; 13 | 14 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] 15 | pub struct ModuleExportsAndReExports { 16 | pub exports: Vec, 17 | pub reexports: Vec, 18 | } 19 | 20 | impl ParsedSource { 21 | /// Analyzes the ES runtime exports for require ESM. 22 | /// 23 | /// This is used during CJS export analysis when a CJS module 24 | /// re-exports an ESM module and the original CJS module needs 25 | /// to know the exports of the ESM module so it can create its 26 | /// wrapper ESM module. 27 | pub fn analyze_es_runtime_exports(&self) -> ModuleExportsAndReExports { 28 | let mut result = ModuleExportsAndReExports::default(); 29 | if let ProgramRef::Module(n) = self.program_ref() { 30 | for m in &n.body { 31 | match m { 32 | ModuleItem::ModuleDecl(m) => match m { 33 | ModuleDecl::Import(_) => {} 34 | ModuleDecl::ExportAll(n) => { 35 | result 36 | .reexports 37 | .push(n.src.value.to_string_lossy().into_owned()); 38 | } 39 | ModuleDecl::ExportDecl(d) => { 40 | match &d.decl { 41 | swc_ecma_ast::Decl::Class(d) => { 42 | result.exports.push(d.ident.sym.to_string()); 43 | } 44 | swc_ecma_ast::Decl::Fn(d) => { 45 | result.exports.push(d.ident.sym.to_string()); 46 | } 47 | swc_ecma_ast::Decl::Var(d) => { 48 | for d in &d.decls { 49 | for id in find_pat_ids::<_, Atom>(&d.name) { 50 | result.exports.push(id.to_string()); 51 | } 52 | } 53 | } 54 | swc_ecma_ast::Decl::TsEnum(d) => { 55 | result.exports.push(d.id.sym.to_string()) 56 | } 57 | swc_ecma_ast::Decl::TsModule(ts_module_decl) => { 58 | match &ts_module_decl.id { 59 | swc_ecma_ast::TsModuleName::Ident(ident) => { 60 | result.exports.push(ident.sym.to_string()) 61 | } 62 | swc_ecma_ast::TsModuleName::Str(_) => { 63 | // ignore 64 | } 65 | } 66 | } 67 | swc_ecma_ast::Decl::Using(d) => { 68 | for d in &d.decls { 69 | for id in find_pat_ids::<_, Atom>(&d.name) { 70 | result.exports.push(id.to_string()); 71 | } 72 | } 73 | } 74 | swc_ecma_ast::Decl::TsInterface(_) 75 | | swc_ecma_ast::Decl::TsTypeAlias(_) => { 76 | // ignore types 77 | } 78 | } 79 | } 80 | ModuleDecl::ExportNamed(n) => { 81 | for s in &n.specifiers { 82 | match s { 83 | ExportSpecifier::Namespace(s) => { 84 | result.exports.push(s.name.atom().to_string()); 85 | } 86 | ExportSpecifier::Default(_) => { 87 | result.exports.push("default".to_string()); 88 | } 89 | ExportSpecifier::Named(n) => { 90 | result.exports.push( 91 | n.exported 92 | .as_ref() 93 | .map(|e| e.atom().to_string()) 94 | .unwrap_or_else(|| n.orig.atom().to_string()), 95 | ); 96 | } 97 | } 98 | } 99 | } 100 | ModuleDecl::ExportDefaultExpr(_) 101 | | ModuleDecl::ExportDefaultDecl(_) => { 102 | result.exports.push("default".to_string()); 103 | } 104 | ModuleDecl::TsImportEquals(_) 105 | | ModuleDecl::TsExportAssignment(_) => { 106 | // ignore because it's cjs 107 | } 108 | ModuleDecl::TsNamespaceExport(_) => { 109 | // ignore `export as namespace x;` as it's type only 110 | } 111 | }, 112 | ModuleItem::Stmt(_) => {} 113 | } 114 | } 115 | } 116 | result 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod test { 122 | use std::cell::RefCell; 123 | 124 | use deno_media_type::MediaType; 125 | 126 | use crate::ModuleSpecifier; 127 | use crate::ParseParams; 128 | use crate::parse_module; 129 | 130 | use super::ModuleExportsAndReExports; 131 | 132 | struct Tester { 133 | analysis: RefCell, 134 | } 135 | 136 | impl Tester { 137 | pub fn assert_exports(&self, values: Vec<&str>) { 138 | let mut analysis = self.analysis.borrow_mut(); 139 | assert_eq!(analysis.exports, values); 140 | analysis.exports.clear(); 141 | } 142 | 143 | pub fn assert_reexports(&self, values: Vec<&str>) { 144 | let mut analysis = self.analysis.borrow_mut(); 145 | assert_eq!(analysis.reexports, values); 146 | analysis.reexports.clear(); 147 | } 148 | 149 | pub fn assert_empty(&self) { 150 | let analysis = self.analysis.borrow(); 151 | if !analysis.exports.is_empty() { 152 | panic!("Had exports: {}", analysis.exports.join(", ")) 153 | } 154 | if !analysis.reexports.is_empty() { 155 | panic!("Had reexports: {}", analysis.reexports.join(", ")) 156 | } 157 | } 158 | } 159 | 160 | impl Drop for Tester { 161 | fn drop(&mut self) { 162 | // ensures that all values have been asserted for 163 | if !std::thread::panicking() { 164 | self.assert_empty(); 165 | } 166 | } 167 | } 168 | 169 | fn parse(source: &str) -> Tester { 170 | let parsed_source = parse_module(ParseParams { 171 | specifier: ModuleSpecifier::parse("file:///example.ts").unwrap(), 172 | text: source.into(), 173 | media_type: MediaType::TypeScript, 174 | capture_tokens: true, 175 | scope_analysis: false, 176 | maybe_syntax: None, 177 | }) 178 | .unwrap(); 179 | let analysis = parsed_source.analyze_es_runtime_exports(); 180 | Tester { 181 | analysis: RefCell::new(analysis), 182 | } 183 | } 184 | 185 | #[test] 186 | fn runtime_exports_basic() { 187 | let tester = parse( 188 | " 189 | export class A {} 190 | export enum B {} 191 | export module C.Test {} 192 | export namespace C2.Test {} 193 | export function d() {} 194 | export const e = 1, f = 2; 195 | export { g, h1 as h, other as 'testing-this' }; 196 | export * as y from './other.js'; 197 | export { z } from './other.js'; 198 | class Ignored1 {} 199 | enum Ignored2 {} 200 | module Ignored3 {} 201 | namespace Ignored4 {} 202 | function Ignored5() {} 203 | const Ignored6 = 1; 204 | ", 205 | ); 206 | tester.assert_exports(vec![ 207 | "A", 208 | "B", 209 | "C", 210 | "C2", 211 | "d", 212 | "e", 213 | "f", 214 | "g", 215 | "h", 216 | "testing-this", 217 | "y", 218 | "z", 219 | ]); 220 | } 221 | 222 | #[test] 223 | fn runtime_exports_default_expr() { 224 | let tester = parse("export default 5;"); 225 | tester.assert_exports(vec!["default"]); 226 | } 227 | 228 | #[test] 229 | fn runtime_exports_default_decl() { 230 | let tester = parse("export default class MyClass {}"); 231 | tester.assert_exports(vec!["default"]); 232 | } 233 | 234 | #[test] 235 | fn runtime_exports_default_named_export() { 236 | let tester = parse("export { a as default }"); 237 | tester.assert_exports(vec!["default"]); 238 | } 239 | 240 | #[test] 241 | fn runtime_re_export() { 242 | let tester = parse("export * from './other.js';"); 243 | tester.assert_reexports(vec!["./other.js"]); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/lexing.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::rc::Rc; 4 | 5 | use crate::ES_VERSION; 6 | use crate::MediaType; 7 | use crate::SourceRangedForSpanned; 8 | use crate::StartSourcePos; 9 | use crate::get_syntax; 10 | use crate::swc::atoms::Atom; 11 | use crate::swc::common::comments::Comment; 12 | use crate::swc::common::comments::CommentKind; 13 | use crate::swc::common::comments::SingleThreadedComments; 14 | use crate::swc::common::input::StringInput; 15 | use crate::swc::lexer::Lexer; 16 | use crate::swc::lexer::token::Token; 17 | 18 | #[derive(Debug, Clone)] 19 | pub enum TokenOrComment { 20 | Token(Token), 21 | Comment { kind: CommentKind, text: Atom }, 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct LexedItem { 26 | /// Range of the token or comment. 27 | pub range: std::ops::Range, 28 | /// Token or comment. 29 | pub inner: TokenOrComment, 30 | } 31 | 32 | /// Given the source text and media type, tokenizes the provided 33 | /// text to a collection of tokens and comments. 34 | pub fn lex(source: &str, media_type: MediaType) -> Vec { 35 | let comments = SingleThreadedComments::default(); 36 | let start_pos = StartSourcePos::START_SOURCE_POS; 37 | let lexer = Lexer::new( 38 | get_syntax(media_type), 39 | ES_VERSION, 40 | StringInput::new( 41 | source, 42 | start_pos.as_byte_pos(), 43 | (start_pos + source.len()).as_byte_pos(), 44 | ), 45 | Some(&comments), 46 | ); 47 | 48 | let mut tokens: Vec = lexer 49 | .map(|token| LexedItem { 50 | range: token.range().as_byte_range(start_pos), 51 | inner: TokenOrComment::Token(token.token), 52 | }) 53 | .collect(); 54 | 55 | tokens.extend(flatten_comments(comments).map(|comment| LexedItem { 56 | range: comment.range().as_byte_range(start_pos), 57 | inner: TokenOrComment::Comment { 58 | kind: comment.kind, 59 | text: comment.text, 60 | }, 61 | })); 62 | 63 | tokens.sort_by_key(|item| item.range.start); 64 | 65 | tokens 66 | } 67 | 68 | fn flatten_comments( 69 | comments: SingleThreadedComments, 70 | ) -> impl Iterator { 71 | let (leading, trailing) = comments.take_all(); 72 | let leading = Rc::try_unwrap(leading).unwrap().into_inner(); 73 | let trailing = Rc::try_unwrap(trailing).unwrap().into_inner(); 74 | let mut comments = leading; 75 | comments.extend(trailing); 76 | comments.into_iter().flat_map(|el| el.1) 77 | } 78 | 79 | #[cfg(test)] 80 | mod test { 81 | use super::*; 82 | use crate::MediaType; 83 | 84 | #[test] 85 | fn tokenize_with_comments() { 86 | let items = lex( 87 | "const /* 1 */ t: number /* 2 */ = 5; // 3", 88 | MediaType::TypeScript, 89 | ); 90 | assert_eq!(items.len(), 10); 91 | 92 | // only bother testing a few 93 | assert!(matches!(items[1].inner, TokenOrComment::Comment { .. })); 94 | assert!(matches!( 95 | items[3].inner, 96 | TokenOrComment::Token(Token::Colon) 97 | )); 98 | assert!(matches!(items[9].inner, TokenOrComment::Comment { .. })); 99 | } 100 | 101 | #[test] 102 | fn handle_bom() { 103 | const BOM_CHAR: char = '\u{FEFF}'; 104 | let items = lex(&format!("{}1", BOM_CHAR), MediaType::JavaScript); 105 | assert_eq!(items.len(), 1); 106 | assert_eq!(items[0].range.start, BOM_CHAR.len_utf8()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | #![deny(clippy::disallowed_methods)] 4 | #![deny(clippy::disallowed_types)] 5 | #![deny(clippy::unnecessary_wraps)] 6 | #![deny(clippy::print_stderr)] 7 | #![deny(clippy::print_stdout)] 8 | 9 | /// Version of this crate, which may be useful for emit caches. 10 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 11 | 12 | #[cfg(feature = "cjs")] 13 | mod cjs_parse; 14 | mod comments; 15 | pub mod diagnostics; 16 | #[cfg(feature = "emit")] 17 | mod emit; 18 | #[cfg(feature = "utils")] 19 | mod exports; 20 | mod lexing; 21 | mod parsed_source; 22 | mod parsing; 23 | #[cfg(feature = "scopes")] 24 | mod scopes; 25 | mod source_map; 26 | mod text_changes; 27 | #[cfg(feature = "transpiling")] 28 | mod transpiling; 29 | #[cfg(feature = "type_strip")] 30 | mod type_strip; 31 | mod types; 32 | 33 | #[cfg(feature = "view")] 34 | pub use dprint_swc_ext::view; 35 | 36 | pub use dprint_swc_ext::common::*; 37 | 38 | #[cfg(feature = "cjs")] 39 | pub use cjs_parse::*; 40 | pub use comments::*; 41 | pub use deno_media_type::*; 42 | #[cfg(feature = "emit")] 43 | pub use emit::*; 44 | #[cfg(feature = "utils")] 45 | pub use exports::*; 46 | pub use lexing::*; 47 | pub use parsed_source::*; 48 | pub use parsing::*; 49 | #[cfg(feature = "scopes")] 50 | pub use scopes::*; 51 | pub use source_map::*; 52 | pub use text_changes::*; 53 | #[cfg(feature = "transpiling")] 54 | pub use transpiling::*; 55 | #[cfg(feature = "type_strip")] 56 | pub use type_strip::*; 57 | pub use types::*; 58 | 59 | pub type ModuleSpecifier = url::Url; 60 | 61 | pub mod swc { 62 | pub use dprint_swc_ext::swc::atoms; 63 | pub use dprint_swc_ext::swc::common; 64 | #[cfg(feature = "bundler")] 65 | pub use swc_bundler as bundler; 66 | pub use swc_ecma_ast as ast; 67 | #[cfg(feature = "codegen")] 68 | pub use swc_ecma_codegen as codegen; 69 | pub use swc_ecma_lexer as lexer; 70 | #[cfg(feature = "transforms")] 71 | pub use swc_ecma_loader as loader; 72 | pub use swc_ecma_parser as parser; 73 | #[cfg(feature = "sourcemap")] 74 | pub use swc_sourcemap as sourcemap; 75 | #[cfg(feature = "transforms")] 76 | pub mod transforms { 77 | pub use self::fixer::fixer; 78 | pub use self::hygiene::hygiene; 79 | pub use swc_ecma_transforms_base::assumptions::Assumptions; 80 | pub use swc_ecma_transforms_base::fixer; 81 | pub use swc_ecma_transforms_base::helpers; 82 | pub use swc_ecma_transforms_base::hygiene; 83 | pub use swc_ecma_transforms_base::perf; 84 | pub use swc_ecma_transforms_base::resolver; 85 | 86 | #[cfg(feature = "compat")] 87 | pub use swc_ecma_transforms_compat as compat; 88 | #[cfg(feature = "proposal")] 89 | pub use swc_ecma_transforms_proposal as proposal; 90 | #[cfg(feature = "react")] 91 | pub use swc_ecma_transforms_react as react; 92 | #[cfg(feature = "typescript")] 93 | pub use swc_ecma_transforms_typescript as typescript; 94 | } 95 | #[cfg(feature = "utils")] 96 | pub use swc_ecma_utils as utils; 97 | #[cfg(feature = "visit")] 98 | pub use swc_ecma_visit as ecma_visit; 99 | #[cfg(feature = "visit")] 100 | pub use swc_visit as visit; 101 | } 102 | -------------------------------------------------------------------------------- /src/parsed_source.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::fmt; 4 | use std::sync::Arc; 5 | use std::sync::OnceLock; 6 | 7 | use dprint_swc_ext::common::SourceRange; 8 | use dprint_swc_ext::common::SourceRanged; 9 | use dprint_swc_ext::common::SourceTextInfo; 10 | use dprint_swc_ext::common::SourceTextProvider; 11 | use dprint_swc_ext::common::StartSourcePos; 12 | use swc_common::Mark; 13 | use swc_ecma_ast::ModuleDecl; 14 | use swc_ecma_ast::ModuleItem; 15 | use swc_ecma_ast::Stmt; 16 | 17 | use crate::MediaType; 18 | use crate::ModuleSpecifier; 19 | use crate::ParseDiagnostic; 20 | use crate::SourceRangedForSpanned; 21 | use crate::comments::MultiThreadedComments; 22 | use crate::scope_analysis_transform; 23 | use crate::swc::ast::Module; 24 | use crate::swc::ast::Program; 25 | use crate::swc::ast::Script; 26 | use crate::swc::common::SyntaxContext; 27 | use crate::swc::common::comments::Comment; 28 | use crate::swc::parser::token::TokenAndSpan; 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct Marks { 32 | pub unresolved: Mark, 33 | pub top_level: Mark, 34 | } 35 | 36 | #[derive(Clone)] 37 | pub struct Globals { 38 | marks: Marks, 39 | globals: Arc, 40 | } 41 | 42 | impl Default for Globals { 43 | fn default() -> Self { 44 | let globals = crate::swc::common::Globals::new(); 45 | let marks = crate::swc::common::GLOBALS.set(&globals, || Marks { 46 | unresolved: Mark::new(), 47 | top_level: Mark::fresh(Mark::root()), 48 | }); 49 | Self { 50 | marks, 51 | globals: Arc::new(globals), 52 | } 53 | } 54 | } 55 | 56 | impl Globals { 57 | pub fn with(&self, action: impl FnOnce(&Marks) -> T) -> T { 58 | crate::swc::common::GLOBALS.set(&self.globals, || action(&self.marks)) 59 | } 60 | 61 | pub fn marks(&self) -> &Marks { 62 | &self.marks 63 | } 64 | } 65 | 66 | /// If the module is an Es module or CommonJs module. 67 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 68 | pub enum ModuleKind { 69 | Esm, 70 | Cjs, 71 | } 72 | 73 | impl ModuleKind { 74 | #[inline(always)] 75 | pub fn from_is_cjs(is_cjs: bool) -> Self { 76 | if is_cjs { 77 | ModuleKind::Cjs 78 | } else { 79 | ModuleKind::Esm 80 | } 81 | } 82 | 83 | #[inline(always)] 84 | pub fn from_is_esm(is_esm: bool) -> Self { 85 | ModuleKind::from_is_cjs(!is_esm) 86 | } 87 | 88 | #[inline(always)] 89 | pub fn is_cjs(&self) -> bool { 90 | matches!(self, Self::Cjs) 91 | } 92 | 93 | #[inline(always)] 94 | pub fn is_esm(&self) -> bool { 95 | matches!(self, Self::Esm) 96 | } 97 | } 98 | 99 | /// A reference to a Program. 100 | /// 101 | /// It is generally preferable for functions to accept this over `&Program` 102 | /// because it doesn't require cloning when only owning a `Module` or `Script`. 103 | #[derive(Debug, Clone, Copy)] 104 | pub enum ProgramRef<'a> { 105 | Module(&'a Module), 106 | Script(&'a Script), 107 | } 108 | 109 | impl<'a> ProgramRef<'a> { 110 | /// Computes whether the program is a script. 111 | pub fn compute_is_script(&self) -> bool { 112 | // Necessary because swc will make a program a module when it contains 113 | // typescript specific CJS imports/exports like `import add = require('./add');`. 114 | match self { 115 | ProgramRef::Module(m) => { 116 | for m in m.body.iter() { 117 | match m { 118 | ModuleItem::ModuleDecl(m) => match m { 119 | ModuleDecl::Import(_) 120 | | ModuleDecl::ExportDecl(_) 121 | | ModuleDecl::ExportNamed(_) 122 | | ModuleDecl::ExportDefaultDecl(_) 123 | | ModuleDecl::ExportDefaultExpr(_) 124 | | ModuleDecl::ExportAll(_) => return false, 125 | // the presence of these means it's a script 126 | ModuleDecl::TsImportEquals(_) 127 | | ModuleDecl::TsExportAssignment(_) => { 128 | return true; 129 | } 130 | ModuleDecl::TsNamespaceExport(_) => { 131 | // ignore `export as namespace x;` as it's type only 132 | } 133 | }, 134 | ModuleItem::Stmt(_) => {} 135 | } 136 | } 137 | 138 | false 139 | } 140 | ProgramRef::Script(_) => true, 141 | } 142 | } 143 | 144 | pub fn unwrap_module(&self) -> &Module { 145 | match self { 146 | ProgramRef::Module(m) => m, 147 | ProgramRef::Script(_) => { 148 | panic!("Cannot get a module when the source was a script.") 149 | } 150 | } 151 | } 152 | 153 | pub fn unwrap_script(&self) -> &Script { 154 | match self { 155 | ProgramRef::Module(_) => { 156 | panic!("Cannot get a script when the source was a module.") 157 | } 158 | ProgramRef::Script(s) => s, 159 | } 160 | } 161 | 162 | pub fn shebang(&self) -> Option<&swc_atoms::Atom> { 163 | match self { 164 | ProgramRef::Module(m) => m.shebang.as_ref(), 165 | ProgramRef::Script(s) => s.shebang.as_ref(), 166 | } 167 | } 168 | 169 | pub fn body(&self) -> impl Iterator> { 170 | match self { 171 | ProgramRef::Module(m) => Box::new(m.body.iter().map(|n| n.into())) 172 | as Box>>, 173 | ProgramRef::Script(s) => Box::new(s.body.iter().map(ModuleItemRef::Stmt)), 174 | } 175 | } 176 | 177 | pub fn to_owned(&self) -> Program { 178 | match self { 179 | ProgramRef::Module(m) => Program::Module((*m).clone()), 180 | ProgramRef::Script(s) => Program::Script((*s).clone()), 181 | } 182 | } 183 | } 184 | 185 | impl<'a> From<&'a Program> for ProgramRef<'a> { 186 | fn from(program: &'a Program) -> Self { 187 | match program { 188 | Program::Module(module) => ProgramRef::Module(module), 189 | Program::Script(script) => ProgramRef::Script(script), 190 | } 191 | } 192 | } 193 | 194 | #[cfg(feature = "visit")] 195 | impl swc_ecma_visit::VisitWith for ProgramRef<'_> { 196 | fn visit_with(&self, visitor: &mut T) { 197 | match self { 198 | ProgramRef::Module(n) => n.visit_with(visitor), 199 | ProgramRef::Script(n) => n.visit_with(visitor), 200 | } 201 | } 202 | 203 | fn visit_children_with(&self, visitor: &mut T) { 204 | match self { 205 | ProgramRef::Module(n) => n.visit_children_with(visitor), 206 | ProgramRef::Script(n) => n.visit_children_with(visitor), 207 | } 208 | } 209 | } 210 | 211 | impl swc_common::Spanned for ProgramRef<'_> { 212 | // ok because we're implementing Spanned 213 | #[allow(clippy::disallowed_methods)] 214 | #[allow(clippy::disallowed_types)] 215 | fn span(&self) -> swc_common::Span { 216 | match self { 217 | Self::Module(m) => m.span, 218 | Self::Script(s) => s.span, 219 | } 220 | } 221 | } 222 | 223 | /// Reference to a ModuleDecl or Stmt in a Program. 224 | /// 225 | /// This is used to allow using the same API for the top level 226 | /// statements when working with a ProgramRef. 227 | #[derive(Debug, Clone, Copy)] 228 | pub enum ModuleItemRef<'a> { 229 | ModuleDecl(&'a ModuleDecl), 230 | Stmt(&'a Stmt), 231 | } 232 | 233 | impl swc_common::Spanned for ModuleItemRef<'_> { 234 | // ok because we're implementing Spanned 235 | #[allow(clippy::disallowed_methods)] 236 | #[allow(clippy::disallowed_types)] 237 | fn span(&self) -> swc_common::Span { 238 | match self { 239 | Self::ModuleDecl(n) => n.span(), 240 | Self::Stmt(n) => n.span(), 241 | } 242 | } 243 | } 244 | 245 | impl<'a> From<&'a ModuleItem> for ModuleItemRef<'a> { 246 | fn from(item: &'a ModuleItem) -> Self { 247 | match item { 248 | ModuleItem::ModuleDecl(n) => ModuleItemRef::ModuleDecl(n), 249 | ModuleItem::Stmt(n) => ModuleItemRef::Stmt(n), 250 | } 251 | } 252 | } 253 | 254 | #[derive(Clone)] 255 | pub(crate) struct SyntaxContexts { 256 | pub unresolved: SyntaxContext, 257 | pub top_level: SyntaxContext, 258 | } 259 | 260 | #[derive(Debug, Clone, Default)] 261 | pub(crate) struct ParseDiagnostics { 262 | pub diagnostics: Vec, 263 | /// Diagnostics that should be surfaced when transpiling if the 264 | /// file parsed as a script is discovered to be a module. 265 | pub script_module_diagnostics: Vec, 266 | } 267 | 268 | impl ParseDiagnostics { 269 | #[cfg(feature = "transpiling")] 270 | pub fn for_module_kind<'a>( 271 | &'a self, 272 | module_kind: ModuleKind, 273 | ) -> Box + 'a> { 274 | match module_kind { 275 | ModuleKind::Esm => Box::new( 276 | self 277 | .diagnostics 278 | .iter() 279 | .chain(self.script_module_diagnostics.iter()), 280 | ), 281 | ModuleKind::Cjs => Box::new(self.diagnostics.iter()), 282 | } 283 | } 284 | } 285 | 286 | pub(crate) struct ParsedSourceInner { 287 | pub specifier: ModuleSpecifier, 288 | pub media_type: MediaType, 289 | pub text: Arc, 290 | pub source_text_info: Arc>, 291 | pub comments: MultiThreadedComments, 292 | pub program: Arc, 293 | pub tokens: Option>>, 294 | pub globals: Globals, 295 | pub syntax_contexts: Option, 296 | pub diagnostics: ParseDiagnostics, 297 | } 298 | 299 | /// A parsed source containing an AST, comments, and possibly tokens. 300 | /// 301 | /// Note: This struct is cheap to clone. 302 | #[derive(Clone)] 303 | pub struct ParsedSource(pub(crate) Arc); 304 | 305 | impl ParsedSource { 306 | /// Gets the module specifier of the module. 307 | pub fn specifier(&self) -> &ModuleSpecifier { 308 | &self.0.specifier 309 | } 310 | 311 | /// Gets the media type of the module. 312 | pub fn media_type(&self) -> MediaType { 313 | self.0.media_type 314 | } 315 | 316 | /// Gets the text content of the module. 317 | pub fn text(&self) -> &Arc { 318 | &self.0.text 319 | } 320 | 321 | /// Gets an object with pre-computed positions for lines and indexes of 322 | /// multi-byte chars. 323 | /// 324 | /// Note: Prefer using `.text()` over this if able because this is lazily 325 | /// created. 326 | pub fn text_info_lazy(&self) -> &SourceTextInfo { 327 | self 328 | .0 329 | .source_text_info 330 | .get_or_init(|| SourceTextInfo::new(self.text().clone())) 331 | } 332 | 333 | /// Gets the source range of the parsed source. 334 | pub fn range(&self) -> SourceRange { 335 | SourceRange::new( 336 | StartSourcePos::START_SOURCE_POS, 337 | StartSourcePos::START_SOURCE_POS + self.text().len(), 338 | ) 339 | } 340 | 341 | /// Gets the parsed program. 342 | pub fn program(&self) -> Arc { 343 | self.0.program.clone() 344 | } 345 | 346 | /// Gets the parsed program as a reference. 347 | pub fn program_ref(&self) -> ProgramRef<'_> { 348 | match self.0.program.as_ref() { 349 | Program::Module(module) => ProgramRef::Module(module), 350 | Program::Script(script) => ProgramRef::Script(script), 351 | } 352 | } 353 | 354 | /// Gets the comments found in the source file. 355 | pub fn comments(&self) -> &MultiThreadedComments { 356 | &self.0.comments 357 | } 358 | 359 | /// Wrapper around globals that swc uses for transpiling. 360 | pub fn globals(&self) -> &Globals { 361 | &self.0.globals 362 | } 363 | 364 | /// Get the source's leading comments, where triple slash directives might 365 | /// be located. 366 | pub fn get_leading_comments(&self) -> Option<&Vec> { 367 | let comments = &self.0.comments; 368 | let program = self.program_ref(); 369 | match program.body().next() { 370 | Some(item) => comments.get_leading(item.start()), 371 | None => match program.shebang() { 372 | Some(_) => comments.get_trailing(program.end()), 373 | None => comments.get_leading(program.start()), 374 | }, 375 | } 376 | } 377 | 378 | /// Gets the tokens found in the source file. 379 | /// 380 | /// This will panic if tokens were not captured during parsing. 381 | pub fn tokens(&self) -> &[TokenAndSpan] { 382 | self 383 | .0 384 | .tokens 385 | .as_ref() 386 | .expect("Tokens not found because they were not captured during parsing.") 387 | } 388 | 389 | /// Adds scope analysis to the parsed source if not parsed 390 | /// with scope analysis. 391 | /// 392 | /// Note: This will attempt to not clone the underlying data, but 393 | /// will clone if multiple clones of the `ParsedSource` exist. 394 | pub fn into_with_scope_analysis(self) -> Self { 395 | if self.has_scope_analysis() { 396 | self 397 | } else { 398 | let mut inner = match Arc::try_unwrap(self.0) { 399 | Ok(inner) => inner, 400 | Err(arc_inner) => ParsedSourceInner { 401 | // all of these are/should be cheap to clone 402 | specifier: arc_inner.specifier.clone(), 403 | media_type: arc_inner.media_type, 404 | text: arc_inner.text.clone(), 405 | source_text_info: arc_inner.source_text_info.clone(), 406 | comments: arc_inner.comments.clone(), 407 | program: arc_inner.program.clone(), 408 | tokens: arc_inner.tokens.clone(), 409 | syntax_contexts: arc_inner.syntax_contexts.clone(), 410 | diagnostics: arc_inner.diagnostics.clone(), 411 | globals: arc_inner.globals.clone(), 412 | }, 413 | }; 414 | let program = match Arc::try_unwrap(inner.program) { 415 | Ok(program) => program, 416 | Err(program) => (*program).clone(), 417 | }; 418 | let (program, context) = 419 | scope_analysis_transform(program, &inner.globals); 420 | inner.program = Arc::new(program); 421 | inner.syntax_contexts = context; 422 | ParsedSource(Arc::new(inner)) 423 | } 424 | } 425 | 426 | /// Gets if the source's program has scope information stored 427 | /// in the identifiers. 428 | pub fn has_scope_analysis(&self) -> bool { 429 | self.0.syntax_contexts.is_some() 430 | } 431 | 432 | /// Gets the top level context used when parsing with scope analysis. 433 | /// 434 | /// This will panic if the source was not parsed with scope analysis. 435 | pub fn top_level_context(&self) -> SyntaxContext { 436 | self.syntax_contexts().top_level 437 | } 438 | 439 | /// Gets the unresolved context used when parsing with scope analysis. 440 | /// 441 | /// This will panic if the source was not parsed with scope analysis. 442 | pub fn unresolved_context(&self) -> SyntaxContext { 443 | self.syntax_contexts().unresolved 444 | } 445 | 446 | fn syntax_contexts(&self) -> &SyntaxContexts { 447 | self.0.syntax_contexts.as_ref().expect("Could not get syntax context because the source was not parsed with scope analysis.") 448 | } 449 | 450 | /// Gets extra non-fatal diagnostics found while parsing. 451 | pub fn diagnostics(&self) -> &Vec { 452 | &self.0.diagnostics.diagnostics 453 | } 454 | 455 | /// Diagnostics that should be surfaced when transpiling if the 456 | /// file parsed as a script is discovered to be a module. 457 | pub fn script_module_diagnostics(&self) -> &Vec { 458 | &self.0.diagnostics.script_module_diagnostics 459 | } 460 | 461 | /// Gets if this source is a module. 462 | #[deprecated(note = "use compute_is_script() instead")] 463 | pub fn is_module(&self) -> bool { 464 | matches!(self.program_ref(), ProgramRef::Module(_)) 465 | } 466 | 467 | /// Gets if this source is a script. 468 | #[deprecated(note = "use compute_is_script() instead")] 469 | pub fn is_script(&self) -> bool { 470 | matches!(self.program_ref(), ProgramRef::Script(_)) 471 | } 472 | 473 | /// Computes whether this program should be treated as a script. 474 | pub fn compute_is_script(&self) -> bool { 475 | if self.media_type().is_typed() { 476 | // for typescript, we need to compute whether it's a script 477 | // because swc parses TsImportEquals as a module 478 | self.program_ref().compute_is_script() 479 | } else { 480 | matches!(self.program_ref(), ProgramRef::Script(_)) 481 | } 482 | } 483 | } 484 | 485 | impl<'a> SourceTextProvider<'a> for &'a ParsedSource { 486 | fn text(&self) -> &'a Arc { 487 | ParsedSource::text(self) 488 | } 489 | 490 | fn start_pos(&self) -> StartSourcePos { 491 | StartSourcePos::START_SOURCE_POS 492 | } 493 | } 494 | 495 | impl SourceRanged for ParsedSource { 496 | fn start(&self) -> dprint_swc_ext::common::SourcePos { 497 | StartSourcePos::START_SOURCE_POS.as_source_pos() 498 | } 499 | 500 | fn end(&self) -> dprint_swc_ext::common::SourcePos { 501 | StartSourcePos::START_SOURCE_POS + self.text().len() 502 | } 503 | } 504 | 505 | impl fmt::Debug for ParsedSource { 506 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 507 | f.debug_struct("ParsedModule") 508 | .field("comments", &self.0.comments) 509 | .field("program", &self.0.program) 510 | .finish() 511 | } 512 | } 513 | 514 | #[cfg(feature = "view")] 515 | impl ParsedSource { 516 | /// Gets a dprint-swc-ext view of the module. 517 | /// 518 | /// This provides a closure to examine an "ast view" of the swc AST 519 | /// which has more helper methods and allows for going up the ancestors 520 | /// of a node. 521 | /// 522 | /// Read more: https://github.com/dprint/dprint-swc-ext 523 | pub fn with_view<'a, T>( 524 | &self, 525 | with_view: impl FnOnce(crate::view::Program<'a>) -> T, 526 | ) -> T { 527 | let program_info = crate::view::ProgramInfo { 528 | program: match self.program_ref() { 529 | ProgramRef::Module(module) => crate::view::ProgramRef::Module(module), 530 | ProgramRef::Script(script) => crate::view::ProgramRef::Script(script), 531 | }, 532 | text_info: Some(self.text_info_lazy()), 533 | tokens: self.0.tokens.as_ref().map(|t| t as &[TokenAndSpan]), 534 | comments: Some(crate::view::Comments { 535 | leading: self.comments().leading_map(), 536 | trailing: self.comments().trailing_map(), 537 | }), 538 | }; 539 | 540 | crate::view::with_ast_view(program_info, with_view) 541 | } 542 | } 543 | 544 | #[cfg(test)] 545 | mod test { 546 | use super::*; 547 | use crate::ParseParams; 548 | use crate::parse_program; 549 | 550 | #[cfg(feature = "view")] 551 | #[test] 552 | fn should_parse_program() { 553 | use crate::ModuleSpecifier; 554 | use crate::view::NodeTrait; 555 | 556 | let program = parse_program(ParseParams { 557 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 558 | text: "// 1\n1 + 1\n// 2".into(), 559 | media_type: MediaType::JavaScript, 560 | capture_tokens: true, 561 | maybe_syntax: None, 562 | scope_analysis: false, 563 | }) 564 | .expect("should parse"); 565 | 566 | let result = program.with_view(|program| { 567 | assert_eq!(program.children().len(), 1); 568 | assert_eq!(program.children()[0].text(), "1 + 1"); 569 | 570 | 2 571 | }); 572 | 573 | assert_eq!(result, 2); 574 | } 575 | 576 | #[test] 577 | fn compute_is_script() { 578 | fn get(text: &str, ext: &str) -> bool { 579 | let specifier = 580 | ModuleSpecifier::parse(&format!("file:///my_file.{}", ext)).unwrap(); 581 | let media_type = MediaType::from_specifier(&specifier); 582 | let program = parse_program(ParseParams { 583 | specifier, 584 | text: text.into(), 585 | media_type, 586 | capture_tokens: true, 587 | maybe_syntax: None, 588 | scope_analysis: false, 589 | }) 590 | .unwrap(); 591 | let is_script = program.compute_is_script(); 592 | assert_eq!( 593 | program.program_ref().compute_is_script(), 594 | is_script, 595 | "text: {}", 596 | text 597 | ); 598 | is_script 599 | } 600 | 601 | // false, tla 602 | assert!(!get( 603 | "const mod = await import('./soljson.js');\nconsole.log(mod)", 604 | "js" 605 | )); 606 | assert!(!get( 607 | "const mod = await import('./soljson.js');\nconsole.log(mod)", 608 | "js" 609 | )); 610 | 611 | // false, import 612 | assert!(!get("import './test';", "js")); 613 | assert!(!get("import './test';", "ts")); 614 | 615 | // true, require 616 | assert!(get("require('test')", "js")); 617 | assert!(get("require('test')", "ts")); 618 | 619 | // true, ts import equals 620 | assert!(get("import value = require('test');", "ts")); 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /src/parsing.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::sync::Arc; 4 | 5 | use dprint_swc_ext::common::SourceTextInfo; 6 | use dprint_swc_ext::common::StartSourcePos; 7 | use swc_ecma_lexer::common::parser::Parser as _; 8 | 9 | use crate::Globals; 10 | use crate::MediaType; 11 | use crate::ModuleSpecifier; 12 | use crate::ParseDiagnostic; 13 | use crate::ParseDiagnostics; 14 | use crate::ParsedSource; 15 | use crate::comments::MultiThreadedComments; 16 | use crate::swc::ast::EsVersion; 17 | use crate::swc::ast::Module; 18 | use crate::swc::ast::Program; 19 | use crate::swc::ast::Script; 20 | use crate::swc::common::comments::SingleThreadedComments; 21 | use crate::swc::common::input::StringInput; 22 | use crate::swc::parser::EsSyntax; 23 | use crate::swc::parser::Syntax; 24 | use crate::swc::parser::TsSyntax; 25 | use crate::swc::parser::error::Error as SwcError; 26 | use crate::swc::parser::token::TokenAndSpan; 27 | 28 | /// Ecmascript version used for lexing and parsing. 29 | pub const ES_VERSION: EsVersion = EsVersion::Es2021; 30 | 31 | /// Parameters for parsing. 32 | pub struct ParseParams { 33 | /// Specifier of the source text. 34 | pub specifier: ModuleSpecifier, 35 | /// Source text. 36 | pub text: Arc, 37 | /// Media type of the source text. 38 | pub media_type: MediaType, 39 | /// Whether to capture tokens or not. 40 | pub capture_tokens: bool, 41 | /// Whether to apply swc's scope analysis. 42 | pub scope_analysis: bool, 43 | /// Syntax to use when parsing. 44 | /// 45 | /// `deno_ast` will get a default `Syntax` to use based on the 46 | /// media type, but you may use this to provide a custom `Syntax`. 47 | pub maybe_syntax: Option, 48 | } 49 | 50 | /// Parses the provided information attempting to figure out if the provided 51 | /// text is for a script or a module. 52 | pub fn parse_program( 53 | params: ParseParams, 54 | ) -> Result { 55 | parse(params, ParseMode::Program, |p, _| p) 56 | } 57 | 58 | /// Parses the provided information as a program with the option of providing some 59 | /// post-processing to the result. 60 | /// 61 | /// # Example 62 | /// 63 | /// ``` 64 | /// deno_ast::parse_program_with_post_process( 65 | /// deno_ast::ParseParams { 66 | /// specifier: deno_ast::ModuleSpecifier::parse("file:///my_file.ts").unwrap(), 67 | /// media_type: deno_ast::MediaType::TypeScript, 68 | /// text: "console.log(4);".into(), 69 | /// capture_tokens: true, 70 | /// maybe_syntax: None, 71 | /// scope_analysis: false, 72 | /// }, 73 | /// |program, _globals| { 74 | /// // do something with the program here before it gets stored 75 | /// program 76 | /// }, 77 | /// ); 78 | /// ``` 79 | pub fn parse_program_with_post_process( 80 | params: ParseParams, 81 | post_process: impl FnOnce(Program, &Globals) -> Program, 82 | ) -> Result { 83 | parse(params, ParseMode::Program, post_process) 84 | } 85 | 86 | /// Parses the provided information to a module. 87 | pub fn parse_module( 88 | params: ParseParams, 89 | ) -> Result { 90 | parse(params, ParseMode::Module, |p, _| p) 91 | } 92 | 93 | /// Parses a module with post processing (see docs on `parse_program_with_post_process`). 94 | pub fn parse_module_with_post_process( 95 | params: ParseParams, 96 | post_process: impl FnOnce(Module, &Globals) -> Module, 97 | ) -> Result { 98 | parse( 99 | params, 100 | ParseMode::Module, 101 | |program, globals| match program { 102 | Program::Module(module) => Program::Module(post_process(module, globals)), 103 | Program::Script(_) => unreachable!(), 104 | }, 105 | ) 106 | } 107 | 108 | /// Parses the provided information to a script. 109 | pub fn parse_script( 110 | params: ParseParams, 111 | ) -> Result { 112 | parse(params, ParseMode::Script, |p, _| p) 113 | } 114 | 115 | /// Parses a script with post processing (see docs on `parse_program_with_post_process`). 116 | pub fn parse_script_with_post_process( 117 | params: ParseParams, 118 | post_process: impl FnOnce(Script, &Globals) -> Script, 119 | ) -> Result { 120 | parse( 121 | params, 122 | ParseMode::Script, 123 | |program, globals| match program { 124 | Program::Module(_) => unreachable!(), 125 | Program::Script(script) => Program::Script(post_process(script, globals)), 126 | }, 127 | ) 128 | } 129 | 130 | enum ParseMode { 131 | Program, 132 | Module, 133 | Script, 134 | } 135 | 136 | fn refine_parse_mode( 137 | parse_mode: ParseMode, 138 | media_type: MediaType, 139 | ) -> ParseMode { 140 | match (parse_mode, media_type) { 141 | (ParseMode::Program, MediaType::Cjs) => ParseMode::Script, 142 | // cts files can contain module declarations like 143 | // `import x = require("./x.ts");` or `export = 5;`, so we need to parse 144 | // them as modules (this may change in the future once 145 | // https://github.com/swc-project/swc/issues/9694 is resolved) 146 | (ParseMode::Program, MediaType::Cts) => ParseMode::Module, 147 | (ParseMode::Program, MediaType::Mjs) => ParseMode::Module, 148 | (ParseMode::Program, MediaType::Mts) => ParseMode::Module, 149 | (parse_mode, _) => parse_mode, 150 | } 151 | } 152 | 153 | fn parse( 154 | params: ParseParams, 155 | parse_mode: ParseMode, 156 | post_process: impl FnOnce(Program, &Globals) -> Program, 157 | ) -> Result { 158 | let source = strip_bom_from_arc(params.text, /* panic in debug */ true); 159 | let specifier = params.specifier; 160 | let input = StringInput::new( 161 | source.as_ref(), 162 | StartSourcePos::START_SOURCE_POS.as_byte_pos(), 163 | (StartSourcePos::START_SOURCE_POS + source.len()).as_byte_pos(), 164 | ); 165 | let media_type = params.media_type; 166 | let syntax = params 167 | .maybe_syntax 168 | .unwrap_or_else(|| get_syntax(media_type)); 169 | let parse_mode = refine_parse_mode(parse_mode, media_type); 170 | let (comments, program, tokens, errors) = 171 | parse_string_input(input, syntax, params.capture_tokens, parse_mode) 172 | .map_err(|err| { 173 | let source_text_info = SourceTextInfo::new(source.clone()); 174 | ParseDiagnostic::from_swc_error(err, &specifier, source_text_info) 175 | })?; 176 | let (diagnostics, maybe_text_info) = if errors.is_empty() { 177 | (ParseDiagnostics::default(), None) 178 | } else { 179 | let source_text_info = SourceTextInfo::new(source.clone()); 180 | ( 181 | errors.into_parse_diagnostics(&specifier, &source_text_info), 182 | Some(source_text_info), 183 | ) 184 | }; 185 | let globals = Globals::default(); 186 | let program = post_process(program, &globals); 187 | 188 | let (program, syntax_contexts) = if params.scope_analysis { 189 | scope_analysis_transform(program, &globals) 190 | } else { 191 | (program, None) 192 | }; 193 | 194 | let inner = crate::ParsedSourceInner { 195 | specifier, 196 | media_type, 197 | text: source, 198 | source_text_info: Default::default(), 199 | comments: MultiThreadedComments::from_single_threaded(comments), 200 | program: Arc::new(program), 201 | tokens: tokens.map(Arc::new), 202 | globals, 203 | syntax_contexts, 204 | diagnostics, 205 | }; 206 | 207 | if let Some(text_info) = maybe_text_info { 208 | inner.source_text_info.set(text_info).unwrap(); 209 | } 210 | 211 | Ok(ParsedSource(Arc::new(inner))) 212 | } 213 | 214 | pub(crate) fn scope_analysis_transform( 215 | _program: Program, 216 | _globals: &crate::Globals, 217 | ) -> (Program, Option) { 218 | #[cfg(feature = "transforms")] 219 | { 220 | scope_analysis_transform_inner(_program, _globals) 221 | } 222 | #[cfg(not(feature = "transforms"))] 223 | panic!( 224 | "Cannot parse with scope analysis. Please enable the 'transforms' feature." 225 | ) 226 | } 227 | 228 | #[cfg(feature = "transforms")] 229 | fn scope_analysis_transform_inner( 230 | program: Program, 231 | globals: &crate::Globals, 232 | ) -> (Program, Option) { 233 | use crate::swc::common::SyntaxContext; 234 | use crate::swc::transforms::resolver; 235 | 236 | globals.with(|marks| { 237 | let program = 238 | program.apply(&mut resolver(marks.unresolved, marks.top_level, true)); 239 | 240 | ( 241 | program, 242 | Some(crate::SyntaxContexts { 243 | unresolved: SyntaxContext::empty().apply_mark(marks.unresolved), 244 | top_level: SyntaxContext::empty().apply_mark(marks.top_level), 245 | }), 246 | ) 247 | }) 248 | } 249 | 250 | struct SwcErrors { 251 | errors: Vec, 252 | script_module_errors: Vec, 253 | } 254 | 255 | impl SwcErrors { 256 | pub fn is_empty(&self) -> bool { 257 | self.errors.is_empty() && self.script_module_errors.is_empty() 258 | } 259 | 260 | pub fn into_parse_diagnostics( 261 | self, 262 | specifier: &ModuleSpecifier, 263 | source_text_info: &SourceTextInfo, 264 | ) -> ParseDiagnostics { 265 | let create_diagnostic = |err: SwcError| { 266 | ParseDiagnostic::from_swc_error(err, specifier, source_text_info.clone()) 267 | }; 268 | ParseDiagnostics { 269 | diagnostics: self.errors.into_iter().map(create_diagnostic).collect(), 270 | script_module_diagnostics: self 271 | .script_module_errors 272 | .into_iter() 273 | .map(create_diagnostic) 274 | .collect(), 275 | } 276 | } 277 | } 278 | 279 | #[allow(clippy::type_complexity)] 280 | fn parse_string_input( 281 | input: StringInput, 282 | syntax: Syntax, 283 | capture_tokens: bool, 284 | parse_mode: ParseMode, 285 | ) -> Result< 286 | ( 287 | SingleThreadedComments, 288 | Program, 289 | Option>, 290 | SwcErrors, 291 | ), 292 | SwcError, 293 | > { 294 | let comments = SingleThreadedComments::default(); 295 | 296 | if capture_tokens { 297 | let lexer = 298 | crate::swc::lexer::Lexer::new(syntax, ES_VERSION, input, Some(&comments)); 299 | let lexer = crate::swc::lexer::Capturing::new(lexer); 300 | let mut parser = crate::swc::lexer::Parser::new_from(lexer); 301 | let program = match parse_mode { 302 | ParseMode::Program => parser.parse_program()?, 303 | ParseMode::Module => Program::Module(parser.parse_module()?), 304 | ParseMode::Script => Program::Script(parser.parse_script()?), 305 | }; 306 | let iter = &mut parser.input_mut().iter; 307 | let tokens = crate::swc::lexer::Capturing::take(iter); 308 | let errors = parser.take_errors(); 309 | let script_module_errors = parser.take_script_module_errors(); 310 | 311 | Ok(( 312 | comments, 313 | program, 314 | Some(tokens), 315 | SwcErrors { 316 | errors, 317 | script_module_errors, 318 | }, 319 | )) 320 | } else { 321 | let lexer = crate::swc::parser::Lexer::new( 322 | syntax, 323 | ES_VERSION, 324 | input, 325 | Some(&comments), 326 | ); 327 | let mut parser = crate::swc::parser::Parser::new_from(lexer); 328 | let program = match parse_mode { 329 | ParseMode::Program => parser.parse_program()?, 330 | ParseMode::Module => Program::Module(parser.parse_module()?), 331 | ParseMode::Script => Program::Script(parser.parse_script()?), 332 | }; 333 | let errors = parser.take_errors(); 334 | let script_module_errors = parser.take_script_module_errors(); 335 | 336 | Ok(( 337 | comments, 338 | program, 339 | None, 340 | SwcErrors { 341 | errors, 342 | script_module_errors, 343 | }, 344 | )) 345 | } 346 | } 347 | 348 | /// Gets the default `Syntax` used by `deno_ast` for the provided media type. 349 | pub fn get_syntax(media_type: MediaType) -> Syntax { 350 | match media_type { 351 | MediaType::TypeScript 352 | | MediaType::Mts 353 | | MediaType::Cts 354 | | MediaType::Dts 355 | | MediaType::Dmts 356 | | MediaType::Dcts 357 | | MediaType::Tsx => { 358 | Syntax::Typescript(TsSyntax { 359 | decorators: true, 360 | // should be true for mts and cts: 361 | // https://babeljs.io/docs/babel-preset-typescript#disallowambiguousjsxlike 362 | disallow_ambiguous_jsx_like: matches!( 363 | media_type, 364 | MediaType::Mts | MediaType::Cts 365 | ), 366 | dts: matches!( 367 | media_type, 368 | MediaType::Dts | MediaType::Dmts | MediaType::Dcts 369 | ), 370 | tsx: media_type == MediaType::Tsx, 371 | no_early_errors: false, 372 | }) 373 | } 374 | MediaType::JavaScript 375 | | MediaType::Mjs 376 | | MediaType::Cjs 377 | | MediaType::Jsx 378 | | MediaType::Json 379 | | MediaType::Jsonc 380 | | MediaType::Json5 381 | | MediaType::Wasm 382 | | MediaType::SourceMap 383 | | MediaType::Css 384 | | MediaType::Sql 385 | | MediaType::Html 386 | | MediaType::Unknown => Syntax::Es(EsSyntax { 387 | allow_return_outside_function: true, 388 | allow_super_outside_method: true, 389 | auto_accessors: true, 390 | decorators: true, 391 | decorators_before_export: false, 392 | export_default_from: true, 393 | fn_bind: false, 394 | import_attributes: true, 395 | jsx: media_type == MediaType::Jsx, 396 | explicit_resource_management: true, 397 | }), 398 | } 399 | } 400 | 401 | pub fn strip_bom(mut s: String) -> String { 402 | if s.starts_with('\u{FEFF}') { 403 | s.drain(..3); 404 | } 405 | s 406 | } 407 | 408 | fn strip_bom_from_arc(s: Arc, should_panic_in_debug: bool) -> Arc { 409 | if let Some(stripped_text) = s.strip_prefix('\u{FEFF}') { 410 | // this is only a perf concern, so don't crash in release 411 | if cfg!(debug_assertions) && should_panic_in_debug { 412 | panic!( 413 | "BOM should be stripped from text before providing it to deno_ast to avoid a file text allocation" 414 | ); 415 | } 416 | stripped_text.into() 417 | } else { 418 | s 419 | } 420 | } 421 | 422 | #[cfg(test)] 423 | mod test { 424 | use pretty_assertions::assert_eq; 425 | 426 | use crate::LineAndColumnDisplay; 427 | use crate::diagnostics::Diagnostic; 428 | 429 | use super::*; 430 | 431 | #[test] 432 | fn should_parse_program() { 433 | let program = parse_program(ParseParams { 434 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 435 | text: "// 1\n1 + 1\n// 2".into(), 436 | media_type: MediaType::JavaScript, 437 | capture_tokens: true, 438 | maybe_syntax: None, 439 | scope_analysis: false, 440 | }) 441 | .unwrap(); 442 | assert_eq!(program.specifier().as_str(), "file:///my_file.js"); 443 | assert_eq!(program.text().as_ref(), "// 1\n1 + 1\n// 2"); 444 | assert_eq!(program.media_type(), MediaType::JavaScript); 445 | assert!(matches!( 446 | program.program_ref().unwrap_script().body[0], 447 | crate::swc::ast::Stmt::Expr(..) 448 | )); 449 | assert_eq!(program.get_leading_comments().unwrap().len(), 1); 450 | assert_eq!(program.get_leading_comments().unwrap()[0].text, " 1"); 451 | assert_eq!(program.tokens().len(), 3); 452 | assert_eq!(program.comments().get_vec().len(), 2); 453 | } 454 | 455 | #[test] 456 | fn should_get_leading_comments_after_hashbang() { 457 | let program = parse_program(ParseParams { 458 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 459 | text: "#!/bin/sh deno\n// 1\n".into(), 460 | media_type: MediaType::JavaScript, 461 | capture_tokens: true, 462 | maybe_syntax: None, 463 | scope_analysis: false, 464 | }) 465 | .unwrap(); 466 | assert_eq!(program.get_leading_comments().unwrap().len(), 1); 467 | assert_eq!(program.get_leading_comments().unwrap()[0].text, " 1"); 468 | } 469 | 470 | #[test] 471 | fn should_parse_module() { 472 | let program = parse_module(ParseParams { 473 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 474 | text: "// 1\n1 + 1\n// 2".into(), 475 | media_type: MediaType::JavaScript, 476 | capture_tokens: true, 477 | maybe_syntax: None, 478 | scope_analysis: false, 479 | }) 480 | .unwrap(); 481 | assert!(matches!( 482 | program.program_ref().unwrap_module().body[0], 483 | crate::swc::ast::ModuleItem::Stmt(..) 484 | )); 485 | } 486 | 487 | #[cfg(feature = "view")] 488 | #[test] 489 | fn should_parse_brand_checks_in_js() { 490 | use crate::view::ClassDecl; 491 | use crate::view::ClassMethod; 492 | use crate::view::NodeTrait; 493 | 494 | let program = parse_module(ParseParams { 495 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 496 | text: "class T { method() { #test in this; } }".into(), 497 | media_type: MediaType::JavaScript, 498 | capture_tokens: true, 499 | maybe_syntax: None, 500 | scope_analysis: false, 501 | }) 502 | .unwrap(); 503 | 504 | program.with_view(|program| { 505 | let class_decl = program.children()[0].expect::(); 506 | let class_method = class_decl.class.body[0].expect::(); 507 | let method_stmt = class_method.function.body.unwrap().stmts[0]; 508 | assert_eq!(method_stmt.text(), "#test in this;"); 509 | }); 510 | } 511 | 512 | #[test] 513 | #[should_panic( 514 | expected = "Tokens not found because they were not captured during parsing." 515 | )] 516 | fn should_panic_when_getting_tokens_and_tokens_not_captured() { 517 | let program = parse_module(ParseParams { 518 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 519 | text: "// 1\n1 + 1\n// 2".into(), 520 | media_type: MediaType::JavaScript, 521 | capture_tokens: false, 522 | maybe_syntax: None, 523 | scope_analysis: false, 524 | }) 525 | .unwrap(); 526 | program.tokens(); 527 | } 528 | 529 | #[test] 530 | fn should_handle_parse_error() { 531 | let diagnostic = parse_module(ParseParams { 532 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 533 | text: "t u".into(), 534 | media_type: MediaType::JavaScript, 535 | capture_tokens: true, 536 | maybe_syntax: None, 537 | scope_analysis: false, 538 | }) 539 | .err() 540 | .unwrap(); 541 | assert_eq!(diagnostic.specifier().as_str(), "file:///my_file.js"); 542 | assert_eq!( 543 | diagnostic.display_position(), 544 | LineAndColumnDisplay { 545 | line_number: 1, 546 | column_number: 3, 547 | } 548 | ); 549 | assert_eq!( 550 | diagnostic.message().to_string(), 551 | "Expected ';', '}' or " 552 | ); 553 | } 554 | 555 | #[test] 556 | fn test_err_trailing_blank_line() { 557 | let diagnostic = parse_ts_module("setTimeout(() => {}),\n").err().unwrap(); 558 | assert_eq!( 559 | diagnostic.to_string(), 560 | // should contain some context by including the previous line 561 | // instead of just a blank line 562 | [ 563 | "Expression expected at file:///my_file.ts:1:22", 564 | "", 565 | " setTimeout(() => {}),", 566 | " ~", 567 | ] 568 | .join("\n") 569 | ); 570 | } 571 | 572 | #[test] 573 | fn test_err_many_trailing_blank_lines() { 574 | let diagnostic = parse_ts_module("setTimeout(() => {}),\n\n\n\n\n\n\n\n") 575 | .err() 576 | .unwrap(); 577 | assert_eq!( 578 | diagnostic.to_string(), 579 | // should contain some context by including the expression 580 | [ 581 | "Expression expected at file:///my_file.ts:1:22", 582 | "", 583 | " setTimeout(() => {}),", 584 | " ~", 585 | ] 586 | .join("\n") 587 | ); 588 | } 589 | 590 | #[test] 591 | fn test_parse_export_equals() { 592 | assert!( 593 | parse_program_with_media_type("export = 5;", MediaType::Cts).is_ok() 594 | ); 595 | } 596 | 597 | #[test] 598 | #[should_panic( 599 | expected = "Could not get syntax context because the source was not parsed with scope analysis." 600 | )] 601 | fn should_panic_when_getting_top_level_context_and_scope_analysis_false() { 602 | get_scope_analysis_false_parsed_source().top_level_context(); 603 | } 604 | 605 | #[test] 606 | #[should_panic( 607 | expected = "Could not get syntax context because the source was not parsed with scope analysis." 608 | )] 609 | fn should_panic_when_getting_unresolved_context_and_scope_analysis_false() { 610 | get_scope_analysis_false_parsed_source().unresolved_context(); 611 | } 612 | 613 | fn get_scope_analysis_false_parsed_source() -> ParsedSource { 614 | parse_module(ParseParams { 615 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 616 | text: "// 1\n1 + 1\n// 2".into(), 617 | media_type: MediaType::JavaScript, 618 | capture_tokens: false, 619 | maybe_syntax: None, 620 | scope_analysis: false, 621 | }) 622 | .unwrap() 623 | } 624 | 625 | #[cfg(all(feature = "view", feature = "transforms"))] 626 | #[test] 627 | fn should_do_scope_analysis() { 628 | let parsed_source = parse_module(ParseParams { 629 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 630 | text: "export function test() { const test = 2; test; } test()".into(), 631 | media_type: MediaType::JavaScript, 632 | capture_tokens: true, 633 | maybe_syntax: None, 634 | scope_analysis: true, 635 | }) 636 | .unwrap(); 637 | 638 | parsed_source.with_view(|view| { 639 | use crate::view::*; 640 | 641 | let func_decl = view.children()[0] 642 | .expect::() 643 | .decl 644 | .expect::(); 645 | let func_decl_inner_expr = func_decl.function.body.unwrap().stmts[1] 646 | .expect::() 647 | .expr 648 | .expect::(); 649 | let call_expr = view.children()[1] 650 | .expect::() 651 | .expr 652 | .expect::(); 653 | let call_expr_id = call_expr.callee.expect::(); 654 | 655 | // these should be the same identifier 656 | assert_eq!(func_decl.ident.to_id(), call_expr_id.to_id()); 657 | // but these shouldn't be 658 | assert_ne!(func_decl.ident.to_id(), func_decl_inner_expr.to_id()); 659 | }); 660 | } 661 | 662 | #[cfg(all(feature = "view", feature = "transforms"))] 663 | #[test] 664 | fn should_allow_scope_analysis_after_the_fact() { 665 | let parsed_source = parse_module(ParseParams { 666 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 667 | text: "export function test() { const test = 2; test; } test()".into(), 668 | media_type: MediaType::JavaScript, 669 | capture_tokens: true, 670 | maybe_syntax: None, 671 | scope_analysis: false, 672 | }) 673 | .unwrap(); 674 | 675 | parsed_source.with_view(|view| { 676 | use crate::view::*; 677 | let func_decl = view.children()[0] 678 | .expect::() 679 | .decl 680 | .expect::(); 681 | let func_decl_inner_expr = func_decl.function.body.unwrap().stmts[1] 682 | .expect::() 683 | .expr 684 | .expect::(); 685 | // these will be equal because scope analysis hasn't been done 686 | assert_eq!(func_decl.ident.to_id(), func_decl_inner_expr.to_id()); 687 | }); 688 | 689 | // now do scope analysis 690 | let parsed_source = parsed_source.into_with_scope_analysis(); 691 | 692 | parsed_source.with_view(|view| { 693 | use crate::view::*; 694 | let func_decl = view.children()[0] 695 | .expect::() 696 | .decl 697 | .expect::(); 698 | let func_decl_inner_expr = func_decl.function.body.unwrap().stmts[1] 699 | .expect::() 700 | .expr 701 | .expect::(); 702 | // now they'll be not equal because scope analysis has occurred 703 | assert_ne!(func_decl.ident.to_id(), func_decl_inner_expr.to_id()); 704 | }); 705 | } 706 | 707 | #[cfg(all(feature = "view", feature = "transforms"))] 708 | #[test] 709 | fn should_scope_analyze_typescript() { 710 | let parsed_source = parse_module(ParseParams { 711 | specifier: ModuleSpecifier::parse("file:///my_file.ts").unwrap(), 712 | text: r#"import type { Foo } from "./foo.ts"; 713 | function _bar(...Foo: Foo) { 714 | console.log(Foo); 715 | }"# 716 | .into(), 717 | media_type: MediaType::TypeScript, 718 | capture_tokens: true, 719 | maybe_syntax: None, 720 | scope_analysis: true, 721 | }) 722 | .unwrap(); 723 | 724 | parsed_source.with_view(|view| { 725 | use crate::view::*; 726 | 727 | let named_import_ident = 728 | view.children()[0].expect::().specifiers[0] 729 | .expect::() 730 | .local; 731 | let bar_func = view.children()[1].expect::(); 732 | let bar_param_rest_pat = 733 | bar_func.function.params[0].pat.expect::(); 734 | let bar_param_ident = bar_param_rest_pat.arg.expect::().id; 735 | let bar_param_type_ident = bar_param_rest_pat 736 | .type_ann 737 | .unwrap() 738 | .type_ann 739 | .expect::() 740 | .type_name 741 | .expect::(); 742 | let console_log_arg_ident = bar_func.function.body.unwrap().stmts[0] 743 | .expect::() 744 | .expr 745 | .expect::() 746 | .args[0] 747 | .expr 748 | .expect::(); 749 | 750 | assert_eq!(console_log_arg_ident.to_id(), bar_param_ident.to_id()); 751 | assert_ne!(console_log_arg_ident.to_id(), named_import_ident.to_id()); 752 | assert_ne!(console_log_arg_ident.to_id(), bar_param_type_ident.to_id()); 753 | 754 | assert_eq!(named_import_ident.to_id(), bar_param_type_ident.to_id()); 755 | assert_ne!(named_import_ident.to_id(), bar_param_ident.to_id()); 756 | }); 757 | } 758 | 759 | #[test] 760 | fn should_error_on_syntax_diagnostic() { 761 | let diagnostic = parse_ts_module("test;\nas#;").err().unwrap(); 762 | assert_eq!( 763 | diagnostic.message().to_string(), 764 | "Expected ';', '}' or " 765 | ); 766 | } 767 | 768 | #[test] 769 | fn should_error_without_issue_when_there_exists_multi_byte_char_on_line_with_syntax_error() 770 | { 771 | let diagnostic = parse_ts_module(concat!( 772 | "test;\n", 773 | r#"console.log("x", `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`),;"#, 774 | )).err().unwrap(); 775 | assert_eq!(diagnostic.message().to_string(), "Expression expected",); 776 | } 777 | 778 | #[test] 779 | fn should_diagnostic_for_no_equals_sign_in_var_decl() { 780 | let diagnostic = 781 | parse_for_diagnostic("const Methods {\nf: (x, y) => x + y,\n};"); 782 | assert_eq!( 783 | diagnostic.message().to_string(), 784 | "'const' declarations must be initialized" 785 | ); 786 | } 787 | 788 | #[test] 789 | fn should_diganotic_when_var_stmts_sep_by_comma() { 790 | let diagnostic = parse_ts_module("let a = 0, let b = 1;").err().unwrap(); 791 | assert_eq!( 792 | diagnostic.message().to_string(), 793 | "Unexpected token `let`. Expected let is reserved in const, let, class declaration" 794 | ); 795 | } 796 | 797 | #[test] 798 | fn should_diagnostic_for_exected_expr_type_alias() { 799 | let diagnostic = 800 | parse_for_diagnostic("type T =\n | unknown\n { } & unknown;"); 801 | assert_eq!(diagnostic.message().to_string(), "Expression expected"); 802 | } 803 | 804 | #[test] 805 | fn should_diganotic_missing_init_in_using() { 806 | let diagnostic = parse_for_diagnostic("using test"); 807 | assert_eq!( 808 | diagnostic.message().to_string(), 809 | "Using declaration requires initializer" 810 | ); 811 | } 812 | 813 | #[test] 814 | fn test_strip_bom() { 815 | let text = "\u{FEFF}test"; 816 | assert_eq!(strip_bom(text.to_string()), "test"); 817 | let text = "test"; 818 | assert_eq!(strip_bom(text.to_string()), "test"); 819 | let text = ""; 820 | assert_eq!(strip_bom(text.to_string()), ""); 821 | } 822 | 823 | #[test] 824 | fn test_strip_bom_arc() { 825 | let text = "\u{FEFF}test"; 826 | assert_eq!(strip_bom_from_arc(text.into(), false), "test".into()); 827 | let text = "test"; 828 | assert_eq!(strip_bom_from_arc(text.into(), false), "test".into()); 829 | let text = ""; 830 | assert_eq!(strip_bom_from_arc(text.into(), false), "".into()); 831 | } 832 | 833 | fn parse_for_diagnostic(text: &str) -> ParseDiagnostic { 834 | let result = parse_ts_module(text).unwrap(); 835 | result.diagnostics().first().unwrap().to_owned() 836 | } 837 | 838 | fn parse_ts_module(text: &str) -> Result { 839 | parse_module(ParseParams { 840 | specifier: ModuleSpecifier::parse("file:///my_file.ts").unwrap(), 841 | text: text.to_string().into(), 842 | media_type: MediaType::TypeScript, 843 | capture_tokens: false, 844 | maybe_syntax: None, 845 | scope_analysis: false, 846 | }) 847 | } 848 | 849 | fn parse_program_with_media_type( 850 | text: &str, 851 | media_type: MediaType, 852 | ) -> Result { 853 | parse_program(ParseParams { 854 | specifier: ModuleSpecifier::parse("file:///my_file.ts").unwrap(), 855 | text: text.to_string().into(), 856 | media_type, 857 | capture_tokens: false, 858 | maybe_syntax: None, 859 | scope_analysis: false, 860 | }) 861 | } 862 | } 863 | -------------------------------------------------------------------------------- /src/scopes.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | use crate::swc::ast::Id; 4 | use crate::swc::ast::{ 5 | ArrowExpr, BlockStmt, BlockStmtOrExpr, CatchClause, ClassDecl, ClassExpr, 6 | DoWhileStmt, Expr, FnDecl, FnExpr, ForInStmt, ForOfStmt, ForStmt, Function, 7 | Ident, ImportDefaultSpecifier, ImportNamedSpecifier, ImportStarAsSpecifier, 8 | Param, Pat, SwitchStmt, TsInterfaceDecl, TsTypeAliasDecl, VarDecl, 9 | VarDeclKind, WhileStmt, WithStmt, 10 | }; 11 | use crate::swc::atoms::Atom; 12 | use crate::swc::ecma_visit::Visit; 13 | use crate::swc::ecma_visit::VisitWith; 14 | use crate::swc::utils::find_pat_ids; 15 | use crate::view; 16 | use std::collections::HashMap; 17 | 18 | #[derive(Debug)] 19 | pub struct Scope { 20 | vars: HashMap, 21 | symbols: HashMap>, 22 | } 23 | 24 | impl Scope { 25 | pub fn analyze(program: view::Program) -> Self { 26 | let mut scope = Self { 27 | vars: Default::default(), 28 | symbols: Default::default(), 29 | }; 30 | let mut path = vec![]; 31 | 32 | match program { 33 | view::Program::Module(module) => { 34 | module.inner.visit_with(&mut Analyzer { 35 | scope: &mut scope, 36 | path: &mut path, 37 | }); 38 | } 39 | view::Program::Script(script) => { 40 | script.inner.visit_with(&mut Analyzer { 41 | scope: &mut scope, 42 | path: &mut path, 43 | }); 44 | } 45 | }; 46 | 47 | scope 48 | } 49 | 50 | // Get all declarations with a symbol. 51 | pub fn ids_with_symbol(&self, sym: &Atom) -> Option<&Vec> { 52 | self.symbols.get(sym) 53 | } 54 | 55 | pub fn var(&self, id: &Id) -> Option<&Var> { 56 | self.vars.get(id) 57 | } 58 | 59 | pub fn var_by_ident(&self, ident: &view::Ident) -> Option<&Var> { 60 | self.var(&ident.inner.to_id()) 61 | } 62 | 63 | pub fn is_global(&self, id: &Id) -> bool { 64 | self.var(id).is_none() 65 | } 66 | } 67 | 68 | #[derive(Debug)] 69 | pub struct Var { 70 | path: Vec, 71 | kind: BindingKind, 72 | } 73 | 74 | impl Var { 75 | /// Empty path means root scope. 76 | #[allow(dead_code)] 77 | pub fn path(&self) -> &[ScopeKind] { 78 | &self.path 79 | } 80 | 81 | pub fn kind(&self) -> BindingKind { 82 | self.kind 83 | } 84 | } 85 | 86 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 87 | pub enum BindingKind { 88 | Var, 89 | Const, 90 | Let, 91 | Function, 92 | Param, 93 | Class, 94 | CatchClause, 95 | 96 | /// This means that the binding comes from `ImportStarAsSpecifier`, like 97 | /// `import * as foo from "foo.ts";` 98 | /// `foo` effectively represents a namespace. 99 | NamespaceImport, 100 | 101 | /// Represents `ImportDefaultSpecifier` or `ImportNamedSpecifier`. 102 | /// e.g. 103 | /// - import foo from "foo.ts"; 104 | /// - import { foo } from "foo.ts"; 105 | ValueImport, 106 | 107 | Type, 108 | } 109 | 110 | impl BindingKind { 111 | pub fn is_import(&self) -> bool { 112 | matches!( 113 | *self, 114 | BindingKind::ValueImport | BindingKind::NamespaceImport 115 | ) 116 | } 117 | } 118 | 119 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 120 | pub enum ScopeKind { 121 | // Module, 122 | Arrow, 123 | Function, 124 | Block, 125 | Loop, 126 | Class, 127 | Switch, 128 | With, 129 | Catch, 130 | } 131 | 132 | struct Analyzer<'a> { 133 | scope: &'a mut Scope, 134 | path: &'a mut Vec, 135 | } 136 | 137 | impl Analyzer<'_> { 138 | fn declare_id(&mut self, kind: BindingKind, i: Id) { 139 | self.scope.vars.insert( 140 | i.clone(), 141 | Var { 142 | kind, 143 | path: self.path.clone(), 144 | }, 145 | ); 146 | self.scope.symbols.entry(i.0.clone()).or_default().push(i); 147 | } 148 | 149 | fn declare(&mut self, kind: BindingKind, i: &Ident) { 150 | self.declare_id(kind, i.to_id()); 151 | } 152 | 153 | fn declare_pat(&mut self, kind: BindingKind, pat: &Pat) { 154 | let ids: Vec = find_pat_ids(pat); 155 | 156 | for id in ids { 157 | self.declare_id(kind, id); 158 | } 159 | } 160 | 161 | fn visit_with_path(&mut self, kind: ScopeKind, node: &T) 162 | where 163 | T: 'static + for<'any> VisitWith>, 164 | { 165 | self.path.push(kind); 166 | node.visit_with(self); 167 | self.path.pop(); 168 | } 169 | 170 | fn with(&mut self, kind: ScopeKind, op: F) 171 | where 172 | F: FnOnce(&mut Analyzer), 173 | { 174 | self.path.push(kind); 175 | op(self); 176 | self.path.pop(); 177 | } 178 | } 179 | 180 | impl Visit for Analyzer<'_> { 181 | fn visit_arrow_expr(&mut self, n: &ArrowExpr) { 182 | self.with(ScopeKind::Arrow, |a| { 183 | // Parameters of `ArrowExpr` are of type `Vec`, not `Vec`, 184 | // which means `visit_param` does _not_ handle parameters of `ArrowExpr`. 185 | // We need to handle them manually here. 186 | for param in &n.params { 187 | a.declare_pat(BindingKind::Param, param); 188 | } 189 | n.visit_children_with(a); 190 | }); 191 | } 192 | 193 | /// Overriden not to add ScopeKind::Block 194 | fn visit_block_stmt_or_expr(&mut self, n: &BlockStmtOrExpr) { 195 | match n { 196 | BlockStmtOrExpr::BlockStmt(s) => s.stmts.visit_with(self), 197 | BlockStmtOrExpr::Expr(e) => e.visit_with(self), 198 | } 199 | } 200 | 201 | fn visit_var_decl(&mut self, n: &VarDecl) { 202 | n.decls.iter().for_each(|v| { 203 | v.init.visit_with(self); 204 | 205 | // If the class name and the variable name are the same like `let Foo = class Foo {}`, 206 | // this binding should be treated as `BindingKind::Class`. 207 | if let Some(expr) = &v.init 208 | && let Expr::Class(ClassExpr { 209 | ident: Some(class_name), 210 | .. 211 | }) = &**expr 212 | && let Pat::Ident(var_name) = &v.name 213 | && var_name.id.sym == class_name.sym 214 | { 215 | self.declare(BindingKind::Class, class_name); 216 | return; 217 | } 218 | 219 | self.declare_pat( 220 | match n.kind { 221 | VarDeclKind::Var => BindingKind::Var, 222 | VarDeclKind::Let => BindingKind::Let, 223 | VarDeclKind::Const => BindingKind::Const, 224 | }, 225 | &v.name, 226 | ); 227 | }); 228 | } 229 | 230 | /// Overriden not to add ScopeKind::Block 231 | fn visit_function(&mut self, n: &Function) { 232 | n.decorators.visit_with(self); 233 | n.params.visit_with(self); 234 | 235 | // Don't add ScopeKind::Block 236 | if let Some(body) = &n.body { 237 | body.stmts.visit_with(self); 238 | } 239 | } 240 | 241 | fn visit_fn_decl(&mut self, n: &FnDecl) { 242 | self.declare(BindingKind::Function, &n.ident); 243 | 244 | self.visit_with_path(ScopeKind::Function, &n.function); 245 | } 246 | 247 | fn visit_fn_expr(&mut self, n: &FnExpr) { 248 | if let Some(ident) = &n.ident { 249 | self.declare(BindingKind::Function, ident); 250 | } 251 | 252 | self.visit_with_path(ScopeKind::Function, &n.function); 253 | } 254 | 255 | fn visit_class_decl(&mut self, n: &ClassDecl) { 256 | self.declare(BindingKind::Class, &n.ident); 257 | 258 | self.visit_with_path(ScopeKind::Class, &n.class); 259 | } 260 | 261 | fn visit_class_expr(&mut self, n: &ClassExpr) { 262 | if let Some(class_name) = n.ident.as_ref() { 263 | self.declare(BindingKind::Class, class_name); 264 | } 265 | 266 | self.visit_with_path(ScopeKind::Class, &n.class); 267 | } 268 | 269 | fn visit_block_stmt(&mut self, n: &BlockStmt) { 270 | self.visit_with_path(ScopeKind::Block, &n.stmts) 271 | } 272 | 273 | fn visit_catch_clause(&mut self, n: &CatchClause) { 274 | if let Some(pat) = &n.param { 275 | self.declare_pat(BindingKind::CatchClause, pat); 276 | } 277 | self.visit_with_path(ScopeKind::Catch, &n.body) 278 | } 279 | 280 | fn visit_param(&mut self, n: &Param) { 281 | self.declare_pat(BindingKind::Param, &n.pat); 282 | } 283 | 284 | fn visit_import_named_specifier(&mut self, n: &ImportNamedSpecifier) { 285 | self.declare(BindingKind::ValueImport, &n.local); 286 | } 287 | 288 | fn visit_import_default_specifier(&mut self, n: &ImportDefaultSpecifier) { 289 | self.declare(BindingKind::ValueImport, &n.local); 290 | } 291 | 292 | fn visit_import_star_as_specifier(&mut self, n: &ImportStarAsSpecifier) { 293 | self.declare(BindingKind::NamespaceImport, &n.local); 294 | } 295 | 296 | fn visit_with_stmt(&mut self, n: &WithStmt) { 297 | n.obj.visit_with(self); 298 | self.with(ScopeKind::With, |a| n.body.visit_children_with(a)) 299 | } 300 | 301 | fn visit_for_stmt(&mut self, n: &ForStmt) { 302 | n.init.visit_with(self); 303 | n.update.visit_with(self); 304 | n.test.visit_with(self); 305 | 306 | self.visit_with_path(ScopeKind::Loop, &n.body); 307 | } 308 | 309 | fn visit_for_of_stmt(&mut self, n: &ForOfStmt) { 310 | n.left.visit_with(self); 311 | n.right.visit_with(self); 312 | 313 | self.visit_with_path(ScopeKind::Loop, &n.body); 314 | } 315 | 316 | fn visit_for_in_stmt(&mut self, n: &ForInStmt) { 317 | n.left.visit_with(self); 318 | n.right.visit_with(self); 319 | 320 | self.visit_with_path(ScopeKind::Loop, &n.body); 321 | } 322 | 323 | fn visit_do_while_stmt(&mut self, n: &DoWhileStmt) { 324 | n.test.visit_with(self); 325 | 326 | self.visit_with_path(ScopeKind::Loop, &n.body); 327 | } 328 | 329 | fn visit_while_stmt(&mut self, n: &WhileStmt) { 330 | n.test.visit_with(self); 331 | 332 | self.visit_with_path(ScopeKind::Loop, &n.body); 333 | } 334 | 335 | fn visit_switch_stmt(&mut self, n: &SwitchStmt) { 336 | n.discriminant.visit_with(self); 337 | 338 | self.visit_with_path(ScopeKind::Switch, &n.cases); 339 | } 340 | 341 | fn visit_ts_type_alias_decl(&mut self, n: &TsTypeAliasDecl) { 342 | self.declare(BindingKind::Type, &n.id); 343 | } 344 | 345 | fn visit_ts_interface_decl(&mut self, n: &TsInterfaceDecl) { 346 | self.declare(BindingKind::Type, &n.id); 347 | } 348 | } 349 | 350 | #[cfg(test)] 351 | mod tests { 352 | use super::{BindingKind, Scope, ScopeKind, Var}; 353 | use crate::MediaType; 354 | use crate::ModuleSpecifier; 355 | use crate::ParseParams; 356 | use crate::parse_module; 357 | use crate::swc::ast::Id; 358 | 359 | fn test_scope(source_code: &str, test: impl Fn(Scope)) { 360 | let parsed_source = parse_module(ParseParams { 361 | specifier: ModuleSpecifier::parse("file:///my_file.js").unwrap(), 362 | text: source_code.to_string().into(), 363 | media_type: MediaType::TypeScript, 364 | capture_tokens: true, 365 | maybe_syntax: None, 366 | scope_analysis: true, 367 | }) 368 | .unwrap(); 369 | 370 | parsed_source.with_view(|view| { 371 | let scope = Scope::analyze(view); 372 | test(scope); 373 | }); 374 | } 375 | 376 | fn id(scope: &Scope, s: &str) -> Id { 377 | let ids = scope.ids_with_symbol(&s.into()); 378 | if ids.is_none() { 379 | panic!("No identifier named {}", s); 380 | } 381 | let ids = ids.unwrap(); 382 | if ids.len() > 1 { 383 | panic!("Multiple identifers named {} found", s); 384 | } 385 | 386 | ids.first().unwrap().clone() 387 | } 388 | 389 | fn var<'a>(scope: &'a Scope, symbol: &str) -> &'a Var { 390 | scope.var(&id(scope, symbol)).unwrap() 391 | } 392 | 393 | #[test] 394 | fn scopes() { 395 | let source_code = r#" 396 | const a = "a"; 397 | const unused = "unused"; 398 | function asdf(b: number, c: string): number { 399 | console.log(a, b); 400 | { 401 | const c = 1; 402 | let d = 2; 403 | } 404 | return 1; 405 | } 406 | class Foo { 407 | #fizz = "fizz"; 408 | bar() { 409 | } 410 | } 411 | try { 412 | // some code that might throw 413 | throw new Error("asdf"); 414 | } catch (e) { 415 | const msg = "asdf " + e.message; 416 | } 417 | "#; 418 | test_scope(source_code, |scope| { 419 | assert_eq!(var(&scope, "a").kind(), BindingKind::Const); 420 | assert_eq!(var(&scope, "a").path(), &[]); 421 | 422 | assert_eq!(var(&scope, "b").kind(), BindingKind::Param); 423 | assert_eq!(scope.ids_with_symbol(&"c".into()).unwrap().len(), 2); 424 | assert_eq!( 425 | var(&scope, "d").path(), 426 | &[ScopeKind::Function, ScopeKind::Block] 427 | ); 428 | 429 | assert_eq!(var(&scope, "Foo").kind(), BindingKind::Class); 430 | assert_eq!(var(&scope, "Foo").path(), &[]); 431 | 432 | assert_eq!(var(&scope, "e").kind(), BindingKind::CatchClause); 433 | assert_eq!(var(&scope, "e").path(), &[]); 434 | }); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /src/source_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use crate::ModuleSpecifier; 4 | use crate::swc::common::FileName; 5 | use crate::swc::common::SourceFile; 6 | use crate::swc::common::sync::Lrc; 7 | 8 | // this is used in JSR, so don't remove it 9 | pub trait IntoSwcFileName { 10 | fn into_file_name(self) -> FileName; 11 | } 12 | 13 | impl IntoSwcFileName for ModuleSpecifier { 14 | fn into_file_name(self) -> FileName { 15 | FileName::Url(self) 16 | } 17 | } 18 | 19 | impl IntoSwcFileName for String { 20 | fn into_file_name(self) -> FileName { 21 | FileName::Custom(self) 22 | } 23 | } 24 | 25 | #[derive(Clone, Default)] 26 | pub struct SourceMap { 27 | inner: Lrc, 28 | } 29 | 30 | impl SourceMap { 31 | pub fn single(file_name: impl IntoSwcFileName, source: String) -> Self { 32 | let map = Self::default(); 33 | map.inner.new_source_file( 34 | Lrc::new(IntoSwcFileName::into_file_name(file_name)), 35 | source, 36 | ); 37 | map 38 | } 39 | 40 | pub fn inner(&self) -> &Lrc { 41 | &self.inner 42 | } 43 | 44 | pub fn new_source_file( 45 | &self, 46 | file_name: impl IntoSwcFileName, 47 | source: String, 48 | ) -> Lrc { 49 | self.inner.new_source_file( 50 | Lrc::new(IntoSwcFileName::into_file_name(file_name)), 51 | source, 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/text_changes.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::cmp::Ordering; 4 | use std::ops::Range; 5 | 6 | use capacity_builder::StringBuilder; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct TextChange { 10 | /// Range start to end byte index. 11 | pub range: Range, 12 | /// New text to insert or replace at the provided range. 13 | pub new_text: String, 14 | } 15 | 16 | impl TextChange { 17 | pub fn new(start: usize, end: usize, new_text: String) -> Self { 18 | Self { 19 | range: start..end, 20 | new_text, 21 | } 22 | } 23 | } 24 | 25 | /// Applies the text changes to the given source text. 26 | pub fn apply_text_changes( 27 | source: &str, 28 | mut changes: Vec, 29 | ) -> String { 30 | changes.sort_by(|a, b| match a.range.start.cmp(&b.range.start) { 31 | Ordering::Equal => a.range.end.cmp(&b.range.end), 32 | ordering => ordering, 33 | }); 34 | 35 | StringBuilder::build(|builder| { 36 | let mut last_index = 0; 37 | for (i, change) in changes.iter().enumerate() { 38 | if change.range.start > change.range.end { 39 | panic!( 40 | "Text change had start index {} greater than end index {}.\n\n{:?}", 41 | change.range.start, 42 | change.range.end, 43 | &changes[0..i + 1], 44 | ) 45 | } 46 | if change.range.start < last_index { 47 | panic!("Text changes were overlapping. Past index was {}, but new change had index {}.\n\n{:?}", last_index, change.range.start, &changes[0..i + 1]); 48 | } else if change.range.start > last_index && last_index < source.len() { 49 | builder.append( 50 | &source[last_index..std::cmp::min(source.len(), change.range.start)], 51 | ); 52 | } 53 | builder.append(&change.new_text); 54 | last_index = change.range.end; 55 | } 56 | 57 | if last_index < source.len() { 58 | builder.append(&source[last_index..]); 59 | } 60 | }).unwrap() 61 | } 62 | 63 | #[cfg(test)] 64 | mod test { 65 | use super::*; 66 | 67 | #[test] 68 | fn applies_text_changes() { 69 | // replacing text 70 | assert_eq!( 71 | apply_text_changes( 72 | "0123456789", 73 | vec![ 74 | TextChange::new(9, 10, "z".to_string()), 75 | TextChange::new(4, 6, "y".to_string()), 76 | TextChange::new(1, 2, "x".to_string()), 77 | ] 78 | ), 79 | "0x23y678z".to_string(), 80 | ); 81 | 82 | // replacing beside 83 | assert_eq!( 84 | apply_text_changes( 85 | "0123456789", 86 | vec![ 87 | TextChange::new(0, 5, "a".to_string()), 88 | TextChange::new(5, 7, "b".to_string()), 89 | TextChange::new(7, 10, "c".to_string()), 90 | ] 91 | ), 92 | "abc".to_string(), 93 | ); 94 | 95 | // full replace 96 | assert_eq!( 97 | apply_text_changes( 98 | "0123456789", 99 | vec![TextChange::new(0, 10, "x".to_string()),] 100 | ), 101 | "x".to_string(), 102 | ); 103 | 104 | // 1 over 105 | assert_eq!( 106 | apply_text_changes( 107 | "0123456789", 108 | vec![TextChange::new(0, 11, "x".to_string()),] 109 | ), 110 | "x".to_string(), 111 | ); 112 | 113 | // insert 114 | assert_eq!( 115 | apply_text_changes( 116 | "0123456789", 117 | vec![TextChange::new(5, 5, "x".to_string()),] 118 | ), 119 | "01234x56789".to_string(), 120 | ); 121 | 122 | // prepend 123 | assert_eq!( 124 | apply_text_changes( 125 | "0123456789", 126 | vec![TextChange::new(0, 0, "x".to_string()),] 127 | ), 128 | "x0123456789".to_string(), 129 | ); 130 | 131 | // append 132 | assert_eq!( 133 | apply_text_changes( 134 | "0123456789", 135 | vec![TextChange::new(10, 10, "x".to_string()),] 136 | ), 137 | "0123456789x".to_string(), 138 | ); 139 | 140 | // append over 141 | assert_eq!( 142 | apply_text_changes( 143 | "0123456789", 144 | vec![TextChange::new(11, 11, "x".to_string()),] 145 | ), 146 | "0123456789x".to_string(), 147 | ); 148 | 149 | // multiple at start 150 | assert_eq!( 151 | apply_text_changes( 152 | "0123456789", 153 | vec![ 154 | TextChange::new(0, 7, "a".to_string()), 155 | TextChange::new(0, 0, "b".to_string()), 156 | TextChange::new(0, 0, "c".to_string()), 157 | TextChange::new(7, 10, "d".to_string()), 158 | ] 159 | ), 160 | "bcad".to_string(), 161 | ); 162 | } 163 | 164 | #[test] 165 | #[should_panic( 166 | expected = "Text changes were overlapping. Past index was 10, but new change had index 5." 167 | )] 168 | fn panics_text_change_within() { 169 | apply_text_changes( 170 | "0123456789", 171 | vec![ 172 | TextChange::new(3, 10, "x".to_string()), 173 | TextChange::new(5, 7, "x".to_string()), 174 | ], 175 | ); 176 | } 177 | 178 | #[test] 179 | #[should_panic( 180 | expected = "Text changes were overlapping. Past index was 4, but new change had index 3." 181 | )] 182 | fn panics_text_change_overlap() { 183 | apply_text_changes( 184 | "0123456789", 185 | vec![ 186 | TextChange::new(2, 4, "x".to_string()), 187 | TextChange::new(3, 5, "x".to_string()), 188 | ], 189 | ); 190 | } 191 | 192 | #[test] 193 | #[should_panic( 194 | expected = "Text change had start index 2 greater than end index 1." 195 | )] 196 | fn panics_start_greater_end() { 197 | apply_text_changes( 198 | "0123456789", 199 | vec![TextChange::new(2, 1, "x".to_string())], 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/transpiling/testdata/tc39_decorator_proposal_output.txt: -------------------------------------------------------------------------------- 1 | function applyDecs2203RFactory() { 2 | function createAddInitializerMethod(initializers, decoratorFinishedRef) { 3 | return function addInitializer(initializer) { 4 | assertNotFinished(decoratorFinishedRef, "addInitializer"); 5 | assertCallable(initializer, "An initializer"); 6 | initializers.push(initializer); 7 | }; 8 | } 9 | function memberDec(dec, name, desc, initializers, kind, isStatic, isPrivate, metadata, value) { 10 | var kindStr; 11 | switch(kind){ 12 | case 1: 13 | kindStr = "accessor"; 14 | break; 15 | case 2: 16 | kindStr = "method"; 17 | break; 18 | case 3: 19 | kindStr = "getter"; 20 | break; 21 | case 4: 22 | kindStr = "setter"; 23 | break; 24 | default: 25 | kindStr = "field"; 26 | } 27 | var ctx = { 28 | kind: kindStr, 29 | name: isPrivate ? "#" + name : name, 30 | static: isStatic, 31 | private: isPrivate, 32 | metadata: metadata 33 | }; 34 | var decoratorFinishedRef = { 35 | v: false 36 | }; 37 | ctx.addInitializer = createAddInitializerMethod(initializers, decoratorFinishedRef); 38 | var get, set; 39 | if (kind === 0) { 40 | if (isPrivate) { 41 | get = desc.get; 42 | set = desc.set; 43 | } else { 44 | get = function() { 45 | return this[name]; 46 | }; 47 | set = function(v) { 48 | this[name] = v; 49 | }; 50 | } 51 | } else if (kind === 2) { 52 | get = function() { 53 | return desc.value; 54 | }; 55 | } else { 56 | if (kind === 1 || kind === 3) { 57 | get = function() { 58 | return desc.get.call(this); 59 | }; 60 | } 61 | if (kind === 1 || kind === 4) { 62 | set = function(v) { 63 | desc.set.call(this, v); 64 | }; 65 | } 66 | } 67 | ctx.access = get && set ? { 68 | get: get, 69 | set: set 70 | } : get ? { 71 | get: get 72 | } : { 73 | set: set 74 | }; 75 | try { 76 | return dec(value, ctx); 77 | } finally{ 78 | decoratorFinishedRef.v = true; 79 | } 80 | } 81 | function assertNotFinished(decoratorFinishedRef, fnName) { 82 | if (decoratorFinishedRef.v) { 83 | throw new Error("attempted to call " + fnName + " after decoration was finished"); 84 | } 85 | } 86 | function assertCallable(fn, hint) { 87 | if (typeof fn !== "function") { 88 | throw new TypeError(hint + " must be a function"); 89 | } 90 | } 91 | function assertValidReturnValue(kind, value) { 92 | var type = typeof value; 93 | if (kind === 1) { 94 | if (type !== "object" || value === null) { 95 | throw new TypeError("accessor decorators must return an object with get, set, or init properties or void 0"); 96 | } 97 | if (value.get !== undefined) { 98 | assertCallable(value.get, "accessor.get"); 99 | } 100 | if (value.set !== undefined) { 101 | assertCallable(value.set, "accessor.set"); 102 | } 103 | if (value.init !== undefined) { 104 | assertCallable(value.init, "accessor.init"); 105 | } 106 | } else if (type !== "function") { 107 | var hint; 108 | if (kind === 0) { 109 | hint = "field"; 110 | } else if (kind === 10) { 111 | hint = "class"; 112 | } else { 113 | hint = "method"; 114 | } 115 | throw new TypeError(hint + " decorators must return a function or void 0"); 116 | } 117 | } 118 | function applyMemberDec(ret, base, decInfo, name, kind, isStatic, isPrivate, initializers, metadata) { 119 | var decs = decInfo[0]; 120 | var desc, init, value; 121 | if (isPrivate) { 122 | if (kind === 0 || kind === 1) { 123 | desc = { 124 | get: decInfo[3], 125 | set: decInfo[4] 126 | }; 127 | } else if (kind === 3) { 128 | desc = { 129 | get: decInfo[3] 130 | }; 131 | } else if (kind === 4) { 132 | desc = { 133 | set: decInfo[3] 134 | }; 135 | } else { 136 | desc = { 137 | value: decInfo[3] 138 | }; 139 | } 140 | } else if (kind !== 0) { 141 | desc = Object.getOwnPropertyDescriptor(base, name); 142 | } 143 | if (kind === 1) { 144 | value = { 145 | get: desc.get, 146 | set: desc.set 147 | }; 148 | } else if (kind === 2) { 149 | value = desc.value; 150 | } else if (kind === 3) { 151 | value = desc.get; 152 | } else if (kind === 4) { 153 | value = desc.set; 154 | } 155 | var newValue, get, set; 156 | if (typeof decs === "function") { 157 | newValue = memberDec(decs, name, desc, initializers, kind, isStatic, isPrivate, metadata, value); 158 | if (newValue !== void 0) { 159 | assertValidReturnValue(kind, newValue); 160 | if (kind === 0) { 161 | init = newValue; 162 | } else if (kind === 1) { 163 | init = newValue.init; 164 | get = newValue.get || value.get; 165 | set = newValue.set || value.set; 166 | value = { 167 | get: get, 168 | set: set 169 | }; 170 | } else { 171 | value = newValue; 172 | } 173 | } 174 | } else { 175 | for(var i = decs.length - 1; i >= 0; i--){ 176 | var dec = decs[i]; 177 | newValue = memberDec(dec, name, desc, initializers, kind, isStatic, isPrivate, metadata, value); 178 | if (newValue !== void 0) { 179 | assertValidReturnValue(kind, newValue); 180 | var newInit; 181 | if (kind === 0) { 182 | newInit = newValue; 183 | } else if (kind === 1) { 184 | newInit = newValue.init; 185 | get = newValue.get || value.get; 186 | set = newValue.set || value.set; 187 | value = { 188 | get: get, 189 | set: set 190 | }; 191 | } else { 192 | value = newValue; 193 | } 194 | if (newInit !== void 0) { 195 | if (init === void 0) { 196 | init = newInit; 197 | } else if (typeof init === "function") { 198 | init = [ 199 | init, 200 | newInit 201 | ]; 202 | } else { 203 | init.push(newInit); 204 | } 205 | } 206 | } 207 | } 208 | } 209 | if (kind === 0 || kind === 1) { 210 | if (init === void 0) { 211 | init = function(instance, init) { 212 | return init; 213 | }; 214 | } else if (typeof init !== "function") { 215 | var ownInitializers = init; 216 | init = function(instance, init) { 217 | var value = init; 218 | for(var i = 0; i < ownInitializers.length; i++){ 219 | value = ownInitializers[i].call(instance, value); 220 | } 221 | return value; 222 | }; 223 | } else { 224 | var originalInitializer = init; 225 | init = function(instance, init) { 226 | return originalInitializer.call(instance, init); 227 | }; 228 | } 229 | ret.push(init); 230 | } 231 | if (kind !== 0) { 232 | if (kind === 1) { 233 | desc.get = value.get; 234 | desc.set = value.set; 235 | } else if (kind === 2) { 236 | desc.value = value; 237 | } else if (kind === 3) { 238 | desc.get = value; 239 | } else if (kind === 4) { 240 | desc.set = value; 241 | } 242 | if (isPrivate) { 243 | if (kind === 1) { 244 | ret.push(function(instance, args) { 245 | return value.get.call(instance, args); 246 | }); 247 | ret.push(function(instance, args) { 248 | return value.set.call(instance, args); 249 | }); 250 | } else if (kind === 2) { 251 | ret.push(value); 252 | } else { 253 | ret.push(function(instance, args) { 254 | return value.call(instance, args); 255 | }); 256 | } 257 | } else { 258 | Object.defineProperty(base, name, desc); 259 | } 260 | } 261 | } 262 | function applyMemberDecs(Class, decInfos, metadata) { 263 | var ret = []; 264 | var protoInitializers; 265 | var staticInitializers; 266 | var existingProtoNonFields = new Map(); 267 | var existingStaticNonFields = new Map(); 268 | for(var i = 0; i < decInfos.length; i++){ 269 | var decInfo = decInfos[i]; 270 | if (!Array.isArray(decInfo)) continue; 271 | var kind = decInfo[1]; 272 | var name = decInfo[2]; 273 | var isPrivate = decInfo.length > 3; 274 | var isStatic = kind >= 5; 275 | var base; 276 | var initializers; 277 | if (isStatic) { 278 | base = Class; 279 | kind = kind - 5; 280 | staticInitializers = staticInitializers || []; 281 | initializers = staticInitializers; 282 | } else { 283 | base = Class.prototype; 284 | protoInitializers = protoInitializers || []; 285 | initializers = protoInitializers; 286 | } 287 | if (kind !== 0 && !isPrivate) { 288 | var existingNonFields = isStatic ? existingStaticNonFields : existingProtoNonFields; 289 | var existingKind = existingNonFields.get(name) || 0; 290 | if (existingKind === true || existingKind === 3 && kind !== 4 || existingKind === 4 && kind !== 3) { 291 | throw new Error("Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: " + name); 292 | } else if (!existingKind && kind > 2) { 293 | existingNonFields.set(name, kind); 294 | } else { 295 | existingNonFields.set(name, true); 296 | } 297 | } 298 | applyMemberDec(ret, base, decInfo, name, kind, isStatic, isPrivate, initializers, metadata); 299 | } 300 | pushInitializers(ret, protoInitializers); 301 | pushInitializers(ret, staticInitializers); 302 | return ret; 303 | } 304 | function pushInitializers(ret, initializers) { 305 | if (initializers) { 306 | ret.push(function(instance) { 307 | for(var i = 0; i < initializers.length; i++){ 308 | initializers[i].call(instance); 309 | } 310 | return instance; 311 | }); 312 | } 313 | } 314 | function applyClassDecs(targetClass, classDecs, metadata) { 315 | if (classDecs.length > 0) { 316 | var initializers = []; 317 | var newClass = targetClass; 318 | var name = targetClass.name; 319 | for(var i = classDecs.length - 1; i >= 0; i--){ 320 | var decoratorFinishedRef = { 321 | v: false 322 | }; 323 | try { 324 | var nextNewClass = classDecs[i](newClass, { 325 | kind: "class", 326 | name: name, 327 | addInitializer: createAddInitializerMethod(initializers, decoratorFinishedRef), 328 | metadata 329 | }); 330 | } finally{ 331 | decoratorFinishedRef.v = true; 332 | } 333 | if (nextNewClass !== undefined) { 334 | assertValidReturnValue(10, nextNewClass); 335 | newClass = nextNewClass; 336 | } 337 | } 338 | return [ 339 | defineMetadata(newClass, metadata), 340 | function() { 341 | for(var i = 0; i < initializers.length; i++){ 342 | initializers[i].call(newClass); 343 | } 344 | } 345 | ]; 346 | } 347 | } 348 | function defineMetadata(Class, metadata) { 349 | return Object.defineProperty(Class, Symbol.metadata || Symbol.for("Symbol.metadata"), { 350 | configurable: true, 351 | enumerable: true, 352 | value: metadata 353 | }); 354 | } 355 | return function applyDecs2203R(targetClass, memberDecs, classDecs, parentClass) { 356 | if (parentClass !== void 0) { 357 | var parentMetadata = parentClass[Symbol.metadata || Symbol.for("Symbol.metadata")]; 358 | } 359 | var metadata = Object.create(parentMetadata === void 0 ? null : parentMetadata); 360 | var e = applyMemberDecs(targetClass, memberDecs, metadata); 361 | if (!classDecs.length) defineMetadata(targetClass, metadata); 362 | return { 363 | e: e, 364 | get c () { 365 | return applyClassDecs(targetClass, classDecs, metadata); 366 | } 367 | }; 368 | }; 369 | } 370 | function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) { 371 | return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass); 372 | } 373 | var _dec, _initProto; 374 | function enumerable(value) { 375 | return function(_target, _propertyKey, descriptor) { 376 | descriptor.enumerable = value; 377 | return descriptor; 378 | }; 379 | } 380 | _dec = enumerable(false); 381 | export class A { 382 | static{ 383 | ({ e: [_initProto] } = _apply_decs_2203_r(this, [ 384 | [ 385 | _dec, 386 | 2, 387 | "a" 388 | ] 389 | ], [])); 390 | } 391 | constructor(){ 392 | _initProto(this); 393 | } 394 | a() { 395 | Test.value; 396 | } 397 | } -------------------------------------------------------------------------------- /src/transpiling/transforms.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use swc_atoms::Atom; 4 | use swc_atoms::Wtf8Atom; 5 | use swc_common::SyntaxContext; 6 | 7 | use crate::swc::ast as swc_ast; 8 | use crate::swc::common::DUMMY_SP; 9 | use crate::swc::ecma_visit::Fold; 10 | use crate::swc::ecma_visit::noop_fold_type; 11 | 12 | /// Transforms import declarations to variable declarations 13 | /// with a dynamic import. This is used to provide import 14 | /// declaration support in script contexts such as the Deno REPL. 15 | pub struct ImportDeclsToVarDeclsFolder; 16 | 17 | impl Fold for ImportDeclsToVarDeclsFolder { 18 | noop_fold_type!(); // skip typescript specific nodes 19 | 20 | fn fold_module_item( 21 | &mut self, 22 | module_item: swc_ast::ModuleItem, 23 | ) -> swc_ast::ModuleItem { 24 | use crate::swc::ast::*; 25 | 26 | match module_item { 27 | ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) => { 28 | // Handle type only imports 29 | if import_decl.type_only { 30 | // should have no side effects 31 | return create_empty_stmt(); 32 | } 33 | 34 | // The initializer (ex. `await import('./mod.ts')`) 35 | let initializer = 36 | create_await_import_expr(&import_decl.src.value, import_decl.with); 37 | 38 | // Handle imports for the side effects 39 | // ex. `import "module.ts"` -> `await import("module.ts");` 40 | if import_decl.specifiers.is_empty() { 41 | return ModuleItem::Stmt(Stmt::Expr(ExprStmt { 42 | span: DUMMY_SP, 43 | expr: initializer, 44 | })); 45 | } 46 | 47 | // Collect the specifiers and create the variable statement 48 | let named_import_props = import_decl 49 | .specifiers 50 | .iter() 51 | .filter_map(|specifier| match specifier { 52 | ImportSpecifier::Default(specifier) => { 53 | Some(create_key_value("default".into(), specifier.local.clone())) 54 | } 55 | ImportSpecifier::Named(specifier) => { 56 | Some(match specifier.imported.as_ref() { 57 | Some(name) => create_key_value( 58 | match name { 59 | ModuleExportName::Ident(ident) => ident.sym.clone().into(), 60 | ModuleExportName::Str(str) => str.value.clone(), 61 | }, 62 | specifier.local.clone(), 63 | ), 64 | None => create_assignment(specifier.local.clone()), 65 | }) 66 | } 67 | ImportSpecifier::Namespace(_) => None, 68 | }) 69 | .collect::>(); 70 | let namespace_import_name = 71 | import_decl 72 | .specifiers 73 | .iter() 74 | .find_map(|specifier| match specifier { 75 | ImportSpecifier::Namespace(specifier) => Some(BindingIdent { 76 | id: specifier.local.clone(), 77 | type_ann: None, 78 | }), 79 | _ => None, 80 | }); 81 | 82 | ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { 83 | span: DUMMY_SP, 84 | kind: VarDeclKind::Const, 85 | ctxt: SyntaxContext::default(), 86 | declare: false, 87 | decls: { 88 | let mut decls = Vec::new(); 89 | 90 | if !named_import_props.is_empty() { 91 | decls.push(VarDeclarator { 92 | span: DUMMY_SP, 93 | name: Pat::Object(ObjectPat { 94 | span: DUMMY_SP, 95 | optional: false, 96 | props: named_import_props, 97 | type_ann: None, 98 | }), 99 | definite: false, 100 | init: Some(initializer.clone()), 101 | }); 102 | } 103 | if let Some(namespace_import) = namespace_import_name { 104 | decls.push(VarDeclarator { 105 | span: DUMMY_SP, 106 | name: Pat::Ident(namespace_import), 107 | definite: false, 108 | init: Some(initializer), 109 | }); 110 | } 111 | 112 | decls 113 | }, 114 | })))) 115 | } 116 | _ => module_item, 117 | } 118 | } 119 | } 120 | 121 | /// Strips export declarations and exports on named exports so the 122 | /// code can be used in script contexts. This is useful for example 123 | /// in the Deno REPL. 124 | pub struct StripExportsFolder; 125 | 126 | impl Fold for StripExportsFolder { 127 | noop_fold_type!(); // skip typescript specific nodes 128 | 129 | fn fold_module_item( 130 | &mut self, 131 | module_item: swc_ast::ModuleItem, 132 | ) -> swc_ast::ModuleItem { 133 | use crate::swc::ast::*; 134 | 135 | match module_item { 136 | ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)) => { 137 | ModuleItem::Stmt(Stmt::Expr(ExprStmt { 138 | span: DUMMY_SP, 139 | expr: create_await_import_expr( 140 | &export_all.src.value, 141 | export_all.with, 142 | ), 143 | })) 144 | } 145 | ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_named)) => { 146 | if let Some(src) = export_named.src { 147 | ModuleItem::Stmt(Stmt::Expr(ExprStmt { 148 | span: DUMMY_SP, 149 | expr: create_await_import_expr(&src.value, export_named.with), 150 | })) 151 | } else { 152 | create_empty_stmt() 153 | } 154 | } 155 | ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(default_expr)) => { 156 | // transform a default export expression to its expression 157 | ModuleItem::Stmt(Stmt::Expr(ExprStmt { 158 | span: DUMMY_SP, 159 | expr: default_expr.expr, 160 | })) 161 | } 162 | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { 163 | // strip the export keyword on an exported declaration 164 | ModuleItem::Stmt(Stmt::Decl(export_decl.decl)) 165 | } 166 | ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(default_decl)) => { 167 | // only keep named default exports 168 | match default_decl.decl { 169 | DefaultDecl::Fn(FnExpr { 170 | ident: Some(ident), 171 | function, 172 | }) => ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { 173 | declare: false, 174 | ident, 175 | function, 176 | }))), 177 | DefaultDecl::Class(ClassExpr { 178 | ident: Some(ident), 179 | class, 180 | }) => ModuleItem::Stmt(Stmt::Decl(Decl::Class(ClassDecl { 181 | declare: false, 182 | ident, 183 | class, 184 | }))), 185 | _ => create_empty_stmt(), 186 | } 187 | } 188 | _ => module_item, 189 | } 190 | } 191 | } 192 | 193 | fn create_empty_stmt() -> swc_ast::ModuleItem { 194 | use swc_ast::*; 195 | ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) 196 | } 197 | 198 | fn create_ident(name: Atom) -> swc_ast::Ident { 199 | swc_ast::Ident { 200 | span: DUMMY_SP, 201 | ctxt: SyntaxContext::default(), 202 | sym: name, 203 | optional: false, 204 | } 205 | } 206 | 207 | fn create_ident_name(name: Atom) -> swc_ast::IdentName { 208 | swc_ast::IdentName { 209 | span: DUMMY_SP, 210 | sym: name, 211 | } 212 | } 213 | 214 | fn create_key_value( 215 | key: Wtf8Atom, 216 | value: swc_ast::Ident, 217 | ) -> swc_ast::ObjectPatProp { 218 | swc_ast::ObjectPatProp::KeyValue(swc_ast::KeyValuePatProp { 219 | // use a string literal because it will work in more scenarios than an identifier 220 | key: swc_ast::PropName::Str(swc_ast::Str { 221 | span: DUMMY_SP, 222 | value: key, 223 | raw: None, 224 | }), 225 | value: Box::new(swc_ast::Pat::Ident(swc_ast::BindingIdent { 226 | id: value, 227 | type_ann: None, 228 | })), 229 | }) 230 | } 231 | 232 | fn create_await_import_expr( 233 | module_specifier: &Wtf8Atom, 234 | maybe_attrs: Option>, 235 | ) -> Box { 236 | use swc_ast::*; 237 | let mut args = vec![ExprOrSpread { 238 | spread: None, 239 | expr: Box::new(Expr::Lit(Lit::Str(Str { 240 | span: DUMMY_SP, 241 | raw: None, 242 | value: module_specifier.clone(), 243 | }))), 244 | }]; 245 | 246 | // add assert object if it exists 247 | if let Some(attrs) = maybe_attrs { 248 | args.push(ExprOrSpread { 249 | spread: None, 250 | expr: Box::new(Expr::Object(ObjectLit { 251 | span: DUMMY_SP, 252 | props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue( 253 | KeyValueProp { 254 | key: PropName::Ident(create_ident_name("with".into())), 255 | value: Box::new(Expr::Object(*attrs)), 256 | }, 257 | )))], 258 | })), 259 | }) 260 | } 261 | 262 | Box::new(Expr::Await(AwaitExpr { 263 | span: DUMMY_SP, 264 | arg: Box::new(Expr::Call(CallExpr { 265 | span: DUMMY_SP, 266 | ctxt: SyntaxContext::default(), 267 | callee: Callee::Expr(Box::new(Expr::Ident(create_ident( 268 | "import".into(), 269 | )))), 270 | args, 271 | type_args: None, 272 | })), 273 | })) 274 | } 275 | 276 | fn create_assignment(key: swc_ast::Ident) -> swc_ast::ObjectPatProp { 277 | swc_ast::ObjectPatProp::Assign(swc_ast::AssignPatProp { 278 | span: DUMMY_SP, 279 | key: swc_ast::BindingIdent { 280 | id: key, 281 | type_ann: None, // not used in swc 282 | }, 283 | value: None, 284 | }) 285 | } 286 | 287 | #[cfg(test)] 288 | mod test { 289 | use crate::ModuleSpecifier; 290 | use crate::swc::ast::Module; 291 | use crate::swc::codegen::Node; 292 | use crate::swc::codegen::text_writer::JsWriter; 293 | use crate::swc::common::FileName; 294 | use crate::swc::common::SourceMap; 295 | use crate::swc::common::sync::Lrc; 296 | use crate::swc::ecma_visit::Fold; 297 | use crate::swc::ecma_visit::FoldWith; 298 | use crate::swc::parser::Parser; 299 | use crate::swc::parser::StringInput; 300 | use crate::swc::parser::Syntax; 301 | use crate::swc::parser::TsSyntax; 302 | use pretty_assertions::assert_eq; 303 | 304 | use super::*; 305 | 306 | #[test] 307 | fn test_downlevel_imports_type_only() { 308 | test_transform( 309 | ImportDeclsToVarDeclsFolder, 310 | r#"import type { test } from "./mod.ts";"#, 311 | ";", 312 | ); 313 | } 314 | 315 | #[test] 316 | fn test_downlevel_imports_specifier_only() { 317 | test_transform( 318 | ImportDeclsToVarDeclsFolder, 319 | r#"import "./mod.ts";"#, 320 | r#"await import("./mod.ts");"#, 321 | ); 322 | 323 | test_transform( 324 | ImportDeclsToVarDeclsFolder, 325 | r#"import {} from "./mod.ts";"#, 326 | r#"await import("./mod.ts");"#, 327 | ); 328 | } 329 | 330 | #[test] 331 | fn test_downlevel_imports_default() { 332 | test_transform( 333 | ImportDeclsToVarDeclsFolder, 334 | r#"import mod from "./mod.ts";"#, 335 | r#"const { "default": mod } = await import("./mod.ts");"#, 336 | ); 337 | } 338 | 339 | #[test] 340 | fn test_downlevel_imports_named() { 341 | test_transform( 342 | ImportDeclsToVarDeclsFolder, 343 | r#"import { A } from "./mod.ts";"#, 344 | r#"const { A } = await import("./mod.ts");"#, 345 | ); 346 | 347 | test_transform( 348 | ImportDeclsToVarDeclsFolder, 349 | r#"import { A, B, C } from "./mod.ts";"#, 350 | r#"const { A, B, C } = await import("./mod.ts");"#, 351 | ); 352 | 353 | test_transform( 354 | ImportDeclsToVarDeclsFolder, 355 | r#"import { A as LocalA, B, C as LocalC } from "./mod.ts";"#, 356 | r#"const { "A": LocalA, B, "C": LocalC } = await import("./mod.ts");"#, 357 | ); 358 | } 359 | 360 | #[test] 361 | fn test_downlevel_imports_namespace() { 362 | test_transform( 363 | ImportDeclsToVarDeclsFolder, 364 | r#"import * as mod from "./mod.ts";"#, 365 | r#"const mod = await import("./mod.ts");"#, 366 | ); 367 | } 368 | 369 | #[test] 370 | fn test_downlevel_imports_mixed() { 371 | test_transform( 372 | ImportDeclsToVarDeclsFolder, 373 | r#"import myDefault, { A, B as LocalB } from "./mod.ts";"#, 374 | r#"const { "default": myDefault, A, "B": LocalB } = await import("./mod.ts");"#, 375 | ); 376 | 377 | test_transform( 378 | ImportDeclsToVarDeclsFolder, 379 | r#"import myDefault, * as mod from "./mod.ts";"#, 380 | r#"const { "default": myDefault } = await import("./mod.ts"), mod = await import("./mod.ts");"#, 381 | ); 382 | } 383 | 384 | #[test] 385 | fn test_downlevel_imports_assertions() { 386 | test_transform( 387 | ImportDeclsToVarDeclsFolder, 388 | r#"import data from "./mod.json" assert { type: "json" };"#, 389 | "const { \"default\": data } = await import(\"./mod.json\", {\n with: {\n type: \"json\"\n }\n});", 390 | ); 391 | } 392 | 393 | #[test] 394 | fn test_strip_exports_export_all() { 395 | test_transform( 396 | StripExportsFolder, 397 | r#"export * from "./test.ts";"#, 398 | r#"await import("./test.ts");"#, 399 | ); 400 | } 401 | 402 | #[test] 403 | fn test_strip_exports_export_named() { 404 | test_transform( 405 | StripExportsFolder, 406 | r#"export { test } from "./test.ts";"#, 407 | r#"await import("./test.ts");"#, 408 | ); 409 | 410 | test_transform(StripExportsFolder, r#"export { test };"#, ";"); 411 | } 412 | 413 | #[test] 414 | fn test_strip_exports_assertions() { 415 | test_transform( 416 | StripExportsFolder, 417 | r#"export { default as data } from "./mod.json" with { type: "json" };"#, 418 | "await import(\"./mod.json\", {\n with: {\n type: \"json\"\n }\n});", 419 | ); 420 | } 421 | 422 | #[test] 423 | fn test_strip_exports_export_all_assertions() { 424 | // even though this doesn't really make sense for someone to do 425 | test_transform( 426 | StripExportsFolder, 427 | r#"export * from "./mod.json" assert { type: "json" };"#, 428 | "await import(\"./mod.json\", {\n with: {\n type: \"json\"\n }\n});", 429 | ); 430 | } 431 | 432 | #[test] 433 | fn test_strip_exports_export_default_expr() { 434 | test_transform(StripExportsFolder, "export default 5;", "5;"); 435 | } 436 | 437 | #[test] 438 | fn test_strip_exports_export_default_decl_name() { 439 | test_transform( 440 | StripExportsFolder, 441 | "export default class Test {}", 442 | "class Test {\n}", 443 | ); 444 | 445 | test_transform( 446 | StripExportsFolder, 447 | "export default function test() {}", 448 | "function test() {}", 449 | ); 450 | } 451 | 452 | #[test] 453 | fn test_strip_exports_export_default_decl_no_name() { 454 | test_transform(StripExportsFolder, "export default class {}", ";"); 455 | 456 | test_transform(StripExportsFolder, "export default function() {}", ";"); 457 | } 458 | 459 | #[test] 460 | fn test_strip_exports_export_named_decls() { 461 | test_transform( 462 | StripExportsFolder, 463 | "export class Test {}", 464 | "class Test {\n}", 465 | ); 466 | 467 | test_transform( 468 | StripExportsFolder, 469 | "export function test() {}", 470 | "function test() {}", 471 | ); 472 | 473 | test_transform(StripExportsFolder, "export enum Test {}", "enum Test {\n}"); 474 | 475 | test_transform( 476 | StripExportsFolder, 477 | "export namespace Test {}", 478 | "namespace Test {\n}", 479 | ); 480 | } 481 | 482 | #[test] 483 | fn test_strip_exports_not_in_namespace() { 484 | test_transform( 485 | StripExportsFolder, 486 | "namespace Test { export class Test {} }", 487 | "namespace Test {\n export class Test {\n }\n}", 488 | ); 489 | } 490 | 491 | #[track_caller] 492 | fn test_transform( 493 | mut transform: impl Fold, 494 | src: &str, 495 | expected_output: &str, 496 | ) { 497 | let (source_map, module) = parse(src); 498 | let output = print(source_map, module.fold_with(&mut transform)); 499 | assert_eq!(output, format!("{}\n", expected_output)); 500 | } 501 | 502 | fn parse(src: &str) -> (Lrc, Module) { 503 | let source_map = Lrc::new(SourceMap::default()); 504 | let source_file = source_map.new_source_file( 505 | Lrc::new(FileName::Url( 506 | ModuleSpecifier::parse("file:///test.ts").unwrap(), 507 | )), 508 | src.to_string(), 509 | ); 510 | let input = StringInput::from(&*source_file); 511 | let syntax = Syntax::Typescript(TsSyntax { 512 | ..Default::default() 513 | }); 514 | let mut parser = Parser::new(syntax, input, None); 515 | (source_map, parser.parse_module().unwrap()) 516 | } 517 | 518 | fn print(source_map: Lrc, module: Module) -> String { 519 | let mut buf = vec![]; 520 | { 521 | let mut writer = 522 | Box::new(JsWriter::new(source_map.clone(), "\n", &mut buf, None)); 523 | writer.set_indent_str(" "); // two spaces 524 | let mut emitter = crate::swc::codegen::Emitter { 525 | cfg: crate::swc_codegen_config(), 526 | comments: None, 527 | cm: source_map, 528 | wr: writer, 529 | }; 530 | module.emit_with(&mut emitter).unwrap(); 531 | } 532 | String::from_utf8(buf).unwrap() 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /src/type_strip.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use deno_error::JsError; 4 | use swc_ecma_lexer::TsSyntax; 5 | use thiserror::Error; 6 | use url::Url; 7 | 8 | use crate::diagnostics::DiagnosticCollector; 9 | use crate::diagnostics::SwcFoldDiagnosticsError; 10 | use crate::diagnostics::ensure_no_fatal_swc_diagnostics; 11 | use crate::swc::common::SourceMap; 12 | use crate::swc::common::sync::Lrc; 13 | 14 | pub struct TypeStripOptions { 15 | pub module: Option, 16 | } 17 | 18 | #[derive(Debug, Error, JsError)] 19 | pub enum TypeStripError { 20 | #[class(type)] 21 | #[error(transparent)] 22 | Fold(#[from] SwcFoldDiagnosticsError), 23 | #[class(type)] 24 | #[error(transparent)] 25 | TsError(#[from] swc_ts_fast_strip::TsError), 26 | } 27 | 28 | /// Type stripts the given source. 29 | pub fn type_strip( 30 | filename: &Url, 31 | code: String, 32 | options: TypeStripOptions, 33 | ) -> Result { 34 | let source_map = Lrc::new(SourceMap::default()); 35 | let emitter = DiagnosticCollector::default(); 36 | let (handler, diagnostics_cell) = emitter.into_handler_and_cell(); 37 | let output = crate::swc::common::errors::HANDLER.set(&handler, || { 38 | swc_ts_fast_strip::operate( 39 | &source_map, 40 | &handler, 41 | code, 42 | swc_ts_fast_strip::Options { 43 | module: options.module, 44 | filename: Some(filename.to_string()), 45 | parser: TsSyntax { 46 | decorators: false, 47 | disallow_ambiguous_jsx_like: true, 48 | dts: false, 49 | no_early_errors: true, 50 | tsx: false, 51 | }, 52 | mode: swc_ts_fast_strip::Mode::StripOnly, 53 | transform: None, 54 | deprecated_ts_module_as_error: Some(true), 55 | source_map: false, 56 | }, 57 | ) 58 | })?; 59 | 60 | let mut diagnostics = diagnostics_cell.borrow_mut(); 61 | let diagnostics = std::mem::take(&mut *diagnostics); 62 | ensure_no_fatal_swc_diagnostics(&source_map, diagnostics.into_iter())?; 63 | Ok(output.code) 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | fn strip(code: &str) -> Result { 71 | type_strip( 72 | &Url::parse("file:///test.ts").unwrap(), 73 | code.to_string(), 74 | TypeStripOptions { module: None }, 75 | ) 76 | } 77 | 78 | #[test] 79 | fn test_type_strip_basic_types() { 80 | let code = "const x: number = 5;"; 81 | let result = strip(code).unwrap(); 82 | assert_eq!(result, "const x = 5;"); 83 | } 84 | 85 | #[test] 86 | fn test_type_strip_function_types() { 87 | let code = "function foo(a: string, b: number): boolean { return true; }"; 88 | let result = strip(code).unwrap(); 89 | assert_eq!( 90 | result, 91 | "function foo(a , b ) { return true; }" 92 | ); 93 | } 94 | 95 | #[test] 96 | fn test_type_strip_interface() { 97 | let code = "interface Person {\n name: string;\n age: number;\n}\nconst p: Person = { name: \"Alice\", age: 30 };"; 98 | let result = strip(code).unwrap(); 99 | assert_eq!( 100 | result, 101 | " \n \n \n \nconst p = { name: \"Alice\", age: 30 };" 102 | ); 103 | } 104 | 105 | #[test] 106 | fn test_type_strip_type_alias() { 107 | let code = "type MyType = string | number;\nconst x: MyType = \"hello\";"; 108 | let result = strip(code).unwrap(); 109 | assert_eq!( 110 | result, 111 | " \nconst x = \"hello\";" 112 | ); 113 | } 114 | 115 | #[test] 116 | fn test_type_strip_generics() { 117 | let code = "function identity(arg: T): T { return arg; }"; 118 | let result = strip(code).unwrap(); 119 | assert_eq!(result, "function identity (arg ) { return arg; }"); 120 | } 121 | 122 | #[test] 123 | fn test_type_strip_class() { 124 | let code = "class MyClass {\n private x: number;\n constructor(x: number) {\n this.x = x;\n }\n getValue(): number {\n return this.x;\n }\n}"; 125 | let result = strip(code).unwrap(); 126 | assert_eq!( 127 | result, 128 | "class MyClass {\n x ;\n constructor(x ) {\n this.x = x;\n }\n getValue() {\n return this.x;\n }\n}" 129 | ); 130 | } 131 | 132 | #[test] 133 | fn test_type_strip_arrow_function() { 134 | let code = 135 | "const fn = (a: string, b: number): string => { return a + b; };"; 136 | let result = strip(code).unwrap(); 137 | assert_eq!( 138 | result, 139 | "const fn = (a , b ) => { return a + b; };" 140 | ); 141 | } 142 | 143 | #[test] 144 | fn test_type_strip_enum() { 145 | let code = 146 | "enum Color {\n Red,\n Green,\n Blue\n}\nconst c: Color = Color.Red;"; 147 | let result = strip(code); 148 | // Enums are not supported in strip-only mode 149 | assert!(result.is_err()); 150 | } 151 | 152 | #[test] 153 | fn test_type_strip_as_assertion() { 154 | let code = "const x = \"hello\" as string;"; 155 | let result = strip(code).unwrap(); 156 | assert_eq!(result, "const x = \"hello\" ;"); 157 | } 158 | 159 | #[test] 160 | fn test_type_strip_non_null_assertion() { 161 | let code = "const x = value!;"; 162 | let result = strip(code).unwrap(); 163 | assert_eq!(result, "const x = value ;"); 164 | } 165 | 166 | #[test] 167 | fn test_type_strip_import_type() { 168 | let code = "import type { MyType } from \"./types\";"; 169 | let result = strip(code).unwrap(); 170 | assert_eq!(result, " "); 171 | } 172 | 173 | #[test] 174 | fn test_type_strip_preserves_runtime_code() { 175 | let code = "const x: number = 5;\nconsole.log(x);\nfunction add(a: number, b: number): number {\n return a + b;\n}\nadd(1, 2);"; 176 | let result = strip(code).unwrap(); 177 | assert_eq!( 178 | result, 179 | "const x = 5;\nconsole.log(x);\nfunction add(a , b ) {\n return a + b;\n}\nadd(1, 2);" 180 | ); 181 | } 182 | 183 | #[test] 184 | fn test_type_strip_optional_params() { 185 | let code = "function foo(a?: string, b?: number) { return a; }"; 186 | let result = strip(code).unwrap(); 187 | assert_eq!(result, "function foo(a , b ) { return a; }"); 188 | } 189 | 190 | #[test] 191 | fn test_type_strip_module_option() { 192 | let code = "const x: number = 5;"; 193 | let result = type_strip( 194 | &Url::parse("file:///test.ts").unwrap(), 195 | code.to_string(), 196 | TypeStripOptions { module: Some(true) }, 197 | ) 198 | .unwrap(); 199 | assert_eq!(result, "const x = 5;"); 200 | } 201 | 202 | #[test] 203 | fn test_type_strip_syntax_error() { 204 | let code = "const x: = 5;"; 205 | let result = strip(code); 206 | assert!(result.is_err()); 207 | } 208 | 209 | #[test] 210 | fn test_type_strip_empty_code() { 211 | let code = ""; 212 | let result = strip(code).unwrap(); 213 | assert_eq!(result, ""); 214 | } 215 | 216 | #[test] 217 | fn test_type_strip_namespace() { 218 | let code = "namespace MyNamespace {\n export const x: number = 5;\n}"; 219 | let result = strip(code); 220 | // This should error due to deprecated_ts_module_as_error being true 221 | assert!(result.is_err()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use crate::LineAndColumnDisplay; 4 | use crate::ModuleSpecifier; 5 | use crate::SourceRange; 6 | use crate::SourceRangedForSpanned; 7 | use crate::SourceTextInfo; 8 | use crate::diagnostics::Diagnostic; 9 | use crate::diagnostics::DiagnosticLevel; 10 | use crate::diagnostics::DiagnosticLocation; 11 | use crate::diagnostics::DiagnosticSnippet; 12 | use crate::diagnostics::DiagnosticSnippetHighlight; 13 | use crate::diagnostics::DiagnosticSnippetHighlightStyle; 14 | use crate::diagnostics::DiagnosticSourcePos; 15 | use crate::diagnostics::DiagnosticSourceRange; 16 | use crate::swc::parser::error::SyntaxError; 17 | use deno_error::JsError; 18 | use std::borrow::Cow; 19 | use std::fmt; 20 | 21 | /// Parsing diagnostic. 22 | #[derive(Debug, Clone, JsError)] 23 | #[class(syntax)] 24 | pub struct ParseDiagnostic(pub(crate) Box); 25 | 26 | #[derive(Debug, Clone)] 27 | pub(crate) struct ParseDiagnosticInner { 28 | pub specifier: ModuleSpecifier, 29 | pub range: SourceRange, 30 | pub kind: SyntaxError, 31 | pub source: SourceTextInfo, 32 | } 33 | 34 | impl Eq for ParseDiagnostic {} 35 | 36 | impl PartialEq for ParseDiagnostic { 37 | fn eq(&self, other: &Self) -> bool { 38 | // excludes the source 39 | self.0.specifier == other.0.specifier 40 | && self.0.range == other.0.range 41 | && self.0.kind == other.0.kind 42 | } 43 | } 44 | 45 | impl ParseDiagnostic { 46 | /// Specifier of the source the diagnostic occurred in. 47 | pub fn specifier(&self) -> &ModuleSpecifier { 48 | &self.0.specifier 49 | } 50 | 51 | /// Range of the diagnostic. 52 | pub fn range(&self) -> SourceRange { 53 | self.0.range 54 | } 55 | 56 | /// Swc syntax error 57 | pub fn kind(&self) -> &SyntaxError { 58 | &self.0.kind 59 | } 60 | 61 | pub(crate) fn source(&self) -> &SourceTextInfo { 62 | &self.0.source 63 | } 64 | 65 | /// 1-indexed display position the diagnostic occurred at. 66 | pub fn display_position(&self) -> LineAndColumnDisplay { 67 | self.0.source.line_and_column_display(self.0.range.start) 68 | } 69 | } 70 | 71 | impl Diagnostic for ParseDiagnostic { 72 | fn level(&self) -> DiagnosticLevel { 73 | DiagnosticLevel::Error 74 | } 75 | 76 | fn code(&self) -> Cow<'_, str> { 77 | Cow::Borrowed(match self.kind() { 78 | SyntaxError::Eof => "eof", 79 | SyntaxError::DeclNotAllowed => "decl-not-allowed", 80 | SyntaxError::UsingDeclNotAllowed => "using-decl-not-allowed", 81 | SyntaxError::UsingDeclNotAllowedForForInLoop => { 82 | "using-decl-not-allowed-for-for-in-loop" 83 | } 84 | SyntaxError::UsingDeclNotEnabled => "using-decl-not-enabled", 85 | SyntaxError::InvalidNameInUsingDecl => "invalid-name-in-using-decl", 86 | SyntaxError::InitRequiredForUsingDecl => "init-required-for-using-decl", 87 | SyntaxError::PrivateNameInInterface => "private-name-in-interface", 88 | SyntaxError::InvalidSuperCall => "invalid-super-call", 89 | SyntaxError::InvalidSuper => "invalid-super", 90 | SyntaxError::InvalidSuperPrivateName => "invalid-super-private-name", 91 | SyntaxError::InvalidNewTarget => "invalid-new-target", 92 | SyntaxError::InvalidImport => "invalid-import", 93 | SyntaxError::ArrowNotAllowed => "arrow-not-allowed", 94 | SyntaxError::ExportNotAllowed => "export-not-allowed", 95 | SyntaxError::GetterSetterCannotBeReadonly => { 96 | "getter-setter-cannot-be-readonly" 97 | } 98 | SyntaxError::GetterParam => "getter-param", 99 | SyntaxError::SetterParam => "setter-param", 100 | SyntaxError::TopLevelAwaitInScript => "top-level-await-in-script", 101 | SyntaxError::LegacyDecimal => "legacy-decimal", 102 | SyntaxError::LegacyOctal => "legacy-octal", 103 | SyntaxError::InvalidIdentChar => "invalid-ident-char", 104 | SyntaxError::ExpectedDigit { .. } => "expected-digit", 105 | SyntaxError::SetterParamRequired => "setter-param-required", 106 | SyntaxError::RestPatInSetter => "rest-pat-in-setter", 107 | SyntaxError::UnterminatedBlockComment => "unterminated-block-comment", 108 | SyntaxError::UnterminatedStrLit => "unterminated-str-lit", 109 | SyntaxError::ExpectedUnicodeEscape => "expected-unicode-escape", 110 | SyntaxError::EscapeInReservedWord { .. } => "escape-in-reserved-word", 111 | SyntaxError::UnterminatedRegExp => "unterminated-reg-exp", 112 | SyntaxError::UnterminatedTpl => "unterminated-tpl", 113 | SyntaxError::IdentAfterNum => "ident-after-num", 114 | SyntaxError::UnexpectedChar { .. } => "unexpected-char", 115 | SyntaxError::InvalidStrEscape => "invalid-str-escape", 116 | SyntaxError::InvalidUnicodeEscape => "invalid-unicode-escape", 117 | SyntaxError::BadCharacterEscapeSequence { .. } => { 118 | "bad-character-escape-sequence" 119 | } 120 | SyntaxError::NumLitTerminatedWithExp => "num-lit-terminated-with-exp", 121 | SyntaxError::LegacyCommentInModule => "legacy-comment-in-module", 122 | SyntaxError::InvalidIdentInStrict(_) => "invalid-ident-in-strict", 123 | SyntaxError::InvalidIdentInAsync => "invalid-ident-in-async", 124 | SyntaxError::EvalAndArgumentsInStrict => "eval-and-arguments-in-strict", 125 | SyntaxError::ArgumentsInClassField => "arguments-in-class-field", 126 | SyntaxError::IllegalLanguageModeDirective => { 127 | "illegal-language-mode-directive" 128 | } 129 | SyntaxError::UnaryInExp { .. } => "unary-in-exp", 130 | SyntaxError::Hash => "hash", 131 | SyntaxError::LineBreakInThrow => "line-break-in-throw", 132 | SyntaxError::LineBreakBeforeArrow => "line-break-before-arrow", 133 | SyntaxError::Unexpected { .. } => "unexpected", 134 | SyntaxError::UnexpectedTokenWithSuggestions { .. } => { 135 | "unexpected-token-with-suggestions" 136 | } 137 | SyntaxError::ReservedWordInImport => "reserved-word-in-import", 138 | SyntaxError::AssignProperty => "assign-property", 139 | SyntaxError::Expected(_, _) => "expected", 140 | SyntaxError::ExpectedSemiForExprStmt { .. } => { 141 | "expected-semi-for-expr-stmt" 142 | } 143 | SyntaxError::AwaitStar => "await-star", 144 | SyntaxError::ReservedWordInObjShorthandOrPat => { 145 | "reserved-word-in-obj-shorthand-or-pat" 146 | } 147 | SyntaxError::NullishCoalescingWithLogicalOp => { 148 | "nullish-coalescing-with-logical-op" 149 | } 150 | SyntaxError::MultipleDefault { .. } => "multiple-default", 151 | SyntaxError::CommaAfterRestElement => "comma-after-rest-element", 152 | SyntaxError::NonLastRestParam => "non-last-rest-param", 153 | SyntaxError::SpreadInParenExpr => "spread-in-paren-expr", 154 | SyntaxError::EmptyParenExpr => "empty-paren-expr", 155 | SyntaxError::InvalidPat => "invalid-pat", 156 | SyntaxError::InvalidExpr => "invalid-expr", 157 | SyntaxError::NotSimpleAssign => "not-simple-assign", 158 | SyntaxError::ExpectedIdent => "expected-ident", 159 | SyntaxError::ExpectedSemi => "expected-semi", 160 | SyntaxError::DuplicateLabel(_) => "duplicate-label", 161 | SyntaxError::AsyncGenerator => "async-generator", 162 | SyntaxError::NonTopLevelImportExport => "non-top-level-import-export", 163 | SyntaxError::ImportExportInScript => "import-export-in-script", 164 | SyntaxError::ImportMetaInScript => "import-meta-in-script", 165 | SyntaxError::PatVarWithoutInit => "pat-var-without-init", 166 | SyntaxError::WithInStrict => "with-in-strict", 167 | SyntaxError::ReturnNotAllowed => "return-not-allowed", 168 | SyntaxError::TooManyVarInForInHead => "too-many-var-in-for-in-head", 169 | SyntaxError::VarInitializerInForInHead => { 170 | "var-initializer-in-for-in-head" 171 | } 172 | SyntaxError::LabelledGeneratorOrAsync => "labelled-generator-or-async", 173 | SyntaxError::LabelledFunctionInStrict => "labelled-function-in-strict", 174 | SyntaxError::YieldParamInGen => "yield-param-in-gen", 175 | SyntaxError::AwaitParamInAsync => "await-param-in-async", 176 | SyntaxError::AwaitForStmt => "await-for-stmt", 177 | SyntaxError::AwaitInFunction => "await-in-function", 178 | SyntaxError::UnterminatedJSXContents => "unterminated-jsx-contents", 179 | SyntaxError::EmptyJSXAttr => "empty-jsx-attr", 180 | SyntaxError::InvalidJSXValue => "invalid-jsx-value", 181 | SyntaxError::JSXExpectedClosingTagForLtGt => { 182 | "jsx-expected-closing-tag-for-lt-gt" 183 | } 184 | SyntaxError::JSXExpectedClosingTag { .. } => "jsx-expected-closing-tag", 185 | SyntaxError::InvalidLeadingDecorator => "invalid-leading-decorator", 186 | SyntaxError::DecoratorOnExport => "decorator-on-export", 187 | SyntaxError::TsRequiredAfterOptional => "ts-required-after-optional", 188 | SyntaxError::TsInvalidParamPropPat => "ts-invalid-param-prop-pat", 189 | SyntaxError::SpaceBetweenHashAndIdent => "space-between-hash-and-ident", 190 | SyntaxError::AsyncConstructor => "async-constructor", 191 | SyntaxError::PropertyNamedConstructor => "property-named-constructor", 192 | SyntaxError::PrivateConstructor => "private-constructor", 193 | SyntaxError::PrivateNameModifier(_) => "private-name-modifier", 194 | SyntaxError::ConstructorAccessor => "constructor-accessor", 195 | SyntaxError::ReadOnlyMethod => "read-only-method", 196 | SyntaxError::GeneratorConstructor => "generator-constructor", 197 | SyntaxError::DuplicateConstructor => "duplicate-constructor", 198 | SyntaxError::TsBindingPatCannotBeOptional => { 199 | "ts-binding-pat-cannot-be-optional" 200 | } 201 | SyntaxError::SuperCallOptional => "super-call-optional", 202 | SyntaxError::OptChainCannotFollowConstructorCall => { 203 | "opt-chain-cannot-follow-constructor-call" 204 | } 205 | SyntaxError::TaggedTplInOptChain => "tagged-tpl-in-opt-chain", 206 | SyntaxError::TrailingCommaInsideImport => "trailing-comma-inside-import", 207 | SyntaxError::ExportDefaultWithOutFrom => "export-default-without-from", 208 | SyntaxError::ExportExpectFrom(_) => "export-expect-from", 209 | SyntaxError::DotsWithoutIdentifier => "dots-without-identifier", 210 | SyntaxError::NumericSeparatorIsAllowedOnlyBetweenTwoDigits => { 211 | "numeric-separator-allowed-only-between-two-digits" 212 | } 213 | SyntaxError::ImportBindingIsString(_) => "import-binding-is-string", 214 | SyntaxError::ExportBindingIsString => "export-binding-is-string", 215 | SyntaxError::ConstDeclarationsRequireInitialization => { 216 | "const-declarations-require-initialization" 217 | } 218 | SyntaxError::DuplicatedRegExpFlags(_) => "duplicated-reg-exp-flags", 219 | SyntaxError::UnknownRegExpFlags => "unknown-reg-exp-flags", 220 | SyntaxError::TS1003 => "TS1003", 221 | SyntaxError::TS1005 => "TS1005", 222 | SyntaxError::TS1009 => "TS1009", 223 | SyntaxError::TS1014 => "TS1014", 224 | SyntaxError::TS1015 => "TS1015", 225 | SyntaxError::TS1029(_, _) => "TS1029", 226 | SyntaxError::TS1030(_) => "TS1030", 227 | SyntaxError::TS1031 => "TS1031", 228 | SyntaxError::TS1038 => "TS1038", 229 | SyntaxError::TS1042 => "TS1042", 230 | SyntaxError::TS1047 => "TS1047", 231 | SyntaxError::TS1048 => "TS1048", 232 | SyntaxError::TS1056 => "TS1056", 233 | SyntaxError::TS1085 => "TS1085", 234 | SyntaxError::TS1089(_) => "TS1089", 235 | SyntaxError::TS1092 => "TS1092", 236 | SyntaxError::TS1096 => "TS1096", 237 | SyntaxError::TS1098 => "TS1098", 238 | SyntaxError::TS1100 => "TS1100", 239 | SyntaxError::TS1102 => "TS1102", 240 | SyntaxError::TS1105 => "TS1105", 241 | SyntaxError::TS1106 => "TS1106", 242 | SyntaxError::TS1107 => "TS1107", 243 | SyntaxError::TS1109 => "TS1109", 244 | SyntaxError::TS1110 => "TS1110", 245 | SyntaxError::TS1114 => "TS1114", 246 | SyntaxError::TS1115 => "TS1115", 247 | SyntaxError::TS1116 => "TS1116", 248 | SyntaxError::TS1123 => "TS1123", 249 | SyntaxError::TS1141 => "TS1141", 250 | SyntaxError::TS1162 => "TS1162", 251 | SyntaxError::TS1164 => "TS1164", 252 | SyntaxError::TS1171 => "TS1171", 253 | SyntaxError::TS1172 => "TS1172", 254 | SyntaxError::TS1173 => "TS1173", 255 | SyntaxError::TS1174 => "TS1174", 256 | SyntaxError::TS1175 => "TS1175", 257 | SyntaxError::TS1183 => "TS1183", 258 | SyntaxError::TS1184 => "TS1184", 259 | SyntaxError::TS1185 => "TS1185", 260 | SyntaxError::TS1093 => "TS1093", 261 | SyntaxError::TS1196 => "TS1196", 262 | SyntaxError::TS1242 => "TS1242", 263 | SyntaxError::TS1243(_, _) => "TS1243", 264 | SyntaxError::TS1244 => "TS1244", 265 | SyntaxError::TS1245 => "TS1245", 266 | SyntaxError::TS1267 => "TS1267", 267 | SyntaxError::TS1273(_) => "TS1273", 268 | SyntaxError::TS1274(_) => "TS1274", 269 | SyntaxError::TS1277(_) => "TS1277", 270 | SyntaxError::TS2206 => "TS2206", 271 | SyntaxError::TS2207 => "TS2207", 272 | SyntaxError::TS2369 => "TS2369", 273 | SyntaxError::TS2371 => "TS2371", 274 | SyntaxError::TS2406 => "TS2406", 275 | SyntaxError::TS2410 => "TS2410", 276 | SyntaxError::TS2414 => "TS2414", 277 | SyntaxError::TS2427 => "TS2427", 278 | SyntaxError::TS2452 => "TS2452", 279 | SyntaxError::TS2483 => "TS2483", 280 | SyntaxError::TS2491 => "TS2491", 281 | SyntaxError::TS2499 => "TS2499", 282 | SyntaxError::TS2703 => "TS2703", 283 | SyntaxError::TS4112 => "TS4112", 284 | SyntaxError::TS8038 => "TS8038", 285 | SyntaxError::TSTypeAnnotationAfterAssign => { 286 | "ts-type-annotation-after-assign" 287 | } 288 | SyntaxError::TsNonNullAssertionNotAllowed(_) => { 289 | "ts-non-null-assertion-not-allowed" 290 | } 291 | SyntaxError::WithLabel { .. } => "with-label", 292 | SyntaxError::ReservedTypeAssertion => "reserved-type-assertion", 293 | SyntaxError::ReservedArrowTypeParam => "reserved-arrow-type-param", 294 | _ => "unknown", 295 | }) 296 | } 297 | 298 | fn message(&self) -> Cow<'_, str> { 299 | self.kind().msg() 300 | } 301 | 302 | fn location(&self) -> DiagnosticLocation<'_> { 303 | DiagnosticLocation::ModulePosition { 304 | specifier: Cow::Borrowed(self.specifier()), 305 | source_pos: DiagnosticSourcePos::SourcePos(self.range().start), 306 | text_info: Cow::Borrowed(self.source()), 307 | } 308 | } 309 | 310 | fn snippet(&self) -> Option> { 311 | let range = self.range(); 312 | Some(DiagnosticSnippet { 313 | source: Cow::Borrowed(self.source()), 314 | highlights: vec![DiagnosticSnippetHighlight { 315 | style: DiagnosticSnippetHighlightStyle::Error, 316 | range: DiagnosticSourceRange { 317 | start: DiagnosticSourcePos::SourcePos(range.start), 318 | end: DiagnosticSourcePos::SourcePos(range.end), 319 | }, 320 | description: None, 321 | }], 322 | }) 323 | } 324 | 325 | fn hint(&self) -> Option> { 326 | None 327 | } 328 | 329 | fn snippet_fixed(&self) -> Option> { 330 | None 331 | } 332 | 333 | fn info(&self) -> Cow<'_, [Cow<'_, str>]> { 334 | Cow::Borrowed(&[]) 335 | } 336 | 337 | fn docs_url(&self) -> Option> { 338 | None 339 | } 340 | } 341 | 342 | impl ParseDiagnostic { 343 | pub fn from_swc_error( 344 | err: crate::swc::parser::error::Error, 345 | specifier: &ModuleSpecifier, 346 | source: SourceTextInfo, 347 | ) -> ParseDiagnostic { 348 | ParseDiagnostic(Box::new(ParseDiagnosticInner { 349 | range: err.range(), 350 | specifier: specifier.clone(), 351 | kind: err.into_kind(), 352 | source, 353 | })) 354 | } 355 | } 356 | 357 | impl std::error::Error for ParseDiagnostic {} 358 | 359 | impl fmt::Display for ParseDiagnostic { 360 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 361 | let display_position = self.display_position(); 362 | write!( 363 | f, 364 | "{} at {}:{}:{}\n\n{}", 365 | self.message(), 366 | self.specifier(), 367 | display_position.line_number, 368 | display_position.column_number, 369 | // todo(dsherret): remove this catch unwind once we've 370 | // tested this out a lot 371 | std::panic::catch_unwind(|| { 372 | get_range_text_highlight(self.source(), self.range()) 373 | .lines() 374 | // indent two spaces 375 | .map(|l| { 376 | if l.trim().is_empty() { 377 | String::new() 378 | } else { 379 | format!(" {}", l) 380 | } 381 | }) 382 | .collect::>() 383 | .join("\n") 384 | }) 385 | .unwrap_or_else(|err| { 386 | format!("Bug in Deno. Please report this issue: {:?}", err) 387 | }), 388 | ) 389 | } 390 | } 391 | 392 | #[derive(Debug, JsError)] 393 | #[class(syntax)] 394 | pub struct ParseDiagnosticsError(pub Vec); 395 | 396 | impl std::error::Error for ParseDiagnosticsError {} 397 | 398 | impl fmt::Display for ParseDiagnosticsError { 399 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 400 | for (i, diagnostic) in self.0.iter().enumerate() { 401 | if i > 0 { 402 | write!(f, "\n\n")?; 403 | } 404 | 405 | write!(f, "{}", diagnostic)? 406 | } 407 | 408 | Ok(()) 409 | } 410 | } 411 | 412 | /// Code in this function was adapted from: 413 | /// https://github.com/dprint/dprint/blob/a026a1350d27a61ea18207cb31897b18eaab51a1/crates/core/src/formatting/utils/string_utils.rs#L62 414 | fn get_range_text_highlight( 415 | source: &SourceTextInfo, 416 | byte_range: SourceRange, 417 | ) -> String { 418 | fn get_column_index_of_pos(text: &str, pos: usize) -> usize { 419 | let line_start_byte_pos = get_line_start_byte_pos(text, pos); 420 | text[line_start_byte_pos..pos].chars().count() 421 | } 422 | 423 | fn get_line_start_byte_pos(text: &str, pos: usize) -> usize { 424 | let text_bytes = text.as_bytes(); 425 | for i in (0..pos).rev() { 426 | if text_bytes.get(i) == Some(&(b'\n')) { 427 | return i + 1; 428 | } 429 | } 430 | 431 | 0 432 | } 433 | 434 | fn get_text_and_error_range( 435 | source: &SourceTextInfo, 436 | byte_range: SourceRange, 437 | ) -> (&str, (usize, usize)) { 438 | let mut first_line_index = source.line_index(byte_range.start); 439 | let mut first_line_start = source.line_start(first_line_index); 440 | let last_line_end = source.line_end(source.line_index(byte_range.end)); 441 | let mut sub_text = 442 | source.range_text(&SourceRange::new(first_line_start, last_line_end)); 443 | 444 | // while the text is empty, show the previous line 445 | while sub_text.trim().is_empty() && first_line_index > 0 { 446 | first_line_index -= 1; 447 | first_line_start = source.line_start(first_line_index); 448 | sub_text = 449 | source.range_text(&SourceRange::new(first_line_start, last_line_end)); 450 | } 451 | 452 | let error_start = byte_range.start - first_line_start; 453 | let error_end = error_start + (byte_range.end - byte_range.start); 454 | (sub_text, (error_start, error_end)) 455 | } 456 | 457 | let (sub_text, (error_start, error_end)) = 458 | get_text_and_error_range(source, byte_range); 459 | 460 | let mut result = String::new(); 461 | // don't use .lines() here because it will trim any empty 462 | // lines, which might for some reason be part of the range 463 | let lines = sub_text.split('\n').collect::>(); 464 | let line_count = lines.len(); 465 | for (i, mut line) in lines.into_iter().enumerate() { 466 | if line.ends_with('\r') { 467 | line = &line[..line.len() - 1]; // trim the \r 468 | } 469 | let is_last_line = i == line_count - 1; 470 | // don't show all the lines if there are more than 3 lines 471 | if i > 2 && !is_last_line { 472 | continue; 473 | } 474 | if i > 0 { 475 | result.push('\n'); 476 | } 477 | if i == 2 && !is_last_line { 478 | result.push_str("..."); 479 | continue; 480 | } 481 | 482 | let mut error_start_char_index = if i == 0 { 483 | get_column_index_of_pos(sub_text, error_start) 484 | } else { 485 | 0 486 | }; 487 | let mut error_end_char_index = if is_last_line { 488 | get_column_index_of_pos(sub_text, error_end) 489 | } else { 490 | line.chars().count() 491 | }; 492 | let line_char_count = line.chars().count(); 493 | if line_char_count > 90 { 494 | let start_char_index = if error_start_char_index > 60 { 495 | std::cmp::min(error_start_char_index - 20, line_char_count - 80) 496 | } else { 497 | 0 498 | }; 499 | error_start_char_index -= start_char_index; 500 | error_end_char_index -= start_char_index; 501 | let code_text = line 502 | .chars() 503 | .skip(start_char_index) 504 | .take(80) 505 | .collect::(); 506 | let mut line_text = String::new(); 507 | if start_char_index > 0 { 508 | line_text.push_str("..."); 509 | error_start_char_index += 3; 510 | error_end_char_index += 3; 511 | } 512 | line_text.push_str(&code_text); 513 | if line_char_count > start_char_index + code_text.chars().count() { 514 | error_end_char_index = 515 | std::cmp::min(error_end_char_index, line_text.chars().count()); 516 | line_text.push_str("..."); 517 | } 518 | result.push_str(&line_text); 519 | } else { 520 | result.push_str(line); 521 | } 522 | result.push('\n'); 523 | 524 | result.push_str(&" ".repeat(error_start_char_index)); 525 | result.push_str(&"~".repeat(std::cmp::max( 526 | 1, // this means it's the end of the line, so display a single ~ 527 | error_end_char_index - error_start_char_index, 528 | ))); 529 | } 530 | result 531 | } 532 | 533 | #[cfg(test)] 534 | mod test { 535 | use dprint_swc_ext::common::SourceRange; 536 | use dprint_swc_ext::common::SourceTextInfo; 537 | use pretty_assertions::assert_eq; 538 | 539 | use super::get_range_text_highlight; 540 | 541 | #[test] 542 | fn range_highlight_all_text() { 543 | let text = SourceTextInfo::from_string( 544 | concat!( 545 | "Line 0 - Testing this out with a long line testing0 testing1 testing2 testing3 testing4 testing5 testing6\n", 546 | "Line 1\n", 547 | "Line 2\n", 548 | "Line 3\n", 549 | "Line 4" 550 | ).to_string(), 551 | ); 552 | assert_eq!( 553 | get_range_text_highlight( 554 | &text, 555 | SourceRange::new(text.line_start(0), text.line_end(4)) 556 | ), 557 | concat!( 558 | "Line 0 - Testing this out with a long line testing0 testing1 testing2 testing3 t...\n", 559 | "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", 560 | "Line 1\n", 561 | "~~~~~~\n", 562 | "...\n", 563 | "Line 4\n", 564 | "~~~~~~", 565 | ), 566 | ); 567 | } 568 | 569 | #[test] 570 | fn range_highlight_all_text_last_line_long() { 571 | let text = SourceTextInfo::from_string( 572 | concat!( 573 | "Line 0\n", 574 | "Line 1\n", 575 | "Line 2\n", 576 | "Line 3\n", 577 | "Line 4 - Testing this out with a long line testing0 testing1 testing2 testing3 testing4 testing5 testing6\n", 578 | ).to_string(), 579 | ); 580 | assert_eq!( 581 | get_range_text_highlight( 582 | &text, 583 | SourceRange::new(text.line_start(0), text.line_end(4)) 584 | ), 585 | concat!( 586 | "Line 0\n", 587 | "~~~~~~\n", 588 | "Line 1\n", 589 | "~~~~~~\n", 590 | "...\n", 591 | "Line 4 - Testing this out with a long line testing0 testing1 testing2 testing3 t...\n", 592 | "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", 593 | ), 594 | ); 595 | } 596 | 597 | #[test] 598 | fn range_highlight_range_start_long_line() { 599 | let text = SourceTextInfo::from_string( 600 | "Testing this out with a long line testing0 testing1 testing2 testing3 testing4 testing5 testing6 testing7".to_string(), 601 | ); 602 | assert_eq!( 603 | get_range_text_highlight( 604 | &text, 605 | SourceRange::new(text.line_start(0), text.line_start(0) + 1) 606 | ), 607 | concat!( 608 | "Testing this out with a long line testing0 testing1 testing2 testing3 testing4 t...\n", 609 | "~", 610 | ), 611 | ); 612 | } 613 | 614 | #[test] 615 | fn range_highlight_range_end_long_line() { 616 | let text = SourceTextInfo::from_string( 617 | "Testing this out with a long line testing0 testing1 testing2 testing3 testing4 testing5 testing6 testing7".to_string(), 618 | ); 619 | assert_eq!( 620 | get_range_text_highlight( 621 | &text, 622 | SourceRange::new(text.line_end(0) - 1, text.line_end(0)) 623 | ), 624 | concat!( 625 | "...ong line testing0 testing1 testing2 testing3 testing4 testing5 testing6 testing7\n", 626 | " ~", 627 | ), 628 | ); 629 | } 630 | 631 | #[test] 632 | fn range_highlight_whitespace_start_line() { 633 | let text = SourceTextInfo::from_string(" testing\r\ntest".to_string()); 634 | assert_eq!( 635 | get_range_text_highlight( 636 | &text, 637 | SourceRange::new(text.line_end(0) - 1, text.line_end(1)) 638 | ), 639 | concat!(" testing\n", " ~\n", "test\n", "~~~~",), 640 | ); 641 | } 642 | 643 | #[test] 644 | fn range_end_of_line() { 645 | let text = 646 | SourceTextInfo::from_string(" testingtestingtestingtesting".to_string()); 647 | assert_eq!( 648 | get_range_text_highlight( 649 | &text, 650 | SourceRange::new(text.line_end(0), text.line_end(0)) 651 | ), 652 | concat!( 653 | " testingtestingtestingtesting\n", 654 | " ~", 655 | ), 656 | ); 657 | } 658 | } 659 | --------------------------------------------------------------------------------