diff --git a/documentation/asset/logo-social-preview.png b/documentation/asset/logo-social-preview.png new file mode 100644 index 0000000..025b8a0 Binary files /dev/null and b/documentation/asset/logo-social-preview.png differ diff --git a/package.json b/package.json index f00d8d6..b7001dc 100644 --- a/package.json +++ b/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" + ] + } } diff --git a/src/interpreter/evaluator/evaluate-binary-expression.ts b/src/interpreter/evaluator/evaluate-binary-expression.ts index 2c7392c..a00fc4a 100644 --- a/src/interpreter/evaluator/evaluate-binary-expression.ts +++ b/src/interpreter/evaluator/evaluate-binary-expression.ts @@ -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); } diff --git a/src/interpreter/evaluator/evaluate-call-expression.ts b/src/interpreter/evaluator/evaluate-call-expression.ts index be3443a..8cf6fe6 100644 --- a/src/interpreter/evaluator/evaluate-call-expression.ts +++ b/src/interpreter/evaluator/evaluate-call-expression.ts @@ -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; @@ -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; } diff --git a/src/interpreter/evaluator/evaluate-element-access-expression.ts b/src/interpreter/evaluator/evaluate-element-access-expression.ts index a8c307f..28cb91d 100644 --- a/src/interpreter/evaluator/evaluate-element-access-expression.ts +++ b/src/interpreter/evaluator/evaluate-element-access-expression.ts @@ -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 diff --git a/src/interpreter/evaluator/evaluate-property-access-expression.ts b/src/interpreter/evaluator/evaluate-property-access-expression.ts index ea893e4..72cdd34 100644 --- a/src/interpreter/evaluator/evaluate-property-access-expression.ts +++ b/src/interpreter/evaluator/evaluate-property-access-expression.ts @@ -10,7 +10,11 @@ import {isBindCallApply} from "../util/function/is-bind-call-apply"; */ export function evaluatePropertyAccessExpression ({node, environment, evaluate, statementTraversalStack}: IEvaluatorOptions): 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 diff --git a/test/nullish-coalescing/nullish-coalescing.test.ts b/test/nullish-coalescing/nullish-coalescing.test.ts new file mode 100644 index 0000000..7336255 --- /dev/null +++ b/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, ""); + } +}); \ No newline at end of file diff --git a/test/optional-chaining/optional-chaining.test.ts b/test/optional-chaining/optional-chaining.test.ts new file mode 100644 index 0000000..15eb9c9 --- /dev/null +++ b/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); + } +}); \ No newline at end of file