Skip to content

Commit

Permalink
Implemented non-null, optional chaining and nullish coalescing operat…
Browse files Browse the repository at this point in the history
…ors (#144)

* Implemented non-null, optional chaining and nullish coalescing operators
  • Loading branch information
arodionov authored Nov 13, 2024
1 parent 92d8196 commit ca38533
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 18 deletions.
24 changes: 21 additions & 3 deletions openrewrite/src/javascript/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ export class JavaScriptParserVisitor {

private mapTypeInfo(node: ts.MethodDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration
| ts.PropertySignature | ts.MethodSignature | ts.ArrowFunction | ts.CallSignatureDeclaration | ts.GetAccessorDeclaration
| ts.FunctionDeclaration | ts.ConstructSignatureDeclaration) {
| ts.FunctionDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionExpression) {
return node.type ? new JS.TypeInfo(randomId(), this.prefix(node.getChildAt(node.getChildren().indexOf(node.type) - 1)), Markers.EMPTY, this.visit(node.type)) : null;
}

Expand Down Expand Up @@ -1217,7 +1217,15 @@ export class JavaScriptParserVisitor {
randomId(),
this.prefix(node),
Markers.EMPTY,
this.convert(node.expression),
node.questionDotToken ?
new JS.Unary(
randomId(),
Space.EMPTY,
Markers.EMPTY,
this.leftPadded(this.suffix(node.expression), JS.Unary.Type.QuestionDot),
this.visit(node.expression),
this.mapType(node)
) : this.convert(node.expression),
this.leftPadded(this.prefix(node.getChildAt(1, this.sourceFile)), this.convert(node.name)),
this.mapType(node)
);
Expand Down Expand Up @@ -1440,6 +1448,9 @@ export class JavaScriptParserVisitor {
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
binaryOperator = JS.JsBinary.Type.IdentityNotEquals;
break;
case ts.SyntaxKind.QuestionQuestionToken:
binaryOperator = JS.JsBinary.Type.QuestionQuestion;
break;
}

if (binaryOperator !== undefined) {
Expand Down Expand Up @@ -1614,7 +1625,14 @@ export class JavaScriptParserVisitor {
}

visitNonNullExpression(node: ts.NonNullExpression) {
return this.visitUnknown(node);
return new JS.Unary(
randomId(),
Space.EMPTY,
Markers.EMPTY,
this.leftPadded(this.suffix(node.expression), JS.Unary.Type.Exclamation),
this.visit(node.expression),
this.mapType(node)
)
}

visitMetaProperty(node: ts.MetaProperty) {
Expand Down
4 changes: 3 additions & 1 deletion openrewrite/src/javascript/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,7 @@ export namespace JsBinary {
IdentityEquals = 1,
IdentityNotEquals = 2,
In = 3,
QuestionQuestion = 4

}

Expand Down Expand Up @@ -2138,7 +2139,8 @@ export namespace Unary {
export enum Type {
Spread = 0,
Optional = 1,

Exclamation = 2,
QuestionDot = 3,
}

}
Expand Down
105 changes: 93 additions & 12 deletions openrewrite/test/javascript/parser/expressionStatement.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,107 @@
import {connect, disconnect, rewriteRunWithOptions, typeScript} from '../testHarness';
import {connect, disconnect, rewriteRun, rewriteRunWithOptions, typeScript} from '../testHarness';

describe('expression statement mapping', () => {
beforeAll(() => connect());
afterAll(() => disconnect());

test('literal with semicolon', () => {
rewriteRunWithOptions(
{normalizeIndent: false},
typeScript('1 ;')
{normalizeIndent: false},
typeScript('1 ;')
);
});
test('multiple', () => {
rewriteRunWithOptions(
{normalizeIndent: false},
typeScript(
//language=ts
`
1; // foo
// bar
/*baz*/
2;`
)
{normalizeIndent: false},
typeScript(
//language=ts
`
1; // foo
// bar
/*baz*/
2;`
)
);
});

test('simple non-null expression', () => {
rewriteRun(
//language=typescript
typeScript(`
const length = user ! . profile ! . username ! . length ;
`)
);
});

test('simple non-null expression with comments', () => {
rewriteRun(
//language=typescript
typeScript(`
const length = /*0*/user/*a*/!/*b*/./*c*/ profile /*d*/!/*e*/ ./*f*/ username /*g*/!/*h*/ ./*j*/ length/*l*/ ;
`)
);
});

test('simple question-dot expression', () => {
rewriteRun(
//language=typescript
typeScript(`
const length = user ?. profile ?. username ?. length ;
`)
);
});

test('simple question-dot expression with comments', () => {
rewriteRun(
//language=typescript
typeScript(`
const length = /*0*/user/*a*/ ?./*b*/ profile/*c*/ ?./*d*/ username /*e*/?./*f*/ length /*g*/;
`)
);
});

test('simple default expression', () => {
rewriteRun(
//language=typescript
typeScript(`
const length = user ?? 'default' ;
`)
);
});

test('simple default expression with comments', () => {
rewriteRun(
//language=typescript
typeScript(`
const length = user /*a*/??/*b*/ 'default' /*c*/;
`)
);
});

test('mixed expression with special tokens', () => {
rewriteRun(
//language=typescript
typeScript(`
class Profile {
username?: string; // Optional property
}
class User {
profile?: Profile; // Optional property
}
function getUser(id: number) : User | null {
if (id === 1) {
return new User();
}
return null;
}
const user = getUser(1);
const length = user ! . profile ?. username !. length /*test*/ ;
const username2 = getUser(1) ! . profile ?. username ; // test;
const username = user!.profile?.username ?? 'Guest' ;
`)
);
});
});
28 changes: 28 additions & 0 deletions openrewrite/test/javascript/parser/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,32 @@ describe('function mapping', () => {
`)
);
});

test('function with return type', () => {
rewriteRun(
//language=typescript
typeScript('function f ( a : string ) : void {}')
);
});

test('function type expressions', () => {
rewriteRun(
//language=typescript
typeScript('function greeter(fn: (a: string) => void) { fn("Hello, World"); }')
);
});

test.skip('function expression', () => {
rewriteRun(
//language=typescript
typeScript('const greet = function(name: string): string { return name; }')
);
});

test.skip('function expression with type parameter', () => {
rewriteRun(
//language=typescript
typeScript('const greet = function<T>(name: T): number { return 1; }')
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ public J visitJsBinary(JS.JsBinary binary, PrintOutputCapture<P> p) {
case In:
keyword = "in";
break;
case QuestionQuestion:
keyword = "??";
break;
}

visitSpace(binary.getPadding().getOperator().getBefore(), JsSpace.Location.BINARY_PREFIX, p);
Expand Down Expand Up @@ -413,6 +416,16 @@ public J visitUnary(JS.Unary unary, PrintOutputCapture<P> p) {
visitSpace(unary.getPadding().getOperator().getBefore(), Space.Location.UNARY_OPERATOR, p);
p.append("?");
break;
case Exclamation:
visit(unary.getExpression(), p);
visitSpace(unary.getPadding().getOperator().getBefore(), Space.Location.UNARY_OPERATOR, p);
p.append("!");
break;
case QuestionDot:
visit(unary.getExpression(), p);
visitSpace(unary.getPadding().getOperator().getBefore(), Space.Location.UNARY_OPERATOR, p);
p.append("?");
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,9 @@ public enum Type {
As,
IdentityEquals,
IdentityNotEquals,
In
In,
QuestionQuestion

}

public JS.JsBinary.Padding getPadding() {
Expand Down Expand Up @@ -1943,7 +1945,8 @@ public List<J> getSideEffects() {
public enum Type {
Spread,
Optional,
;
Exclamation,
QuestionDot;

public boolean isModifying() {
switch (this) {
Expand Down

0 comments on commit ca38533

Please sign in to comment.