Skip to content

Commit

Permalink
fix: avoid ASI hazard with computed class members (#22)
Browse files Browse the repository at this point in the history
Fixes #21

This change defends against a ASI hazard. When a computed class member
name has a leading type modifier
(`public/private/protected/readonly/override`) a `;` is inserted in it's
place to avoid being interpreted as index lookup of the previous class
member.

Signed-off-by: Ashley Claymore <aclaymore@bloomberg.net>
  • Loading branch information
acutmore authored Nov 7, 2024
1 parent 49dd1dc commit c4071ba
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 12 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ There are two cases, described here, where it does more than replace the TypeScr

### ASI (automatic semicolon insertion)

To guard against [ASI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#automatic_semicolon_insertion) issues in the output, `ts-blank-space` will add `;` to the end of type-only statements.
To guard against [ASI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#automatic_semicolon_insertion) issues in the output, `ts-blank-space` will add `;` to the end of type-only statements, and when removing a leading type annotation could introduce an ASI hazard.

Example input:
#### Example one - type-only statement

<!-- prettier-ignore -->
```typescript
Expand All @@ -159,6 +159,26 @@ statementWithNoSemiColon
("not calling above statement");
```

#### Example two - computed class fields/methods

<!-- prettier-ignore -->
```typescript
class C {
field = 1/* no ; */
public ["computed field not accessing above"] = 2
}
```

becomes:

<!-- prettier-ignore -->
```javascript
class C {
field = 1/* no ; */
; ["computed field not accessing above"] = 2
}
```

### Arrow function type annotations that introduce a new line

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.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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.2",
"version": "0.4.3",
"license": "Apache-2.0",
"homepage": "https://bloomberg.github.io/ts-blank-space",
"contributors": [
Expand Down
20 changes: 13 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function visitClassLike(node: ts.ClassLikeDeclaration): VisitResult {
blankStatement(node);
return VISIT_BLANKED;
}
visitModifiers(node.modifiers);
visitModifiers(node.modifiers, /* addSemi:*/ false);
}

// ... <T>
Expand Down Expand Up @@ -280,12 +280,17 @@ function isRemovedModifier(kind: ts.SyntaxKind): kind is (typeof classElementMod
return classElementModifiersToRemove.has(kind as never);
}

function visitModifiers(modifiers: ArrayLike<ts.ModifierLike>): void {
function visitModifiers(modifiers: ArrayLike<ts.ModifierLike>, addSemi: boolean): void {
for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i];
const kind = modifier.kind;
if (isRemovedModifier(kind)) {
blankExact(modifier);
if (addSemi && i === 0) {
str.blankButStartWithSemi(modifier.getStart(ast), modifier.end);
addSemi = false;
} else {
blankExact(modifier);
}
continue;
} else if (kind === SK.Decorator) {
visitor(modifier);
Expand Down Expand Up @@ -328,7 +333,7 @@ function visitPropertyDeclaration(node: ts.PropertyDeclaration): VisitResult {
blankStatement(node);
return VISIT_BLANKED;
}
visitModifiers(node.modifiers);
visitModifiers(node.modifiers, /* addSemi */ node.name.kind === SK.ComputedPropertyName);
}
node.exclamationToken && blankExact(node.exclamationToken);
node.questionToken && blankExact(node.questionToken);
Expand Down Expand Up @@ -394,12 +399,13 @@ function visitFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration, kind: ts
return VISIT_BLANKED;
}

const nodeName = node.name;
if (node.modifiers) {
visitModifiers(node.modifiers);
visitModifiers(node.modifiers, /* addSemi */ !!nodeName && nodeName.kind === SK.ComputedPropertyName);
}

if (node.name) {
visitor(node.name);
if (nodeName) {
visitor(nodeName);
}

let moveOpenParen = false;
Expand Down
12 changes: 12 additions & 0 deletions tests/fixture/cases/asi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,16 @@ class ASI {
((() => { 1/*trailing*/})(), 1) + 1 as number/*trailing*/
(1);
}
g = 2/*missing ; */
public ["computed-field"] = 1
// ;^^^^^
h = 3/*missing ; */
public ["computed-method"]() {}
// ;^^^^^
}

class NoASI {
f = 1/*missing ; */
static readonly ["computed-field"] = 1
// ^^^^^^^^
}
12 changes: 12 additions & 0 deletions tests/fixture/output/asi.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c4071ba

Please sign in to comment.