Skip to content

Commit

Permalink
feat: make behavior when not passing in a typechecker far more robust
Browse files Browse the repository at this point in the history
  • Loading branch information
wessberg committed Jul 22, 2022
1 parent faabab5 commit 5f6634f
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 9 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -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

Expand Down
14 changes: 12 additions & 2 deletions src/interpreter/evaluator/evaluate-binary-expression.ts
Expand Up @@ -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
Expand Down Expand Up @@ -149,14 +151,22 @@ export function evaluateBinaryExpression(options: EvaluatorOptions<TS.BinaryExpr
if (leftIdentifier != null) {
const innerLeftIdentifier = getInnerNode(node.left, typescript);
const leftIdentifierSymbol = typeChecker?.getSymbolAtLocation(innerLeftIdentifier);
const leftIdentifierValueDeclaration = leftIdentifierSymbol?.valueDeclaration;
let leftIdentifierValueDeclaration = leftIdentifierSymbol?.valueDeclaration;

// If we don't have a typechecker to work it, try parsing the SourceFile in order to locate the declaration
if (leftIdentifierValueDeclaration == null && typeChecker == null && typescript.isIdentifier(innerLeftIdentifier)) {
const result = findNearestParentNodeWithName<TS.Declaration>(innerLeftIdentifier.parent, innerLeftIdentifier.text, options as EvaluatorOptions<TS.Declaration>);

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});
}
Expand Down
34 changes: 33 additions & 1 deletion src/interpreter/evaluator/evaluate-enum-member.ts
Expand Up @@ -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<TS.EnumMember>, 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
Expand Down
16 changes: 16 additions & 0 deletions src/interpreter/evaluator/evaluate-identifier.ts
Expand Up @@ -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
Expand Down Expand Up @@ -37,6 +39,20 @@ export function evaluateIdentifier(options: EvaluatorOptions<TS.Identifier | TS.
}
}

// If we don't have a typechecker to work it, try parsing the SourceFile in order to locate the declaration
if (valueDeclaration == null && typeChecker == null) {
const result = findNearestParentNodeWithName<TS.Declaration>(node.parent, node.text, options as EvaluatorOptions<TS.Declaration>);

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) {
Expand Down
7 changes: 6 additions & 1 deletion src/interpreter/util/declaration/is-declaration.ts
Expand Up @@ -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;
}
Expand Up @@ -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<TS.Node>): 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});
}
}
132 changes: 131 additions & 1 deletion 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
Expand All @@ -19,6 +23,117 @@ export function findNearestParentNodeOfKind<T extends TS.Node>(from: TS.Node, ki
}
}

/**
* Finds the nearest parent node with the given name from the given Node
*/
export function findNearestParentNodeWithName<T extends TS.Node>(
from: TS.Node,
name: string,
options: EvaluatorOptions<T>,
visitedRoots = new WeakSet<TS.Node>()
): 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<T>(from, (nextNode): nextNode is T => visit(nextNode));
return !suceeded ? undefined : (result as T | undefined);
}

export function getStatementContext<T extends TS.Declaration = TS.Declaration>(from: TS.Node, typescript: typeof TS): T | undefined {
let currentParent = from;
while (true) {
Expand All @@ -29,3 +144,18 @@ export function getStatementContext<T extends TS.Declaration = TS.Declaration>(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;
}*/
5 changes: 5 additions & 0 deletions src/interpreter/util/node/is-node.ts
@@ -0,0 +1,5 @@
import { TS } from "../../../type/ts";

export function isTypescriptNode<T extends TS.Node>(node: T|unknown): node is T {
return node != null && typeof node === "object" && "kind" in node && "flags" in node && "pos" in node && "end" in node;
}
2 changes: 1 addition & 1 deletion src/type/ts.ts
@@ -1,2 +1,2 @@
import type * as TS from "typescript";
export type {TS};
export type {TS};
1 change: 0 additions & 1 deletion test/environment/node-cjs.test.ts
Expand Up @@ -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")});
}
});
Expand Down
19 changes: 19 additions & 0 deletions test/import-declaration/import-declaration.test.ts
Expand Up @@ -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");
});
2 changes: 1 addition & 1 deletion test/new-target/new-target.test.ts
Expand Up @@ -23,4 +23,4 @@ test("Can handle new.target syntax. #1", withTypeScript, (t, {typescript}) => {
else {
t.deepEqual(result.value, true);
}
});
});

0 comments on commit 5f6634f

Please sign in to comment.