From 5f6634f55929955a9d37a9263f99d0227f1a3956 Mon Sep 17 00:00:00 2001 From: Frederik Wessberg Date: Fri, 22 Jul 2022 23:17:03 +0200 Subject: [PATCH] feat: make behavior when not passing in a typechecker far more robust --- README.md | 4 +- .../evaluator/evaluate-binary-expression.ts | 14 +- .../evaluator/evaluate-enum-member.ts | 34 ++++- .../evaluator/evaluate-identifier.ts | 16 +++ .../util/declaration/is-declaration.ts | 7 +- ...for-declaration-within-declaration-file.ts | 16 +++ .../node/find-nearest-parent-node-of-kind.ts | 132 +++++++++++++++++- src/interpreter/util/node/is-node.ts | 5 + src/type/ts.ts | 2 +- test/environment/node-cjs.test.ts | 1 - .../import-declaration.test.ts | 19 +++ test/new-target/new-target.test.ts | 2 +- 12 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 src/interpreter/util/node/is-node.ts diff --git a/README.md b/README.md index 68bc323..4309fc9 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,9 @@ you don't have to evaluate the entire program to produce a value which may poten ### Behavior without a typechecker If you do not have access to a typechecker, for example if you don't have a TypeScript _Program_ to work with, you can avoid passing in a typechecker as an option. -This will work for evaluating literal values, but `ts-evaluator` won't be able to resolve and dealias symbols and identifiers. +This won't be as robust as when a typechecker is given, as `ts-evaluator` won't understand the full type hierarchy of your Program, and most importantly not understand +how to resolve and dealias symbols and identifiers across source files, but it will still be able to resolve and evaluate identifiers and symbols that are located in the same +SourceFile. Uou may find that it works perfectly well for your use case. ### Setting up an environment diff --git a/src/interpreter/evaluator/evaluate-binary-expression.ts b/src/interpreter/evaluator/evaluate-binary-expression.ts index e791d43..ff50ce6 100644 --- a/src/interpreter/evaluator/evaluate-binary-expression.ts +++ b/src/interpreter/evaluator/evaluate-binary-expression.ts @@ -6,6 +6,8 @@ import {UnexpectedNodeError} from "../error/unexpected-node-error/unexpected-nod import {UndefinedLeftValueError} from "../error/undefined-left-value-error/undefined-left-value-error.js"; import {TS} from "../../type/ts.js"; import {getInnerNode} from "../util/node/get-inner-node.js"; +import {findNearestParentNodeWithName} from "../util/node/find-nearest-parent-node-of-kind.js"; +import {isTypescriptNode} from "../util/node/is-node.js"; /** * Evaluates, or attempts to evaluate, a BinaryExpression @@ -149,14 +151,22 @@ export function evaluateBinaryExpression(options: EvaluatorOptions(innerLeftIdentifier.parent, innerLeftIdentifier.text, options as EvaluatorOptions); + + if (isTypescriptNode(result)) { + leftIdentifierValueDeclaration = result; + } + } const bestLexicalEnvironment = leftIdentifierValueDeclaration == null ? environment : findLexicalEnvironmentInSameContext(environment, leftIdentifierValueDeclaration, typescript) ?? environment; setInLexicalEnvironment({env: bestLexicalEnvironment, path: leftIdentifier, value: rightValue, reporting, node}); logger.logBinding(leftIdentifier, rightValue, "Assignment"); - } else { throw new UndefinedLeftValueError({node: node.left}); } diff --git a/src/interpreter/evaluator/evaluate-enum-member.ts b/src/interpreter/evaluator/evaluate-enum-member.ts index 0c0b0df..db8d019 100644 --- a/src/interpreter/evaluator/evaluate-enum-member.ts +++ b/src/interpreter/evaluator/evaluate-enum-member.ts @@ -2,11 +2,43 @@ import {EvaluatorOptions} from "./evaluator-options.js"; import {IndexLiteral, IndexLiteralKey} from "../literal/literal.js"; import {TS} from "../../type/ts.js"; + /** * Evaluates, or attempts to evaluate, an EnumMember */ export function evaluateEnumMember({node, typeChecker, evaluate, environment, statementTraversalStack}: EvaluatorOptions, parent: IndexLiteral): void { - const constantValue = typeChecker?.getConstantValue(node); + let constantValue = typeChecker?.getConstantValue(node); + + // If the constant value is not defined, that must be due to the type checker either not being given or functioning incorrectly. + // Calculate it manually instead + if (constantValue == null) { + if (node.initializer != null) { + constantValue = evaluate.expression(node.initializer, environment, statementTraversalStack) as string | number | undefined; + } else { + const siblings = node.parent.members; + + const thisIndex = siblings.findIndex(member => member === node); + const beforeSiblings = siblings.slice(0, thisIndex); + let traversal = 0; + + for (const sibling of [...beforeSiblings].reverse()) { + traversal++; + if (sibling.initializer != null) { + const siblingConstantValue = evaluate.expression(sibling.initializer, environment, statementTraversalStack) as string | number | undefined; + if (typeof siblingConstantValue === "number") { + constantValue = siblingConstantValue + traversal; + break; + } + + } + } + + if (constantValue == null) { + constantValue = thisIndex; + } + } + } + const propertyName = evaluate.nodeWithValue(node.name, environment, statementTraversalStack) as IndexLiteralKey; // If it is a String enum, all keys will be initialized to strings diff --git a/src/interpreter/evaluator/evaluate-identifier.ts b/src/interpreter/evaluator/evaluate-identifier.ts index 4aedc22..580c110 100644 --- a/src/interpreter/evaluator/evaluate-identifier.ts +++ b/src/interpreter/evaluator/evaluate-identifier.ts @@ -5,6 +5,8 @@ import {UndefinedIdentifierError} from "../error/undefined-identifier-error/unde import {isVarDeclaration} from "../util/flags/is-var-declaration.js"; import {getImplementationForDeclarationWithinDeclarationFile} from "../util/module/get-implementation-for-declaration-within-declaration-file.js"; import {TS} from "../../type/ts.js"; +import {findNearestParentNodeWithName} from "../util/node/find-nearest-parent-node-of-kind.js"; +import {isTypescriptNode} from "../util/node/is-node.js"; /** * Evaluates, or attempts to evaluate, an Identifier or a PrivateIdentifier @@ -37,6 +39,20 @@ export function evaluateIdentifier(options: EvaluatorOptions(node.parent, node.text, options as EvaluatorOptions); + + if (isTypescriptNode(result) && !typescript.isIdentifier(result)) { + valueDeclaration = result; + } else if (result != null) { + // Bind the value placed on the top of the stack to the local environment + setInLexicalEnvironment({env: environment, path: node.text, value: result, reporting, node: node}); + logger.logBinding(node.text, result, `Discovered declaration value`); + return result; + } + } + // If it has a value declaration, go forward with that one if (valueDeclaration != null) { if (valueDeclaration.getSourceFile().isDeclarationFile) { diff --git a/src/interpreter/util/declaration/is-declaration.ts b/src/interpreter/util/declaration/is-declaration.ts index bc358d3..f305c91 100644 --- a/src/interpreter/util/declaration/is-declaration.ts +++ b/src/interpreter/util/declaration/is-declaration.ts @@ -2,8 +2,13 @@ import {TS} from "../../../type/ts.js"; /** * Returns true if the given Node is a Declaration - * Uses an internal non-exposed Typescript helper to decide whether or not the Node is an Expression + * Uses an internal non-exposed Typescript helper to decide whether or not the Node is a declaration */ export function isDeclaration(node: TS.Node, typescript: typeof TS): node is TS.Declaration { return (typescript as unknown as {isDeclaration(node: TS.Node): boolean}).isDeclaration(node); } + +export function isNamedDeclaration(node: TS.Node | TS.NamedDeclaration, typescript: typeof TS): node is TS.NamedDeclaration { + if (typescript.isPropertyAccessExpression(node)) return false; + return "name" in node && node.name != null; +} diff --git a/src/interpreter/util/module/get-implementation-for-declaration-within-declaration-file.ts b/src/interpreter/util/module/get-implementation-for-declaration-within-declaration-file.ts index e9005d6..4a5d4c1 100644 --- a/src/interpreter/util/module/get-implementation-for-declaration-within-declaration-file.ts +++ b/src/interpreter/util/module/get-implementation-for-declaration-within-declaration-file.ts @@ -45,3 +45,19 @@ export function getImplementationForDeclarationWithinDeclarationFile(options: Ev else throw new ModuleNotFoundError({node: moduleDeclaration, path: moduleDeclaration.name.text}); } } + + + export function getImplementationFromExternalFile(name: string, moduleSpecifier: string, options: EvaluatorOptions): Literal { + const {node} = options; + + const require = getFromLexicalEnvironment(node, options.environment, "require")!.literal as NodeRequire; + + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires + const module = require(moduleSpecifier); + return module[name] ?? module.default ?? module; + } catch (ex) { + if (ex instanceof EvaluationError) throw ex; + else throw new ModuleNotFoundError({node, path: moduleSpecifier}); + } +} diff --git a/src/interpreter/util/node/find-nearest-parent-node-of-kind.ts b/src/interpreter/util/node/find-nearest-parent-node-of-kind.ts index 96be40e..19ee68e 100644 --- a/src/interpreter/util/node/find-nearest-parent-node-of-kind.ts +++ b/src/interpreter/util/node/find-nearest-parent-node-of-kind.ts @@ -1,5 +1,9 @@ import {TS} from "../../../type/ts.js"; -import {isDeclaration} from "../declaration/is-declaration.js"; +import {EvaluatorOptions} from "../../evaluator/evaluator-options.js"; +import {Literal} from "../../literal/literal.js"; +import {isDeclaration, isNamedDeclaration} from "../declaration/is-declaration.js"; +import {isVarDeclaration} from "../flags/is-var-declaration.js"; +import {getImplementationFromExternalFile} from "../module/get-implementation-for-declaration-within-declaration-file.js"; /** * Finds the nearest parent node of the given kind from the given Node @@ -19,6 +23,117 @@ export function findNearestParentNodeOfKind(from: TS.Node, ki } } +/** + * Finds the nearest parent node with the given name from the given Node + */ +export function findNearestParentNodeWithName( + from: TS.Node, + name: string, + options: EvaluatorOptions, + visitedRoots = new WeakSet() +): T | Literal | undefined { + const {typescript} = options; + let result: TS.Node | Literal | undefined; + + function visit(nextNode: TS.Node, nestingLayer = 0): boolean { + if (visitedRoots.has(nextNode)) return false; + visitedRoots.add(nextNode); + + if (typescript.isIdentifier(nextNode)) { + if (nextNode.text === name) { + result = nextNode; + return true; + } + } else if (typescript.isShorthandPropertyAssignment(nextNode)) { + return false; + } else if (typescript.isPropertyAssignment(nextNode)) { + return false; + } else if (typescript.isImportDeclaration(nextNode)) { + if (nextNode.importClause != null) { + if (nextNode.importClause.name != null && visit(nextNode.importClause.name)) { + const moduleSpecifier = nextNode.moduleSpecifier; + if (moduleSpecifier != null && typescript.isStringLiteralLike(moduleSpecifier)) { + result = getImplementationFromExternalFile(name, moduleSpecifier.text, options); + return true; + } + } else if (nextNode.importClause.namedBindings != null && visit(nextNode.importClause.namedBindings)) { + return true; + } + } + return false; + } else if (typescript.isImportEqualsDeclaration(nextNode)) { + if (nextNode.name != null && visit(nextNode.name)) { + if (typescript.isIdentifier(nextNode.moduleReference)) { + result = findNearestParentNodeWithName(nextNode.parent, nextNode.moduleReference.text, options, visitedRoots); + return result != null; + } else if (typescript.isQualifiedName(nextNode.moduleReference)) { + return false; + } else { + const moduleSpecifier = nextNode.moduleReference.expression; + if (moduleSpecifier != null && typescript.isStringLiteralLike(moduleSpecifier)) { + result = getImplementationFromExternalFile(name, moduleSpecifier.text, options); + return true; + } + } + } + return false; + } else if (typescript.isNamespaceImport(nextNode)) { + if (visit(nextNode.name)) { + const moduleSpecifier = nextNode.parent?.parent?.moduleSpecifier; + if (moduleSpecifier == null || !typescript.isStringLiteralLike(moduleSpecifier)) { + return false; + } + + result = getImplementationFromExternalFile(name, moduleSpecifier.text, options); + return true; + } + } else if (typescript.isNamedImports(nextNode)) { + for (const importSpecifier of nextNode.elements) { + if (visit(importSpecifier)) { + return true; + } + } + } else if (typescript.isImportSpecifier(nextNode)) { + if (visit(nextNode.name)) { + const moduleSpecifier = nextNode.parent?.parent?.parent?.moduleSpecifier; + if (moduleSpecifier == null || !typescript.isStringLiteralLike(moduleSpecifier)) { + return false; + } + + result = getImplementationFromExternalFile(name, moduleSpecifier.text, options); + return true; + } + } else if (typescript.isSourceFile(nextNode)) { + for (const statement of nextNode.statements) { + if (visit(statement)) { + return true; + } + } + } else if (typescript.isVariableStatement(nextNode)) { + for (const declaration of nextNode.declarationList.declarations) { + if (visit(declaration) && (isVarDeclaration(nextNode.declarationList, typescript) || nestingLayer < 1)) { + return true; + } + } + } else if (typescript.isBlock(nextNode)) { + for (const statement of nextNode.statements) { + if (visit(statement, nestingLayer + 1)) { + return true; + } + } + } else if (isNamedDeclaration(nextNode, typescript)) { + if (nextNode.name != null && visit(nextNode.name)) { + result = nextNode; + return true; + } + } + return false; + } + + const suceeded = typescript.findAncestor(from, (nextNode): nextNode is T => visit(nextNode)); + return !suceeded ? undefined : (result as T | undefined); +} + export function getStatementContext(from: TS.Node, typescript: typeof TS): T | undefined { let currentParent = from; while (true) { @@ -29,3 +144,18 @@ export function getStatementContext(f } } } + +/** + * TODO: You could in principle use the information from the environment preset + * to resolve bindings from built-in node modules + + + else if (typescript.isImportDeclaration(nextNode)) { + return false; +} else if (typescript.isNamespaceImport(nextNode)) { + return false; +} else if (typescript.isNamedImports(nextNode)) { + return false; +} else if (typescript.isImportSpecifier(nextNode)) { + return false; +}*/ diff --git a/src/interpreter/util/node/is-node.ts b/src/interpreter/util/node/is-node.ts new file mode 100644 index 0000000..f4b0572 --- /dev/null +++ b/src/interpreter/util/node/is-node.ts @@ -0,0 +1,5 @@ +import { TS } from "../../../type/ts"; + + export function isTypescriptNode(node: T|unknown): node is T { + return node != null && typeof node === "object" && "kind" in node && "flags" in node && "pos" in node && "end" in node; +} diff --git a/src/type/ts.ts b/src/type/ts.ts index 3f87866..7600b63 100644 --- a/src/type/ts.ts +++ b/src/type/ts.ts @@ -1,2 +1,2 @@ import type * as TS from "typescript"; -export type {TS}; +export type {TS}; \ No newline at end of file diff --git a/test/environment/node-cjs.test.ts b/test/environment/node-cjs.test.ts index 662622b..471430d 100644 --- a/test/environment/node-cjs.test.ts +++ b/test/environment/node-cjs.test.ts @@ -51,7 +51,6 @@ test("Can handle the '__dirname' and '__filename' meta properties in a CommonJS- if (!result.success) t.fail(result.reason.stack); else { - console.log(result.value); t.deepEqual(result.value, {dirname: path.native.join(setup.fileStructure.dir.src), filename: path.native.join(setup.fileStructure.dir.src, "bar.ts")}); } }); diff --git a/test/import-declaration/import-declaration.test.ts b/test/import-declaration/import-declaration.test.ts index 49b55ac..8cccf2a 100644 --- a/test/import-declaration/import-declaration.test.ts +++ b/test/import-declaration/import-declaration.test.ts @@ -229,3 +229,22 @@ test("Can resolve symbols via ImportDeclarations for built-in node modules. #4", if (!result.success) t.fail(result.reason.stack); else t.deepEqual(result.value, true); }); + +test("Can resolve symbols via ImportDeclarations for built-in node modules. #5", withTypeScript, (t, {typescript}) => { + const {result} = executeProgram( + [ + // language=TypeScript + ` + import fs from "fs"; + + const alias = fs.readFileSync; + const foo = JSON.parse(fs.readFileSync("${path.join(path.dirname(path.urlToFilename(import.meta.url)), "../../package.json").replace(/\\/g, "\\\\")}")).name; + ` + ], + "foo", + {typescript} + ); + + if (!result.success) t.fail(result.reason.stack); + else t.deepEqual(result.value, "ts-evaluator"); +}); \ No newline at end of file diff --git a/test/new-target/new-target.test.ts b/test/new-target/new-target.test.ts index 0228674..c52829b 100644 --- a/test/new-target/new-target.test.ts +++ b/test/new-target/new-target.test.ts @@ -23,4 +23,4 @@ test("Can handle new.target syntax. #1", withTypeScript, (t, {typescript}) => { else { t.deepEqual(result.value, true); } -}); \ No newline at end of file +});