diff --git a/README.md b/README.md index 213b3fd..8b23173 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The benefits of this library are: - No [native-addons](https://nodejs.org/api/addons.html) - No [child process](https://nodejs.org/api/child_process.html) - It is small - - Less than 700 lines of code and one dependency ([`TypeScript`](https://www.npmjs.com/package/typescript)) + - Around 700 lines of code and one dependency ([`TypeScript`](https://www.npmjs.com/package/typescript)) - By doing so little, the code should be relatively easy to maintain - Delegates the parsing to the [official TypeScript parser](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) - No need for additional SourceMap processing; see ["where are my SourceMaps?"](#where-are-my-sourcemaps) @@ -159,13 +159,11 @@ statementWithNoSemiColon ("not calling above statement"); ``` -### Arrow function return types that introduce a new line +### Arrow function type annotations that introduce a new line -If the annotation marking the return type of an arrow function introduces a new line before the `=>`, then only replacing it with blank space would be incorrect. +If the type annotations around an arrow function's parameters introduce a new line then only replacing them with blank space can be incorrect. Therefore, in addition to removing the type annotation, the `(` or `)` surrounding the function parameters may also be moved. -Therefore, in addition to removing the type annotation, the `)` is moved down to the end of the type annotation. - -Example input: +#### Example one - multi-line return type: ```typescript @@ -183,6 +181,24 @@ let f = (a , b ) => [a, b]; ``` +#### Example two - `async` with multi-line type arguments: + + +```typescript +let f = async < + T +>(v: T) => {}; +``` + +becomes: + + +```javascript +let f = async ( + + v ) => {}; +``` + ## Unsupported Syntax Some parts of TypeScript are not supported because they can't be erased in place due to having runtime semantics. See [unsupported_syntax.md](./docs/unsupported_syntax.md). diff --git a/package.json b/package.json index d0c4fb7..c8ac604 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ts-blank-space", "description": "A small, fast, pure JavaScript type-stripper that uses the official TypeScript parser.", - "version": "0.4.1", + "version": "0.4.2", "license": "Apache-2.0", "homepage": "https://bloomberg.github.io/ts-blank-space", "contributors": [ diff --git a/src/blank-string.ts b/src/blank-string.ts index 79e3170..4fde629 100644 --- a/src/blank-string.ts +++ b/src/blank-string.ts @@ -1,8 +1,9 @@ // Copyright 2024 Bloomberg Finance L.P. // Distributed under the terms of the Apache 2.0 license. -const FLAG_REPLACE_WITH_CLOSE_PAREN = 1; -const FLAG_REPLACE_WITH_SEMI = 2; +const FLAG_REPLACE_WITH_OPEN_PAREN = 1; +const FLAG_REPLACE_WITH_CLOSE_PAREN = 2; +const FLAG_REPLACE_WITH_SEMI = 3; function getSpace(input: string, start: number, end: number): string { let out = ""; @@ -31,6 +32,10 @@ export default class BlankString { this.__ranges = []; } + blankButStartWithOpenParen(start: number, end: number): void { + this.__ranges.push(FLAG_REPLACE_WITH_OPEN_PAREN, start, end); + } + blankButEndWithCloseParen(start: number, end: number): void { this.__ranges.push(0, start, end - 1); this.__ranges.push(FLAG_REPLACE_WITH_CLOSE_PAREN, end - 1, end); @@ -69,6 +74,9 @@ export default class BlankString { } else if (flags === FLAG_REPLACE_WITH_SEMI) { out += ";"; rangeStart += 1; + } else if (flags === FLAG_REPLACE_WITH_OPEN_PAREN) { + out += "("; + rangeStart += 1; } previousEnd = rangeEnd; diff --git a/src/index.ts b/src/index.ts index a5d5d64..f1a65b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -177,7 +177,7 @@ function visitVariableStatement(node: ts.VariableStatement): VisitResult { function visitCallOrNewExpression(node: ts.NewExpression | ts.CallExpression): VisitResult { visitor(node.expression); if (node.typeArguments) { - blankGenerics(node, node.typeArguments); + blankGenerics(node, node.typeArguments, /*startWithParen*/ false); } if (node.arguments) { const args = node.arguments; @@ -194,7 +194,7 @@ function visitCallOrNewExpression(node: ts.NewExpression | ts.CallExpression): V function visitTaggedTemplate(node: ts.TaggedTemplateExpression): VisitResult { visitor(node.tag); if (node.typeArguments) { - blankGenerics(node, node.typeArguments); + blankGenerics(node, node.typeArguments, /*startWithParen*/ false); } visitor(node.template); return VISITED_JS; @@ -233,7 +233,7 @@ function visitClassLike(node: ts.ClassLikeDeclaration): VisitResult { // ... if (node.typeParameters && node.typeParameters.length) { - blankGenerics(node, node.typeParameters); + blankGenerics(node, node.typeParameters, /*startWithParen*/ false); } const { heritageClauses } = node; @@ -260,7 +260,7 @@ function visitClassLike(node: ts.ClassLikeDeclaration): VisitResult { function visitExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments): VisitResult { visitor(node.expression); if (node.typeArguments) { - blankGenerics(node, node.typeArguments); + blankGenerics(node, node.typeArguments, /*startWithParen*/ false); } return VISITED_JS; } @@ -311,6 +311,14 @@ function visitModifiers(modifiers: ArrayLike): void { } } +function isAsync(modifiers: ArrayLike | undefined): boolean { + if (!modifiers) return false; + for (let i = 0; i < modifiers.length; i++) { + if (modifiers[i].kind === SK.AsyncKeyword) return true; + } + return false; +} + /** * prop: T */ @@ -394,14 +402,19 @@ function visitFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration, kind: ts visitor(node.name); } + let moveOpenParen = false; if (node.typeParameters && node.typeParameters.length) { - blankGenerics(node, node.typeParameters); + moveOpenParen = isAsync(node.modifiers) && spansLines(node.typeParameters.pos, node.typeParameters.end); + blankGenerics(node, node.typeParameters, moveOpenParen); } // method? node.questionToken && blankExact(node.questionToken); const params = node.parameters; + if (moveOpenParen) { + str.blank(params.pos - 1, params.pos); + } for (let i = 0; i < params.length; i++) { const p = params[i]; if (i === 0 && p.name.getText(ast) === "this") { @@ -594,10 +607,10 @@ function blankExactAndOptionalTrailingComma(n: ts.Node): void { /** * `` */ -function blankGenerics(node: ts.Node, arr: ts.NodeArray): void { +function blankGenerics(node: ts.Node, arr: ts.NodeArray, startWithParen: boolean): void { const start = arr.pos - 1; const end = scanRange(arr.end, node.end, getGreaterThanToken); - str.blank(start, end); + startWithParen ? str.blankButStartWithOpenParen(start, end) : str.blank(start, end); } function getClosingParenthesisPos(node: ts.NodeArray): number { diff --git a/tests/fixture/cases/async-generic-arrow.ts b/tests/fixture/cases/async-generic-arrow.ts new file mode 100644 index 0000000..7cf63e4 --- /dev/null +++ b/tests/fixture/cases/async-generic-arrow.ts @@ -0,0 +1,18 @@ + +// Simple case +const a = async(v: T) => {}; +// ^^^ ^^^ + +// Hard case - generic spans multiple lines +const b = async < + T +>/**/(/**/v: T) => {}; +// ^ ^^^ + +// Harder case - generic and return type spans multiple lines +const c = async < + T +>(v: T): Promise< +// ^^^ ^^^^^^^^^^ + T +> => v; diff --git a/tests/fixture/output/async-generic-arrow.js b/tests/fixture/output/async-generic-arrow.js new file mode 100644 index 0000000..43a1153 --- /dev/null +++ b/tests/fixture/output/async-generic-arrow.js @@ -0,0 +1,18 @@ + +// Simple case +const a = async (v ) => {}; +// ^^^ ^^^ + +// Hard case - generic spans multiple lines +const b = async ( + + /**/ /**/v ) => {}; +// ^ ^^^ + +// Harder case - generic and return type spans multiple lines +const c = async ( + + v + + +) => v;