Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(optional chaining): add support for optional chaining and nullis…
…h coalescing
  • Loading branch information
wessberg committed Nov 9, 2019
1 parent 8dd51a2 commit 7a79b34
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 105 deletions.
Binary file added documentation/asset/logo-social-preview.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
204 changes: 104 additions & 100 deletions package.json
@@ -1,102 +1,106 @@
{
"name": "@wessberg/ts-evaluator",
"version": "0.0.23",
"description": "An interpreter for Typescript that can evaluate an arbitrary Node within a Typescript AST",
"scripts": {
"generate:readme": "scaffold readme --yes",
"generate:license": "scaffold license --yes",
"generate:contributing": "scaffold contributing --yes",
"generate:coc": "scaffold coc --yes",
"generate:changelog": "standard-changelog --first-release",
"generate:all": "npm run generate:license & npm run generate:contributing & npm run generate:coc & npm run generate:readme && npm run generate:changelog",
"clean": "rm -rf dist",
"lint": "tsc --noEmit && tslint -c tslint.json --project tsconfig.json",
"prettier": "prettier --write '{src,test,documentation}/**/*.{js,ts,json,html,xml,css,md}'",
"test": "ava",
"prebuild": "npm run clean",
"build": "npm run rollup",
"rollup": "rollup -c rollup.config.js",
"preversion": "npm run lint && NODE_ENV=production npm run build",
"version": "npm run generate:all && git add .",
"release": "np --no-cleanup --no-yarn"
},
"keywords": [
"typescript",
"ts",
"interpreter",
"evaluate",
"evaluator",
"ast"
],
"files": [
"dist/**/*.*"
],
"contributors": [
{
"name": "Frederik Wessberg",
"email": "frederikwessberg@hotmail.com",
"url": "https://github.com/wessberg",
"imageUrl": "https://avatars2.githubusercontent.com/u/20454213?s=460&v=4",
"role": "Lead Developer",
"twitter": "FredWessberg"
}
],
"license": "MIT",
"devDependencies": {
"@wessberg/scaffold": "1.0.19",
"@wessberg/rollup-plugin-ts": "1.1.66",
"@wessberg/ts-config": "0.0.41",
"standard-changelog": "2.0.15",
"tslint": "5.20.0",
"ava": "2.4.0",
"prettier": "^1.18.2",
"pretty-quick": "^2.0.0",
"husky": "^3.0.9",
"np": "^5.1.1",
"find-up": "^4.1.0",
"ts-node": "^8.4.1",
"rollup": "^1.23.1"
},
"dependencies": {
"@types/deasync": "0.1.0",
"@types/node": "12.7.12",
"@types/object-path": "0.11.0",
"chalk": "2.4.2",
"object-path": "0.11.4",
"tslib": "1.10.0",
"typescript": "3.6.4"
},
"optionalDependencies": {
"@types/jsdom": "12.2.4",
"jsdom": "15.1.1",
"deasync": "0.1.15"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"browser": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"typings": "./dist/esm/index.d.ts",
"es2015": "./dist/esm/index.js",
"repository": {
"type": "git",
"url": "https://github.com/wessberg/ts-evaluator.git"
},
"bugs": {
"url": "https://github.com/wessberg/ts-evaluator/issues"
},
"engines": {
"node": ">=10.1.0"
},
"ava": {
"files": [
"test/**/*.test.ts"
],
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
}
"name": "@wessberg/ts-evaluator",
"version": "0.0.23",
"description": "An interpreter for Typescript that can evaluate an arbitrary Node within a Typescript AST",
"scripts": {
"generate:readme": "scaffold readme --yes",
"generate:license": "scaffold license --yes",
"generate:contributing": "scaffold contributing --yes",
"generate:coc": "scaffold coc --yes",
"generate:changelog": "standard-changelog --first-release",
"generate:all": "npm run generate:license & npm run generate:contributing & npm run generate:coc & npm run generate:readme && npm run generate:changelog",
"clean": "rm -rf dist",
"lint": "tsc --noEmit && tslint -c tslint.json --project tsconfig.json",
"prettier": "prettier --write '{src,test,documentation}/**/*.{js,ts,json,html,xml,css,md}'",
"test": "ava",
"prebuild": "npm run clean",
"build": "npm run rollup",
"rollup": "rollup -c rollup.config.js",
"preversion": "npm run lint && NODE_ENV=production npm run build",
"version": "npm run generate:all && git add .",
"release": "np --no-cleanup --no-yarn"
},
"keywords": [
"typescript",
"ts",
"interpreter",
"evaluate",
"evaluator",
"ast"
],
"files": [
"dist/**/*.*"
],
"contributors": [
{
"name": "Frederik Wessberg",
"email": "frederikwessberg@hotmail.com",
"url": "https://github.com/wessberg",
"imageUrl": "https://avatars2.githubusercontent.com/u/20454213?s=460&v=4",
"role": "Lead Developer",
"twitter": "FredWessberg",
"github": "wessberg"
}
],
"license": "MIT",
"devDependencies": {
"@wessberg/scaffold": "1.0.19",
"@wessberg/rollup-plugin-ts": "1.1.74",
"@wessberg/ts-config": "0.0.42",
"standard-changelog": "2.0.15",
"tslint": "5.20.1",
"ava": "2.4.0",
"prettier": "^1.19.1",
"pretty-quick": "^2.0.1",
"husky": "^3.0.9",
"np": "^5.1.3",
"find-up": "^4.1.0",
"ts-node": "^8.4.1",
"rollup": "^1.26.4",
"typescript": "3.7.2"
},
"dependencies": {
"@types/deasync": "0.1.0",
"@types/node": "12.12.7",
"@types/object-path": "0.11.0",
"chalk": "3.0.0",
"object-path": "0.11.4",
"tslib": "1.10.0"
},
"peerDependencies": {
"typescript": "^3.x"
},
"optionalDependencies": {
"@types/jsdom": "12.2.4",
"jsdom": "15.2.1",
"deasync": "0.1.15"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"browser": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"typings": "./dist/esm/index.d.ts",
"es2015": "./dist/esm/index.js",
"repository": {
"type": "git",
"url": "https://github.com/wessberg/ts-evaluator.git"
},
"bugs": {
"url": "https://github.com/wessberg/ts-evaluator/issues"
},
"engines": {
"node": ">=10.1.0"
},
"ava": {
"files": [
"test/**/*.test.ts"
],
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
}
}
4 changes: 4 additions & 0 deletions src/interpreter/evaluator/evaluate-binary-expression.ts
Expand Up @@ -180,6 +180,10 @@ export function evaluateBinaryExpression ({node, environment, evaluate, logger,
return leftValue in (rightValue as unknown as object);
}

// Nullish coalescing (A ?? B)
case SyntaxKind.QuestionQuestionToken:
return leftValue != null ? leftValue : rightValue;

case SyntaxKind.InstanceOfKeyword: {
return leftValue as unknown as object instanceof (rightValue as unknown as Function);
}
Expand Down
7 changes: 4 additions & 3 deletions src/interpreter/evaluator/evaluate-call-expression.ts
Expand Up @@ -20,7 +20,7 @@ export function evaluateCallExpression ({node, environment, evaluate, statementT
}

// Evaluate the expression
const expressionResult = (evaluate.expression(node.expression, environment, statementTraversalStack)) as Function;
const expressionResult = (evaluate.expression(node.expression, environment, statementTraversalStack)) as Function|undefined;

if (isLazyCall(expressionResult)) {
const currentThisBinding = expressionContainsSuperKeyword(node.expression) ? getFromLexicalEnvironment(node, environment, THIS_SYMBOL) : undefined;
Expand All @@ -36,11 +36,12 @@ export function evaluateCallExpression ({node, environment, evaluate, statementT

// Otherwise, assume that the expression still needs calling
else {
if (typeof expressionResult !== "function") {
// Unless optional chaining is being used, throw a NotCallableError
if (node.questionDotToken == null && typeof expressionResult !== "function") {
throw new NotCallableError({value: expressionResult, node: node.expression});
}

const value = expressionResult(...evaluatedArgs);
const value = typeof expressionResult !== "function" ? undefined : expressionResult(...evaluatedArgs);
logger.logResult(value, "CallExpression");
return value;
}
Expand Down
Expand Up @@ -12,7 +12,11 @@ export function evaluateElementAccessExpression ({node, environment, evaluate, s
const expressionResult = (evaluate.expression(node.expression, environment, statementTraversalStack)) as IndexLiteral;
const argumentExpressionResult = (evaluate.expression(node.argumentExpression, environment, statementTraversalStack)) as IndexLiteralKey;

const match = expressionResult[argumentExpressionResult];

const match = node.questionDotToken != null && expressionResult == null
// If optional chaining are being used and the expressionResult is undefined or null, assign undefined to 'match'
? undefined
: expressionResult[argumentExpressionResult];

// If it is a function, wrap it in a lazy call to preserve implicit this bindings. This is to avoid losing the this binding or having to
// explicitly bind a 'this' value
Expand Down
Expand Up @@ -10,7 +10,11 @@ import {isBindCallApply} from "../util/function/is-bind-call-apply";
*/
export function evaluatePropertyAccessExpression ({node, environment, evaluate, statementTraversalStack}: IEvaluatorOptions<PropertyAccessExpression>): Literal {
const expressionResult = (evaluate.expression(node.expression, environment, statementTraversalStack)) as IndexLiteral;
const match = expressionResult[node.name.text];

const match = node.questionDotToken != null && expressionResult == null
// If optional chaining are being used and the expressionResult is undefined or null, assign undefined to 'match'
? undefined
: expressionResult[node.name.text];

// If it is a function, wrap it in a lazy call to preserve implicit 'this' bindings. This is to avoid losing the 'this' binding or having to
// explicitly bind a 'this' value
Expand Down
20 changes: 20 additions & 0 deletions test/nullish-coalescing/nullish-coalescing.test.ts
@@ -0,0 +1,20 @@
import test from "ava";
import {prepareTest} from "../setup";

test("Supports nullish coalescing with null-like values. #1", t => {
const {evaluate} = prepareTest(
// language=TypeScript
`
const foo = "";
const bar = foo ?? "bar";
`,
`foo ?? "bar"`
);

const result = evaluate();

if (!result.success) t.fail(result.reason.stack);
else {
t.deepEqual(result.value, "");
}
});
56 changes: 56 additions & 0 deletions test/optional-chaining/optional-chaining.test.ts
@@ -0,0 +1,56 @@
import test from "ava";
import {prepareTest} from "../setup";

test("Supports optional CallExpressions. #1", t => {
const {evaluate} = prepareTest(
// language=TypeScript
`
const foo = {bar: {baz: undefined}};
const bar = foo.bar.baz?.();
`,
"foo.bar.baz?.()"
);

const result = evaluate();

if (!result.success) t.fail(result.reason.stack);
else {
t.deepEqual(result.value, undefined);
}
});

test("Supports optional PropertyAccessExpressions. #1", t => {
const {evaluate} = prepareTest(
// language=TypeScript
`
const foo = {bar: undefined};
const bar = foo.bar?.baz;
`,
"foo.bar?.baz"
);

const result = evaluate();

if (!result.success) t.fail(result.reason.stack);
else {
t.deepEqual(result.value, undefined);
}
});

test("Supports optional ElementAccessExpressions. #1", t => {
const {evaluate} = prepareTest(
// language=TypeScript
`
const foo = {bar: undefined};
const bar = foo.bar?.["baz"];
`,
`foo.bar?.["baz"]`
);

const result = evaluate();

if (!result.success) t.fail(result.reason.stack);
else {
t.deepEqual(result.value, undefined);
}
});

0 comments on commit 7a79b34

Please sign in to comment.