├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── ext │ └── coffee-script.ts ├── index.ts ├── mappers │ ├── mapAny.ts │ ├── mapArr.ts │ ├── mapAssign.ts │ ├── mapBlock.ts │ ├── mapCSX.ts │ ├── mapCall.ts │ ├── mapClass.ts │ ├── mapCode.ts │ ├── mapComputedPropertyName.ts │ ├── mapElision.ts │ ├── mapExistence.ts │ ├── mapExpansion.ts │ ├── mapExtends.ts │ ├── mapFor.ts │ ├── mapIf.ts │ ├── mapIn.ts │ ├── mapLiteral.ts │ ├── mapModuleDeclaration.ts │ ├── mapObj.ts │ ├── mapOp.ts │ ├── mapParam.ts │ ├── mapParens.ts │ ├── mapPossiblyEmptyBlock.ts │ ├── mapProgram.ts │ ├── mapRange.ts │ ├── mapReturn.ts │ ├── mapSplat.ts │ ├── mapSuper.ts │ ├── mapSwitch.ts │ ├── mapTaggedTemplateCall.ts │ ├── mapThrow.ts │ ├── mapTry.ts │ ├── mapValue.ts │ └── mapWhile.ts ├── nodes.ts ├── parseCS1AsCS2.ts ├── parseCS2.ts ├── parser.ts └── util │ ├── ParseContext.ts │ ├── UnsupportedNodeError.ts │ ├── expandToIncludeParens.ts │ ├── fixInvalidLocationData.ts │ ├── fixLocations.ts │ ├── getLocation.ts │ ├── getOperatorInfoInRange.ts │ ├── getTemplateLiteralComponents.ts │ ├── isChainedComparison.ts │ ├── isCommentOnlyNode.ts │ ├── isComparisonOperator.ts │ ├── isHeregexTemplateNode.ts │ ├── isImplicitPlusOp.ts │ ├── isPlusTokenBetweenRanges.ts │ ├── locationDataFromSourceRange.ts │ ├── locationWithLastPosition.ts │ ├── locationsEqual.ts │ ├── makeHeregex.ts │ ├── makeString.ts │ ├── mergeLocations.ts │ ├── notNull.ts │ ├── parseNumber.ts │ ├── parseRegExp.ts │ ├── parseString.ts │ ├── rangeOfBracketTokensForIndexNode.ts │ ├── sourceRangeFromLocationData.ts │ └── unwindChainedComparison.ts ├── test ├── __snapshots__ │ └── examples.test.ts.snap ├── cs1-examples │ ├── block-comment-in-function.coffee │ ├── class-extends.coffee │ ├── class-member-with-block-comment.coffee │ ├── class-super-call.coffee │ ├── function-ending-in-block-comment.coffee │ ├── heregex-with-interpolations-and-flags.coffee │ └── object-with-block-comments.coffee ├── cs2-examples │ ├── await-bound-function.coffee │ ├── await-return.coffee │ ├── await.coffee │ ├── block-comment-in-function.coffee │ ├── class-member-with-block-comment.coffee │ ├── complex-computed-object-key.coffee │ ├── computed-object-key.coffee │ ├── computed-shorthand-object-key.coffee │ ├── csx-fragment.coffee │ ├── csx-simple.coffee │ ├── csx-with-interpolation.coffee │ ├── csx-with-props.coffee │ ├── elision.coffee │ ├── function-ending-in-block-comment.coffee │ ├── heregex-with-interpolations-and-flags.coffee │ ├── left-side-spread.coffee │ ├── object-spread.coffee │ ├── object-with-block-comments.coffee │ ├── super-index-access.coffee │ └── super-property-access.coffee ├── examples.test.ts ├── examples │ ├── addition.coffee │ ├── array-with-multiple-members.coffee │ ├── array-with-single-member.coffee │ ├── assign.coffee │ ├── backticks-with-string-inside.coffee │ ├── bare-yield.coffee │ ├── bitshift-left.coffee │ ├── bitshift-right-unsigned.coffee │ ├── bitshift-right.coffee │ ├── bitwise-and.coffee │ ├── bitwise-or.coffee │ ├── bitwise-xor.coffee │ ├── block-comment-only-file.coffee │ ├── block-comment.coffee │ ├── bound-function-with-parameters.coffee │ ├── bound-generator-function.coffee │ ├── break.coffee │ ├── call-dynamic-member-access-result.coffee │ ├── chain-calls-with-parens.coffee │ ├── chain-calls-without-indent.coffee │ ├── chain-calls-without-parens.coffee │ ├── chained-comparison-equals.coffee │ ├── chained-comparison-extended.coffee │ ├── chained-comparison-greater-than.coffee │ ├── chained-comparison-less-than.coffee │ ├── chained-comparison-mixed.coffee │ ├── chained-comparison-nested.coffee │ ├── chained-comparison-not-equal.coffee │ ├── chained-comparison-three.coffee │ ├── chained-comparison-with-increment.coffee │ ├── chained-comparison-with-other-operators.coffee │ ├── chained-comparison-with-unary-negate.coffee │ ├── chained-prototype-member-access.coffee │ ├── class-with-body-statements.coffee │ ├── class-with-bound-methods.coffee │ ├── class-with-conditional-bound-method.coffee │ ├── class-with-conditional-method.coffee │ ├── class-with-constructor.coffee │ ├── class-with-explicit-object-literal.coffee │ ├── class-with-implicit-object-literal-within-function.coffee │ ├── class-with-members.coffee │ ├── class-with-non-identifier-assignee.coffee │ ├── class-with-outer-assign-blocking-conditional-assign.coffee │ ├── class-with-parenthesized-value.coffee │ ├── class-with-proto-access-name.coffee │ ├── class-with-static-members.coffee │ ├── comment-in-parenthesized-block.coffee │ ├── comment-only-file.coffee │ ├── complex-template-literal.coffee │ ├── compound-assignment-addition.coffee │ ├── compound-assignment-subtraction.coffee │ ├── conditional-empty-consequent-alternate.coffee │ ├── conditional-ending-in-semicolon.coffee │ ├── conditional-on-one-line.coffee │ ├── conditional-unless-equal-condition.coffee │ ├── conditional-unless-exists-op.coffee │ ├── conditional-unless-virtual-parens.coffee │ ├── conditional-using-unless.coffee │ ├── conditional-with-alternate.coffee │ ├── conditional-with-braces-on-one-line.coffee │ ├── conditional-with-post-if.coffee │ ├── conditional-with-post-unless.coffee │ ├── conditional-without-alternate.coffee │ ├── continue.coffee │ ├── dangling-prototype-access-of-call.coffee │ ├── dangling-prototype-access.coffee │ ├── debugger.coffee │ ├── delete.coffee │ ├── destructure-this-assignment.coffee │ ├── division.coffee │ ├── do-expression.coffee │ ├── do-with-assign-and-defaults.coffee │ ├── do-with-assign-expression.coffee │ ├── do-with-bound-function.coffee │ ├── do-with-default-parameters.coffee │ ├── do-with-defaults-with-same-name.coffee │ ├── do-with-simple-parameters.coffee │ ├── do.coffee │ ├── double-negation.coffee │ ├── dynamic-member-expressions.coffee │ ├── empty-anonymous-class.coffee │ ├── empty-array.coffee │ ├── empty-bound-function-without-body.coffee │ ├── empty-class-with-superclass.coffee │ ├── empty-class.coffee │ ├── empty-function-without-parameters.coffee │ ├── empty-heregex-interpolation.coffee │ ├── empty-loop.coffee │ ├── empty-object.coffee │ ├── empty-program.coffee │ ├── empty-string-interpolation.coffee │ ├── equality-longhand.coffee │ ├── equality.coffee │ ├── existential-binary.coffee │ ├── existential-unary.coffee │ ├── expansion.coffee │ ├── export-assignment.coffee │ ├── export-default-from-source.coffee │ ├── export-default.coffee │ ├── export-multiple-bindings.coffee │ ├── export-star.coffee │ ├── external-constructor.coffee │ ├── false.coffee │ ├── float-int-value.coffee │ ├── float-leading-period.coffee │ ├── float.coffee │ ├── floor-division.coffee │ ├── for-comprehension.coffee │ ├── for-from.coffee │ ├── for-in-by.coffee │ ├── for-in-when.coffee │ ├── for-in-with-key-and-value-assignees.coffee │ ├── for-in.coffee │ ├── for-of-expression.coffee │ ├── for-of-when.coffee │ ├── for-of-with-key-and-value-assignees.coffee │ ├── for-of.coffee │ ├── for-own-of.coffee │ ├── for-repeater.coffee │ ├── function-ending-in-semicolon.coffee │ ├── function-followed-by-block-comment.coffee │ ├── function-trailing-spaces.coffee │ ├── function-with-body.coffee │ ├── function-with-default-parameter.coffee │ ├── function-with-parameters.coffee │ ├── function-with-statement-after-block-and-comments.coffee │ ├── greater-than-equal.coffee │ ├── greater-than.coffee │ ├── heregex-in-method-call.coffee │ ├── heregex-with-flags.coffee │ ├── heregex-with-interpolations.coffee │ ├── heregex-with-spaces-and-comments.coffee │ ├── heregex-with-strange-whitespace.coffee │ ├── heregex.coffee │ ├── hexidecimal-number.coffee │ ├── iife-in-function-call.coffee │ ├── implicit-object-with-trailing-comma.coffee │ ├── import-default.coffee │ ├── import-named-with-alias.coffee │ ├── import-star.coffee │ ├── import-without-specifiers.coffee │ ├── in-not.coffee │ ├── in.coffee │ ├── instanceof-not.coffee │ ├── instanceof.coffee │ ├── integer.coffee │ ├── js.coffee │ ├── keyword-member-access.coffee │ ├── less-than-equal.coffee │ ├── less-than.coffee │ ├── logical-and-longform.coffee │ ├── logical-and.coffee │ ├── logical-or-longform.coffee │ ├── logical-or.coffee │ ├── loop.coffee │ ├── many-expressions-in-parens.coffee │ ├── modulo.coffee │ ├── multiline-interpolated-string-with-escaped-newline.coffee │ ├── multiline-string-with-interpolations-and-quotes.coffee │ ├── multiline-string-with-quoted-interpolations-and-non-interpolations.coffee │ ├── multiple-expressions-in-parens.coffee │ ├── multiplication.coffee │ ├── negated-equality-longhand.coffee │ ├── negated-equality.coffee │ ├── negation-with-not.coffee │ ├── negation.coffee │ ├── nested-code-with-outdent.coffee │ ├── nested-conditionals.coffee │ ├── nested-member-expressions.coffee │ ├── nested-object-literals.coffee │ ├── nested-object-with-inner-semicolon.coffee │ ├── nested-string-interpolation.coffee │ ├── new-with-method-call.coffee │ ├── new-without-parens.coffee │ ├── new.coffee │ ├── null.coffee │ ├── number-object-key.coffee │ ├── object-destructure-with-default.coffee │ ├── object-with-braces.coffee │ ├── object-with-combined-key-value.coffee │ ├── object-with-multiple-properties.coffee │ ├── object-with-parenthesized-value.coffee │ ├── object-without-braces.coffee │ ├── octal-number.coffee │ ├── of-not.coffee │ ├── of.coffee │ ├── only-empty-string-interpolation.coffee │ ├── parentheses.coffee │ ├── parenthesized-arrow-function.coffee │ ├── parenthesized-break-in-postfix-while.coffee │ ├── parenthesized-member-access.coffee │ ├── post-decrement.coffee │ ├── post-for.coffee │ ├── post-increment.coffee │ ├── post-unless-not-if.coffee │ ├── post-while-with-loop.coffee │ ├── pow.coffee │ ├── pre-decrement.coffee │ ├── pre-increment.coffee │ ├── prototype-member-access.coffee │ ├── range-exclusive.coffee │ ├── range-inclusive.coffee │ ├── regex-with-flags.coffee │ ├── regexp.coffee │ ├── remainder.coffee │ ├── rest-param-in-bound-function.coffee │ ├── rest-param-in-function.coffee │ ├── return-with-expression.coffee │ ├── return-without-expression.coffee │ ├── shorthand-object-with-interpolated-strings.coffee │ ├── shorthand-object-with-strings.coffee │ ├── shorthand-object-with-this.coffee │ ├── shorthand-this-member-expression-with-dot.coffee │ ├── shorthand-this-member-expression.coffee │ ├── shorthand-this.coffee │ ├── simple-call.coffee │ ├── simple-member-expression.coffee │ ├── slice-with-lower-and-upper-bounds.coffee │ ├── slice-with-no-bounds.coffee │ ├── soaked-dynamic-member-access.coffee │ ├── soaked-function-call.coffee │ ├── soaked-member-access.coffee │ ├── soaked-method-call.coffee │ ├── soaked-new.coffee │ ├── soaked-prototype-access.coffee │ ├── soaked-slice.coffee │ ├── soaked-splice.coffee │ ├── splat-in-array-with-other-members.coffee │ ├── splat-in-array.coffee │ ├── splat-in-function-call-with-other-args.coffee │ ├── splat-in-function-call.coffee │ ├── splat-in-new-call.coffee │ ├── string-ending-with-interpolation.coffee │ ├── string-interpolation-in-object-literal.coffee │ ├── string-interpolation-plus-normal-string.coffee │ ├── string-interpolation-preceded-by-parenthesis.coffee │ ├── string-interpolation-with-escaped-newline.coffee │ ├── string-interpolation-with-plus.coffee │ ├── string-starting-with-interpolation.coffee │ ├── string-with-double-quotes.coffee │ ├── string-with-interpolation.coffee │ ├── string-with-interpolations-at-start-and-end.coffee │ ├── string-with-noop-escape.coffee │ ├── string-with-only-multiple-interpolations.coffee │ ├── string-with-only-single-interpolation.coffee │ ├── string-with-parentheses-inside.coffee │ ├── string-with-single-quotes.coffee │ ├── string-with-triple-double-quotes.coffee │ ├── string-with-triple-quote-interpolation-containing-quotes.coffee │ ├── string-with-triple-quote-interpolation.coffee │ ├── string-with-triple-single-quotes.coffee │ ├── subtraction.coffee │ ├── switch-with-alternate.coffee │ ├── switch-with-multiple-cases.coffee │ ├── switch-with-multiple-conditions.coffee │ ├── switch-with-one-case.coffee │ ├── tagged-template-literal.coffee │ ├── this-assign-with-keyword.coffee │ ├── throw.coffee │ ├── triple-backtick-inline-js.coffee │ ├── true.coffee │ ├── try-with-catch-and-finally.coffee │ ├── try-with-catch-assignee.coffee │ ├── try-with-catch-single-line.coffee │ ├── try-with-catch-without-assignee.coffee │ ├── try-without-catch-or-finally.coffee │ ├── try-without-catch-single-line.coffee │ ├── try-without-catch-with-finally.coffee │ ├── typeof.coffee │ ├── unary-bitwise-negation.coffee │ ├── unary-minus.coffee │ ├── unary-plus.coffee │ ├── undefined.coffee │ ├── unless-in.coffee │ ├── unless-not-in.coffee │ ├── unless-not-instanceof.coffee │ ├── until.coffee │ ├── while-on-multiple-lines.coffee │ ├── while-on-one-line.coffee │ ├── while-with-guard.coffee │ ├── yield-from.coffee │ ├── yield-return-empty.coffee │ ├── yield-return.coffee │ └── yield.coffee └── util │ └── fixInvalidLocationData.test.ts ├── tsconfig.json └── tsup.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:jest/all", 6 | "plugin:import/recommended", 7 | "plugin:import/typescript", 8 | "prettier" 9 | ], 10 | "env": { 11 | "es6": true, 12 | "node": true 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["prettier", "jest", "@typescript-eslint/eslint-plugin", "import"], 19 | "rules": { 20 | "jest/require-hook": "off", 21 | "no-unused-vars": "off", 22 | "@typescript-eslint/camelcase": "off", 23 | "@typescript-eslint/no-unused-vars": "error", 24 | "@typescript-eslint/array-type": ["error", { "default": "generic" }], 25 | "@typescript-eslint/no-use-before-define": [ 26 | "error", 27 | { "functions": false, "classes": false } 28 | ], 29 | "@typescript-eslint/explicit-member-accessibility": "off", 30 | "@typescript-eslint/explicit-function-return-type": [ 31 | "error", 32 | { "allowExpressions": true } 33 | ] 34 | }, 35 | "overrides": [ 36 | { 37 | "files": ["**/__tests__/**/*.{js,ts}", "test/**/*.{js,ts}"], 38 | "rules": { 39 | "jest/prefer-expect-assertions": "off", 40 | "jest/prefer-inline-snapshots": "off", 41 | "jest/expect-expect": "off", 42 | "jest/no-if": "off" 43 | }, 44 | "env": { 45 | "jest/globals": true 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: '@babel/types' 10 | versions: 11 | - 7.13.16 12 | - dependency-name: eslint-config-prettier 13 | versions: 14 | - 8.0.0 15 | - 8.1.0 16 | - 8.2.0 17 | - dependency-name: eslint-plugin-jest 18 | versions: 19 | - 24.1.4 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x, 18.x] 12 | 13 | env: 14 | CI: true 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Enable Corepack 24 | run: corepack enable 25 | 26 | - name: Install Dependencies 27 | run: pnpm install --frozen-lockfile 28 | 29 | - name: Run Linter 30 | run: pnpm lint 31 | 32 | - name: Run Tests 33 | run: pnpm test 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | environment: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 14 17 | 18 | - name: Enable Corepack 19 | run: corepack enable 20 | 21 | - name: Install Dependencies 22 | run: pnpm install --frozen-lockfile 23 | 24 | - name: Build Package 25 | run: pnpm build 26 | 27 | - name: Release 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | run: npx semantic-release 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | _actual_cs1.json 4 | _actual_cs2.json 5 | .idea/ 6 | *.log 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm dlx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Brian Donovan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # decaffeinate-parser [![CI](https://github.com/decaffeinate/decaffeinate-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/decaffeinate/decaffeinate-parser/actions/workflows/ci.yml) [![package version](https://badge.fury.io/js/decaffeinate-parser.svg)](https://badge.fury.io/js/decaffeinate-parser) 2 | 3 | This project uses the [official CoffeeScript 4 | parser](https://github.com/jashkenas/coffeescript) to parse CoffeeScript source 5 | code, then maps the AST generated by the parser to one more suitable for the 6 | [decaffeinate project](https://github.com/eventualbuddha/decaffeinate) (based on 7 | the AST generated by 8 | [CoffeeScriptRedux](https://github.com/michaelficarra/CoffeeScriptRedux)). 9 | 10 | This project might be useful to anyone who wants to work with a CoffeeScript 11 | AST and prefers working with a saner AST. 12 | 13 | ## Install 14 | 15 | ```bash 16 | # via yarn 17 | $ yarn add decaffeinate-parser 18 | # via npm 19 | $ npm install decaffeinate-parser 20 | ``` 21 | 22 | ## Usage 23 | 24 | This example gets the names of the parameters in the `add` function: 25 | 26 | ```js 27 | import { parse } from 'decaffeinate-parser'; 28 | 29 | const program = parse('add = (a, b) -> a + b'); 30 | const assignment = program.body.statements[0]; 31 | const fn = assignment.expression; 32 | 33 | console.log(fn.parameters.map((param) => param.data)); // [ 'a', 'b' ] 34 | ``` 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | /** 5 | * @type {import('@jest/types').Config.InitialOptions} 6 | */ 7 | module.exports = { 8 | preset: 'ts-jest', 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decaffeinate-parser", 3 | "version": "0.0.0-development", 4 | "description": "A better AST for CoffeeScript, inspired by CoffeeScriptRedux.", 5 | "keywords": [ 6 | "ast", 7 | "coffeescript", 8 | "parse" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/decaffeinate/decaffeinate-parser.git" 13 | }, 14 | "license": "MIT", 15 | "author": "Brian Donovan", 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.js" 21 | } 22 | }, 23 | "main": "dist/index.js", 24 | "module": "dist/index.mjs", 25 | "types": "dist/index.d.ts", 26 | "files": [ 27 | "dist/" 28 | ], 29 | "scripts": { 30 | "build": "tsup", 31 | "lint": "eslint '{src,test}/**/*.ts'", 32 | "lint:fix": "pnpm lint --fix", 33 | "prepare": "husky install", 34 | "prepublishOnly": "pnpm build", 35 | "pretest": "pnpm lint", 36 | "test": "jest", 37 | "test:ci": "jest --ci" 38 | }, 39 | "lint-staged": { 40 | "*.{ts,md,json}": [ 41 | "prettier --write" 42 | ], 43 | "package.json": [ 44 | "sort-package-json" 45 | ] 46 | }, 47 | "release": { 48 | "branches": [ 49 | "main" 50 | ] 51 | }, 52 | "dependencies": { 53 | "@babel/types": "^7.18.4", 54 | "@codemod/parser": "^1.2.1", 55 | "coffee-lex": "^9.3.1", 56 | "decaffeinate-coffeescript": "^1.12.7-patch.4", 57 | "decaffeinate-coffeescript2": "^2.2.1-patch.6", 58 | "lines-and-columns": "^2.0.3" 59 | }, 60 | "devDependencies": { 61 | "@types/jest": "^28.1.1", 62 | "@types/node": "^14.0.0", 63 | "@typescript-eslint/eslint-plugin": "^5.27.1", 64 | "@typescript-eslint/parser": "^5.27.1", 65 | "eslint": "^8.17.0", 66 | "eslint-config-prettier": "^8.5.0", 67 | "eslint-plugin-import": "^2.26.0", 68 | "eslint-plugin-jest": "^26.5.3", 69 | "eslint-plugin-prettier": "^4.0.0", 70 | "husky": "^8.0.0", 71 | "jest": "^28.1.0", 72 | "lint-staged": "^13.0.3", 73 | "prettier": "^2.6.2", 74 | "sort-package-json": "^1.57.0", 75 | "string-repeat": "^1.1.1", 76 | "ts-jest": "^28.0.4", 77 | "ts-node": "^10.8.1", 78 | "tsup": "^6.1.0", 79 | "typescript": "^4.7.3" 80 | }, 81 | "packageManager": "pnpm@7.3.0", 82 | "engines": { 83 | "node": ">=6" 84 | }, 85 | "publishConfig": { 86 | "registry": "https://registry.npmjs.org/" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ext/coffee-script.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Base as CS1Base, 3 | Op as CS1Op, 4 | } from 'decaffeinate-coffeescript/lib/coffee-script/nodes.js'; 5 | import { Base, Op } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 6 | 7 | export function patchCoffeeScript(): void { 8 | CS1Op.prototype.invert = invert; 9 | CS1Base.prototype.invert = invert; 10 | Op.prototype.invert = invert; 11 | Base.prototype.invert = invert; 12 | } 13 | 14 | function invert(this: T): T { 15 | this.inverted = !this.inverted; 16 | return this; 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser'; 2 | export * from './nodes'; 3 | -------------------------------------------------------------------------------- /src/mappers/mapAny.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Arr, 3 | Assign, 4 | Base, 5 | Block, 6 | Call, 7 | Class, 8 | Code, 9 | ComputedPropertyName, 10 | Elision, 11 | Existence, 12 | Expansion, 13 | Extends, 14 | For, 15 | If, 16 | In, 17 | Literal, 18 | ModuleDeclaration, 19 | Obj, 20 | Op, 21 | Param, 22 | Parens, 23 | Range, 24 | Return, 25 | Splat, 26 | StringWithInterpolations, 27 | Super, 28 | Switch, 29 | TaggedTemplateCall, 30 | Throw, 31 | Try, 32 | Value, 33 | While, 34 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 35 | import { Node } from '../nodes'; 36 | import ParseContext from '../util/ParseContext'; 37 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 38 | import mapArr from './mapArr'; 39 | import mapAssign from './mapAssign'; 40 | import mapBlock from './mapBlock'; 41 | import mapCall from './mapCall'; 42 | import mapClass from './mapClass'; 43 | import mapCode from './mapCode'; 44 | import mapComputedPropertyName from './mapComputedPropertyName'; 45 | import mapElision from './mapElision'; 46 | import mapExistence from './mapExistence'; 47 | import mapExpansion from './mapExpansion'; 48 | import mapExtends from './mapExtends'; 49 | import mapFor from './mapFor'; 50 | import mapIf from './mapIf'; 51 | import mapIn from './mapIn'; 52 | import mapLiteral from './mapLiteral'; 53 | import mapModuleDeclaration from './mapModuleDeclaration'; 54 | import mapObj from './mapObj'; 55 | import mapOp from './mapOp'; 56 | import mapParam from './mapParam'; 57 | import mapParens from './mapParens'; 58 | import mapRange from './mapRange'; 59 | import mapReturn from './mapReturn'; 60 | import mapSplat from './mapSplat'; 61 | import mapSuper from './mapSuper'; 62 | import mapSwitch from './mapSwitch'; 63 | import mapTaggedTemplateCall from './mapTaggedTemplateCall'; 64 | import mapThrow from './mapThrow'; 65 | import mapTry from './mapTry'; 66 | import mapValue from './mapValue'; 67 | import mapWhile from './mapWhile'; 68 | 69 | export default function mapAny(context: ParseContext, node: Base): Node { 70 | if (node instanceof Value) { 71 | return mapValue(context, node); 72 | } 73 | 74 | if (node instanceof ComputedPropertyName) { 75 | return mapComputedPropertyName(context, node); 76 | } 77 | 78 | if (node instanceof Literal) { 79 | return mapLiteral(context, node); 80 | } 81 | 82 | if (node instanceof Op) { 83 | return mapOp(context, node); 84 | } 85 | 86 | if (node instanceof TaggedTemplateCall) { 87 | return mapTaggedTemplateCall(context, node); 88 | } 89 | 90 | if (node instanceof Call) { 91 | return mapCall(context, node); 92 | } 93 | 94 | if (node instanceof Super) { 95 | return mapSuper(context, node); 96 | } 97 | 98 | if (node instanceof Arr) { 99 | return mapArr(context, node); 100 | } 101 | 102 | if (node instanceof Assign) { 103 | return mapAssign(context, node); 104 | } 105 | 106 | if (node instanceof Param) { 107 | return mapParam(context, node); 108 | } 109 | 110 | if (node instanceof Return) { 111 | return mapReturn(context, node); 112 | } 113 | 114 | if (node instanceof If) { 115 | return mapIf(context, node); 116 | } 117 | 118 | if (node instanceof Obj) { 119 | return mapObj(context, node); 120 | } 121 | 122 | if (node instanceof Parens || node instanceof StringWithInterpolations) { 123 | return mapParens(context, node); 124 | } 125 | 126 | if (node instanceof For) { 127 | return mapFor(context, node); 128 | } 129 | 130 | if (node instanceof Throw) { 131 | return mapThrow(context, node); 132 | } 133 | 134 | if (node instanceof Block) { 135 | return mapBlock(context, node); 136 | } 137 | 138 | if (node instanceof Code) { 139 | return mapCode(context, node); 140 | } 141 | 142 | if (node instanceof While && !(node instanceof For)) { 143 | return mapWhile(context, node); 144 | } 145 | 146 | if (node instanceof Try) { 147 | return mapTry(context, node); 148 | } 149 | 150 | if (node instanceof Existence) { 151 | return mapExistence(context, node); 152 | } 153 | 154 | if (node instanceof Class) { 155 | return mapClass(context, node); 156 | } 157 | 158 | if (node instanceof Splat) { 159 | return mapSplat(context, node); 160 | } 161 | 162 | if (node instanceof Expansion) { 163 | return mapExpansion(context, node); 164 | } 165 | 166 | if (node instanceof Elision) { 167 | return mapElision(context, node); 168 | } 169 | 170 | if (node instanceof Switch) { 171 | return mapSwitch(context, node); 172 | } 173 | 174 | if (node instanceof In) { 175 | return mapIn(context, node); 176 | } 177 | 178 | if (node instanceof Range) { 179 | return mapRange(context, node); 180 | } 181 | 182 | if (node instanceof Extends) { 183 | return mapExtends(context, node); 184 | } 185 | 186 | if (node instanceof ModuleDeclaration) { 187 | return mapModuleDeclaration(context, node); 188 | } 189 | 190 | throw new UnsupportedNodeError(node); 191 | } 192 | -------------------------------------------------------------------------------- /src/mappers/mapArr.ts: -------------------------------------------------------------------------------- 1 | import { Arr } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { ArrayInitialiser } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapArr( 8 | context: ParseContext, 9 | node: Arr 10 | ): ArrayInitialiser { 11 | const { line, column, start, end, raw } = getLocation(context, node); 12 | const members = node.objects.map((object) => mapAny(context, object)); 13 | return new ArrayInitialiser(line, column, start, end, raw, members); 14 | } 15 | -------------------------------------------------------------------------------- /src/mappers/mapAssign.ts: -------------------------------------------------------------------------------- 1 | import { Assign } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { 3 | AssignOp, 4 | BaseAssignOp, 5 | BitAndOp, 6 | BitOrOp, 7 | BitXorOp, 8 | CompoundAssignOp, 9 | DivideOp, 10 | ExistsOp, 11 | ExpOp, 12 | FloorDivideOp, 13 | LeftShiftOp, 14 | LogicalAndOp, 15 | LogicalOrOp, 16 | ModuloOp, 17 | MultiplyOp, 18 | PlusOp, 19 | RemOp, 20 | SignedRightShiftOp, 21 | SubtractOp, 22 | UnsignedRightShiftOp, 23 | } from '../nodes'; 24 | import getLocation from '../util/getLocation'; 25 | import ParseContext from '../util/ParseContext'; 26 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 27 | import mapAny from './mapAny'; 28 | 29 | const COMPOUND_ASSIGN_OPS: { [op: string]: string | undefined } = { 30 | '-=': SubtractOp.name, 31 | '+=': PlusOp.name, 32 | '/=': DivideOp.name, 33 | '*=': MultiplyOp.name, 34 | '%=': RemOp.name, 35 | '||=': LogicalOrOp.name, 36 | '&&=': LogicalAndOp.name, 37 | '?=': ExistsOp.name, 38 | '<<=': LeftShiftOp.name, 39 | '>>=': SignedRightShiftOp.name, 40 | '>>>=': UnsignedRightShiftOp.name, 41 | '&=': BitAndOp.name, 42 | '^=': BitXorOp.name, 43 | '|=': BitOrOp.name, 44 | '**=': ExpOp.name, 45 | '//=': FloorDivideOp.name, 46 | '%%=': ModuloOp.name, 47 | }; 48 | 49 | export default function mapAssign( 50 | context: ParseContext, 51 | node: Assign 52 | ): BaseAssignOp { 53 | if (node.context === 'object') { 54 | throw new UnsupportedNodeError( 55 | node, 56 | 'Unexpected object context when mapping regular assign op.' 57 | ); 58 | } 59 | 60 | const { line, column, start, end, raw } = getLocation(context, node); 61 | if (node.context) { 62 | const opName = COMPOUND_ASSIGN_OPS[node.context]; 63 | if (!opName) { 64 | throw new UnsupportedNodeError( 65 | node, 66 | 'Unexpected operator context for assign op.' 67 | ); 68 | } 69 | return new CompoundAssignOp( 70 | line, 71 | column, 72 | start, 73 | end, 74 | raw, 75 | mapAny(context, node.variable), 76 | mapAny(context, node.value), 77 | opName 78 | ); 79 | } else { 80 | return new AssignOp( 81 | line, 82 | column, 83 | start, 84 | end, 85 | raw, 86 | mapAny(context, node.variable), 87 | mapAny(context, node.value) 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/mappers/mapBlock.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { 3 | Assign, 4 | Base, 5 | Block as CoffeeBlock, 6 | Obj, 7 | Value, 8 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 9 | import { inspect } from 'util'; 10 | import { 11 | AssignOp, 12 | Block, 13 | BoundAsyncFunction, 14 | BoundFunction, 15 | BoundGeneratorFunction, 16 | ClassProtoAssignOp, 17 | Constructor, 18 | Identifier, 19 | MemberAccessOp, 20 | Node, 21 | This, 22 | } from '../nodes'; 23 | import getLocation from '../util/getLocation'; 24 | import isCommentOnlyNode from '../util/isCommentOnlyNode'; 25 | import ParseContext from '../util/ParseContext'; 26 | import mapAny from './mapAny'; 27 | 28 | export default function mapBlock( 29 | context: ParseContext, 30 | node: CoffeeBlock 31 | ): Block { 32 | let childContext = context; 33 | if (context.parseState.isInClassBody()) { 34 | // Replicate a bug in CoffeeScript: at any block where we see an 35 | // object-style proto assignment, stop considering proto assignments in any 36 | // sub-traversals. This is taken from the walkBody implementation. 37 | const hasProtoAssignChild = node.expressions.some( 38 | (child) => child instanceof Value && child.isObject(true) 39 | ); 40 | if (hasProtoAssignChild) { 41 | childContext = childContext.updateState((s) => s.dropCurrentClass()); 42 | } 43 | } 44 | 45 | const { line, column, start, end, raw } = getLocation(context, node); 46 | const previousTokenIndex = context.sourceTokens 47 | .indexOfTokenNearSourceIndex(start) 48 | .previous(); 49 | const previousToken = previousTokenIndex 50 | ? context.sourceTokens.tokenAtIndex(previousTokenIndex) 51 | : null; 52 | const inline = previousToken 53 | ? previousToken.type !== SourceType.NEWLINE 54 | : false; 55 | 56 | return new Block( 57 | line, 58 | column, 59 | start, 60 | end, 61 | raw, 62 | node.expressions 63 | .filter((expression) => !isCommentOnlyNode(expression)) 64 | .map((expression) => mapChild(context, childContext, expression)) 65 | .reduce((arr, current) => arr.concat(current), []), 66 | inline 67 | ); 68 | } 69 | 70 | function mapChild( 71 | blockContext: ParseContext, 72 | childContext: ParseContext, 73 | node: Base 74 | ): Array { 75 | if ( 76 | blockContext.parseState.isInClassBody() && 77 | node instanceof Value && 78 | node.isObject(true) 79 | ) { 80 | const obj = node.base; 81 | if (!(obj instanceof Obj)) { 82 | throw new Error('Expected isObject node to be an object.'); 83 | } 84 | 85 | const statements: Array = []; 86 | for (const property of obj.properties) { 87 | if (isCommentOnlyNode(property)) { 88 | continue; 89 | } 90 | if (property instanceof Assign) { 91 | const { line, column, start, end, raw } = getLocation( 92 | childContext, 93 | property 94 | ); 95 | const key = mapAny(childContext, property.variable); 96 | const value = mapAny(childContext, property.value); 97 | let Node = ClassProtoAssignOp; 98 | 99 | if (key instanceof Identifier && key.data === 'constructor') { 100 | Node = Constructor; 101 | } else if ( 102 | key instanceof MemberAccessOp && 103 | key.expression instanceof This 104 | ) { 105 | Node = AssignOp; 106 | } 107 | 108 | const assignment = new Node(line, column, start, end, raw, key, value); 109 | 110 | statements.push(assignment); 111 | 112 | if ( 113 | assignment instanceof ClassProtoAssignOp && 114 | (assignment.expression instanceof BoundFunction || 115 | assignment.expression instanceof BoundGeneratorFunction || 116 | assignment.expression instanceof BoundAsyncFunction) 117 | ) { 118 | blockContext.parseState.recordBoundMethod(assignment); 119 | } 120 | 121 | if (assignment instanceof Constructor) { 122 | blockContext.parseState.recordConstructor(assignment); 123 | } 124 | } else { 125 | throw new Error(`unexpected class assignment: ${inspect(property)}`); 126 | } 127 | } 128 | return statements; 129 | } 130 | return [mapAny(childContext, node)]; 131 | } 132 | -------------------------------------------------------------------------------- /src/mappers/mapCSX.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Arr, 3 | Assign, 4 | Base, 5 | Call, 6 | IdentifierLiteral, 7 | Obj, 8 | Splat, 9 | StringLiteral, 10 | StringWithInterpolations, 11 | Value, 12 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 13 | import { CSXElement, Node } from '../nodes'; 14 | import getLocation from '../util/getLocation'; 15 | import getTemplateLiteralComponents from '../util/getTemplateLiteralComponents'; 16 | import ParseContext from '../util/ParseContext'; 17 | import mapAny from './mapAny'; 18 | 19 | export default function mapCSX(context: ParseContext, node: Call): Node { 20 | const { line, column, start, end, raw } = getLocation(context, node); 21 | const [properties, children] = node.args; 22 | const mappedProperties = mapCSXProperties(context, properties); 23 | const mappedChildren = mapCSXChildren(context, children); 24 | return new CSXElement( 25 | line, 26 | column, 27 | start, 28 | end, 29 | raw, 30 | mappedProperties, 31 | mappedChildren 32 | ); 33 | } 34 | 35 | function mapCSXProperties( 36 | context: ParseContext, 37 | properties: Base 38 | ): Array { 39 | if (!(properties instanceof Value) || !(properties.base instanceof Arr)) { 40 | throw new Error('Expected a value for the CSX properties arg.'); 41 | } 42 | const values = properties.base.objects; 43 | const resultProperties = []; 44 | for (const value of values) { 45 | if (!(value instanceof Value)) { 46 | throw new Error('Expected value for CSX property.'); 47 | } 48 | if (value.base instanceof Obj) { 49 | for (const propertyAssignment of value.base.objects) { 50 | if (propertyAssignment instanceof Splat) { 51 | resultProperties.push(mapAny(context, propertyAssignment)); 52 | } else if (propertyAssignment instanceof Assign) { 53 | if (!(propertyAssignment.value instanceof Value)) { 54 | throw new Error('Unexpected property assignment value.'); 55 | } 56 | if (!(propertyAssignment.value.base instanceof StringLiteral)) { 57 | resultProperties.push(mapAny(context, propertyAssignment.value)); 58 | } 59 | } else { 60 | throw new Error( 61 | 'Unexpected property assignment object field in CSX.' 62 | ); 63 | } 64 | } 65 | } else if (value.base instanceof IdentifierLiteral) { 66 | // Do nothing; we don't need to consider this as a node to transform. 67 | } else { 68 | throw new Error('Unexpected property assignment in CSX.'); 69 | } 70 | } 71 | return resultProperties; 72 | } 73 | 74 | function mapCSXChildren( 75 | context: ParseContext, 76 | children: Base | null 77 | ): Array { 78 | if (!children) { 79 | return []; 80 | } 81 | if (!(children instanceof Value)) { 82 | throw new Error('Expected a value for the CSX children arg.'); 83 | } 84 | if (children.base instanceof StringLiteral) { 85 | return []; 86 | } else if (!(children.base instanceof StringWithInterpolations)) { 87 | throw new Error('Expected a valid CSX children arg.'); 88 | } 89 | const childInterpolatedString = children.base.body.expressions[0]; 90 | const { unmappedExpressions } = getTemplateLiteralComponents( 91 | context, 92 | childInterpolatedString 93 | ); 94 | return unmappedExpressions.map((child) => 95 | child ? mapAny(context, child) : null 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/mappers/mapCall.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { 3 | Call, 4 | Literal, 5 | Splat, 6 | StringWithInterpolations, 7 | SuperCall, 8 | Value, 9 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 10 | import { inspect } from 'util'; 11 | import { 12 | AssignOp, 13 | BareSuperFunctionApplication, 14 | BaseFunction, 15 | DefaultParam, 16 | DoOp, 17 | FunctionApplication, 18 | Identifier, 19 | NewOp, 20 | Node, 21 | SoakedFunctionApplication, 22 | SoakedNewOp, 23 | Super, 24 | } from '../nodes'; 25 | import getLocation from '../util/getLocation'; 26 | import isHeregexTemplateNode from '../util/isHeregexTemplateNode'; 27 | import locationsEqual from '../util/locationsEqual'; 28 | import makeHeregex from '../util/makeHeregex'; 29 | import ParseContext from '../util/ParseContext'; 30 | import parseString from '../util/parseString'; 31 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 32 | import mapAny from './mapAny'; 33 | import mapCSX from './mapCSX'; 34 | 35 | export default function mapCall(context: ParseContext, node: Call): Node { 36 | const { line, column, start, end, raw } = getLocation(context, node); 37 | 38 | if (node.csx) { 39 | return mapCSX(context, node); 40 | } 41 | 42 | if (isHeregexTemplateNode(node, context)) { 43 | const firstArg = node.args[0]; 44 | if ( 45 | !(firstArg instanceof Value) || 46 | !(firstArg.base instanceof StringWithInterpolations) 47 | ) { 48 | throw new Error('Expected a valid first heregex arg in the AST.'); 49 | } 50 | const strNode = firstArg.base.body.expressions[0]; 51 | let flags; 52 | if (node.args.length > 1) { 53 | const secondArg = node.args[1]; 54 | if ( 55 | !(secondArg instanceof Value) || 56 | !(secondArg.base instanceof Literal) 57 | ) { 58 | throw new Error('Expected a string flags value in the heregex AST.'); 59 | } 60 | flags = parseString(secondArg.base.value); 61 | } else { 62 | flags = ''; 63 | } 64 | return makeHeregex(context, strNode, flags); 65 | } 66 | 67 | const args = node.args.map((arg) => mapAny(context, arg)); 68 | 69 | if (node instanceof SuperCall) { 70 | if ( 71 | node.args.length === 1 && 72 | node.args[0] instanceof Splat && 73 | locationsEqual(node.args[0].locationData, node.locationData) 74 | ) { 75 | return new BareSuperFunctionApplication(line, column, start, end, raw); 76 | } 77 | 78 | const superIndex = 79 | context.sourceTokens.indexOfTokenStartingAtSourceIndex(start); 80 | const superToken = 81 | superIndex && context.sourceTokens.tokenAtIndex(superIndex); 82 | 83 | if (!superToken || superToken.type !== SourceType.SUPER) { 84 | throw new Error( 85 | `unable to find SUPER token in 'super' function call: ${inspect(node)}` 86 | ); 87 | } 88 | 89 | const superLocation = context.linesAndColumns.locationForIndex( 90 | superToken.start 91 | ); 92 | 93 | if (!superLocation) { 94 | throw new Error( 95 | `unable to locate SUPER token for 'super' function call: ${inspect( 96 | node 97 | )}` 98 | ); 99 | } 100 | 101 | return new FunctionApplication( 102 | line, 103 | column, 104 | start, 105 | end, 106 | raw, 107 | new Super( 108 | superLocation.line + 1, 109 | superLocation.column + 1, 110 | superToken.start, 111 | superToken.end, 112 | context.source.slice(superToken.start, superToken.end) 113 | ), 114 | args 115 | ); 116 | } 117 | 118 | if (!node.variable) { 119 | throw new Error('Expected non-super call to have a variable defined.'); 120 | } 121 | const callee = mapAny(context, node.variable); 122 | 123 | if (node.isNew) { 124 | return mapNewOp(context, node); 125 | } 126 | 127 | if (node.soak) { 128 | return new SoakedFunctionApplication( 129 | line, 130 | column, 131 | start, 132 | end, 133 | raw, 134 | callee, 135 | args 136 | ); 137 | } 138 | 139 | if (node.do) { 140 | return mapDoOp(context, node); 141 | } 142 | 143 | return new FunctionApplication(line, column, start, end, raw, callee, args); 144 | } 145 | 146 | function mapNewOp(context: ParseContext, node: Call): NewOp { 147 | if (!node.variable) { 148 | // This should only happen when `isSuper` is true. 149 | throw new UnsupportedNodeError(node); 150 | } 151 | 152 | const { line, column, start, end, raw } = getLocation(context, node); 153 | const callee = mapAny(context, node.variable); 154 | const args = node.args.map((arg) => mapAny(context, arg)); 155 | 156 | if (node.soak) { 157 | return new SoakedNewOp(line, column, start, end, raw, callee, args); 158 | } else { 159 | return new NewOp(line, column, start, end, raw, callee, args); 160 | } 161 | } 162 | 163 | function mapDoOp(context: ParseContext, node: Call): DoOp { 164 | if (!node.variable) { 165 | // This should only happen when `isSuper` is true. 166 | throw new UnsupportedNodeError(node); 167 | } 168 | 169 | const { line, column, start, end, raw } = getLocation(context, node); 170 | 171 | let expression = mapAny(context, node.variable); 172 | 173 | const args = node.args.map((arg) => mapAny(context, arg)); 174 | 175 | if (expression instanceof BaseFunction) { 176 | expression = augmentDoFunctionWithArgs(context, expression, args); 177 | } else if ( 178 | expression instanceof AssignOp && 179 | expression.expression instanceof BaseFunction 180 | ) { 181 | const newRhs = augmentDoFunctionWithArgs( 182 | context, 183 | expression.expression, 184 | args 185 | ); 186 | expression = expression.withExpression(newRhs); 187 | } 188 | 189 | return new DoOp(line, column, start, end, raw, expression); 190 | } 191 | 192 | function augmentDoFunctionWithArgs( 193 | context: ParseContext, 194 | func: BaseFunction, 195 | args: Array 196 | ): BaseFunction { 197 | const newParameters = func.parameters.map((param, i) => { 198 | const arg = args[i]; 199 | 200 | // If there's a parameter with no default, CoffeeScript will insert a fake 201 | // arg with the same value and location. 202 | if ( 203 | arg instanceof Identifier && 204 | param instanceof Identifier && 205 | arg.data === param.data && 206 | arg.start === param.start && 207 | arg.end === param.end 208 | ) { 209 | return param; 210 | } 211 | 212 | return new DefaultParam( 213 | param.line, 214 | param.column, 215 | param.start, 216 | arg.end, 217 | context.source.slice(param.start, arg.end), 218 | param, 219 | arg 220 | ); 221 | }); 222 | 223 | return func.withParameters(newParameters); 224 | } 225 | -------------------------------------------------------------------------------- /src/mappers/mapClass.ts: -------------------------------------------------------------------------------- 1 | import { Class as CoffeeClass } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Class } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 7 | 8 | export default function mapClass( 9 | context: ParseContext, 10 | node: CoffeeClass 11 | ): Class { 12 | const { line, column, start, end, raw } = getLocation(context, node); 13 | 14 | const nameAssignee = node.variable ? mapAny(context, node.variable) : null; 15 | const parent = node.parent ? mapAny(context, node.parent) : null; 16 | 17 | const childContext = context.updateState((s) => s.pushCurrentClass()); 18 | const body = mapPossiblyEmptyBlock(childContext, node.body); 19 | const boundMethods = childContext.parseState.currentClassBoundMethods; 20 | if (!boundMethods) { 21 | throw new Error('Expected a non-null bound method name array.'); 22 | } 23 | 24 | return new Class( 25 | line, 26 | column, 27 | start, 28 | end, 29 | raw, 30 | nameAssignee, 31 | nameAssignee, 32 | body, 33 | boundMethods, 34 | parent, 35 | childContext.parseState.currentClassCtor 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/mappers/mapCode.ts: -------------------------------------------------------------------------------- 1 | import { Code } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { 3 | AsyncFunction, 4 | BaseFunction, 5 | BoundAsyncFunction, 6 | BoundFunction, 7 | BoundGeneratorFunction, 8 | Function, 9 | GeneratorFunction, 10 | } from '../nodes'; 11 | import getLocation from '../util/getLocation'; 12 | import ParseContext from '../util/ParseContext'; 13 | import mapAny from './mapAny'; 14 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 15 | 16 | export default function mapCode( 17 | context: ParseContext, 18 | node: Code 19 | ): BaseFunction { 20 | const { line, column, start, end, raw } = getLocation(context, node); 21 | 22 | const Node = getNodeTypeForCode(node); 23 | 24 | const childContext = context.updateState((s) => s.dropCurrentClass()); 25 | 26 | return new Node( 27 | line, 28 | column, 29 | start, 30 | end, 31 | raw, 32 | node.params.map((param) => mapAny(childContext, param)), 33 | mapPossiblyEmptyBlock(childContext, node.body) 34 | ); 35 | } 36 | 37 | function getNodeTypeForCode( 38 | node: Code 39 | ): 40 | | typeof BoundFunction 41 | | typeof BoundGeneratorFunction 42 | | typeof AsyncFunction 43 | | typeof BoundAsyncFunction 44 | | typeof GeneratorFunction 45 | | typeof Function { 46 | if (node.isGenerator) { 47 | if (node.bound) { 48 | return BoundGeneratorFunction; 49 | } else { 50 | return GeneratorFunction; 51 | } 52 | } else if (node.isAsync) { 53 | if (node.bound) { 54 | return BoundAsyncFunction; 55 | } else { 56 | return AsyncFunction; 57 | } 58 | } else { 59 | if (node.bound) { 60 | return BoundFunction; 61 | } else { 62 | return Function; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/mappers/mapComputedPropertyName.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Base, 3 | ComputedPropertyName, 4 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 5 | import { Node } from '../nodes'; 6 | import ParseContext from '../util/ParseContext'; 7 | import mapAny from './mapAny'; 8 | 9 | export default function mapComputedPropertyName( 10 | context: ParseContext, 11 | node: ComputedPropertyName 12 | ): Node { 13 | // ComputedPropertyName is the only Literal where the value isn't a primitive, so just 14 | // fake the type here for now. 15 | return mapAny(context, node.value as unknown as Base); 16 | } 17 | -------------------------------------------------------------------------------- /src/mappers/mapElision.ts: -------------------------------------------------------------------------------- 1 | import { Elision as CoffeeElision } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Elision } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | 6 | export default function mapElision( 7 | context: ParseContext, 8 | node: CoffeeElision 9 | ): Elision { 10 | const { line, column, start, end, raw } = getLocation(context, node); 11 | return new Elision(line, column, start, end, raw); 12 | } 13 | -------------------------------------------------------------------------------- /src/mappers/mapExistence.ts: -------------------------------------------------------------------------------- 1 | import { Existence } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { UnaryExistsOp } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapExistence( 8 | context: ParseContext, 9 | node: Existence 10 | ): UnaryExistsOp { 11 | const { line, column, start, end, raw } = getLocation(context, node); 12 | return new UnaryExistsOp( 13 | line, 14 | column, 15 | start, 16 | end, 17 | raw, 18 | mapAny(context, node.expression) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/mappers/mapExpansion.ts: -------------------------------------------------------------------------------- 1 | import { Expansion as CoffeeExpansion } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Expansion } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | 6 | export default function mapExpansion( 7 | context: ParseContext, 8 | node: CoffeeExpansion 9 | ): Expansion { 10 | const { line, column, start, end, raw } = getLocation(context, node); 11 | return new Expansion(line, column, start, end, raw); 12 | } 13 | -------------------------------------------------------------------------------- /src/mappers/mapExtends.ts: -------------------------------------------------------------------------------- 1 | import { Extends } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { ExtendsOp } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapExtends( 8 | context: ParseContext, 9 | node: Extends 10 | ): ExtendsOp { 11 | const { line, column, start, end, raw } = getLocation(context, node); 12 | return new ExtendsOp( 13 | line, 14 | column, 15 | start, 16 | end, 17 | raw, 18 | mapAny(context, node.child), 19 | mapAny(context, node.parent) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/mappers/mapFor.ts: -------------------------------------------------------------------------------- 1 | import { For as CoffeeFor } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { For, ForFrom, ForIn, ForOf } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 7 | 8 | export default function mapFor(context: ParseContext, node: CoffeeFor): For { 9 | const { line, column, start, end, raw } = getLocation(context, node); 10 | 11 | const keyAssignee = node.index ? mapAny(context, node.index) : null; 12 | const valAssignee = node.name ? mapAny(context, node.name) : null; 13 | let body = mapPossiblyEmptyBlock(context, node.body); 14 | const target = mapAny(context, node.source); 15 | const filter = node.guard ? mapAny(context, node.guard) : null; 16 | 17 | if (body && body.start < target.start) { 18 | body = body.withInline(true); 19 | } 20 | 21 | if (node.object) { 22 | const isOwn = node.own; 23 | 24 | return new ForOf( 25 | line, 26 | column, 27 | start, 28 | end, 29 | raw, 30 | keyAssignee, 31 | valAssignee, 32 | target, 33 | filter, 34 | body, 35 | isOwn 36 | ); 37 | } else { 38 | if (node.from) { 39 | if (keyAssignee) { 40 | throw new Error('Unexpected key assignee in for...from.'); 41 | } 42 | return new ForFrom( 43 | line, 44 | column, 45 | start, 46 | end, 47 | raw, 48 | valAssignee, 49 | target, 50 | filter, 51 | body 52 | ); 53 | } else { 54 | const step = node.step ? mapAny(context, node.step) : null; 55 | return new ForIn( 56 | line, 57 | column, 58 | start, 59 | end, 60 | raw, 61 | keyAssignee, 62 | valAssignee, 63 | target, 64 | filter, 65 | body, 66 | step 67 | ); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/mappers/mapIf.ts: -------------------------------------------------------------------------------- 1 | import { SourceType, SourceTokenListIndex } from 'coffee-lex'; 2 | import { If } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { Conditional } from '../nodes'; 4 | import getLocation from '../util/getLocation'; 5 | import ParseContext from '../util/ParseContext'; 6 | import mapAny from './mapAny'; 7 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 8 | 9 | export default function mapIf(context: ParseContext, node: If): Conditional { 10 | const { line, column, start, end, raw } = getLocation(context, node); 11 | 12 | const condition = mapAny(context, node.condition); 13 | let consequent = mapPossiblyEmptyBlock(context, node.body); 14 | const alternate = mapPossiblyEmptyBlock(context, node.elseBody); 15 | let isUnless = false; 16 | 17 | let left: SourceTokenListIndex | null = null; 18 | let right: SourceTokenListIndex | null = null; 19 | 20 | if (consequent && consequent.start < condition.start) { 21 | consequent = consequent.withInline(true); 22 | // POST-if, so look for tokens between the consequent and the condition 23 | left = context.sourceTokens.indexOfTokenEndingAtSourceIndex(consequent.end); 24 | right = context.sourceTokens.indexOfTokenStartingAtSourceIndex( 25 | condition.start 26 | ); 27 | } else { 28 | // regular `if`, so look from the start of the node until the condition 29 | left = context.sourceTokens.indexOfTokenStartingAtSourceIndex(start); 30 | right = context.sourceTokens.indexOfTokenStartingAtSourceIndex( 31 | condition.start 32 | ); 33 | } 34 | 35 | if (left && right) { 36 | isUnless = 37 | context.sourceTokens.indexOfTokenMatchingPredicate( 38 | (token) => 39 | token.type === SourceType.IF && 40 | context.source.slice(token.start, token.end) === 'unless', 41 | left, 42 | right 43 | ) !== null; 44 | } 45 | 46 | return new Conditional( 47 | line, 48 | column, 49 | start, 50 | end, 51 | raw, 52 | condition, 53 | consequent, 54 | alternate, 55 | isUnless 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/mappers/mapIn.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { In as CoffeeIn } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { InOp } from '../nodes'; 4 | import getLocation from '../util/getLocation'; 5 | import ParseContext from '../util/ParseContext'; 6 | import mapAny from './mapAny'; 7 | 8 | export default function mapIn(context: ParseContext, node: CoffeeIn): InOp { 9 | // We don't use the `negated` flag on `node` because it gets set to 10 | // `true` when a parent `If` is an `unless`. 11 | const { line, column, start, end, raw } = getLocation(context, node); 12 | const left = mapAny(context, node.object); 13 | const right = mapAny(context, node.array); 14 | let isNot = false; 15 | 16 | const lastTokenIndexOfLeft = 17 | context.sourceTokens.indexOfTokenEndingAtSourceIndex(left.end); 18 | const firstTokenIndexOfRight = 19 | context.sourceTokens.indexOfTokenStartingAtSourceIndex(right.start); 20 | const relationTokenIndex = context.sourceTokens.indexOfTokenMatchingPredicate( 21 | (token) => token.type === SourceType.RELATION, 22 | lastTokenIndexOfLeft, 23 | firstTokenIndexOfRight 24 | ); 25 | 26 | if (relationTokenIndex) { 27 | const relationToken = context.sourceTokens.tokenAtIndex(relationTokenIndex); 28 | 29 | if (relationToken) { 30 | isNot = 31 | context.source.slice(relationToken.start, relationToken.end) !== 'in'; 32 | 33 | return new InOp(line, column, start, end, raw, left, right, isNot); 34 | } 35 | } 36 | 37 | throw new Error(`unable to find RELATION token between operands`); 38 | } 39 | -------------------------------------------------------------------------------- /src/mappers/mapLiteral.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanLiteral, 3 | IdentifierLiteral, 4 | Literal, 5 | NullLiteral, 6 | NumberLiteral, 7 | PassthroughLiteral, 8 | PropertyName, 9 | RegexLiteral, 10 | StatementLiteral, 11 | StringLiteral, 12 | ThisLiteral, 13 | UndefinedLiteral, 14 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 15 | import { 16 | Bool, 17 | Break, 18 | Continue, 19 | Debugger, 20 | Float, 21 | Identifier, 22 | Int, 23 | JavaScript, 24 | Node, 25 | Null, 26 | Regex, 27 | RegexFlags, 28 | This, 29 | Undefined, 30 | } from '../nodes'; 31 | import getLocation from '../util/getLocation'; 32 | import makeHeregex from '../util/makeHeregex'; 33 | import makeString from '../util/makeString'; 34 | import ParseContext from '../util/ParseContext'; 35 | import parseNumber from '../util/parseNumber'; 36 | import parseRegExp from '../util/parseRegExp'; 37 | 38 | const HEREGEX_PATTERN = /^\/\/\/((?:.|\s)*)\/\/\/([gimuy]*)$/; 39 | 40 | export default function mapLiteral(context: ParseContext, node: Literal): Node { 41 | const { line, column, start, end, raw } = getLocation(context, node); 42 | 43 | if (node instanceof ThisLiteral) { 44 | return new This(line, column, start, end, raw); 45 | } else if (node instanceof NullLiteral) { 46 | return new Null(line, column, start, end, raw); 47 | } else if (node instanceof UndefinedLiteral) { 48 | return new Undefined(line, column, start, end, raw); 49 | } else if (node instanceof BooleanLiteral) { 50 | return new Bool(line, column, start, end, raw, JSON.parse(node.value)); 51 | } else if ( 52 | node instanceof IdentifierLiteral || 53 | node instanceof PropertyName 54 | ) { 55 | // Sometimes the CoffeeScript AST contains a string object instead of a 56 | // string primitive. Convert to string primitive if necessary. 57 | const value = node.value.valueOf(); 58 | return new Identifier(line, column, start, end, raw, value); 59 | } else if (node instanceof PassthroughLiteral) { 60 | return new JavaScript(line, column, start, end, raw, node.value); 61 | } else if (node instanceof NumberLiteral) { 62 | if (raw.includes('.')) { 63 | return new Float(line, column, start, end, raw, parseNumber(node.value)); 64 | } else { 65 | return new Int(line, column, start, end, raw, parseNumber(node.value)); 66 | } 67 | } else if (node instanceof RegexLiteral) { 68 | const heregexMatch = raw.match(HEREGEX_PATTERN); 69 | if (heregexMatch) { 70 | const flags = heregexMatch[2]; 71 | return makeHeregex(context, node, flags); 72 | } else { 73 | const regExp = parseRegExp(node.value); 74 | return new Regex( 75 | line, 76 | column, 77 | start, 78 | end, 79 | raw, 80 | regExp.pattern, 81 | RegexFlags.parse(regExp.flags || '') 82 | ); 83 | } 84 | } else if (node instanceof StringLiteral) { 85 | return makeString(context, node); 86 | } else if (node instanceof StatementLiteral) { 87 | if (node.value === 'break') { 88 | return new Break(line, column, start, end, raw); 89 | } else if (node.value === 'continue') { 90 | return new Continue(line, column, start, end, raw); 91 | } else if (node.value === 'debugger') { 92 | return new Debugger(line, column, start, end, raw); 93 | } 94 | } 95 | 96 | throw new Error(`Unexpected literal: ${JSON.stringify(node)}`); 97 | } 98 | -------------------------------------------------------------------------------- /src/mappers/mapModuleDeclaration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExportAllDeclaration as CoffeeExportAllDeclaration, 3 | ExportDefaultDeclaration as CoffeeExportDefaultDeclaration, 4 | ExportNamedDeclaration as CoffeeExportNamedDeclaration, 5 | ExportSpecifierList, 6 | IdentifierLiteral, 7 | ImportDeclaration as CoffeeImportDeclaration, 8 | ImportNamespaceSpecifier, 9 | ImportSpecifierList, 10 | Literal, 11 | ModuleDeclaration, 12 | ModuleSpecifier as CoffeeModuleSpecifier, 13 | StringLiteral, 14 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 15 | import * as nodes from '../nodes'; 16 | import getLocation from '../util/getLocation'; 17 | import ParseContext from '../util/ParseContext'; 18 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 19 | import mapAny from './mapAny'; 20 | import mapLiteral from './mapLiteral'; 21 | import notNull from '../util/notNull'; 22 | 23 | export default function mapModuleDeclaration( 24 | context: ParseContext, 25 | node: ModuleDeclaration 26 | ): nodes.Node { 27 | const { line, column, start, end, raw } = getLocation(context, node); 28 | 29 | if (node instanceof CoffeeImportDeclaration) { 30 | const clause = node.clause; 31 | let defaultBinding = null; 32 | let namespaceImport = null; 33 | let namedImports = null; 34 | 35 | if (clause) { 36 | defaultBinding = 37 | clause.defaultBinding && 38 | mapIdentifierSpecifier(context, clause.defaultBinding); 39 | if (clause.namedImports instanceof ImportNamespaceSpecifier) { 40 | namespaceImport = mapStarSpecifier(context, clause.namedImports); 41 | } else if (clause.namedImports instanceof ImportSpecifierList) { 42 | namedImports = clause.namedImports.specifiers.map((specifier) => 43 | mapSpecifier(context, specifier) 44 | ); 45 | } 46 | } 47 | 48 | if (!node.source) { 49 | throw new Error('Expected non-null source for import.'); 50 | } 51 | const source = mapSource(context, node.source); 52 | return new nodes.ImportDeclaration( 53 | line, 54 | column, 55 | start, 56 | end, 57 | raw, 58 | defaultBinding, 59 | namespaceImport, 60 | namedImports, 61 | source 62 | ); 63 | } else if (node instanceof CoffeeExportNamedDeclaration) { 64 | if (node.clause instanceof ExportSpecifierList) { 65 | const namedExports = node.clause.specifiers.map((specifier) => 66 | mapSpecifier(context, specifier) 67 | ); 68 | const source = node.source ? mapSource(context, node.source) : null; 69 | return new nodes.ExportBindingsDeclaration( 70 | line, 71 | column, 72 | start, 73 | end, 74 | raw, 75 | namedExports, 76 | source 77 | ); 78 | } else { 79 | const expression = mapAny(context, notNull(node.clause)); 80 | return new nodes.ExportNamedDeclaration( 81 | line, 82 | column, 83 | start, 84 | end, 85 | raw, 86 | expression 87 | ); 88 | } 89 | } else if (node instanceof CoffeeExportDefaultDeclaration) { 90 | const expression = mapAny(context, notNull(node.clause)); 91 | return new nodes.ExportDefaultDeclaration( 92 | line, 93 | column, 94 | start, 95 | end, 96 | raw, 97 | expression 98 | ); 99 | } else if (node instanceof CoffeeExportAllDeclaration) { 100 | if (!node.source) { 101 | throw new Error('Expected non-null source for star export.'); 102 | } 103 | const source = mapSource(context, node.source); 104 | return new nodes.ExportAllDeclaration( 105 | line, 106 | column, 107 | start, 108 | end, 109 | raw, 110 | source 111 | ); 112 | } else { 113 | throw new UnsupportedNodeError(node); 114 | } 115 | } 116 | 117 | function mapSource( 118 | context: ParseContext, 119 | coffeeSource: StringLiteral 120 | ): nodes.String { 121 | const source = mapLiteral(context, coffeeSource); 122 | if (!(source instanceof nodes.String)) { 123 | throw new Error('Expected string literal as import source.'); 124 | } 125 | return source; 126 | } 127 | 128 | function mapIdentifierSpecifier( 129 | context: ParseContext, 130 | specifier: CoffeeModuleSpecifier 131 | ): nodes.Identifier { 132 | if (specifier.alias) { 133 | throw new Error('Expected no alias for identifier specifier.'); 134 | } 135 | return mapLiteralToIdentifier(context, specifier.original); 136 | } 137 | 138 | function mapStarSpecifier( 139 | context: ParseContext, 140 | specifier: CoffeeModuleSpecifier 141 | ): nodes.Identifier { 142 | if (specifier.original.value !== '*') { 143 | throw new Error('Expected a star on the LHS of a star specifier.'); 144 | } 145 | if (!specifier.alias) { 146 | throw new Error('Expected node on the RHS of star specifier.'); 147 | } 148 | return mapLiteralToIdentifier(context, specifier.alias); 149 | } 150 | 151 | function mapSpecifier( 152 | context: ParseContext, 153 | specifier: CoffeeModuleSpecifier 154 | ): nodes.ModuleSpecifier { 155 | const { line, column, start, end, raw } = getLocation(context, specifier); 156 | const original = mapLiteralToIdentifier(context, specifier.original); 157 | const alias = specifier.alias 158 | ? mapLiteralToIdentifier(context, specifier.alias) 159 | : null; 160 | return new nodes.ModuleSpecifier( 161 | line, 162 | column, 163 | start, 164 | end, 165 | raw, 166 | original, 167 | alias 168 | ); 169 | } 170 | 171 | function mapLiteralToIdentifier( 172 | context: ParseContext, 173 | literal: Literal 174 | ): nodes.Identifier { 175 | if (literal instanceof IdentifierLiteral) { 176 | return mapLiteral(context, literal) as nodes.Identifier; 177 | } else if (literal.constructor === Literal) { 178 | const { line, column, start, end, raw } = getLocation(context, literal); 179 | return new nodes.Identifier(line, column, start, end, raw, literal.value); 180 | } else { 181 | throw new Error('Expected identifier in module declaration.'); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/mappers/mapObj.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Assign, 3 | ComputedPropertyName, 4 | Obj, 5 | Splat, 6 | Value, 7 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 8 | import { 9 | AssignOp, 10 | ObjectInitialiser, 11 | ObjectInitialiserMember, 12 | Spread, 13 | } from '../nodes'; 14 | import getLocation from '../util/getLocation'; 15 | import isCommentOnlyNode from '../util/isCommentOnlyNode'; 16 | import ParseContext from '../util/ParseContext'; 17 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 18 | import mapAny from './mapAny'; 19 | import mapValue from './mapValue'; 20 | 21 | export default function mapObj( 22 | context: ParseContext, 23 | node: Obj 24 | ): ObjectInitialiser { 25 | const { line, column, start, end, raw } = getLocation(context, node); 26 | 27 | const members: Array = []; 28 | 29 | for (const property of node.properties) { 30 | if (isCommentOnlyNode(property)) { 31 | continue; 32 | } 33 | const { line, column, start, end, raw } = getLocation(context, property); 34 | 35 | if (property instanceof Value) { 36 | // shorthand property 37 | const value = mapValue(context, property); 38 | const isComputed = property.base instanceof ComputedPropertyName; 39 | 40 | members.push( 41 | new ObjectInitialiserMember( 42 | line, 43 | column, 44 | start, 45 | end, 46 | raw, 47 | value, 48 | null, 49 | isComputed 50 | ) 51 | ); 52 | } else if (property instanceof Assign && property.context === 'object') { 53 | const key = mapAny(context, property.variable); 54 | const expression = mapAny(context, property.value); 55 | const isComputed = 56 | property.variable instanceof Value && 57 | property.variable.base instanceof ComputedPropertyName; 58 | 59 | members.push( 60 | new ObjectInitialiserMember( 61 | line, 62 | column, 63 | start, 64 | end, 65 | raw, 66 | key, 67 | expression, 68 | isComputed 69 | ) 70 | ); 71 | } else if (property instanceof Assign) { 72 | const assignee = mapAny(context, property.variable); 73 | const expression = mapAny(context, property.value); 74 | 75 | members.push( 76 | new AssignOp(line, column, start, end, raw, assignee, expression) 77 | ); 78 | } else if (property instanceof Splat) { 79 | members.push( 80 | new Spread( 81 | line, 82 | column, 83 | start, 84 | end, 85 | raw, 86 | mapAny(context, property.name) 87 | ) 88 | ); 89 | } else { 90 | throw new UnsupportedNodeError(property, 'Unexpected object member.'); 91 | } 92 | } 93 | 94 | return new ObjectInitialiser(line, column, start, end, raw, members); 95 | } 96 | -------------------------------------------------------------------------------- /src/mappers/mapOp.ts: -------------------------------------------------------------------------------- 1 | import { SourceType, SourceToken } from 'coffee-lex'; 2 | import { 3 | Literal, 4 | Op as CoffeeOp, 5 | Value, 6 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 7 | import { inspect } from 'util'; 8 | import { 9 | Await, 10 | BinaryOp, 11 | BitAndOp, 12 | BitNotOp, 13 | BitOrOp, 14 | BitXorOp, 15 | ChainedComparisonOp, 16 | DeleteOp, 17 | DivideOp, 18 | ExistsOp, 19 | ExpOp, 20 | EQOp, 21 | FloorDivideOp, 22 | GTEOp, 23 | GTOp, 24 | InstanceofOp, 25 | LeftShiftOp, 26 | LogicalAndOp, 27 | LogicalNotOp, 28 | LogicalOrOp, 29 | LTEOp, 30 | LTOp, 31 | ModuloOp, 32 | MultiplyOp, 33 | NewOp, 34 | Node, 35 | NEQOp, 36 | OfOp, 37 | Op, 38 | OperatorInfo, 39 | PlusOp, 40 | PostDecrementOp, 41 | PostIncrementOp, 42 | PreDecrementOp, 43 | PreIncrementOp, 44 | RemOp, 45 | SignedRightShiftOp, 46 | SubtractOp, 47 | TypeofOp, 48 | UnaryNegateOp, 49 | UnaryOp, 50 | UnaryPlusOp, 51 | UnsignedRightShiftOp, 52 | Yield, 53 | YieldFrom, 54 | } from '../nodes'; 55 | import getLocation from '../util/getLocation'; 56 | import getOperatorInfoInRange from '../util/getOperatorInfoInRange'; 57 | import isChainedComparison from '../util/isChainedComparison'; 58 | import isImplicitPlusOp from '../util/isImplicitPlusOp'; 59 | import makeString from '../util/makeString'; 60 | import ParseContext from '../util/ParseContext'; 61 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 62 | import unwindChainedComparison from '../util/unwindChainedComparison'; 63 | import mapAny from './mapAny'; 64 | 65 | export default function mapOp(context: ParseContext, node: CoffeeOp): Node { 66 | if (isChainedComparison(node)) { 67 | return mapChainedComparisonOp(context, node); 68 | } else { 69 | return mapOpWithoutChainedComparison(context, node); 70 | } 71 | } 72 | 73 | function mapChainedComparisonOp( 74 | context: ParseContext, 75 | node: CoffeeOp 76 | ): ChainedComparisonOp { 77 | const { line, column, start, end, raw } = getLocation(context, node); 78 | const coffeeOperands = unwindChainedComparison(node); 79 | const operands = coffeeOperands.map((operand) => mapAny(context, operand)); 80 | const operators: Array = []; 81 | 82 | for (let i = 0; i < operands.length - 1; i++) { 83 | const left = operands[i]; 84 | const right = operands[i + 1]; 85 | 86 | operators.push(getOperatorInfoInRange(context, left.end - 1, right.start)); 87 | } 88 | 89 | return new ChainedComparisonOp( 90 | line, 91 | column, 92 | start, 93 | end, 94 | raw, 95 | operands, 96 | operators 97 | ); 98 | } 99 | 100 | function mapOpWithoutChainedComparison( 101 | context: ParseContext, 102 | node: CoffeeOp 103 | ): Node { 104 | switch (node.operator) { 105 | case '===': 106 | return mapBinaryOp(context, node, EQOp); 107 | 108 | case '!==': 109 | return mapBinaryOp(context, node, NEQOp); 110 | 111 | case '!': 112 | return mapUnaryOp(context, node, LogicalNotOp); 113 | 114 | case '+': 115 | return mapPlusOp(context, node); 116 | 117 | case '-': 118 | return mapBinaryOrUnaryOp(context, node, SubtractOp, UnaryNegateOp); 119 | 120 | case '&&': 121 | return mapBinaryOp(context, node, LogicalAndOp); 122 | 123 | case '||': 124 | return mapBinaryOp(context, node, LogicalOrOp); 125 | 126 | case '*': 127 | return mapBinaryOp(context, node, MultiplyOp); 128 | 129 | case '/': 130 | return mapBinaryOp(context, node, DivideOp); 131 | 132 | case '//': 133 | return mapBinaryOp(context, node, FloorDivideOp); 134 | 135 | case '?': 136 | return mapBinaryOp(context, node, ExistsOp); 137 | 138 | case '<': 139 | return mapBinaryOp(context, node, LTOp); 140 | 141 | case '<=': 142 | return mapBinaryOp(context, node, LTEOp); 143 | 144 | case '>': 145 | return mapBinaryOp(context, node, GTOp); 146 | 147 | case '>=': 148 | return mapBinaryOp(context, node, GTEOp); 149 | 150 | case '++': 151 | return mapUnaryOp( 152 | context, 153 | node, 154 | node.flip ? PostIncrementOp : PreIncrementOp 155 | ); 156 | 157 | case '--': 158 | return mapUnaryOp( 159 | context, 160 | node, 161 | node.flip ? PostDecrementOp : PreDecrementOp 162 | ); 163 | 164 | case 'typeof': 165 | return mapUnaryOp(context, node, TypeofOp); 166 | 167 | case 'instanceof': 168 | return mapNegateableBinaryOp(context, node, InstanceofOp); 169 | 170 | case 'delete': 171 | return mapUnaryOp(context, node, DeleteOp); 172 | 173 | case 'in': 174 | return mapNegateableBinaryOp(context, node, OfOp); 175 | 176 | case 'new': 177 | return mapNewOp(context, node); 178 | 179 | case '**': 180 | return mapBinaryOp(context, node, ExpOp); 181 | 182 | case '%': 183 | return mapBinaryOp(context, node, RemOp); 184 | 185 | case '%%': 186 | return mapBinaryOp(context, node, ModuloOp); 187 | 188 | case '&': 189 | return mapBinaryOp(context, node, BitAndOp); 190 | 191 | case '|': 192 | return mapBinaryOp(context, node, BitOrOp); 193 | 194 | case '^': 195 | return mapBinaryOp(context, node, BitXorOp); 196 | 197 | case '<<': 198 | return mapBinaryOp(context, node, LeftShiftOp); 199 | 200 | case '>>': 201 | return mapBinaryOp(context, node, SignedRightShiftOp); 202 | 203 | case '>>>': 204 | return mapBinaryOp(context, node, UnsignedRightShiftOp); 205 | 206 | case '~': 207 | return mapUnaryOp(context, node, BitNotOp); 208 | 209 | case 'yield': 210 | return mapYieldOp(context, node); 211 | 212 | case 'yield*': 213 | return mapUnaryOp(context, node, YieldFrom); 214 | 215 | case 'await': 216 | return mapUnaryOp(context, node, Await); 217 | } 218 | 219 | throw new UnsupportedNodeError(node); 220 | } 221 | 222 | function mapPlusOp(context: ParseContext, node: CoffeeOp): Node { 223 | if (node.second) { 224 | if (isImplicitPlusOp(node, context)) { 225 | return makeString(context, node); 226 | } else { 227 | return mapBinaryOp(context, node, PlusOp); 228 | } 229 | } 230 | return mapUnaryOp(context, node, UnaryPlusOp); 231 | } 232 | 233 | function mapNewOp(context: ParseContext, node: CoffeeOp): NewOp { 234 | if (node.second) { 235 | throw new Error( 236 | `unexpected 'new' operator with multiple operands: ${inspect(node)}` 237 | ); 238 | } 239 | 240 | const { line, column, start, end, raw } = getLocation(context, node); 241 | 242 | return new NewOp( 243 | line, 244 | column, 245 | start, 246 | end, 247 | raw, 248 | mapAny(context, node.first), 249 | [] 250 | ); 251 | } 252 | 253 | function mapYieldOp(context: ParseContext, node: CoffeeOp): Yield { 254 | const { line, column, start, end, raw } = getLocation(context, node); 255 | if (isBareYield(node)) { 256 | return new Yield(line, column, start, end, raw, null); 257 | } else { 258 | return new Yield( 259 | line, 260 | column, 261 | start, 262 | end, 263 | raw, 264 | mapAny(context, node.first) 265 | ); 266 | } 267 | } 268 | 269 | function isBareYield(node: CoffeeOp): boolean { 270 | return ( 271 | node.first instanceof Value && 272 | node.first.base instanceof Literal && 273 | node.first.base.value === '' 274 | ); 275 | } 276 | 277 | interface BinaryOpCtor { 278 | new ( 279 | line: number, 280 | column: number, 281 | start: number, 282 | end: number, 283 | raw: string, 284 | left: Node, 285 | right: Node 286 | ): BinaryOp; 287 | } 288 | 289 | function mapBinaryOp( 290 | context: ParseContext, 291 | node: CoffeeOp, 292 | Op: T 293 | ): BinaryOp { 294 | const { line, column, start, end, raw } = getLocation(context, node); 295 | 296 | if (!node.second) { 297 | throw new Error( 298 | `unexpected '${node.operator}' operator with only one operand: ${inspect( 299 | node 300 | )}` 301 | ); 302 | } 303 | 304 | return new Op( 305 | line, 306 | column, 307 | start, 308 | end, 309 | raw, 310 | mapAny(context, node.first), 311 | mapAny(context, node.second) 312 | ); 313 | } 314 | 315 | interface UnaryOpCtor { 316 | new ( 317 | line: number, 318 | column: number, 319 | start: number, 320 | end: number, 321 | raw: string, 322 | expression: Node 323 | ): UnaryOp; 324 | } 325 | 326 | function mapUnaryOp( 327 | context: ParseContext, 328 | node: CoffeeOp, 329 | Op: T 330 | ): UnaryOp { 331 | const { line, column, start, end, raw } = getLocation(context, node); 332 | 333 | if (node.second) { 334 | throw new Error( 335 | `unexpected '${node.operator}' operator with two operands: ${inspect( 336 | node 337 | )}` 338 | ); 339 | } 340 | 341 | return new Op(line, column, start, end, raw, mapAny(context, node.first)); 342 | } 343 | 344 | function mapBinaryOrUnaryOp( 345 | context: ParseContext, 346 | node: CoffeeOp, 347 | BinaryOp: B, 348 | UnaryOp: U 349 | ): Op { 350 | if (node.second) { 351 | return mapBinaryOp(context, node, BinaryOp); 352 | } else { 353 | return mapUnaryOp(context, node, UnaryOp); 354 | } 355 | } 356 | 357 | interface NegateableBinaryOpCtor { 358 | new ( 359 | line: number, 360 | column: number, 361 | start: number, 362 | end: number, 363 | raw: string, 364 | left: Node, 365 | right: Node, 366 | isNot: boolean 367 | ): BinaryOp; 368 | } 369 | 370 | /** 371 | * This class exists only to serve as a temporary binary operator, do not use. 372 | */ 373 | class TemporaryBinaryOp extends BinaryOp { 374 | constructor( 375 | line: number, 376 | column: number, 377 | start: number, 378 | end: number, 379 | raw: string, 380 | left: Node, 381 | right: Node 382 | ) { 383 | super('TEMPORARY', line, column, start, end, raw, left, right); 384 | } 385 | } 386 | 387 | function tokenStartsWith( 388 | prefix: string, 389 | context: ParseContext, 390 | token: SourceToken 391 | ): boolean { 392 | return ( 393 | context.source.slice(token.start, token.start + prefix.length) === prefix 394 | ); 395 | } 396 | 397 | function mapNegateableBinaryOp( 398 | context: ParseContext, 399 | node: CoffeeOp, 400 | Op: T 401 | ): BinaryOp { 402 | const { line, column, start, end, raw, left, right } = mapBinaryOp( 403 | context, 404 | node, 405 | TemporaryBinaryOp 406 | ); 407 | 408 | const lastTokenIndexOfLeft = 409 | context.sourceTokens.indexOfTokenEndingAtSourceIndex(left.end); 410 | const firstTokenIndexOfRight = 411 | context.sourceTokens.indexOfTokenStartingAtSourceIndex(right.start); 412 | let isNot = false; 413 | 414 | if (lastTokenIndexOfLeft) { 415 | for ( 416 | let i = lastTokenIndexOfLeft.next(); 417 | i && i !== firstTokenIndexOfRight; 418 | i = i.next() 419 | ) { 420 | const token = context.sourceTokens.tokenAtIndex(i); 421 | if ( 422 | token && 423 | (token.type === SourceType.OPERATOR || 424 | token.type === SourceType.RELATION) 425 | ) { 426 | isNot = 427 | tokenStartsWith('not', context, token) || 428 | tokenStartsWith('!', context, token); 429 | break; 430 | } 431 | } 432 | } 433 | 434 | return new Op(line, column, start, end, raw, left, right, isNot); 435 | } 436 | -------------------------------------------------------------------------------- /src/mappers/mapParam.ts: -------------------------------------------------------------------------------- 1 | import { Param } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { DefaultParam, Node, Rest } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapParam(context: ParseContext, node: Param): Node { 8 | const { line, column, start, end, raw } = getLocation(context, node); 9 | const param = mapAny(context, node.name); 10 | 11 | if (node.value) { 12 | const value = mapAny(context, node.value); 13 | return new DefaultParam(line, column, start, end, raw, param, value); 14 | } 15 | 16 | if (node.splat) { 17 | return new Rest(line, column, start, end, raw, param); 18 | } 19 | 20 | return param; 21 | } 22 | -------------------------------------------------------------------------------- /src/mappers/mapParens.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Block, 3 | Parens, 4 | StringWithInterpolations, 5 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 6 | import { Node, SeqOp } from '../nodes'; 7 | import isCommentOnlyNode from '../util/isCommentOnlyNode'; 8 | import ParseContext from '../util/ParseContext'; 9 | import mapAny from './mapAny'; 10 | 11 | export default function mapParens( 12 | context: ParseContext, 13 | node: Parens | StringWithInterpolations 14 | ): Node { 15 | if (!(node.body instanceof Block)) { 16 | return mapAny(context, node.body); 17 | } 18 | 19 | let { expressions } = node.body; 20 | expressions = expressions.filter((expr) => !isCommentOnlyNode(expr)); 21 | 22 | if (expressions.length === 1) { 23 | return mapAny(context, expressions[0]); 24 | } 25 | 26 | return expressions 27 | .map((expression) => mapAny(context, expression)) 28 | .reduceRight( 29 | (right, left) => 30 | new SeqOp( 31 | left.line, 32 | left.column, 33 | left.start, 34 | right.end, 35 | context.source.slice(left.start, right.end), 36 | left, 37 | right 38 | ) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/mappers/mapPossiblyEmptyBlock.ts: -------------------------------------------------------------------------------- 1 | import { Block as CoffeeBlock } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Block } from '../nodes'; 3 | import isCommentOnlyNode from '../util/isCommentOnlyNode'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapBlock from './mapBlock'; 6 | 7 | /** 8 | * Convert a CoffeeScript block node to a decaffeinate-parser Block node, 9 | * carefully handling the empty block case. 10 | * 11 | * Ideally, there wouldn't need to be any special cases here, but there are a 12 | * few things that add complexity for now. 13 | * - Both decaffeinate-parser and decaffeinate expect every AST node to contain 14 | * at least one token. That means that a whitespace-only empty block will 15 | * cause a crash. 16 | * - The CoffeeScript compiler seems to sometimes give incorrect location 17 | * information for empty blocks, e.g. for empty class bodies. 18 | * - Blocks ending in semicolons need to sometimes have that semicolon removed 19 | * (e.g. when the block is treated as an expression), so an empty block that 20 | * is just a semicolon should become an actual empty block and not null. 21 | * 22 | * We convert a CoffeeScript empty block to a decaffeinate-parser empty block if 23 | * the block is only a semicolon or if the block only consists of comments. All 24 | * other empty blocks result in null. 25 | */ 26 | export default function mapPossiblyEmptyBlock( 27 | context: ParseContext, 28 | node: CoffeeBlock | null | undefined 29 | ): Block | null { 30 | if (!node) { 31 | return null; 32 | } 33 | if (node.expressions.every((expression) => isCommentOnlyNode(expression))) { 34 | return null; 35 | } 36 | 37 | const lastSourceIndex = context.linesAndColumns.indexForLocation({ 38 | line: node.locationData.last_line, 39 | column: node.locationData.last_column, 40 | }); 41 | if (lastSourceIndex === null) { 42 | throw new Error('Expected to find last source index of block.'); 43 | } 44 | if (context.source[lastSourceIndex] === ';') { 45 | return mapBlock(context, node); 46 | } 47 | 48 | if (node.expressions.length === 0) { 49 | return null; 50 | } 51 | return mapBlock(context, node); 52 | } 53 | -------------------------------------------------------------------------------- /src/mappers/mapProgram.ts: -------------------------------------------------------------------------------- 1 | import { Program } from '../nodes'; 2 | import ParseContext from '../util/ParseContext'; 3 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 4 | 5 | export default function mapProgram(context: ParseContext): Program { 6 | return new Program( 7 | 1, 8 | 1, 9 | 0, 10 | context.source.length, 11 | context.source, 12 | mapPossiblyEmptyBlock(context, context.ast), 13 | context 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/mappers/mapRange.ts: -------------------------------------------------------------------------------- 1 | import { Range as CoffeeRange } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { inspect } from 'util'; 3 | import { Range } from '../nodes'; 4 | import getLocation from '../util/getLocation'; 5 | import ParseContext from '../util/ParseContext'; 6 | import mapAny from './mapAny'; 7 | 8 | export default function mapRange( 9 | context: ParseContext, 10 | node: CoffeeRange 11 | ): Range { 12 | const { line, column, start, end, raw } = getLocation(context, node); 13 | 14 | if (!node.from || !node.to) { 15 | throw new Error(`'from' or 'to' unexpectedly missing: ${inspect(node)}`); 16 | } 17 | 18 | return new Range( 19 | line, 20 | column, 21 | start, 22 | end, 23 | raw, 24 | mapAny(context, node.from), 25 | mapAny(context, node.to), 26 | !node.exclusive 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/mappers/mapReturn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AwaitReturn as CoffeeAwaitReturn, 3 | Return as CoffeeReturn, 4 | YieldReturn as CoffeeYieldReturn, 5 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 6 | import { AwaitReturn, Return, YieldReturn } from '../nodes'; 7 | import getLocation from '../util/getLocation'; 8 | import ParseContext from '../util/ParseContext'; 9 | import mapAny from './mapAny'; 10 | 11 | export default function mapReturn( 12 | context: ParseContext, 13 | node: CoffeeReturn 14 | ): Return { 15 | const { line, column, start, end, raw } = getLocation(context, node); 16 | const argument = node.expression ? mapAny(context, node.expression) : null; 17 | if (node instanceof CoffeeYieldReturn) { 18 | return new YieldReturn(line, column, start, end, raw, argument); 19 | } else if (node instanceof CoffeeAwaitReturn) { 20 | return new AwaitReturn(line, column, start, end, raw, argument); 21 | } else { 22 | return new Return(line, column, start, end, raw, argument); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/mappers/mapSplat.ts: -------------------------------------------------------------------------------- 1 | import { Splat } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Spread } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapSplat(context: ParseContext, node: Splat): Spread { 8 | const { line, column, start, end, raw } = getLocation(context, node); 9 | return new Spread(line, column, start, end, raw, mapAny(context, node.name)); 10 | } 11 | -------------------------------------------------------------------------------- /src/mappers/mapSuper.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { Super as CoffeeSuper } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { Node, Super } from '../nodes'; 4 | import getLocation from '../util/getLocation'; 5 | import ParseContext from '../util/ParseContext'; 6 | import { reduceProperty } from './mapValue'; 7 | 8 | export default function mapSuper( 9 | context: ParseContext, 10 | node: CoffeeSuper 11 | ): Node { 12 | const location = getLocation(context, node); 13 | const { line, column, start } = location; 14 | 15 | const superTokenIndex = 16 | context.sourceTokens.indexOfTokenStartingAtSourceIndex(start); 17 | if (!superTokenIndex) { 18 | throw new Error('Expected token at the start of super node.'); 19 | } 20 | const superToken = context.sourceTokens.tokenAtIndex(superTokenIndex); 21 | if (!superToken || superToken.type !== SourceType.SUPER) { 22 | throw new Error('Expected super token at the start of super node.'); 23 | } 24 | const superNode = new Super( 25 | line, 26 | column, 27 | start, 28 | superToken.end, 29 | context.source.slice(superToken.start, superToken.end) 30 | ); 31 | return reduceProperty(context, location, superNode, node.accessor); 32 | } 33 | -------------------------------------------------------------------------------- /src/mappers/mapSwitch.ts: -------------------------------------------------------------------------------- 1 | import { SourceToken, SourceType } from 'coffee-lex'; 2 | import { Switch as CoffeeSwitch } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { Switch, SwitchCase } from '../nodes'; 4 | import getLocation from '../util/getLocation'; 5 | import ParseContext from '../util/ParseContext'; 6 | import mapAny from './mapAny'; 7 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 8 | 9 | export default function mapSwitch( 10 | context: ParseContext, 11 | node: CoffeeSwitch 12 | ): Switch { 13 | const { line, column, start, end, raw } = getLocation(context, node); 14 | 15 | const expression = node.subject ? mapAny(context, node.subject) : null; 16 | const cases = node.cases.map(([conditions, body]) => { 17 | if (!Array.isArray(conditions)) { 18 | conditions = [conditions]; 19 | } 20 | const switchConditions = conditions.map((condition) => 21 | mapAny(context, condition) 22 | ); 23 | const consequent = mapPossiblyEmptyBlock(context, body); 24 | const whenToken = getWhenTokenBeforeOffset( 25 | context, 26 | switchConditions[0].start, 27 | start 28 | ); 29 | const locationForIndex = context.linesAndColumns.locationForIndex( 30 | whenToken.start 31 | ); 32 | 33 | if (!locationForIndex) { 34 | throw new Error( 35 | `cannot map WHEN token start to line/column: ${whenToken.start}` 36 | ); 37 | } 38 | 39 | const { line: caseLine, column: caseColumn } = locationForIndex; 40 | const { end } = getLocation(context, body); 41 | return new SwitchCase( 42 | caseLine + 1, 43 | caseColumn + 1, 44 | whenToken.start, 45 | end, 46 | context.source.slice(whenToken.start, end), 47 | switchConditions, 48 | consequent 49 | ); 50 | }); 51 | 52 | return new Switch( 53 | line, 54 | column, 55 | start, 56 | end, 57 | raw, 58 | expression, 59 | cases, 60 | mapPossiblyEmptyBlock(context, node.otherwise) 61 | ); 62 | } 63 | 64 | function getWhenTokenBeforeOffset( 65 | context: ParseContext, 66 | offset: number, 67 | lowerBound: number 68 | ): SourceToken { 69 | const offsetTokenIndex = 70 | context.sourceTokens.indexOfTokenNearSourceIndex(offset); 71 | const lowerBoundTokenIndex = 72 | context.sourceTokens.indexOfTokenNearSourceIndex(lowerBound); 73 | const whenTokenIndex = context.sourceTokens.lastIndexOfTokenMatchingPredicate( 74 | (token) => token.type === SourceType.WHEN, 75 | offsetTokenIndex, 76 | lowerBoundTokenIndex 77 | ); 78 | 79 | if (whenTokenIndex) { 80 | const whenToken = context.sourceTokens.tokenAtIndex(whenTokenIndex); 81 | 82 | if (whenToken) { 83 | return whenToken; 84 | } 85 | } 86 | 87 | throw new Error( 88 | `unable to find WHEN token before ${offset} (lower bound: ${lowerBound})` 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/mappers/mapTaggedTemplateCall.ts: -------------------------------------------------------------------------------- 1 | import { TaggedTemplateCall } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Node, String, TaggedTemplateLiteral } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapTaggedTemplateCall( 8 | context: ParseContext, 9 | node: TaggedTemplateCall 10 | ): Node { 11 | const { line, column, start, end, raw } = getLocation(context, node); 12 | if (!node.variable) { 13 | throw new Error('Expected tag in tagged template literal.'); 14 | } 15 | const tag = mapAny(context, node.variable); 16 | if (node.args.length !== 1) { 17 | throw new Error( 18 | 'Expected tagged template literal call to have exactly one argument.' 19 | ); 20 | } 21 | const template = mapAny(context, node.args[0]); 22 | if (!(template instanceof String)) { 23 | throw new Error( 24 | 'Expected tagged template literal argument to be a string.' 25 | ); 26 | } 27 | return new TaggedTemplateLiteral( 28 | line, 29 | column, 30 | start, 31 | end, 32 | raw, 33 | tag, 34 | template 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/mappers/mapThrow.ts: -------------------------------------------------------------------------------- 1 | import { Throw as CoffeeThrow } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Throw } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | 7 | export default function mapThrow( 8 | context: ParseContext, 9 | node: CoffeeThrow 10 | ): Throw { 11 | const { line, column, start, end, raw } = getLocation(context, node); 12 | const expression = node.expression ? mapAny(context, node.expression) : null; 13 | return new Throw(line, column, start, end, raw, expression); 14 | } 15 | -------------------------------------------------------------------------------- /src/mappers/mapTry.ts: -------------------------------------------------------------------------------- 1 | import { Try as CoffeeTry } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { Try } from '../nodes'; 3 | import getLocation from '../util/getLocation'; 4 | import ParseContext from '../util/ParseContext'; 5 | import mapAny from './mapAny'; 6 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 7 | 8 | export default function mapTry(context: ParseContext, node: CoffeeTry): Try { 9 | const { line, column, start, end, raw } = getLocation(context, node); 10 | 11 | return new Try( 12 | line, 13 | column, 14 | start, 15 | end, 16 | raw, 17 | mapPossiblyEmptyBlock(context, node.attempt), 18 | node.errorVariable ? mapAny(context, node.errorVariable) : null, 19 | mapPossiblyEmptyBlock(context, node.recovery), 20 | mapPossiblyEmptyBlock(context, node.ensure) 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/mappers/mapValue.ts: -------------------------------------------------------------------------------- 1 | import { SourceType, SourceTokenListIndex } from 'coffee-lex'; 2 | import { 3 | Access, 4 | Index, 5 | Literal, 6 | LocationData, 7 | Slice as CoffeeSlice, 8 | Value, 9 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 10 | import { inspect } from 'util'; 11 | import { 12 | DynamicMemberAccessOp, 13 | Identifier, 14 | MemberAccessOp, 15 | Node, 16 | ProtoMemberAccessOp, 17 | Slice, 18 | SoakedDynamicMemberAccessOp, 19 | SoakedMemberAccessOp, 20 | SoakedProtoMemberAccessOp, 21 | SoakedSlice, 22 | } from '../nodes'; 23 | import getLocation, { NodeLocation } from '../util/getLocation'; 24 | import ParseContext from '../util/ParseContext'; 25 | import UnsupportedNodeError from '../util/UnsupportedNodeError'; 26 | import mapAny from './mapAny'; 27 | 28 | export default function mapValue(context: ParseContext, node: Value): Node { 29 | const location = getLocation(context, node); 30 | 31 | return node.properties.reduce( 32 | (reduced, property) => reduceProperty(context, location, reduced, property), 33 | mapAny(context, node.base) 34 | ); 35 | } 36 | 37 | export function reduceProperty( 38 | context: ParseContext, 39 | location: NodeLocation, 40 | reduced: Node, 41 | property: Access | Index | CoffeeSlice 42 | ): Node { 43 | if (property instanceof Access) { 44 | const name = property.name; 45 | 46 | if (!(name instanceof Literal)) { 47 | throw new Error( 48 | `unexpected non-Literal property access name: ${inspect(name)}` 49 | ); 50 | } 51 | 52 | let startTokenIndex = tokenIndexAtLocation(context, property.locationData); 53 | let startToken = 54 | startTokenIndex && context.sourceTokens.tokenAtIndex(startTokenIndex); 55 | 56 | if (startToken && property.soak) { 57 | if (startToken.type !== SourceType.EXISTENCE) { 58 | throw new Error( 59 | `expected EXISTENCE token ('?') but got ${ 60 | SourceType[startToken.type] 61 | }: ${inspect(startToken)}` 62 | ); 63 | } 64 | 65 | startTokenIndex = startTokenIndex && startTokenIndex.next(); 66 | startToken = 67 | startTokenIndex && context.sourceTokens.tokenAtIndex(startTokenIndex); 68 | } 69 | 70 | if (!startToken) { 71 | throw new Error( 72 | `cannot find token at start of property: ${inspect(property)}` 73 | ); 74 | } 75 | 76 | const last = context.linesAndColumns.indexForLocation({ 77 | line: property.locationData.last_line, 78 | column: property.locationData.last_column, 79 | }); 80 | 81 | if (last === null) { 82 | throw new Error( 83 | `cannot find offset of last character of property: ${inspect(property)}` 84 | ); 85 | } 86 | 87 | const isPrototypeAccess = startToken.type === SourceType.PROTO; 88 | 89 | if (isPrototypeAccess) { 90 | const AccessOp = property.soak 91 | ? SoakedProtoMemberAccessOp 92 | : ProtoMemberAccessOp; 93 | 94 | return new AccessOp( 95 | location.line, 96 | location.column, 97 | location.start, 98 | last + 1, 99 | context.source.slice(location.start, last + 1), 100 | reduced 101 | ); 102 | } else { 103 | const member = mapAny(context, name); 104 | 105 | if (!(member instanceof Identifier)) { 106 | throw new Error( 107 | `unexpected non-Identifier access member: ${inspect(member)}` 108 | ); 109 | } 110 | 111 | const AccessOp = property.soak ? SoakedMemberAccessOp : MemberAccessOp; 112 | 113 | return new AccessOp( 114 | location.line, 115 | location.column, 116 | location.start, 117 | last + 1, 118 | context.source.slice(location.start, last + 1), 119 | reduced, 120 | member 121 | ); 122 | } 123 | } else if (property instanceof Index) { 124 | const NodeClass = property.soak 125 | ? SoakedDynamicMemberAccessOp 126 | : DynamicMemberAccessOp; 127 | const last = context.linesAndColumns.indexForLocation({ 128 | line: property.locationData.last_line, 129 | column: property.locationData.last_column, 130 | }); 131 | if (last === null) { 132 | throw new Error( 133 | `cannot find offset of last character of index: ${inspect(property)}` 134 | ); 135 | } 136 | 137 | return new NodeClass( 138 | location.line, 139 | location.column, 140 | location.start, 141 | last + 1, 142 | context.source.slice(location.start, last + 1), 143 | reduced, 144 | mapAny(context, property.index) 145 | ); 146 | } else if (property instanceof CoffeeSlice) { 147 | const last = context.linesAndColumns.indexForLocation({ 148 | line: property.locationData.last_line, 149 | column: property.locationData.last_column, 150 | }); 151 | 152 | if (last === null) { 153 | throw new Error( 154 | `cannot find offset of last character of slice: ${inspect(property)}` 155 | ); 156 | } 157 | 158 | const SliceClass = property.soak ? SoakedSlice : Slice; 159 | return new SliceClass( 160 | location.line, 161 | location.column, 162 | location.start, 163 | last + 1, 164 | context.source.slice(location.start, last + 1), 165 | reduced, 166 | property.range.from ? mapAny(context, property.range.from) : null, 167 | property.range.to ? mapAny(context, property.range.to) : null, 168 | !property.range.exclusive 169 | ); 170 | } else { 171 | throw new UnsupportedNodeError(property, 'Unexpected property access.'); 172 | } 173 | } 174 | 175 | function tokenIndexAtLocation( 176 | context: ParseContext, 177 | location: LocationData 178 | ): SourceTokenListIndex | null { 179 | const start = context.linesAndColumns.indexForLocation({ 180 | line: location.first_line, 181 | column: location.first_column, 182 | }); 183 | 184 | if (start === null) { 185 | return null; 186 | } 187 | 188 | return context.sourceTokens.indexOfTokenContainingSourceIndex(start); 189 | } 190 | -------------------------------------------------------------------------------- /src/mappers/mapWhile.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { While as CoffeeWhile } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { Block, Loop, While } from '../nodes'; 4 | import getLocation from '../util/getLocation'; 5 | import ParseContext from '../util/ParseContext'; 6 | import mapAny from './mapAny'; 7 | import mapPossiblyEmptyBlock from './mapPossiblyEmptyBlock'; 8 | 9 | export default function mapWhile( 10 | context: ParseContext, 11 | node: CoffeeWhile 12 | ): While | Loop { 13 | const { line, column, start, end, raw } = getLocation(context, node); 14 | const startTokenIndex = 15 | context.sourceTokens.indexOfTokenStartingAtSourceIndex(start); 16 | const startToken = 17 | startTokenIndex && context.sourceTokens.tokenAtIndex(startTokenIndex); 18 | 19 | if (startToken && startToken.type === SourceType.LOOP) { 20 | return new Loop( 21 | line, 22 | column, 23 | start, 24 | end, 25 | raw, 26 | mapPossiblyEmptyBlock(context, node.body) 27 | ); 28 | } 29 | 30 | const condition = mapAny(context, node.condition); 31 | const guard = node.guard ? mapAny(context, node.guard) : null; 32 | let body = mapPossiblyEmptyBlock(context, node.body); 33 | 34 | if (body instanceof Block && body.start < condition.start) { 35 | body = body.withInline(true); 36 | } 37 | 38 | return new While( 39 | line, 40 | column, 41 | start, 42 | end, 43 | raw, 44 | condition, 45 | guard, 46 | body, 47 | node.condition.inverted === true 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/parseCS1AsCS2.ts: -------------------------------------------------------------------------------- 1 | import { nodes } from 'decaffeinate-coffeescript'; 2 | import { 3 | Access as CS1Access, 4 | Arr as CS1Arr, 5 | Assign as CS1Assign, 6 | Base as CS1Base, 7 | Block as CS1Block, 8 | BooleanLiteral as CS1BooleanLiteral, 9 | Call as CS1Call, 10 | Class as CS1Class, 11 | Code as CS1Code, 12 | Comment as CS1Comment, 13 | Existence as CS1Existence, 14 | Expansion as CS1Expansion, 15 | ExportAllDeclaration as CS1ExportAllDeclaration, 16 | ExportDeclaration as CS1ExportDeclaration, 17 | ExportDefaultDeclaration as CS1ExportDefaultDeclaration, 18 | ExportNamedDeclaration as CS1ExportNamedDeclaration, 19 | ExportSpecifier as CS1ExportSpecifier, 20 | ExportSpecifierList as CS1ExportSpecifierList, 21 | Extends as CS1Extends, 22 | For as CS1For, 23 | IdentifierLiteral as CS1IdentifierLiteral, 24 | If as CS1If, 25 | ImportClause as CS1ImportClause, 26 | ImportDeclaration as CS1ImportDeclaration, 27 | ImportDefaultSpecifier as CS1ImportDefaultSpecifier, 28 | ImportNamespaceSpecifier as CS1ImportNamespaceSpecifier, 29 | ImportSpecifier as CS1ImportSpecifier, 30 | ImportSpecifierList as CS1ImportSpecifierList, 31 | In as CS1In, 32 | Index as CS1Index, 33 | InfinityLiteral as CS1InfinityLiteral, 34 | Literal as CS1Literal, 35 | ModuleDeclaration as CS1ModuleDeclaration, 36 | ModuleSpecifier as CS1ModuleSpecifier, 37 | ModuleSpecifierList as CS1ModuleSpecifierList, 38 | NaNLiteral as CS1NaNLiteral, 39 | NullLiteral as CS1NullLiteral, 40 | NumberLiteral as CS1NumberLiteral, 41 | Obj as CS1Obj, 42 | Op as CS1Op, 43 | Param as CS1Param, 44 | Parens as CS1Parens, 45 | PassthroughLiteral as CS1PassthroughLiteral, 46 | PropertyName as CS1PropertyName, 47 | Range as CS1Range, 48 | RegexLiteral as CS1RegexLiteral, 49 | RegexWithInterpolations as CS1RegexWithInterpolations, 50 | Return as CS1Return, 51 | Slice as CS1Slice, 52 | Splat as CS1Splat, 53 | StatementLiteral as CS1StatementLiteral, 54 | StringLiteral as CS1StringLiteral, 55 | StringWithInterpolations as CS1StringWithInterpolations, 56 | SuperCall as CS1SuperCall, 57 | Switch as CS1Switch, 58 | SwitchCaseCondition as CS1SwitchCaseCondition, 59 | TaggedTemplateCall as CS1TaggedTemplateCall, 60 | ThisLiteral as CS1ThisLiteral, 61 | Throw as CS1Throw, 62 | Try as CS1Try, 63 | UndefinedLiteral as CS1UndefinedLiteral, 64 | Value as CS1Value, 65 | While as CS1While, 66 | YieldReturn as CS1YieldReturn, 67 | } from 'decaffeinate-coffeescript/lib/coffee-script/nodes.js'; 68 | import { 69 | Access, 70 | Arr, 71 | Assign, 72 | Base, 73 | Block, 74 | BooleanLiteral, 75 | Call, 76 | Class, 77 | Code, 78 | Existence, 79 | Expansion, 80 | ExportAllDeclaration, 81 | ExportDeclaration, 82 | ExportDefaultDeclaration, 83 | ExportNamedDeclaration, 84 | ExportSpecifier, 85 | ExportSpecifierList, 86 | Extends, 87 | For, 88 | IdentifierLiteral, 89 | If, 90 | ImportClause, 91 | ImportDeclaration, 92 | ImportDefaultSpecifier, 93 | ImportNamespaceSpecifier, 94 | ImportSpecifier, 95 | ImportSpecifierList, 96 | In, 97 | Index, 98 | InfinityLiteral, 99 | Literal, 100 | ModuleDeclaration, 101 | ModuleSpecifier, 102 | ModuleSpecifierList, 103 | NaNLiteral, 104 | NullLiteral, 105 | NumberLiteral, 106 | Obj, 107 | Op, 108 | Param, 109 | Parens, 110 | PassthroughLiteral, 111 | PropertyName, 112 | Range, 113 | RegexLiteral, 114 | RegexWithInterpolations, 115 | Return, 116 | Slice, 117 | Splat, 118 | StatementLiteral, 119 | StringLiteral, 120 | StringWithInterpolations, 121 | SuperCall, 122 | Switch, 123 | TaggedTemplateCall, 124 | ThisLiteral, 125 | Throw, 126 | Try, 127 | UndefinedLiteral, 128 | Value, 129 | While, 130 | YieldReturn, 131 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 132 | 133 | const nodeTypeMap = new Map(); 134 | nodeTypeMap.set(CS1Base, Base); 135 | nodeTypeMap.set(CS1Block, Block); 136 | nodeTypeMap.set(CS1Literal, Literal); 137 | nodeTypeMap.set(CS1NumberLiteral, NumberLiteral); 138 | nodeTypeMap.set(CS1InfinityLiteral, InfinityLiteral); 139 | nodeTypeMap.set(CS1NaNLiteral, NaNLiteral); 140 | nodeTypeMap.set(CS1StringLiteral, StringLiteral); 141 | nodeTypeMap.set(CS1RegexLiteral, RegexLiteral); 142 | nodeTypeMap.set(CS1PassthroughLiteral, PassthroughLiteral); 143 | nodeTypeMap.set(CS1IdentifierLiteral, IdentifierLiteral); 144 | nodeTypeMap.set(CS1PropertyName, PropertyName); 145 | nodeTypeMap.set(CS1StatementLiteral, StatementLiteral); 146 | nodeTypeMap.set(CS1ThisLiteral, ThisLiteral); 147 | nodeTypeMap.set(CS1UndefinedLiteral, UndefinedLiteral); 148 | nodeTypeMap.set(CS1NullLiteral, NullLiteral); 149 | nodeTypeMap.set(CS1BooleanLiteral, BooleanLiteral); 150 | nodeTypeMap.set(CS1Return, Return); 151 | nodeTypeMap.set(CS1YieldReturn, YieldReturn); 152 | nodeTypeMap.set(CS1Value, Value); 153 | nodeTypeMap.set(CS1Call, Call); 154 | nodeTypeMap.set(CS1SuperCall, SuperCall); 155 | nodeTypeMap.set(CS1RegexWithInterpolations, RegexWithInterpolations); 156 | nodeTypeMap.set(CS1TaggedTemplateCall, TaggedTemplateCall); 157 | nodeTypeMap.set(CS1Extends, Extends); 158 | nodeTypeMap.set(CS1Access, Access); 159 | nodeTypeMap.set(CS1Index, Index); 160 | nodeTypeMap.set(CS1Range, Range); 161 | nodeTypeMap.set(CS1Slice, Slice); 162 | nodeTypeMap.set(CS1Obj, Obj); 163 | nodeTypeMap.set(CS1Arr, Arr); 164 | nodeTypeMap.set(CS1Class, Class); 165 | nodeTypeMap.set(CS1ModuleDeclaration, ModuleDeclaration); 166 | nodeTypeMap.set(CS1ImportDeclaration, ImportDeclaration); 167 | nodeTypeMap.set(CS1ImportClause, ImportClause); 168 | nodeTypeMap.set(CS1ExportDeclaration, ExportDeclaration); 169 | nodeTypeMap.set(CS1ExportNamedDeclaration, ExportNamedDeclaration); 170 | nodeTypeMap.set(CS1ExportDefaultDeclaration, ExportDefaultDeclaration); 171 | nodeTypeMap.set(CS1ExportAllDeclaration, ExportAllDeclaration); 172 | nodeTypeMap.set(CS1ModuleSpecifierList, ModuleSpecifierList); 173 | nodeTypeMap.set(CS1ImportSpecifierList, ImportSpecifierList); 174 | nodeTypeMap.set(CS1ExportSpecifierList, ExportSpecifierList); 175 | nodeTypeMap.set(CS1ModuleSpecifier, ModuleSpecifier); 176 | nodeTypeMap.set(CS1ImportSpecifier, ImportSpecifier); 177 | nodeTypeMap.set(CS1ImportDefaultSpecifier, ImportDefaultSpecifier); 178 | nodeTypeMap.set(CS1ImportNamespaceSpecifier, ImportNamespaceSpecifier); 179 | nodeTypeMap.set(CS1ExportSpecifier, ExportSpecifier); 180 | nodeTypeMap.set(CS1Assign, Assign); 181 | nodeTypeMap.set(CS1Code, Code); 182 | nodeTypeMap.set(CS1Param, Param); 183 | nodeTypeMap.set(CS1Splat, Splat); 184 | nodeTypeMap.set(CS1Expansion, Expansion); 185 | nodeTypeMap.set(CS1While, While); 186 | nodeTypeMap.set(CS1Op, Op); 187 | nodeTypeMap.set(CS1In, In); 188 | nodeTypeMap.set(CS1Try, Try); 189 | nodeTypeMap.set(CS1Throw, Throw); 190 | nodeTypeMap.set(CS1Existence, Existence); 191 | nodeTypeMap.set(CS1Parens, Parens); 192 | nodeTypeMap.set(CS1StringWithInterpolations, StringWithInterpolations); 193 | nodeTypeMap.set(CS1For, For); 194 | nodeTypeMap.set(CS1Switch, Switch); 195 | nodeTypeMap.set(CS1If, If); 196 | 197 | /** 198 | * Run the CS1 parser and convert the resulting AST into a CS2-compatible AST. 199 | */ 200 | export default function parseCS1AsCS2(source: string): Block { 201 | const cs1AST = nodes(source); 202 | const cs2AST = convertCS1NodeToCS2(cs1AST); 203 | if (!(cs2AST instanceof Block)) { 204 | throw new Error('Expected top-level CS file to convert to a Block'); 205 | } 206 | return cs2AST; 207 | } 208 | 209 | function convertCS1NodeToCS2(node: CS1Base): Base { 210 | if (node instanceof CS1Comment) { 211 | return makeCS2Comment(node); 212 | } 213 | const cs1Constructor = node.constructor; 214 | const cs2Constructor = nodeTypeMap.get(cs1Constructor); 215 | if (!cs2Constructor) { 216 | throw new Error(`Unexpected CS1 type for node ${node}`); 217 | } 218 | const result = Object.create(cs2Constructor.prototype); 219 | for (const key of Object.keys(node)) { 220 | const value = node[key]; 221 | if ( 222 | Array.isArray(value) && 223 | value.length > 0 && 224 | value[0] instanceof CS1Base 225 | ) { 226 | result[key] = value.map((child: CS1Base) => convertCS1NodeToCS2(child)); 227 | } else if (key === 'cases') { 228 | // Switch cases have a complex structure, so special-case those. 229 | result[key] = value.map( 230 | ([switchCaseCondition, block]: [CS1SwitchCaseCondition, CS1Block]) => { 231 | if (Array.isArray(switchCaseCondition)) { 232 | return [ 233 | switchCaseCondition.map((condition) => 234 | convertCS1NodeToCS2(condition) 235 | ), 236 | convertCS1NodeToCS2(block), 237 | ]; 238 | } else { 239 | return [ 240 | convertCS1NodeToCS2(switchCaseCondition), 241 | convertCS1NodeToCS2(block), 242 | ]; 243 | } 244 | } 245 | ); 246 | } else if (value instanceof CS1Base) { 247 | result[key] = convertCS1NodeToCS2(value); 248 | } else { 249 | result[key] = value; 250 | } 251 | } 252 | return result; 253 | } 254 | 255 | /** 256 | * The CS2 comment format is an empty passthrough node with comments attached. We need to keep 257 | * these nodes from CS1 in some cases so that we properly determine the end of functions ending 258 | * in block comments. 259 | */ 260 | function makeCS2Comment(comment: CS1Comment): Value { 261 | const valueNode = Object.create(Value.prototype); 262 | const passthroughNode = Object.create(PassthroughLiteral.prototype); 263 | valueNode.properties = []; 264 | valueNode.base = passthroughNode; 265 | passthroughNode.value = ''; 266 | valueNode.locationData = comment.locationData; 267 | passthroughNode.locationData = comment.locationData; 268 | return valueNode; 269 | } 270 | -------------------------------------------------------------------------------- /src/parseCS2.ts: -------------------------------------------------------------------------------- 1 | import { nodes } from 'decaffeinate-coffeescript2'; 2 | import { Block } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | 4 | export default function parseCS2(source: string): Block { 5 | return nodes(source); 6 | } 7 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import lex, { SourceTokenList } from 'coffee-lex'; 2 | import { patchCoffeeScript } from './ext/coffee-script'; 3 | import mapProgram from './mappers/mapProgram'; 4 | import { Node, Program } from './nodes'; 5 | import parseCS1AsCS2 from './parseCS1AsCS2'; 6 | import parseCS2 from './parseCS2'; 7 | import fixLocations from './util/fixLocations'; 8 | import ParseContext from './util/ParseContext'; 9 | 10 | export type Options = { 11 | useCS2: boolean; 12 | }; 13 | 14 | export const DEFAULT_OPTIONS: Options = { 15 | useCS2: false, 16 | }; 17 | 18 | export function parse( 19 | source: string, 20 | options: Options = DEFAULT_OPTIONS 21 | ): Program { 22 | patchCoffeeScript(); 23 | const parse = options.useCS2 ? parseCS2 : parseCS1AsCS2; 24 | const sourceLex = (s: string): SourceTokenList => 25 | lex(s, { useCS2: options.useCS2 }); 26 | const context = ParseContext.fromSource(source, sourceLex, parse); 27 | fixLocations(context, context.ast); 28 | const program = mapProgram(context); 29 | traverse(program, (node, parent) => { 30 | node.parentNode = parent; 31 | }); 32 | return program; 33 | } 34 | 35 | export function traverse( 36 | node: Node, 37 | callback: (node: Node, parent: Node | null) => boolean | void 38 | ): void { 39 | function traverseRec(currentNode: Node, currentParent: Node | null): void { 40 | const shouldDescend = callback(currentNode, currentParent); 41 | if (shouldDescend !== false) { 42 | for (const child of currentNode.getChildren()) { 43 | traverseRec(child, currentNode); 44 | } 45 | } 46 | } 47 | traverseRec(node, null); 48 | } 49 | -------------------------------------------------------------------------------- /src/util/ParseContext.ts: -------------------------------------------------------------------------------- 1 | import { SourceTokenList } from 'coffee-lex'; 2 | import { 3 | Base, 4 | Block, 5 | LocationData, 6 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 7 | import { LinesAndColumns } from 'lines-and-columns'; 8 | import { ClassProtoAssignOp, Constructor } from '../nodes'; 9 | 10 | class ParseError extends Error { 11 | syntaxError: SyntaxError; 12 | 13 | constructor(syntaxError: SyntaxError) { 14 | super(syntaxError.message); 15 | this.syntaxError = syntaxError; 16 | } 17 | } 18 | 19 | /** 20 | * Any information we need to know about the current state of parsing. While the 21 | * hope is that this is mostly immutable, with replace operations as we walk the 22 | * AST, it is partially mutable to collect bound method names in a class. 23 | */ 24 | export class ParseState { 25 | currentClassCtor: Constructor | null; 26 | constructor( 27 | readonly currentClassBoundMethods: Array | null 28 | ) { 29 | this.currentClassCtor = null; 30 | } 31 | 32 | isInClassBody(): boolean { 33 | return this.currentClassBoundMethods !== null; 34 | } 35 | 36 | recordBoundMethod(method: ClassProtoAssignOp): void { 37 | if (!this.currentClassBoundMethods) { 38 | throw new Error( 39 | 'Cannot assign a bound method name when there is no current class.' 40 | ); 41 | } 42 | this.currentClassBoundMethods.push(method); 43 | } 44 | 45 | recordConstructor(ctor: Constructor): void { 46 | this.currentClassCtor = ctor; 47 | } 48 | 49 | pushCurrentClass(): ParseState { 50 | return new ParseState([]); 51 | } 52 | 53 | dropCurrentClass(): ParseState { 54 | return new ParseState(null); 55 | } 56 | 57 | static initialState(): ParseState { 58 | return new ParseState(null); 59 | } 60 | } 61 | 62 | export default class ParseContext { 63 | constructor( 64 | readonly source: string, 65 | readonly linesAndColumns: LinesAndColumns, 66 | readonly sourceTokens: SourceTokenList, 67 | readonly ast: Block, 68 | readonly parseState: ParseState 69 | ) {} 70 | 71 | getRange(locatable: Base | LocationData): [number, number] | null { 72 | if (locatable instanceof Base) { 73 | return this.getRange(locatable.locationData); 74 | } else { 75 | const locationData = locatable as LocationData; 76 | const start = this.linesAndColumns.indexForLocation({ 77 | line: locationData.first_line, 78 | column: locationData.first_column, 79 | }); 80 | const end = this.linesAndColumns.indexForLocation({ 81 | line: locationData.last_line, 82 | column: locationData.last_column, 83 | }); 84 | 85 | if (start === null || end === null) { 86 | return null; 87 | } 88 | 89 | return [start, end + 1]; 90 | } 91 | } 92 | 93 | static fromSource( 94 | source: string, 95 | sourceLex: (source: string) => SourceTokenList, 96 | parse: (source: string) => Block 97 | ): ParseContext { 98 | try { 99 | const sourceTokens = sourceLex(source); 100 | return new ParseContext( 101 | source, 102 | new LinesAndColumns(source), 103 | sourceTokens, 104 | parse(source), 105 | ParseState.initialState() 106 | ); 107 | } catch (ex) { 108 | if (ex instanceof SyntaxError) { 109 | throw new ParseError(ex); 110 | } else { 111 | throw ex; 112 | } 113 | } 114 | } 115 | 116 | updateState(updater: (oldState: ParseState) => ParseState): ParseContext { 117 | return new ParseContext( 118 | this.source, 119 | this.linesAndColumns, 120 | this.sourceTokens, 121 | this.ast, 122 | updater(this.parseState) 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/util/UnsupportedNodeError.ts: -------------------------------------------------------------------------------- 1 | import { Base } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { inspect } from 'util'; 3 | 4 | export default class UnsupportedNodeError extends Error { 5 | readonly node: Base; 6 | 7 | constructor(node: Base, message: string | null = null) { 8 | const prefix = message ? `${message}\n\n` : ''; 9 | super( 10 | `${prefix}node type '${ 11 | node.constructor.name 12 | }' is not supported: ${inspect(node)}` 13 | ); 14 | 15 | // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work 16 | Object.setPrototypeOf(this, UnsupportedNodeError.prototype); 17 | 18 | this.node = node; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/util/expandToIncludeParens.ts: -------------------------------------------------------------------------------- 1 | import { SourceToken, SourceType } from 'coffee-lex'; 2 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { 4 | firstSemanticTokenAfter, 5 | firstSemanticTokenBefore, 6 | } from './getLocation'; 7 | import locationDataFromSourceRange from './locationDataFromSourceRange'; 8 | import ParseContext from './ParseContext'; 9 | import sourceRangeFromLocationData, { 10 | SourceRange, 11 | } from './sourceRangeFromLocationData'; 12 | 13 | export default function expandToIncludeParens( 14 | context: ParseContext, 15 | locationData: LocationData 16 | ): LocationData { 17 | let sourceRange = sourceRangeFromLocationData(context, locationData); 18 | // eslint-disable-next-line no-constant-condition 19 | while (true) { 20 | const tokens = getSurroundingParens(context, sourceRange); 21 | if (tokens === null) { 22 | break; 23 | } 24 | sourceRange = { start: tokens.openParen.start, end: tokens.closeParen.end }; 25 | } 26 | return locationDataFromSourceRange(context, sourceRange); 27 | } 28 | 29 | type Tokens = { openParen: SourceToken; closeParen: SourceToken }; 30 | 31 | function getSurroundingParens( 32 | context: ParseContext, 33 | sourceRange: SourceRange 34 | ): Tokens | null { 35 | const tokenBefore = firstSemanticTokenBefore(context, sourceRange.start); 36 | const tokenAfter = firstSemanticTokenAfter(context, sourceRange.end); 37 | if (tokenBefore === null || tokenBefore.type !== SourceType.LPAREN) { 38 | return null; 39 | } 40 | if (tokenAfter === null || tokenAfter.type !== SourceType.RPAREN) { 41 | return null; 42 | } 43 | return { openParen: tokenBefore, closeParen: tokenAfter }; 44 | } 45 | -------------------------------------------------------------------------------- /src/util/fixInvalidLocationData.ts: -------------------------------------------------------------------------------- 1 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { LinesAndColumns } from 'lines-and-columns'; 3 | 4 | /** 5 | * Assumes first_line/first_column are correct. 6 | */ 7 | export default function fixInvalidLocationData( 8 | locationData: LocationData, 9 | linesAndColumns: LinesAndColumns 10 | ): LocationData { 11 | let { last_line, last_column } = locationData; 12 | const indexForLocation = linesAndColumns.indexForLocation({ 13 | line: last_line, 14 | column: last_column, 15 | }); 16 | 17 | if (indexForLocation !== null) { 18 | return locationData; 19 | } else { 20 | let offset = 1; 21 | 22 | for (;;) { 23 | const index = linesAndColumns.indexForLocation({ 24 | line: last_line, 25 | column: last_column - offset, 26 | }); 27 | 28 | offset++; 29 | 30 | if (index !== null) { 31 | const location = linesAndColumns.locationForIndex(index + offset); 32 | 33 | if (!location) { 34 | throw new Error( 35 | `Unable to determine adjustment offset for incorrect location data: ` + 36 | `${JSON.stringify( 37 | locationData 38 | )}. No valid location found for index: ` + 39 | `${index + offset}` 40 | ); 41 | } 42 | 43 | last_line = location.line; 44 | last_column = location.column; 45 | break; 46 | } 47 | } 48 | 49 | return { 50 | ...locationData, 51 | last_line, 52 | last_column, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/util/fixLocations.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { 3 | Assign, 4 | Base, 5 | Block, 6 | Call, 7 | Class, 8 | Code, 9 | Extends, 10 | For, 11 | If, 12 | In, 13 | Index, 14 | Literal, 15 | Obj, 16 | Op, 17 | Param, 18 | Slice, 19 | Switch, 20 | Try, 21 | Value, 22 | While, 23 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 24 | import expandToIncludeParens from './expandToIncludeParens'; 25 | import fixInvalidLocationData from './fixInvalidLocationData'; 26 | import locationDataFromSourceRange from './locationDataFromSourceRange'; 27 | import locationWithLastPosition from './locationWithLastPosition'; 28 | import mergeLocations from './mergeLocations'; 29 | import ParseContext from './ParseContext'; 30 | import rangeOfBracketTokensForIndexNode from './rangeOfBracketTokensForIndexNode'; 31 | import sourceRangeFromLocationData from './sourceRangeFromLocationData'; 32 | 33 | export default function fixLocations(context: ParseContext, node: Base): void { 34 | const { linesAndColumns, source } = context; 35 | node.eachChild((child) => { 36 | if (child && child.locationData) { 37 | fixLocations(context, child); 38 | } 39 | return undefined; 40 | }); 41 | 42 | node.locationData = fixInvalidLocationData( 43 | node.locationData, 44 | context.linesAndColumns 45 | ); 46 | 47 | if (node instanceof Value) { 48 | const lastChild = node.properties[node.properties.length - 1] || node.base; 49 | if (lastChild) { 50 | node.locationData = locationWithLastPosition( 51 | node.locationData, 52 | lastChild.locationData 53 | ); 54 | } 55 | } 56 | 57 | if (node instanceof Index || node instanceof Slice) { 58 | const rangeOfBrackets = rangeOfBracketTokensForIndexNode(context, node); 59 | const lbracket = context.sourceTokens.tokenAtIndex(rangeOfBrackets[0]); 60 | if (lbracket === null) { 61 | throw new Error('Expected to find left-bracket token.'); 62 | } 63 | const lbracketLoc = linesAndColumns.locationForIndex(lbracket.start); 64 | if (lbracketLoc === null) { 65 | throw new Error( 66 | 'Expected to find a location for the left-bracket token.' 67 | ); 68 | } 69 | const rbracketIndex = rangeOfBrackets[1].previous(); 70 | if (rbracketIndex === null) { 71 | throw new Error('Expected to find a non-null right-bracket token index.'); 72 | } 73 | const rbracket = context.sourceTokens.tokenAtIndex(rbracketIndex); 74 | if (rbracket === null) { 75 | throw new Error('Expected to find right-bracket token.'); 76 | } 77 | const rbracketLoc = linesAndColumns.locationForIndex(rbracket.start); 78 | if (rbracketLoc === null) { 79 | throw new Error( 80 | 'Expected to find a location for the right-bracket token.' 81 | ); 82 | } 83 | node.locationData = { 84 | first_line: lbracketLoc.line, 85 | first_column: lbracketLoc.column, 86 | last_line: rbracketLoc.line, 87 | last_column: rbracketLoc.column, 88 | }; 89 | } 90 | 91 | if (node instanceof Obj) { 92 | const loc = node.locationData; 93 | const start = linesAndColumns.indexForLocation({ 94 | line: loc.first_line, 95 | column: loc.first_column, 96 | }); 97 | if (start === null) { 98 | throw new Error('Expected to find a start index for object.'); 99 | } 100 | const end = linesAndColumns.indexForLocation({ 101 | line: loc.last_line, 102 | column: loc.last_column, 103 | }); 104 | if (end === null) { 105 | throw new Error('Expected to find an end index for object.'); 106 | } 107 | const isImplicitObject = source[start] !== '{'; 108 | if (isImplicitObject && source[end] !== ',') { 109 | const lastChild = node.properties[node.properties.length - 1]; 110 | node.locationData = locationWithLastPosition( 111 | node.locationData, 112 | lastChild.locationData 113 | ); 114 | } 115 | } 116 | 117 | if (node instanceof Op) { 118 | const lastChild = node.second; 119 | if (lastChild) { 120 | node.locationData = locationWithLastPosition( 121 | node.locationData, 122 | lastChild.locationData 123 | ); 124 | } 125 | } 126 | 127 | if (node instanceof Assign) { 128 | const lastChild = node.value; 129 | node.locationData = locationWithLastPosition( 130 | node.locationData, 131 | lastChild.locationData 132 | ); 133 | } 134 | 135 | if (node instanceof In) { 136 | const lastChild = node.array; 137 | node.locationData = locationWithLastPosition( 138 | node.locationData, 139 | lastChild.locationData 140 | ); 141 | } 142 | 143 | if (node instanceof Call) { 144 | if (node.variable && !node.do && !node.csx) { 145 | // `super` won't have a callee (i.e. `node.variable`) 146 | const calleeLoc = node.variable.locationData; 147 | let calleeEnd = linesAndColumns.indexForLocation({ 148 | line: calleeLoc.last_line, 149 | column: calleeLoc.last_column, 150 | }); 151 | if (calleeEnd === null) { 152 | throw new Error('Expected to find index for callee end.'); 153 | } 154 | calleeEnd++; 155 | // Account for soaked calls, e.g. `a?()`. 156 | if (source[calleeEnd] === '?') { 157 | calleeEnd += 1; 158 | } 159 | const isImplicitCall = source[calleeEnd] !== '('; 160 | if (isImplicitCall) { 161 | const lastChild = node.args[node.args.length - 1] || node.variable; 162 | if (lastChild) { 163 | node.locationData = locationWithLastPosition( 164 | node.locationData, 165 | lastChild.locationData 166 | ); 167 | } 168 | } 169 | } 170 | } 171 | 172 | if (node instanceof Block) { 173 | const lastChild = node.expressions[node.expressions.length - 1]; 174 | if (lastChild) { 175 | node.locationData = locationWithLastPosition( 176 | node.locationData, 177 | lastChild.locationData 178 | ); 179 | } else { 180 | // Shorten range (usually length 1, the shortest range expressible by the CoffeeScript parser) to length 0. 181 | const sourceRange = sourceRangeFromLocationData( 182 | context, 183 | node.locationData 184 | ); 185 | node.locationData = locationDataFromSourceRange(context, { 186 | start: sourceRange.end, 187 | end: sourceRange.end, 188 | }); 189 | } 190 | // Blocks can sometimes end one index before their terminating semicolon 191 | // when really they should end exactly at that semicolon. 192 | let blockEnd = linesAndColumns.indexForLocation({ 193 | line: node.locationData.last_line, 194 | column: node.locationData.last_column, 195 | }); 196 | if (blockEnd === null) { 197 | throw new Error('Expected to find index for block end.'); 198 | } 199 | if (source[blockEnd + 1] === ';') { 200 | blockEnd++; 201 | const loc = linesAndColumns.locationForIndex(blockEnd); 202 | if (loc === null) { 203 | throw new Error('Expected to find location for block end.'); 204 | } 205 | node.locationData.last_line = loc.line; 206 | node.locationData.last_column = loc.column; 207 | } 208 | // The CS2 AST doesn't include the surrounding parens in a block, which can cause trouble with 209 | // things like postfix loops with parenthesized bodies. Expand every block to include any 210 | // surrounding parens. 211 | node.locationData = expandToIncludeParens(context, node.locationData); 212 | } 213 | 214 | if (node instanceof If) { 215 | const lastChild = node.elseBody || node.body; 216 | node.locationData = mergeLocations( 217 | node.locationData, 218 | lastChild.locationData 219 | ); 220 | } 221 | 222 | if (node instanceof For || node instanceof While) { 223 | const lastChild = node.body; 224 | if (lastChild) { 225 | node.locationData = mergeLocations( 226 | node.locationData, 227 | lastChild.locationData 228 | ); 229 | } 230 | } 231 | 232 | if (node instanceof Param) { 233 | if (!node.splat) { 234 | const lastChild = node.value || node.name; 235 | node.locationData = locationWithLastPosition( 236 | node.locationData, 237 | lastChild.locationData 238 | ); 239 | } 240 | } 241 | 242 | if (node instanceof Code) { 243 | if (node.body) { 244 | node.locationData = locationWithLastPosition( 245 | node.locationData, 246 | node.body.locationData 247 | ); 248 | } 249 | } 250 | 251 | if (node instanceof Class) { 252 | const lastChild = node.body; 253 | node.locationData = locationWithLastPosition( 254 | node.locationData, 255 | lastChild.locationData 256 | ); 257 | } 258 | 259 | if (node instanceof Switch) { 260 | const lastChild = node.otherwise || node.cases[node.cases.length - 1][1]; 261 | node.locationData = locationWithLastPosition( 262 | node.locationData, 263 | lastChild.locationData 264 | ); 265 | } 266 | 267 | if (node instanceof Try) { 268 | const lastChild = 269 | node.ensure || node.recovery || node.errorVariable || node.attempt; 270 | if (lastChild) { 271 | node.locationData = locationWithLastPosition( 272 | node.locationData, 273 | lastChild.locationData 274 | ); 275 | } 276 | } 277 | 278 | if (node instanceof Extends) { 279 | const lastChild = node.parent; 280 | node.locationData = locationWithLastPosition( 281 | node.locationData, 282 | lastChild.locationData 283 | ); 284 | } 285 | 286 | if (node instanceof Literal) { 287 | // Heregexp flags have an incorrect location, so detect that case and adjust 288 | // the end location to be correct. 289 | const endIndex = linesAndColumns.indexForLocation({ 290 | line: node.locationData.last_line, 291 | column: node.locationData.last_column, 292 | }); 293 | if (endIndex !== null) { 294 | const tokenIndex = 295 | context.sourceTokens.indexOfTokenNearSourceIndex(endIndex); 296 | const token = context.sourceTokens.tokenAtIndex(tokenIndex); 297 | if (token && token.type === SourceType.HEREGEXP_END) { 298 | const location = linesAndColumns.locationForIndex(token.end - 1); 299 | if (location) { 300 | node.locationData = { 301 | ...node.locationData, 302 | last_line: location.line, 303 | last_column: location.column, 304 | }; 305 | } 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/util/getLocation.ts: -------------------------------------------------------------------------------- 1 | import { SourceType, SourceToken } from 'coffee-lex'; 2 | import { Base } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 3 | import { inspect } from 'util'; 4 | import ParseContext from './ParseContext'; 5 | 6 | export type NodeLocation = { 7 | line: number; 8 | column: number; 9 | start: number; 10 | end: number; 11 | raw: string; 12 | }; 13 | 14 | export default function getLocation( 15 | context: ParseContext, 16 | node: Base 17 | ): NodeLocation { 18 | const loc = node.locationData; 19 | let start = context.linesAndColumns.indexForLocation({ 20 | line: loc.first_line, 21 | column: loc.first_column, 22 | }); 23 | const last = context.linesAndColumns.indexForLocation({ 24 | line: loc.last_line, 25 | column: loc.last_column, 26 | }); 27 | 28 | if (start === null || last === null) { 29 | throw new Error(`unable to determine range for location: ${inspect(loc)}}`); 30 | } 31 | 32 | const line = loc.first_line + 1; 33 | const column = loc.first_column + 1; 34 | let end = last + 1; 35 | 36 | // Shrink to be within the size of the source. 37 | if (start < 0) { 38 | start = 0; 39 | } 40 | if (end > context.source.length) { 41 | end = context.source.length; 42 | } 43 | 44 | const firstTokenOfNode = requireToken( 45 | firstSemanticTokenAfter(context, start), 46 | node 47 | ); 48 | const lastTokenOfNode = requireToken( 49 | firstSemanticTokenBefore(context, end), 50 | node 51 | ); 52 | 53 | start = firstTokenOfNode.start; 54 | end = lastTokenOfNode.end; 55 | const raw = context.source.slice(start, end); 56 | 57 | return { line, column, start, end, raw }; 58 | } 59 | 60 | export function firstSemanticTokenAfter( 61 | context: ParseContext, 62 | index: number 63 | ): SourceToken | null { 64 | const tokenIndex = context.sourceTokens.indexOfTokenMatchingPredicate( 65 | (token) => { 66 | return ( 67 | token.start >= index && 68 | token.type !== SourceType.NEWLINE && 69 | token.type !== SourceType.COMMENT 70 | ); 71 | }, 72 | context.sourceTokens.indexOfTokenNearSourceIndex(index) 73 | ); 74 | return tokenIndex === null 75 | ? null 76 | : context.sourceTokens.tokenAtIndex(tokenIndex); 77 | } 78 | 79 | export function firstSemanticTokenBefore( 80 | context: ParseContext, 81 | index: number 82 | ): SourceToken | null { 83 | const tokenIndex = context.sourceTokens.lastIndexOfTokenMatchingPredicate( 84 | (token) => { 85 | return ( 86 | token.end <= index && 87 | token.type !== SourceType.NEWLINE && 88 | token.type !== SourceType.COMMENT 89 | ); 90 | }, 91 | context.sourceTokens.indexOfTokenNearSourceIndex(index) 92 | ); 93 | return tokenIndex === null 94 | ? null 95 | : context.sourceTokens.tokenAtIndex(tokenIndex); 96 | } 97 | 98 | function requireToken(token: SourceToken | null, node: Base): SourceToken { 99 | if (token === null) { 100 | throw new Error(`unable to find token for node: ${inspect(node)}`); 101 | } 102 | return token; 103 | } 104 | -------------------------------------------------------------------------------- /src/util/getOperatorInfoInRange.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { OperatorInfo } from '../nodes'; 3 | import ParseContext from './ParseContext'; 4 | 5 | /** 6 | * Gets information about an operator token found between start and end source 7 | * offsets, exclusive. When calling, make sure `start` is before the text of 8 | * the operator in the range, and `end` is after it. 9 | */ 10 | export default function getOperatorInfoInRange( 11 | context: ParseContext, 12 | start: number, 13 | end: number 14 | ): OperatorInfo { 15 | const startIndex = context.sourceTokens.indexOfTokenNearSourceIndex(start); 16 | const endIndex = context.sourceTokens.indexOfTokenNearSourceIndex(end); 17 | 18 | if (!startIndex || !endIndex) { 19 | throw new Error( 20 | `cannot find token indexes of range bounds: [${start}, ${end}]` 21 | ); 22 | } 23 | 24 | const operatorIndex = context.sourceTokens.indexOfTokenMatchingPredicate( 25 | (token) => 26 | token.type !== SourceType.LPAREN && token.type !== SourceType.RPAREN, 27 | startIndex.next(), 28 | endIndex 29 | ); 30 | 31 | const operatorToken = 32 | operatorIndex && context.sourceTokens.tokenAtIndex(operatorIndex); 33 | 34 | if (!operatorToken) { 35 | throw new Error(`cannot find operator token in range: [${start}, ${end}]`); 36 | } 37 | 38 | return new OperatorInfo( 39 | context.source.slice(operatorToken.start, operatorToken.end), 40 | operatorToken 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/util/getTemplateLiteralComponents.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SourceToken, 3 | SourceTokenList, 4 | SourceTokenListIndex, 5 | SourceType, 6 | } from 'coffee-lex'; 7 | import { 8 | Base, 9 | Literal, 10 | Op, 11 | Value, 12 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 13 | import { Quasi } from '../nodes'; 14 | import isImplicitPlusOp from './isImplicitPlusOp'; 15 | import ParseContext from './ParseContext'; 16 | import parseString from './parseString'; 17 | 18 | export type TemplateLiteralComponents = { 19 | quasis: Array; 20 | unmappedExpressions: Array; 21 | start: number; 22 | end: number; 23 | }; 24 | 25 | /** 26 | * Reconstruct template literal information given the coffee-lex tokens and the 27 | * CoffeeScript AST. Since the CoffeeScript AST doesn't attempt to represent a 28 | * template literal (it's a bunch of + operations instead), the source locations 29 | * are generally unreliable and we need to rely on the token locations instead. 30 | */ 31 | export default function getTemplateLiteralComponents( 32 | context: ParseContext, 33 | node: Base 34 | ): TemplateLiteralComponents { 35 | const tokens = context.sourceTokens; 36 | 37 | const quasis: Array = []; 38 | const unmappedExpressions: Array = []; 39 | 40 | const elements = getElements(node, context); 41 | const nodeRange = context.getRange(node); 42 | if (!nodeRange) { 43 | throw new Error('Expected valid range on template literal node.'); 44 | } 45 | const { startTokenIndex, startToken } = getStartToken(nodeRange[0], tokens); 46 | 47 | let depth = 0; 48 | let lastToken = startToken; 49 | 50 | for ( 51 | let tokenIndex: SourceTokenListIndex | null = startTokenIndex; 52 | tokenIndex; 53 | tokenIndex = tokenIndex.next() 54 | ) { 55 | const token = tokens.tokenAtIndex(tokenIndex); 56 | if (!token) { 57 | break; 58 | } 59 | if ( 60 | token.type === SourceType.INTERPOLATION_START || 61 | token.type === SourceType.CSX_OPEN_TAG_START 62 | ) { 63 | depth++; 64 | if (depth === 1) { 65 | quasis.push(findQuasi(lastToken, token, context, elements)); 66 | lastToken = token; 67 | } 68 | } else if ( 69 | token.type === SourceType.INTERPOLATION_END || 70 | token.type === SourceType.CSX_SELF_CLOSING_TAG_END || 71 | token.type === SourceType.CSX_CLOSE_TAG_END 72 | ) { 73 | depth--; 74 | if (depth === 0) { 75 | unmappedExpressions.push( 76 | findExpression(lastToken, token, context, elements) 77 | ); 78 | lastToken = token; 79 | } 80 | } else if (depth === 0 && isTemplateLiteralEnd(token)) { 81 | quasis.push(findQuasi(lastToken, token, context, elements)); 82 | lastToken = token; 83 | break; 84 | } 85 | } 86 | return { 87 | quasis, 88 | unmappedExpressions, 89 | start: startToken.start, 90 | end: lastToken.end, 91 | }; 92 | } 93 | 94 | function getElements(node: Base, context: ParseContext): Array { 95 | if (node instanceof Op && isImplicitPlusOp(node, context)) { 96 | if (!node.second) { 97 | throw new Error('Expected second operand on plus op.'); 98 | } 99 | return [ 100 | ...getElements(node.first, context), 101 | ...getElements(node.second, context), 102 | ]; 103 | } 104 | return [node]; 105 | } 106 | 107 | /** 108 | * Usually the start token is at the start index of the relevant AST node, but 109 | * if the start of the template literal is an interpolation, it's two before 110 | * that one, so check to see which case we are and return what we find. 111 | */ 112 | function getStartToken( 113 | start: number, 114 | tokens: SourceTokenList 115 | ): { startToken: SourceToken; startTokenIndex: SourceTokenListIndex } { 116 | let tokenIndex = tokens.indexOfTokenNearSourceIndex(start); 117 | for (let i = 0; i < 5; i++) { 118 | const token = tokens.tokenAtIndex(tokenIndex); 119 | if (!token) { 120 | throw new Error('Expected to find a start token in a template literal.'); 121 | } 122 | if (isTemplateLiteralStart(token)) { 123 | return { startToken: token, startTokenIndex: tokenIndex }; 124 | } 125 | const prevToken = tokenIndex.previous(); 126 | if (!prevToken) { 127 | throw new Error( 128 | 'Expected a previous token when searching for a template start.' 129 | ); 130 | } 131 | tokenIndex = prevToken; 132 | } 133 | throw new Error('Expected a template literal start token.'); 134 | } 135 | 136 | function findQuasi( 137 | leftToken: SourceToken, 138 | rightToken: SourceToken, 139 | context: ParseContext, 140 | elements: Array 141 | ): Quasi { 142 | const matchingElements = elements.filter((elem) => { 143 | const range = context.getRange(elem); 144 | if (!range) { 145 | throw new Error('Unexpected invalid range.'); 146 | } 147 | return range[0] >= leftToken.start && range[1] <= rightToken.end; 148 | }); 149 | 150 | const start = leftToken.end; 151 | const end = rightToken.start; 152 | const startLoc = context.linesAndColumns.locationForIndex(leftToken.end); 153 | if (!startLoc) { 154 | throw new Error(`Expected to find a location for index ${leftToken.end}.`); 155 | } 156 | const raw = context.source.slice(start, end); 157 | 158 | if (matchingElements.length === 0) { 159 | return new Quasi( 160 | startLoc.line + 1, 161 | startLoc.column + 1, 162 | start, 163 | end, 164 | raw, 165 | '' 166 | ); 167 | } else if (matchingElements.length === 1) { 168 | const element = matchingElements[0]; 169 | let literal; 170 | if (element instanceof Literal) { 171 | literal = element; 172 | } else if ( 173 | element instanceof Value && 174 | element.properties.length === 0 && 175 | element.base instanceof Literal 176 | ) { 177 | literal = element.base; 178 | } else { 179 | throw new Error( 180 | 'Expected quasi element to be either a literal or a value containing only a literal.' 181 | ); 182 | } 183 | const stringValue = parseString(literal.value); 184 | return new Quasi( 185 | startLoc.line + 1, 186 | startLoc.column + 1, 187 | start, 188 | end, 189 | raw, 190 | stringValue !== undefined ? stringValue : literal.value 191 | ); 192 | } else { 193 | throw new Error( 194 | 'Unexpectedly found multiple elements in string interpolation.' 195 | ); 196 | } 197 | } 198 | 199 | function findExpression( 200 | leftToken: SourceToken, 201 | rightToken: SourceToken, 202 | context: ParseContext, 203 | elements: Array 204 | ): Base | null { 205 | const matchingElements = elements.filter((elem) => { 206 | const range = context.getRange(elem); 207 | if (!range) { 208 | throw new Error('Unexpected invalid range.'); 209 | } 210 | return range[0] >= leftToken.start && range[1] <= rightToken.end; 211 | }); 212 | 213 | if (matchingElements.length === 0) { 214 | return null; 215 | } else if (matchingElements.length === 1) { 216 | return matchingElements[0]; 217 | } else { 218 | throw new Error( 219 | 'Unexpectedly found multiple elements in string interpolation.' 220 | ); 221 | } 222 | } 223 | 224 | function isTemplateLiteralStart(token: SourceToken): boolean { 225 | return ( 226 | [ 227 | SourceType.DSTRING_START, 228 | SourceType.SSTRING_START, 229 | SourceType.TDSTRING_START, 230 | SourceType.TSSTRING_START, 231 | SourceType.HEREGEXP_START, 232 | SourceType.CSX_OPEN_TAG_END, 233 | ].indexOf(token.type) >= 0 234 | ); 235 | } 236 | 237 | function isTemplateLiteralEnd(token: SourceToken): boolean { 238 | return ( 239 | [ 240 | SourceType.DSTRING_END, 241 | SourceType.SSTRING_END, 242 | SourceType.TDSTRING_END, 243 | SourceType.TSSTRING_END, 244 | SourceType.HEREGEXP_END, 245 | SourceType.CSX_CLOSE_TAG_START, 246 | ].indexOf(token.type) >= 0 247 | ); 248 | } 249 | -------------------------------------------------------------------------------- /src/util/isChainedComparison.ts: -------------------------------------------------------------------------------- 1 | import { Base, Op } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | 3 | import isComparisonOperator from './isComparisonOperator'; 4 | 5 | export default function isChainedComparison(node: Base): boolean { 6 | if (node instanceof Op && isComparisonOperator(node)) { 7 | return ( 8 | isComparisonOperator(node.first) || 9 | (node.second ? isComparisonOperator(node.second) : false) 10 | ); 11 | } else { 12 | return false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/util/isCommentOnlyNode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Base, 3 | PassthroughLiteral, 4 | Value, 5 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 6 | 7 | export default function isCommentOnlyNode(base: Base): boolean { 8 | return ( 9 | base instanceof Value && 10 | base.base instanceof PassthroughLiteral && 11 | base.base.value === '' 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/util/isComparisonOperator.ts: -------------------------------------------------------------------------------- 1 | import { Base, Op } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | 3 | export default function isComparisonOperator(node: Base): boolean { 4 | if (!(node instanceof Op)) { 5 | return false; 6 | } 7 | 8 | switch (node.operator) { 9 | case '<': 10 | case '>': 11 | case '<=': 12 | case '>=': 13 | case '===': 14 | case '!==': 15 | return true; 16 | 17 | default: 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/util/isHeregexTemplateNode.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import { 3 | Base, 4 | Call, 5 | Literal, 6 | Value, 7 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 8 | import ParseContext from './ParseContext'; 9 | 10 | /** 11 | * Determine if the given CoffeeScript AST node is an interpolated heregex node 12 | * that's pretending to be a function call to the RegExp function. 13 | */ 14 | export default function isHeregexTemplateNode( 15 | node: Base, 16 | context: ParseContext 17 | ): boolean { 18 | if ( 19 | !(node instanceof Call) || 20 | !node.variable || 21 | !(node.variable instanceof Value) || 22 | !node.variable.base || 23 | !(node.variable.base instanceof Literal) || 24 | node.variable.base.value !== 'RegExp' 25 | ) { 26 | return false; 27 | } 28 | const { sourceTokens, linesAndColumns } = context; 29 | const start = linesAndColumns.indexForLocation({ 30 | line: node.locationData.first_line, 31 | column: node.locationData.first_column, 32 | }); 33 | if (start === null) { 34 | return false; 35 | } 36 | const startTokenIndex = sourceTokens.indexOfTokenContainingSourceIndex(start); 37 | if (startTokenIndex === null) { 38 | return false; 39 | } 40 | const startToken = sourceTokens.tokenAtIndex(startTokenIndex); 41 | if (startToken === null) { 42 | return false; 43 | } 44 | return startToken.type === SourceType.HEREGEXP_START; 45 | } 46 | -------------------------------------------------------------------------------- /src/util/isImplicitPlusOp.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import isPlusTokenBetweenRanges from './isPlusTokenBetweenRanges'; 3 | import ParseContext from './ParseContext'; 4 | 5 | /** 6 | * Determine if the operator is a fake + operator for string interpolation. 7 | */ 8 | export default function isImplicitPlusOp( 9 | op: Op, 10 | context: ParseContext 11 | ): boolean { 12 | if (op.operator !== '+' || !op.second) { 13 | return false; 14 | } 15 | const firstRange = context.getRange(op.first); 16 | const secondRange = context.getRange(op.second); 17 | if (!firstRange || !secondRange) { 18 | throw new Error('Expected valid location data on plus operation.'); 19 | } 20 | return !isPlusTokenBetweenRanges(firstRange, secondRange, context); 21 | } 22 | -------------------------------------------------------------------------------- /src/util/isPlusTokenBetweenRanges.ts: -------------------------------------------------------------------------------- 1 | import { SourceType } from 'coffee-lex'; 2 | import ParseContext from './ParseContext'; 3 | 4 | /** 5 | * Given the ranges of two operands, determine from the token list whether there 6 | * is a real '+' operator between them. A plus operation without an actual '+' 7 | * operator is an implicit string interpolation operation. 8 | */ 9 | export default function isPlusTokenBetweenRanges( 10 | leftRange: [number, number], 11 | rightRange: [number, number], 12 | context: ParseContext 13 | ): boolean { 14 | const tokens = context.sourceTokens; 15 | const leftEnd = tokens.indexOfTokenContainingSourceIndex(leftRange[1] - 1); 16 | const rightStart = tokens.indexOfTokenContainingSourceIndex(rightRange[0]); 17 | // Normal '+' operators should find tokens here, so if we don't, this must be 18 | // an implicit '+' operator. 19 | if (!leftEnd || !rightStart) { 20 | return false; 21 | } 22 | const afterLeftEnd = leftEnd.next(); 23 | if (!afterLeftEnd) { 24 | return false; 25 | } 26 | const tokensBetweenOperands = tokens.slice(afterLeftEnd, rightStart); 27 | // If we find an actual operator, this must have been a real '+'. Otherwise, 28 | // this must be an implicit '+'. 29 | let foundPlusToken = false; 30 | tokensBetweenOperands.forEach(({ type, start, end }) => { 31 | if ( 32 | type === SourceType.OPERATOR && 33 | context.source.slice(start, end) === '+' 34 | ) { 35 | foundPlusToken = true; 36 | } 37 | }); 38 | return foundPlusToken; 39 | } 40 | -------------------------------------------------------------------------------- /src/util/locationDataFromSourceRange.ts: -------------------------------------------------------------------------------- 1 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import ParseContext from './ParseContext'; 3 | import { SourceRange } from './sourceRangeFromLocationData'; 4 | 5 | export default function locationDataFromSourceRange( 6 | context: ParseContext, 7 | sourceRange: SourceRange 8 | ): LocationData { 9 | const startLocationInclusive = context.linesAndColumns.locationForIndex( 10 | sourceRange.start 11 | ); 12 | if (startLocationInclusive === null) { 13 | throw new Error('Expected start location for source range.'); 14 | } 15 | const endLocationInclusive = context.linesAndColumns.locationForIndex( 16 | sourceRange.end - 1 17 | ); 18 | if (endLocationInclusive === null) { 19 | throw new Error('Expected end location for source range.'); 20 | } 21 | return { 22 | first_line: startLocationInclusive.line, 23 | first_column: startLocationInclusive.column, 24 | last_line: endLocationInclusive.line, 25 | last_column: endLocationInclusive.column, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/util/locationWithLastPosition.ts: -------------------------------------------------------------------------------- 1 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | 3 | export default function locationWithLastPosition( 4 | loc: LocationData, 5 | last: LocationData 6 | ): LocationData { 7 | return { 8 | first_line: loc.first_line, 9 | first_column: loc.first_column, 10 | last_line: last.last_line, 11 | last_column: last.last_column, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/util/locationsEqual.ts: -------------------------------------------------------------------------------- 1 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | 3 | export default function locationsEqual( 4 | first: LocationData, 5 | second: LocationData 6 | ): boolean { 7 | return ( 8 | first.first_line === second.first_line && 9 | first.first_column === second.first_column && 10 | first.last_line === second.last_line && 11 | first.last_column === second.last_column 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/util/makeHeregex.ts: -------------------------------------------------------------------------------- 1 | import { Base } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import mapAny from '../mappers/mapAny'; 3 | import { Heregex, RegexFlags } from '../nodes'; 4 | import getTemplateLiteralComponents from './getTemplateLiteralComponents'; 5 | import ParseContext from './ParseContext'; 6 | 7 | export default function makeHeregex( 8 | context: ParseContext, 9 | node: Base, 10 | flags: string 11 | ): Heregex { 12 | const { quasis, unmappedExpressions, start, end } = 13 | getTemplateLiteralComponents(context, node); 14 | const startLoc = context.linesAndColumns.locationForIndex(start); 15 | if (!startLoc) { 16 | throw new Error(`Expected to find a location for index ${start}.`); 17 | } 18 | const raw = context.source.slice(start, end); 19 | return new Heregex( 20 | startLoc.line + 1, 21 | startLoc.column + 1, 22 | start, 23 | end, 24 | raw, 25 | quasis, 26 | unmappedExpressions.map((expr) => (expr ? mapAny(context, expr) : null)), 27 | RegexFlags.parse(flags) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/util/makeString.ts: -------------------------------------------------------------------------------- 1 | import { Base } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import mapAny from '../mappers/mapAny'; 3 | import * as nodes from '../nodes'; 4 | import getTemplateLiteralComponents from './getTemplateLiteralComponents'; 5 | import ParseContext from './ParseContext'; 6 | 7 | export default function makeString( 8 | context: ParseContext, 9 | node: Base 10 | ): nodes.String { 11 | const { quasis, unmappedExpressions, start, end } = 12 | getTemplateLiteralComponents(context, node); 13 | const startLoc = context.linesAndColumns.locationForIndex(start); 14 | if (!startLoc) { 15 | throw new Error(`Expected to find a location for index ${start}.`); 16 | } 17 | const raw = context.source.slice(start, end); 18 | return new nodes.String( 19 | startLoc.line + 1, 20 | startLoc.column + 1, 21 | start, 22 | end, 23 | raw, 24 | quasis, 25 | unmappedExpressions.map((expr) => (expr ? mapAny(context, expr) : null)) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/util/mergeLocations.ts: -------------------------------------------------------------------------------- 1 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | 3 | export default function mergeLocations( 4 | left: LocationData, 5 | right: LocationData 6 | ): LocationData { 7 | let first_line; 8 | let first_column; 9 | let last_line; 10 | let last_column; 11 | 12 | if (left.first_line < right.first_line) { 13 | ({ first_line, first_column } = left); 14 | } else if (left.first_line > right.first_line) { 15 | ({ first_line, first_column } = right); 16 | } else if (left.first_column < right.first_column) { 17 | ({ first_line, first_column } = left); 18 | } else { 19 | ({ first_line, first_column } = right); 20 | } 21 | 22 | if (left.last_line < right.last_line) { 23 | ({ last_line, last_column } = right); 24 | } else if (left.last_line > right.last_line) { 25 | ({ last_line, last_column } = left); 26 | } else if (left.last_column < right.last_column) { 27 | ({ last_line, last_column } = right); 28 | } else { 29 | ({ last_line, last_column } = left); 30 | } 31 | 32 | return { first_line, first_column, last_line, last_column }; 33 | } 34 | -------------------------------------------------------------------------------- /src/util/notNull.ts: -------------------------------------------------------------------------------- 1 | export default function notNull(value: T | undefined | null): T { 2 | if (typeof value === 'undefined' || value === null) { 3 | throw new Error(`expected value not to be null/undefined: ${value}`); 4 | } 5 | 6 | return value; 7 | } 8 | -------------------------------------------------------------------------------- /src/util/parseNumber.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@codemod/parser'; 2 | import { ExpressionStatement, NumericLiteral } from '@babel/types'; 3 | 4 | /** 5 | * Parses JavaScript source representing a number. 6 | */ 7 | export default function parseNumber(string: string): number { 8 | const expressionStatement = parse(`(${string})`).program 9 | .body[0] as ExpressionStatement; 10 | const literal = expressionStatement.expression as NumericLiteral; 11 | return literal.value; 12 | } 13 | -------------------------------------------------------------------------------- /src/util/parseRegExp.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@codemod/parser'; 2 | import { ExpressionStatement, RegExpLiteral } from '@babel/types'; 3 | 4 | /** 5 | * Parses JavaScript source representing a regular expression. 6 | */ 7 | export default function parseRegExp(string: string): { 8 | pattern: string; 9 | flags?: string; 10 | } { 11 | const expressionStatement = parse(`(${string})`).program 12 | .body[0] as ExpressionStatement; 13 | const literal = expressionStatement.expression as RegExpLiteral; 14 | return literal; 15 | } 16 | -------------------------------------------------------------------------------- /src/util/parseString.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@codemod/parser'; 2 | import { ExpressionStatement, StringLiteral } from '@babel/types'; 3 | 4 | /** 5 | * Parses JavaScript source representing a string. 6 | */ 7 | export default function parseString(string: string): string { 8 | const expressionStatement = parse(`(${string})`).program 9 | .body[0] as ExpressionStatement; 10 | const literal = expressionStatement.expression as StringLiteral; 11 | return literal.value; 12 | } 13 | -------------------------------------------------------------------------------- /src/util/rangeOfBracketTokensForIndexNode.ts: -------------------------------------------------------------------------------- 1 | import { SourceType, SourceTokenListIndex } from 'coffee-lex'; 2 | import { 3 | Index, 4 | Slice, 5 | } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 6 | import { inspect } from 'util'; 7 | import ParseContext from './ParseContext'; 8 | 9 | export default function rangeOfBracketTokensForIndexNode( 10 | context: ParseContext, 11 | indexNode: Index | Slice 12 | ): [SourceTokenListIndex, SourceTokenListIndex] { 13 | const start = context.linesAndColumns.indexForLocation({ 14 | line: indexNode.locationData.first_line, 15 | column: indexNode.locationData.first_column, 16 | }); 17 | 18 | if (start !== null) { 19 | const startTokenIndex = 20 | context.sourceTokens.indexOfTokenStartingAtSourceIndex(start); 21 | 22 | if (startTokenIndex !== null) { 23 | const range = 24 | context.sourceTokens.rangeOfMatchingTokensContainingTokenIndex( 25 | SourceType.LBRACKET, 26 | SourceType.RBRACKET, 27 | startTokenIndex 28 | ); 29 | 30 | if (range !== null) { 31 | return range; 32 | } 33 | } 34 | } 35 | 36 | throw new Error( 37 | `cannot find braces surrounding index at ` + 38 | `${indexNode.locationData.first_line + 1}:${ 39 | indexNode.locationData.first_column 40 | }: ` + 41 | `${inspect(indexNode)}` 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/util/sourceRangeFromLocationData.ts: -------------------------------------------------------------------------------- 1 | import { LocationData } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import ParseContext from './ParseContext'; 3 | 4 | export type SourceRange = { 5 | start: number; 6 | end: number; 7 | }; 8 | 9 | export default function sourceRangeFromLocationData( 10 | context: ParseContext, 11 | locationData: LocationData 12 | ): SourceRange { 13 | const startIndexInclusive = context.linesAndColumns.indexForLocation({ 14 | line: locationData.first_line, 15 | column: locationData.first_column, 16 | }); 17 | if (startIndexInclusive === null) { 18 | throw new Error('Expected index for start of range.'); 19 | } 20 | const endIndexInclusive = context.linesAndColumns.indexForLocation({ 21 | line: locationData.last_line, 22 | column: locationData.last_column, 23 | }); 24 | if (endIndexInclusive === null) { 25 | throw new Error('Expected index for end of range.'); 26 | } 27 | return { 28 | start: startIndexInclusive, 29 | end: endIndexInclusive + 1, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/util/unwindChainedComparison.ts: -------------------------------------------------------------------------------- 1 | import { Base, Op } from 'decaffeinate-coffeescript2/lib/coffeescript/nodes.js'; 2 | import { inspect } from 'util'; 3 | 4 | import isComparisonOperator from './isComparisonOperator'; 5 | 6 | export default function unwindChainedComparison(node: Op): Array { 7 | let operands: Array = []; 8 | 9 | for (let link: Base = node; ; ) { 10 | if (link instanceof Op && isComparisonOperator(link)) { 11 | const { first, second } = link; 12 | 13 | if (!second) { 14 | throw new Error( 15 | `unexpected unary operator inside chained comparison: ${inspect( 16 | node 17 | )}` 18 | ); 19 | } 20 | 21 | operands = [second, ...operands]; 22 | 23 | link = first; 24 | } else { 25 | operands = [link, ...operands]; 26 | break; 27 | } 28 | } 29 | 30 | return operands; 31 | } 32 | -------------------------------------------------------------------------------- /test/cs1-examples/block-comment-in-function.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | ### 3 | a 4 | ### 5 | -------------------------------------------------------------------------------- /test/cs1-examples/class-extends.coffee: -------------------------------------------------------------------------------- 1 | A extends B -------------------------------------------------------------------------------- /test/cs1-examples/class-member-with-block-comment.coffee: -------------------------------------------------------------------------------- 1 | class Foo 2 | ### 3 | # foo 4 | ### 5 | foo: -> -------------------------------------------------------------------------------- /test/cs1-examples/class-super-call.coffee: -------------------------------------------------------------------------------- 1 | class Foo extends Bar 2 | constructor: -> 3 | super 4 | 5 | foo: -> 6 | super(1, 2) -------------------------------------------------------------------------------- /test/cs1-examples/function-ending-in-block-comment.coffee: -------------------------------------------------------------------------------- 1 | a -> 2 | b 3 | ### c ### 4 | -------------------------------------------------------------------------------- /test/cs1-examples/heregex-with-interpolations-and-flags.coffee: -------------------------------------------------------------------------------- 1 | /// 2 | abc # def #{ghi} j 3 | ///gi 4 | -------------------------------------------------------------------------------- /test/cs1-examples/object-with-block-comments.coffee: -------------------------------------------------------------------------------- 1 | obj = 2 | ### 3 | # @returns {boolean} 4 | ### 5 | a: 1 -------------------------------------------------------------------------------- /test/cs2-examples/await-bound-function.coffee: -------------------------------------------------------------------------------- 1 | f = => 2 | await sleep() 3 | -------------------------------------------------------------------------------- /test/cs2-examples/await-return.coffee: -------------------------------------------------------------------------------- 1 | f = -> 2 | await return 3 | -------------------------------------------------------------------------------- /test/cs2-examples/await.coffee: -------------------------------------------------------------------------------- 1 | f = -> 2 | await sleep() 3 | -------------------------------------------------------------------------------- /test/cs2-examples/block-comment-in-function.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | ### 3 | a 4 | ### 5 | -------------------------------------------------------------------------------- /test/cs2-examples/class-member-with-block-comment.coffee: -------------------------------------------------------------------------------- 1 | class Foo 2 | ### 3 | # foo 4 | ### 5 | foo: -> -------------------------------------------------------------------------------- /test/cs2-examples/complex-computed-object-key.coffee: -------------------------------------------------------------------------------- 1 | o = { 2 | [a b]: c 3 | } 4 | -------------------------------------------------------------------------------- /test/cs2-examples/computed-object-key.coffee: -------------------------------------------------------------------------------- 1 | o = { 2 | [a]: b 3 | } 4 | -------------------------------------------------------------------------------- /test/cs2-examples/computed-shorthand-object-key.coffee: -------------------------------------------------------------------------------- 1 | o = { 2 | [a] 3 | } 4 | -------------------------------------------------------------------------------- /test/cs2-examples/csx-fragment.coffee: -------------------------------------------------------------------------------- 1 | x = <> 2 | Hello 3 | World 4 | 5 | -------------------------------------------------------------------------------- /test/cs2-examples/csx-simple.coffee: -------------------------------------------------------------------------------- 1 | x =
2 | -------------------------------------------------------------------------------- /test/cs2-examples/csx-with-interpolation.coffee: -------------------------------------------------------------------------------- 1 | x =
2 | foo 3 | 4 | {bar} 5 | baz 6 |
7 | -------------------------------------------------------------------------------- /test/cs2-examples/csx-with-props.coffee: -------------------------------------------------------------------------------- 1 | x = hello 2 | -------------------------------------------------------------------------------- /test/cs2-examples/elision.coffee: -------------------------------------------------------------------------------- 1 | [a,, b] = [1, 2, 3] 2 | -------------------------------------------------------------------------------- /test/cs2-examples/function-ending-in-block-comment.coffee: -------------------------------------------------------------------------------- 1 | a -> 2 | b 3 | ### c ### 4 | -------------------------------------------------------------------------------- /test/cs2-examples/heregex-with-interpolations-and-flags.coffee: -------------------------------------------------------------------------------- 1 | /// 2 | abc # def #{ghi} j 3 | ///gi 4 | -------------------------------------------------------------------------------- /test/cs2-examples/left-side-spread.coffee: -------------------------------------------------------------------------------- 1 | a = [...b, c] 2 | -------------------------------------------------------------------------------- /test/cs2-examples/object-spread.coffee: -------------------------------------------------------------------------------- 1 | o = {a..., b} 2 | -------------------------------------------------------------------------------- /test/cs2-examples/object-with-block-comments.coffee: -------------------------------------------------------------------------------- 1 | obj = 2 | ### 3 | # @returns {boolean} 4 | ### 5 | a: 1 -------------------------------------------------------------------------------- /test/cs2-examples/super-index-access.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | b: -> 3 | super[c] 4 | -------------------------------------------------------------------------------- /test/cs2-examples/super-property-access.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | b: -> 3 | super.c 4 | -------------------------------------------------------------------------------- /test/examples.test.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync } from 'fs'; 2 | import { basename, join } from 'path'; 3 | import { Node, Program } from '../src/nodes'; 4 | import { parse, traverse } from '../src/parser'; 5 | 6 | function defineTests(): void { 7 | defineExamplesDir(join(__dirname, 'examples'), true, true); 8 | defineExamplesDir(join(__dirname, 'cs1-examples'), true, false); 9 | defineExamplesDir(join(__dirname, 'cs2-examples'), false, true); 10 | } 11 | 12 | function defineExamplesDir( 13 | examplesPath: string, 14 | shouldTestCS1: boolean, 15 | shouldTestCS2: boolean 16 | ): void { 17 | readdirSync(examplesPath).forEach((entry) => 18 | defineTestsForEntry(examplesPath, entry, shouldTestCS1, shouldTestCS2) 19 | ); 20 | } 21 | 22 | function defineTestsForEntry( 23 | examplesPath: string, 24 | entry: string, 25 | shouldTestCS1: boolean, 26 | shouldTestCS2: boolean 27 | ): void { 28 | const dir = join(examplesPath, entry); 29 | const testFn = test; 30 | const testName = basename(entry, '.coffee'); 31 | if (shouldTestCS1) { 32 | defineTest(dir, testFn, testName, false); 33 | } 34 | if (shouldTestCS2) { 35 | defineTest(dir, testFn, testName, true); 36 | } 37 | } 38 | 39 | function defineTest( 40 | path: string, 41 | testFn: typeof it.skip, 42 | testName: string, 43 | useCS2: boolean 44 | ): void { 45 | const fullTestName = `${useCS2 ? 'CS2' : 'CS1'}: ${testName}`; 46 | testFn(fullTestName, () => { 47 | const input = readFileSync(path, { encoding: 'utf8' }); 48 | const actual = stripExtraInfo(stripContext(parse(input, { useCS2 }))); 49 | expect(actual).toMatchSnapshot(); 50 | }); 51 | } 52 | 53 | function stripContext(programNode: Program): Program { 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | delete (programNode as any).context; 56 | return programNode; 57 | } 58 | 59 | function stripExtraInfo(node: Node): Node { 60 | traverse(node, (node, parent) => { 61 | expect(node.parentNode).toBe(parent); 62 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 63 | delete (node as any).parentNode; 64 | }); 65 | return node; 66 | } 67 | 68 | defineTests(); 69 | -------------------------------------------------------------------------------- /test/examples/addition.coffee: -------------------------------------------------------------------------------- 1 | 3 + 4 -------------------------------------------------------------------------------- /test/examples/array-with-multiple-members.coffee: -------------------------------------------------------------------------------- 1 | [1, 2, 3] -------------------------------------------------------------------------------- /test/examples/array-with-single-member.coffee: -------------------------------------------------------------------------------- 1 | [1] -------------------------------------------------------------------------------- /test/examples/assign.coffee: -------------------------------------------------------------------------------- 1 | a = 1 -------------------------------------------------------------------------------- /test/examples/backticks-with-string-inside.coffee: -------------------------------------------------------------------------------- 1 | `import foo from 'foo'` 2 | -------------------------------------------------------------------------------- /test/examples/bare-yield.coffee: -------------------------------------------------------------------------------- 1 | f = -> 2 | yield 3 | -------------------------------------------------------------------------------- /test/examples/bitshift-left.coffee: -------------------------------------------------------------------------------- 1 | a << b -------------------------------------------------------------------------------- /test/examples/bitshift-right-unsigned.coffee: -------------------------------------------------------------------------------- 1 | a >>> b -------------------------------------------------------------------------------- /test/examples/bitshift-right.coffee: -------------------------------------------------------------------------------- 1 | a >> b -------------------------------------------------------------------------------- /test/examples/bitwise-and.coffee: -------------------------------------------------------------------------------- 1 | a & b -------------------------------------------------------------------------------- /test/examples/bitwise-or.coffee: -------------------------------------------------------------------------------- 1 | a | b -------------------------------------------------------------------------------- /test/examples/bitwise-xor.coffee: -------------------------------------------------------------------------------- 1 | a ^ b -------------------------------------------------------------------------------- /test/examples/block-comment-only-file.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # hey there 3 | ### 4 | -------------------------------------------------------------------------------- /test/examples/block-comment.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # hey there 3 | ### 4 | a -------------------------------------------------------------------------------- /test/examples/bound-function-with-parameters.coffee: -------------------------------------------------------------------------------- 1 | (a, b) => -------------------------------------------------------------------------------- /test/examples/bound-generator-function.coffee: -------------------------------------------------------------------------------- 1 | => yield 3 2 | -------------------------------------------------------------------------------- /test/examples/break.coffee: -------------------------------------------------------------------------------- 1 | loop 2 | break 3 | -------------------------------------------------------------------------------- /test/examples/call-dynamic-member-access-result.coffee: -------------------------------------------------------------------------------- 1 | a[b]() 2 | -------------------------------------------------------------------------------- /test/examples/chain-calls-with-parens.coffee: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app') 3 | .controller('MyCtrl', MyCtrl) -------------------------------------------------------------------------------- /test/examples/chain-calls-without-indent.coffee: -------------------------------------------------------------------------------- 1 | $stateProvider 2 | .state 'foo' 3 | .state 'bar' -------------------------------------------------------------------------------- /test/examples/chain-calls-without-parens.coffee: -------------------------------------------------------------------------------- 1 | $stateProvider 2 | .state 'foo' 3 | .state 'bar' -------------------------------------------------------------------------------- /test/examples/chained-comparison-equals.coffee: -------------------------------------------------------------------------------- 1 | a == b == c 2 | -------------------------------------------------------------------------------- /test/examples/chained-comparison-extended.coffee: -------------------------------------------------------------------------------- 1 | a < b < c < d < e -------------------------------------------------------------------------------- /test/examples/chained-comparison-greater-than.coffee: -------------------------------------------------------------------------------- 1 | a > b > c -------------------------------------------------------------------------------- /test/examples/chained-comparison-less-than.coffee: -------------------------------------------------------------------------------- 1 | a < b < c -------------------------------------------------------------------------------- /test/examples/chained-comparison-mixed.coffee: -------------------------------------------------------------------------------- 1 | a < b > c -------------------------------------------------------------------------------- /test/examples/chained-comparison-nested.coffee: -------------------------------------------------------------------------------- 1 | (a == b == c) == d == e 2 | -------------------------------------------------------------------------------- /test/examples/chained-comparison-not-equal.coffee: -------------------------------------------------------------------------------- 1 | a != b != c 2 | -------------------------------------------------------------------------------- /test/examples/chained-comparison-three.coffee: -------------------------------------------------------------------------------- 1 | a == b == c == d 2 | -------------------------------------------------------------------------------- /test/examples/chained-comparison-with-increment.coffee: -------------------------------------------------------------------------------- 1 | 0<++c<2 2 | -------------------------------------------------------------------------------- /test/examples/chained-comparison-with-other-operators.coffee: -------------------------------------------------------------------------------- 1 | Math.PI/2 < angle < 3*Math.PI/2 2 | -------------------------------------------------------------------------------- /test/examples/chained-comparison-with-unary-negate.coffee: -------------------------------------------------------------------------------- 1 | -1 < 0 < 1 2 | -------------------------------------------------------------------------------- /test/examples/chained-prototype-member-access.coffee: -------------------------------------------------------------------------------- 1 | Object::toString.constructor::valueOf 2 | -------------------------------------------------------------------------------- /test/examples/class-with-body-statements.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | a = 1 3 | b: a -------------------------------------------------------------------------------- /test/examples/class-with-bound-methods.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | a: => b -------------------------------------------------------------------------------- /test/examples/class-with-conditional-bound-method.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | if b 3 | c: => d 4 | -------------------------------------------------------------------------------- /test/examples/class-with-conditional-method.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | if b 3 | c: -> d 4 | -------------------------------------------------------------------------------- /test/examples/class-with-constructor.coffee: -------------------------------------------------------------------------------- 1 | class Point 2 | constructor: (@x, @y) -> 3 | -------------------------------------------------------------------------------- /test/examples/class-with-explicit-object-literal.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | {b: c} 3 | -------------------------------------------------------------------------------- /test/examples/class-with-implicit-object-literal-within-function.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | b = -> 3 | c: d 4 | return 1 5 | -------------------------------------------------------------------------------- /test/examples/class-with-members.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | a: 1 3 | b: -> 2 -------------------------------------------------------------------------------- /test/examples/class-with-non-identifier-assignee.coffee: -------------------------------------------------------------------------------- 1 | class A?.B 2 | a: 1 3 | -------------------------------------------------------------------------------- /test/examples/class-with-outer-assign-blocking-conditional-assign.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | if b 3 | c: d 4 | e: f 5 | -------------------------------------------------------------------------------- /test/examples/class-with-parenthesized-value.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | b: (c) 3 | -------------------------------------------------------------------------------- /test/examples/class-with-proto-access-name.coffee: -------------------------------------------------------------------------------- 1 | class A:: 2 | -------------------------------------------------------------------------------- /test/examples/class-with-static-members.coffee: -------------------------------------------------------------------------------- 1 | class A 2 | @b: c -------------------------------------------------------------------------------- /test/examples/comment-in-parenthesized-block.coffee: -------------------------------------------------------------------------------- 1 | () -> ( 2 | ###yo### 3 | a 4 | ) 5 | -------------------------------------------------------------------------------- /test/examples/comment-only-file.coffee: -------------------------------------------------------------------------------- 1 | # Testing 2 | -------------------------------------------------------------------------------- /test/examples/complex-template-literal.coffee: -------------------------------------------------------------------------------- 1 | "#{}A#{} #{} #{}B#{}" 2 | -------------------------------------------------------------------------------- /test/examples/compound-assignment-addition.coffee: -------------------------------------------------------------------------------- 1 | a += 1 -------------------------------------------------------------------------------- /test/examples/compound-assignment-subtraction.coffee: -------------------------------------------------------------------------------- 1 | a -= b -------------------------------------------------------------------------------- /test/examples/conditional-empty-consequent-alternate.coffee: -------------------------------------------------------------------------------- 1 | if false 2 | else if false 3 | else -------------------------------------------------------------------------------- /test/examples/conditional-ending-in-semicolon.coffee: -------------------------------------------------------------------------------- 1 | x = if a 2 | ; 3 | -------------------------------------------------------------------------------- /test/examples/conditional-on-one-line.coffee: -------------------------------------------------------------------------------- 1 | if a then b else c -------------------------------------------------------------------------------- /test/examples/conditional-unless-equal-condition.coffee: -------------------------------------------------------------------------------- 1 | unless a == b 2 | c -------------------------------------------------------------------------------- /test/examples/conditional-unless-exists-op.coffee: -------------------------------------------------------------------------------- 1 | unless a? 2 | b -------------------------------------------------------------------------------- /test/examples/conditional-unless-virtual-parens.coffee: -------------------------------------------------------------------------------- 1 | unless a + b 2 | c -------------------------------------------------------------------------------- /test/examples/conditional-using-unless.coffee: -------------------------------------------------------------------------------- 1 | unless a 2 | b -------------------------------------------------------------------------------- /test/examples/conditional-with-alternate.coffee: -------------------------------------------------------------------------------- 1 | if a 2 | b 3 | else 4 | c -------------------------------------------------------------------------------- /test/examples/conditional-with-braces-on-one-line.coffee: -------------------------------------------------------------------------------- 1 | a if (b and c) 2 | -------------------------------------------------------------------------------- /test/examples/conditional-with-post-if.coffee: -------------------------------------------------------------------------------- 1 | a if b -------------------------------------------------------------------------------- /test/examples/conditional-with-post-unless.coffee: -------------------------------------------------------------------------------- 1 | a unless b -------------------------------------------------------------------------------- /test/examples/conditional-without-alternate.coffee: -------------------------------------------------------------------------------- 1 | if a 2 | b -------------------------------------------------------------------------------- /test/examples/continue.coffee: -------------------------------------------------------------------------------- 1 | loop 2 | continue 3 | -------------------------------------------------------------------------------- /test/examples/dangling-prototype-access-of-call.coffee: -------------------------------------------------------------------------------- 1 | a():: 2 | -------------------------------------------------------------------------------- /test/examples/dangling-prototype-access.coffee: -------------------------------------------------------------------------------- 1 | Object:: 2 | -------------------------------------------------------------------------------- /test/examples/debugger.coffee: -------------------------------------------------------------------------------- 1 | debugger -------------------------------------------------------------------------------- /test/examples/delete.coffee: -------------------------------------------------------------------------------- 1 | delete a -------------------------------------------------------------------------------- /test/examples/destructure-this-assignment.coffee: -------------------------------------------------------------------------------- 1 | ({@a}) -> 2 | -------------------------------------------------------------------------------- /test/examples/division.coffee: -------------------------------------------------------------------------------- 1 | 3 / 4 -------------------------------------------------------------------------------- /test/examples/do-expression.coffee: -------------------------------------------------------------------------------- 1 | a(do => 2 | b) -------------------------------------------------------------------------------- /test/examples/do-with-assign-and-defaults.coffee: -------------------------------------------------------------------------------- 1 | do a = (b = c, d) -> 2 | e 3 | -------------------------------------------------------------------------------- /test/examples/do-with-assign-expression.coffee: -------------------------------------------------------------------------------- 1 | do wait = -> 2 | wait() 3 | -------------------------------------------------------------------------------- /test/examples/do-with-bound-function.coffee: -------------------------------------------------------------------------------- 1 | do => 2 | a -------------------------------------------------------------------------------- /test/examples/do-with-default-parameters.coffee: -------------------------------------------------------------------------------- 1 | do (a=1, b=@b) -> 2 | a + b -------------------------------------------------------------------------------- /test/examples/do-with-defaults-with-same-name.coffee: -------------------------------------------------------------------------------- 1 | do (a=a, b=b) -> 2 | a + b 3 | -------------------------------------------------------------------------------- /test/examples/do-with-simple-parameters.coffee: -------------------------------------------------------------------------------- 1 | do (a, b) -> 2 | a + b -------------------------------------------------------------------------------- /test/examples/do.coffee: -------------------------------------------------------------------------------- 1 | do -> 2 | a -------------------------------------------------------------------------------- /test/examples/double-negation.coffee: -------------------------------------------------------------------------------- 1 | !!a -------------------------------------------------------------------------------- /test/examples/dynamic-member-expressions.coffee: -------------------------------------------------------------------------------- 1 | a[b] -------------------------------------------------------------------------------- /test/examples/empty-anonymous-class.coffee: -------------------------------------------------------------------------------- 1 | class -------------------------------------------------------------------------------- /test/examples/empty-array.coffee: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/examples/empty-bound-function-without-body.coffee: -------------------------------------------------------------------------------- 1 | => -------------------------------------------------------------------------------- /test/examples/empty-class-with-superclass.coffee: -------------------------------------------------------------------------------- 1 | class A extends B -------------------------------------------------------------------------------- /test/examples/empty-class.coffee: -------------------------------------------------------------------------------- 1 | class A -------------------------------------------------------------------------------- /test/examples/empty-function-without-parameters.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | -------------------------------------------------------------------------------- /test/examples/empty-heregex-interpolation.coffee: -------------------------------------------------------------------------------- 1 | ///a#{}b/// 2 | -------------------------------------------------------------------------------- /test/examples/empty-loop.coffee: -------------------------------------------------------------------------------- 1 | loop then 2 | -------------------------------------------------------------------------------- /test/examples/empty-object.coffee: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/examples/empty-program.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaffeinate/decaffeinate-parser/91a4f995a3aaacfb0f011efb861b69005c0b903e/test/examples/empty-program.coffee -------------------------------------------------------------------------------- /test/examples/empty-string-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "a#{}b" 2 | -------------------------------------------------------------------------------- /test/examples/equality-longhand.coffee: -------------------------------------------------------------------------------- 1 | a is b -------------------------------------------------------------------------------- /test/examples/equality.coffee: -------------------------------------------------------------------------------- 1 | a == b -------------------------------------------------------------------------------- /test/examples/existential-binary.coffee: -------------------------------------------------------------------------------- 1 | a ? b -------------------------------------------------------------------------------- /test/examples/existential-unary.coffee: -------------------------------------------------------------------------------- 1 | a? -------------------------------------------------------------------------------- /test/examples/expansion.coffee: -------------------------------------------------------------------------------- 1 | [a, ..., b] = c -------------------------------------------------------------------------------- /test/examples/export-assignment.coffee: -------------------------------------------------------------------------------- 1 | export a = 1 2 | -------------------------------------------------------------------------------- /test/examples/export-default-from-source.coffee: -------------------------------------------------------------------------------- 1 | export { default } from './source'; 2 | -------------------------------------------------------------------------------- /test/examples/export-default.coffee: -------------------------------------------------------------------------------- 1 | export default a 2 | -------------------------------------------------------------------------------- /test/examples/export-multiple-bindings.coffee: -------------------------------------------------------------------------------- 1 | export {a, b as c} from 'd' 2 | export {e as f} 3 | -------------------------------------------------------------------------------- /test/examples/export-star.coffee: -------------------------------------------------------------------------------- 1 | export * from 'a' 2 | -------------------------------------------------------------------------------- /test/examples/external-constructor.coffee: -------------------------------------------------------------------------------- 1 | f = -> 2 | class A 3 | constructor: f 4 | -------------------------------------------------------------------------------- /test/examples/false.coffee: -------------------------------------------------------------------------------- 1 | false -------------------------------------------------------------------------------- /test/examples/float-int-value.coffee: -------------------------------------------------------------------------------- 1 | 1.0 -------------------------------------------------------------------------------- /test/examples/float-leading-period.coffee: -------------------------------------------------------------------------------- 1 | .25 2 | -------------------------------------------------------------------------------- /test/examples/float.coffee: -------------------------------------------------------------------------------- 1 | 1.2 -------------------------------------------------------------------------------- /test/examples/floor-division.coffee: -------------------------------------------------------------------------------- 1 | 7 // 3 -------------------------------------------------------------------------------- /test/examples/for-comprehension.coffee: -------------------------------------------------------------------------------- 1 | a for b in c -------------------------------------------------------------------------------- /test/examples/for-from.coffee: -------------------------------------------------------------------------------- 1 | for a from b 2 | c 3 | -------------------------------------------------------------------------------- /test/examples/for-in-by.coffee: -------------------------------------------------------------------------------- 1 | for a in b by c 2 | d -------------------------------------------------------------------------------- /test/examples/for-in-when.coffee: -------------------------------------------------------------------------------- 1 | for a in b when c 2 | d -------------------------------------------------------------------------------- /test/examples/for-in-with-key-and-value-assignees.coffee: -------------------------------------------------------------------------------- 1 | for a, i in b 2 | c -------------------------------------------------------------------------------- /test/examples/for-in.coffee: -------------------------------------------------------------------------------- 1 | for a in b 2 | c -------------------------------------------------------------------------------- /test/examples/for-of-expression.coffee: -------------------------------------------------------------------------------- 1 | a(for b of c 2 | b) 3 | -------------------------------------------------------------------------------- /test/examples/for-of-when.coffee: -------------------------------------------------------------------------------- 1 | for a of b when c 2 | d -------------------------------------------------------------------------------- /test/examples/for-of-with-key-and-value-assignees.coffee: -------------------------------------------------------------------------------- 1 | for k, v of a 2 | b -------------------------------------------------------------------------------- /test/examples/for-of.coffee: -------------------------------------------------------------------------------- 1 | for a of b 2 | c -------------------------------------------------------------------------------- /test/examples/for-own-of.coffee: -------------------------------------------------------------------------------- 1 | for own a of b 2 | c -------------------------------------------------------------------------------- /test/examples/for-repeater.coffee: -------------------------------------------------------------------------------- 1 | for [0..1] 2 | 2 3 | -------------------------------------------------------------------------------- /test/examples/function-ending-in-semicolon.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | ; 3 | -------------------------------------------------------------------------------- /test/examples/function-followed-by-block-comment.coffee: -------------------------------------------------------------------------------- 1 | a -> 2 | b 3 | ### c ### 4 | -------------------------------------------------------------------------------- /test/examples/function-trailing-spaces.coffee: -------------------------------------------------------------------------------- 1 | main = -> 2 | foo 3 | bar 4 | 5 | baz -------------------------------------------------------------------------------- /test/examples/function-with-body.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | a 3 | -------------------------------------------------------------------------------- /test/examples/function-with-default-parameter.coffee: -------------------------------------------------------------------------------- 1 | (a=1) -> 2 | -------------------------------------------------------------------------------- /test/examples/function-with-parameters.coffee: -------------------------------------------------------------------------------- 1 | (a, b) -> -------------------------------------------------------------------------------- /test/examples/function-with-statement-after-block-and-comments.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | a 3 | 4 | # hey 5 | ## foo ### 6 | b 7 | -------------------------------------------------------------------------------- /test/examples/greater-than-equal.coffee: -------------------------------------------------------------------------------- 1 | a >= b -------------------------------------------------------------------------------- /test/examples/greater-than.coffee: -------------------------------------------------------------------------------- 1 | a > b -------------------------------------------------------------------------------- /test/examples/heregex-in-method-call.coffee: -------------------------------------------------------------------------------- 1 | ///foo///.test('a') 2 | -------------------------------------------------------------------------------- /test/examples/heregex-with-flags.coffee: -------------------------------------------------------------------------------- 1 | ///a/b/c///gimuy 2 | -------------------------------------------------------------------------------- /test/examples/heregex-with-interpolations.coffee: -------------------------------------------------------------------------------- 1 | /// 2 | foo 3 | #{bar} # baz 4 | /// 5 | -------------------------------------------------------------------------------- /test/examples/heregex-with-spaces-and-comments.coffee: -------------------------------------------------------------------------------- 1 | OPERATOR = /// ^ ( 2 | ?: [-=]> # function 3 | | [-+*/%<>&|^!?=]= # compound assign / compare 4 | | >>>=? # zero-fill right shift 5 | | ([-+:])\1 # doubles 6 | | ([&|<>])\2=? # logic / shift 7 | | \?\. # soak access 8 | | \.{2,3} # range or splat 9 | ) /// 10 | -------------------------------------------------------------------------------- /test/examples/heregex-with-strange-whitespace.coffee: -------------------------------------------------------------------------------- 1 | ///
/// # This has a \u2028 character in it. 2 | -------------------------------------------------------------------------------- /test/examples/heregex.coffee: -------------------------------------------------------------------------------- 1 | ///a/b/c/// -------------------------------------------------------------------------------- /test/examples/hexidecimal-number.coffee: -------------------------------------------------------------------------------- 1 | 0x1B000 -------------------------------------------------------------------------------- /test/examples/iife-in-function-call.coffee: -------------------------------------------------------------------------------- 1 | a((=> 2 | 0)()) -------------------------------------------------------------------------------- /test/examples/implicit-object-with-trailing-comma.coffee: -------------------------------------------------------------------------------- 1 | a = 2 | b: c, 3 | d: e, 4 | -------------------------------------------------------------------------------- /test/examples/import-default.coffee: -------------------------------------------------------------------------------- 1 | import a from 'b' 2 | -------------------------------------------------------------------------------- /test/examples/import-named-with-alias.coffee: -------------------------------------------------------------------------------- 1 | import {a, b as c} from 'd' 2 | -------------------------------------------------------------------------------- /test/examples/import-star.coffee: -------------------------------------------------------------------------------- 1 | import * as a from 'b' 2 | -------------------------------------------------------------------------------- /test/examples/import-without-specifiers.coffee: -------------------------------------------------------------------------------- 1 | import 'a'; 2 | -------------------------------------------------------------------------------- /test/examples/in-not.coffee: -------------------------------------------------------------------------------- 1 | a not in b 2 | a !in b 3 | -------------------------------------------------------------------------------- /test/examples/in.coffee: -------------------------------------------------------------------------------- 1 | a in b -------------------------------------------------------------------------------- /test/examples/instanceof-not.coffee: -------------------------------------------------------------------------------- 1 | a not instanceof b -------------------------------------------------------------------------------- /test/examples/instanceof.coffee: -------------------------------------------------------------------------------- 1 | a instanceof b -------------------------------------------------------------------------------- /test/examples/integer.coffee: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /test/examples/js.coffee: -------------------------------------------------------------------------------- 1 | a = `void 0` -------------------------------------------------------------------------------- /test/examples/keyword-member-access.coffee: -------------------------------------------------------------------------------- 1 | a.break 2 | -------------------------------------------------------------------------------- /test/examples/less-than-equal.coffee: -------------------------------------------------------------------------------- 1 | a <= b -------------------------------------------------------------------------------- /test/examples/less-than.coffee: -------------------------------------------------------------------------------- 1 | a < b -------------------------------------------------------------------------------- /test/examples/logical-and-longform.coffee: -------------------------------------------------------------------------------- 1 | a and b -------------------------------------------------------------------------------- /test/examples/logical-and.coffee: -------------------------------------------------------------------------------- 1 | a && b -------------------------------------------------------------------------------- /test/examples/logical-or-longform.coffee: -------------------------------------------------------------------------------- 1 | a or b -------------------------------------------------------------------------------- /test/examples/logical-or.coffee: -------------------------------------------------------------------------------- 1 | a || b -------------------------------------------------------------------------------- /test/examples/loop.coffee: -------------------------------------------------------------------------------- 1 | loop 2 | a -------------------------------------------------------------------------------- /test/examples/many-expressions-in-parens.coffee: -------------------------------------------------------------------------------- 1 | (a; b; c; d; e) + f -------------------------------------------------------------------------------- /test/examples/modulo.coffee: -------------------------------------------------------------------------------- 1 | a %% b 2 | -------------------------------------------------------------------------------- /test/examples/multiline-interpolated-string-with-escaped-newline.coffee: -------------------------------------------------------------------------------- 1 | "#{a}\ 2 | #{b}" 3 | -------------------------------------------------------------------------------- /test/examples/multiline-string-with-interpolations-and-quotes.coffee: -------------------------------------------------------------------------------- 1 | """ 2 | a 3 | b"#{c}" 4 | d"#{e}" 5 | f 6 | """ 7 | -------------------------------------------------------------------------------- /test/examples/multiline-string-with-quoted-interpolations-and-non-interpolations.coffee: -------------------------------------------------------------------------------- 1 | """ 2 | a 3 | b"#{c}" 4 | d"e" 5 | f"#{g}" 6 | h 7 | """ 8 | -------------------------------------------------------------------------------- /test/examples/multiple-expressions-in-parens.coffee: -------------------------------------------------------------------------------- 1 | a + (b; c) -------------------------------------------------------------------------------- /test/examples/multiplication.coffee: -------------------------------------------------------------------------------- 1 | 3 * 4 -------------------------------------------------------------------------------- /test/examples/negated-equality-longhand.coffee: -------------------------------------------------------------------------------- 1 | a isnt b -------------------------------------------------------------------------------- /test/examples/negated-equality.coffee: -------------------------------------------------------------------------------- 1 | a != b -------------------------------------------------------------------------------- /test/examples/negation-with-not.coffee: -------------------------------------------------------------------------------- 1 | not a -------------------------------------------------------------------------------- /test/examples/negation.coffee: -------------------------------------------------------------------------------- 1 | !a -------------------------------------------------------------------------------- /test/examples/nested-code-with-outdent.coffee: -------------------------------------------------------------------------------- 1 | a { 2 | b: -> 3 | return c d, 4 | if e 5 | f 6 | } 7 | g 8 | -------------------------------------------------------------------------------- /test/examples/nested-conditionals.coffee: -------------------------------------------------------------------------------- 1 | if a 2 | b 3 | else if c 4 | d 5 | else 6 | e -------------------------------------------------------------------------------- /test/examples/nested-member-expressions.coffee: -------------------------------------------------------------------------------- 1 | a.b.c -------------------------------------------------------------------------------- /test/examples/nested-object-literals.coffee: -------------------------------------------------------------------------------- 1 | a: 2 | b: c -------------------------------------------------------------------------------- /test/examples/nested-object-with-inner-semicolon.coffee: -------------------------------------------------------------------------------- 1 | a: 2 | b: -> 3 | c; 4 | -------------------------------------------------------------------------------- /test/examples/nested-string-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "a#{"b#{c}"}" -------------------------------------------------------------------------------- /test/examples/new-with-method-call.coffee: -------------------------------------------------------------------------------- 1 | -> new A().b(c) 2 | -------------------------------------------------------------------------------- /test/examples/new-without-parens.coffee: -------------------------------------------------------------------------------- 1 | new A -------------------------------------------------------------------------------- /test/examples/new.coffee: -------------------------------------------------------------------------------- 1 | new a.B() -------------------------------------------------------------------------------- /test/examples/null.coffee: -------------------------------------------------------------------------------- 1 | null -------------------------------------------------------------------------------- /test/examples/number-object-key.coffee: -------------------------------------------------------------------------------- 1 | arr = {length: 1, 0: 'Hello', 3.14: 0} 2 | -------------------------------------------------------------------------------- /test/examples/object-destructure-with-default.coffee: -------------------------------------------------------------------------------- 1 | {a = 1} = b 2 | -------------------------------------------------------------------------------- /test/examples/object-with-braces.coffee: -------------------------------------------------------------------------------- 1 | {a: 1} -------------------------------------------------------------------------------- /test/examples/object-with-combined-key-value.coffee: -------------------------------------------------------------------------------- 1 | { a, b: 1 } -------------------------------------------------------------------------------- /test/examples/object-with-multiple-properties.coffee: -------------------------------------------------------------------------------- 1 | { 2 | a: 1, 3 | b: 2 4 | } -------------------------------------------------------------------------------- /test/examples/object-with-parenthesized-value.coffee: -------------------------------------------------------------------------------- 1 | {a: (b), c} 2 | -------------------------------------------------------------------------------- /test/examples/object-without-braces.coffee: -------------------------------------------------------------------------------- 1 | a: 1 -------------------------------------------------------------------------------- /test/examples/octal-number.coffee: -------------------------------------------------------------------------------- 1 | 0o1234 -------------------------------------------------------------------------------- /test/examples/of-not.coffee: -------------------------------------------------------------------------------- 1 | a not of b 2 | a !of b 3 | -------------------------------------------------------------------------------- /test/examples/of.coffee: -------------------------------------------------------------------------------- 1 | a of b -------------------------------------------------------------------------------- /test/examples/only-empty-string-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "#{}" 2 | -------------------------------------------------------------------------------- /test/examples/parentheses.coffee: -------------------------------------------------------------------------------- 1 | (a + b) * c -------------------------------------------------------------------------------- /test/examples/parenthesized-arrow-function.coffee: -------------------------------------------------------------------------------- 1 | a (->) 2 | -------------------------------------------------------------------------------- /test/examples/parenthesized-break-in-postfix-while.coffee: -------------------------------------------------------------------------------- 1 | (break) while true 2 | -------------------------------------------------------------------------------- /test/examples/parenthesized-member-access.coffee: -------------------------------------------------------------------------------- 1 | (a).b 2 | -------------------------------------------------------------------------------- /test/examples/post-decrement.coffee: -------------------------------------------------------------------------------- 1 | a-- -------------------------------------------------------------------------------- /test/examples/post-for.coffee: -------------------------------------------------------------------------------- 1 | a for a in b 2 | -------------------------------------------------------------------------------- /test/examples/post-increment.coffee: -------------------------------------------------------------------------------- 1 | a++ -------------------------------------------------------------------------------- /test/examples/post-unless-not-if.coffee: -------------------------------------------------------------------------------- 1 | c unless a not in b 2 | -------------------------------------------------------------------------------- /test/examples/post-while-with-loop.coffee: -------------------------------------------------------------------------------- 1 | loop a while b 2 | -------------------------------------------------------------------------------- /test/examples/pow.coffee: -------------------------------------------------------------------------------- 1 | a ** b -------------------------------------------------------------------------------- /test/examples/pre-decrement.coffee: -------------------------------------------------------------------------------- 1 | --a -------------------------------------------------------------------------------- /test/examples/pre-increment.coffee: -------------------------------------------------------------------------------- 1 | ++a -------------------------------------------------------------------------------- /test/examples/prototype-member-access.coffee: -------------------------------------------------------------------------------- 1 | Object::toString -------------------------------------------------------------------------------- /test/examples/range-exclusive.coffee: -------------------------------------------------------------------------------- 1 | [a...b] -------------------------------------------------------------------------------- /test/examples/range-inclusive.coffee: -------------------------------------------------------------------------------- 1 | [a..b] -------------------------------------------------------------------------------- /test/examples/regex-with-flags.coffee: -------------------------------------------------------------------------------- 1 | /a/gimuy 2 | -------------------------------------------------------------------------------- /test/examples/regexp.coffee: -------------------------------------------------------------------------------- 1 | /a/ 2 | -------------------------------------------------------------------------------- /test/examples/remainder.coffee: -------------------------------------------------------------------------------- 1 | 3 % 4 -------------------------------------------------------------------------------- /test/examples/rest-param-in-bound-function.coffee: -------------------------------------------------------------------------------- 1 | (rest...) => -------------------------------------------------------------------------------- /test/examples/rest-param-in-function.coffee: -------------------------------------------------------------------------------- 1 | (rest...) -> -------------------------------------------------------------------------------- /test/examples/return-with-expression.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | return a -------------------------------------------------------------------------------- /test/examples/return-without-expression.coffee: -------------------------------------------------------------------------------- 1 | -> 2 | return -------------------------------------------------------------------------------- /test/examples/shorthand-object-with-interpolated-strings.coffee: -------------------------------------------------------------------------------- 1 | x = {"a#{b}c"} 2 | -------------------------------------------------------------------------------- /test/examples/shorthand-object-with-strings.coffee: -------------------------------------------------------------------------------- 1 | x = {"FOO", "BAR", "BAZ"} 2 | -------------------------------------------------------------------------------- /test/examples/shorthand-object-with-this.coffee: -------------------------------------------------------------------------------- 1 | {@a} 2 | -------------------------------------------------------------------------------- /test/examples/shorthand-this-member-expression-with-dot.coffee: -------------------------------------------------------------------------------- 1 | @.a -------------------------------------------------------------------------------- /test/examples/shorthand-this-member-expression.coffee: -------------------------------------------------------------------------------- 1 | @a -------------------------------------------------------------------------------- /test/examples/shorthand-this.coffee: -------------------------------------------------------------------------------- 1 | @ -------------------------------------------------------------------------------- /test/examples/simple-call.coffee: -------------------------------------------------------------------------------- 1 | a() -------------------------------------------------------------------------------- /test/examples/simple-member-expression.coffee: -------------------------------------------------------------------------------- 1 | a.b -------------------------------------------------------------------------------- /test/examples/slice-with-lower-and-upper-bounds.coffee: -------------------------------------------------------------------------------- 1 | a[b..c] -------------------------------------------------------------------------------- /test/examples/slice-with-no-bounds.coffee: -------------------------------------------------------------------------------- 1 | a[..] 2 | -------------------------------------------------------------------------------- /test/examples/soaked-dynamic-member-access.coffee: -------------------------------------------------------------------------------- 1 | a?[b] -------------------------------------------------------------------------------- /test/examples/soaked-function-call.coffee: -------------------------------------------------------------------------------- 1 | a?() -------------------------------------------------------------------------------- /test/examples/soaked-member-access.coffee: -------------------------------------------------------------------------------- 1 | a?.b -------------------------------------------------------------------------------- /test/examples/soaked-method-call.coffee: -------------------------------------------------------------------------------- 1 | a?.b() -------------------------------------------------------------------------------- /test/examples/soaked-new.coffee: -------------------------------------------------------------------------------- 1 | new A? b 2 | -------------------------------------------------------------------------------- /test/examples/soaked-prototype-access.coffee: -------------------------------------------------------------------------------- 1 | a?::b 2 | -------------------------------------------------------------------------------- /test/examples/soaked-slice.coffee: -------------------------------------------------------------------------------- 1 | a?[b..c] 2 | -------------------------------------------------------------------------------- /test/examples/soaked-splice.coffee: -------------------------------------------------------------------------------- 1 | a?[b..c] = d 2 | -------------------------------------------------------------------------------- /test/examples/splat-in-array-with-other-members.coffee: -------------------------------------------------------------------------------- 1 | [a, b..., c] -------------------------------------------------------------------------------- /test/examples/splat-in-array.coffee: -------------------------------------------------------------------------------- 1 | [a...] -------------------------------------------------------------------------------- /test/examples/splat-in-function-call-with-other-args.coffee: -------------------------------------------------------------------------------- 1 | a b, c..., d -------------------------------------------------------------------------------- /test/examples/splat-in-function-call.coffee: -------------------------------------------------------------------------------- 1 | a(b...) -------------------------------------------------------------------------------- /test/examples/splat-in-new-call.coffee: -------------------------------------------------------------------------------- 1 | new Foo(args...) -------------------------------------------------------------------------------- /test/examples/string-ending-with-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "a#{b}" -------------------------------------------------------------------------------- /test/examples/string-interpolation-in-object-literal.coffee: -------------------------------------------------------------------------------- 1 | {a: "#{b}"} 2 | -------------------------------------------------------------------------------- /test/examples/string-interpolation-plus-normal-string.coffee: -------------------------------------------------------------------------------- 1 | "#{a}" + "b" 2 | -------------------------------------------------------------------------------- /test/examples/string-interpolation-preceded-by-parenthesis.coffee: -------------------------------------------------------------------------------- 1 | "(#{a}" # https://github.com/decaffeinate/decaffeinate/issues/212 2 | -------------------------------------------------------------------------------- /test/examples/string-interpolation-with-escaped-newline.coffee: -------------------------------------------------------------------------------- 1 | "#{a}\ 2 | #{b}" 3 | -------------------------------------------------------------------------------- /test/examples/string-interpolation-with-plus.coffee: -------------------------------------------------------------------------------- 1 | "#{a + b}c" 2 | -------------------------------------------------------------------------------- /test/examples/string-starting-with-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "#{a}b" -------------------------------------------------------------------------------- /test/examples/string-with-double-quotes.coffee: -------------------------------------------------------------------------------- 1 | "coffee me" -------------------------------------------------------------------------------- /test/examples/string-with-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "a#{b}c" -------------------------------------------------------------------------------- /test/examples/string-with-interpolations-at-start-and-end.coffee: -------------------------------------------------------------------------------- 1 | "#{a} #{b}" -------------------------------------------------------------------------------- /test/examples/string-with-noop-escape.coffee: -------------------------------------------------------------------------------- 1 | '\.' -------------------------------------------------------------------------------- /test/examples/string-with-only-multiple-interpolations.coffee: -------------------------------------------------------------------------------- 1 | "#{a}#{b}#{c}" -------------------------------------------------------------------------------- /test/examples/string-with-only-single-interpolation.coffee: -------------------------------------------------------------------------------- 1 | "#{a}" -------------------------------------------------------------------------------- /test/examples/string-with-parentheses-inside.coffee: -------------------------------------------------------------------------------- 1 | "( a" -------------------------------------------------------------------------------- /test/examples/string-with-single-quotes.coffee: -------------------------------------------------------------------------------- 1 | 'coffee script' -------------------------------------------------------------------------------- /test/examples/string-with-triple-double-quotes.coffee: -------------------------------------------------------------------------------- 1 | """ 2 | multi-line strings 3 | """ -------------------------------------------------------------------------------- /test/examples/string-with-triple-quote-interpolation-containing-quotes.coffee: -------------------------------------------------------------------------------- 1 | """ 2 | bar="#{bar}" 3 | """ -------------------------------------------------------------------------------- /test/examples/string-with-triple-quote-interpolation.coffee: -------------------------------------------------------------------------------- 1 | """ 2 | #{a} 3 | """ -------------------------------------------------------------------------------- /test/examples/string-with-triple-single-quotes.coffee: -------------------------------------------------------------------------------- 1 | ''' 2 | multi-line strings 3 | ''' -------------------------------------------------------------------------------- /test/examples/subtraction.coffee: -------------------------------------------------------------------------------- 1 | 3 - 4 -------------------------------------------------------------------------------- /test/examples/switch-with-alternate.coffee: -------------------------------------------------------------------------------- 1 | switch a 2 | when b 3 | c 4 | 5 | else 6 | d -------------------------------------------------------------------------------- /test/examples/switch-with-multiple-cases.coffee: -------------------------------------------------------------------------------- 1 | switch a 2 | when b 3 | c 4 | when d 5 | e 6 | -------------------------------------------------------------------------------- /test/examples/switch-with-multiple-conditions.coffee: -------------------------------------------------------------------------------- 1 | switch a 2 | when b, c 3 | d -------------------------------------------------------------------------------- /test/examples/switch-with-one-case.coffee: -------------------------------------------------------------------------------- 1 | switch a 2 | when b 3 | c 4 | -------------------------------------------------------------------------------- /test/examples/tagged-template-literal.coffee: -------------------------------------------------------------------------------- 1 | s = f"a#{b}c" 2 | -------------------------------------------------------------------------------- /test/examples/this-assign-with-keyword.coffee: -------------------------------------------------------------------------------- 1 | (@case) -> 2 | -------------------------------------------------------------------------------- /test/examples/throw.coffee: -------------------------------------------------------------------------------- 1 | throw 42 -------------------------------------------------------------------------------- /test/examples/triple-backtick-inline-js.coffee: -------------------------------------------------------------------------------- 1 | ``` 2 | a(b); 3 | ``` 4 | -------------------------------------------------------------------------------- /test/examples/true.coffee: -------------------------------------------------------------------------------- 1 | true -------------------------------------------------------------------------------- /test/examples/try-with-catch-and-finally.coffee: -------------------------------------------------------------------------------- 1 | try 2 | a 3 | catch 4 | b 5 | finally 6 | c 7 | -------------------------------------------------------------------------------- /test/examples/try-with-catch-assignee.coffee: -------------------------------------------------------------------------------- 1 | try 2 | a 3 | catch err 4 | b -------------------------------------------------------------------------------- /test/examples/try-with-catch-single-line.coffee: -------------------------------------------------------------------------------- 1 | try a catch b -------------------------------------------------------------------------------- /test/examples/try-with-catch-without-assignee.coffee: -------------------------------------------------------------------------------- 1 | try 2 | a 3 | catch 4 | b -------------------------------------------------------------------------------- /test/examples/try-without-catch-or-finally.coffee: -------------------------------------------------------------------------------- 1 | try 2 | a -------------------------------------------------------------------------------- /test/examples/try-without-catch-single-line.coffee: -------------------------------------------------------------------------------- 1 | try a -------------------------------------------------------------------------------- /test/examples/try-without-catch-with-finally.coffee: -------------------------------------------------------------------------------- 1 | try 2 | a 3 | finally 4 | b -------------------------------------------------------------------------------- /test/examples/typeof.coffee: -------------------------------------------------------------------------------- 1 | typeof a -------------------------------------------------------------------------------- /test/examples/unary-bitwise-negation.coffee: -------------------------------------------------------------------------------- 1 | ~a -------------------------------------------------------------------------------- /test/examples/unary-minus.coffee: -------------------------------------------------------------------------------- 1 | -1 -------------------------------------------------------------------------------- /test/examples/unary-plus.coffee: -------------------------------------------------------------------------------- 1 | +1 -------------------------------------------------------------------------------- /test/examples/undefined.coffee: -------------------------------------------------------------------------------- 1 | undefined -------------------------------------------------------------------------------- /test/examples/unless-in.coffee: -------------------------------------------------------------------------------- 1 | unless a in b 2 | c 3 | -------------------------------------------------------------------------------- /test/examples/unless-not-in.coffee: -------------------------------------------------------------------------------- 1 | unless a not in b 2 | c 3 | -------------------------------------------------------------------------------- /test/examples/unless-not-instanceof.coffee: -------------------------------------------------------------------------------- 1 | unless a not instanceof b 2 | c 3 | -------------------------------------------------------------------------------- /test/examples/until.coffee: -------------------------------------------------------------------------------- 1 | until a 2 | b -------------------------------------------------------------------------------- /test/examples/while-on-multiple-lines.coffee: -------------------------------------------------------------------------------- 1 | while a 2 | b -------------------------------------------------------------------------------- /test/examples/while-on-one-line.coffee: -------------------------------------------------------------------------------- 1 | a while b -------------------------------------------------------------------------------- /test/examples/while-with-guard.coffee: -------------------------------------------------------------------------------- 1 | while a when b 2 | c -------------------------------------------------------------------------------- /test/examples/yield-from.coffee: -------------------------------------------------------------------------------- 1 | -> yield from fn() -------------------------------------------------------------------------------- /test/examples/yield-return-empty.coffee: -------------------------------------------------------------------------------- 1 | -> yield return 2 | -------------------------------------------------------------------------------- /test/examples/yield-return.coffee: -------------------------------------------------------------------------------- 1 | -> yield return 3 2 | -------------------------------------------------------------------------------- /test/examples/yield.coffee: -------------------------------------------------------------------------------- 1 | -> yield 1 -------------------------------------------------------------------------------- /test/util/fixInvalidLocationData.test.ts: -------------------------------------------------------------------------------- 1 | import { LinesAndColumns } from 'lines-and-columns'; 2 | import fixInvalidLocationData from '../../src/util/fixInvalidLocationData'; 3 | 4 | describe('fixInvalidLocationData', () => { 5 | it('returns the location data unchanged when the last location exists', () => { 6 | const linesAndColumns = new LinesAndColumns('a'); 7 | expect( 8 | fixInvalidLocationData( 9 | { first_line: 0, first_column: 0, last_line: 0, last_column: 0 }, 10 | linesAndColumns 11 | ) 12 | ).toStrictEqual({ 13 | first_line: 0, 14 | first_column: 0, 15 | last_line: 0, 16 | last_column: 0, 17 | }); 18 | }); 19 | 20 | it('adjusts the last line and column back when past the end of a line', () => { 21 | const linesAndColumns = new LinesAndColumns('"""\na\n"""'); 22 | expect( 23 | fixInvalidLocationData( 24 | { first_line: 0, first_column: 0, last_line: 1, last_column: 4 }, 25 | linesAndColumns 26 | ) 27 | ).toStrictEqual({ 28 | first_line: 0, 29 | first_column: 0, 30 | last_line: 2, 31 | last_column: 3, 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "module": "commonjs", 5 | "typeRoots": ["node_modules/@types"], 6 | "declaration": true, 7 | "noEmit": true, 8 | "lib": ["es2018"], 9 | "experimentalDecorators": true, 10 | "noImplicitAny": true, 11 | "noUnusedParameters": true, 12 | "noUnusedLocals": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "skipLibCheck": true 15 | }, 16 | "exclude": ["node_modules", "dist/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/index.ts'], 6 | dts: true, 7 | format: ['cjs', 'esm'], 8 | target: 'es2020', 9 | }, 10 | ]); 11 | --------------------------------------------------------------------------------