ts-evaluator
Version:
An interpreter for Typescript that can evaluate an arbitrary Node within a Typescript AST
1,564 lines (1,511 loc) • 207 kB
JavaScript
'use strict';
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const TSModule = require('typescript');
const objectPath = require('object-path');
const path = require('crosspath');
const module$1 = require('module');
const util = require('util');
const color = require('ansi-colors');
function _interopNamespaceDefault(e) {
const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
if (e) {
for (const k in e) {
if (k !== 'default') {
const d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: () => e[k]
});
}
}
}
n.default = e;
return Object.freeze(n);
}
const TSModule__namespace = /*#__PURE__*/_interopNamespaceDefault(TSModule);
const ECMA_GLOBALS = () => {
/* eslint-disable @typescript-eslint/naming-convention */
const base = {
Infinity,
NaN,
undefined,
isNaN,
parseFloat,
parseInt,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
Array,
Boolean,
Date,
Error,
EvalError,
Number,
Object,
RangeError,
ReferenceError,
RegExp,
String,
SyntaxError,
TypeError,
URIError,
JSON,
Math,
escape,
unescape,
// eslint-disable-next-line no-eval
eval,
Function
/* eslint-enable @typescript-eslint/naming-convention */
};
try {
base.AggregateError = AggregateError;
}
catch { }
try {
base.FinalizationRegistry = FinalizationRegistry;
}
catch { }
try {
base.WeakRef = WeakRef;
}
catch { }
try {
base.BigInt = BigInt;
}
catch { }
try {
base.Reflect = Reflect;
}
catch { }
try {
base.WeakMap = WeakMap;
}
catch { }
try {
base.WeakSet = WeakSet;
}
catch { }
try {
base.Set = Set;
}
catch { }
try {
base.Map = Map;
}
catch { }
try {
base.Uint8Array = Uint8Array;
}
catch { }
try {
base.BigUint64Array = BigUint64Array;
}
catch { }
try {
base.BigInt64Array = BigInt64Array;
}
catch { }
try {
base.Atomics = Atomics;
}
catch { }
try {
base.SharedArrayBuffer = SharedArrayBuffer;
}
catch { }
try {
base.WebAssembly = WebAssembly;
}
catch { }
try {
base.Uint8ClampedArray = Uint8ClampedArray;
}
catch { }
try {
base.Uint16Array = Uint16Array;
}
catch { }
try {
base.Uint32Array = Uint32Array;
}
catch { }
try {
base.Intl = Intl;
}
catch { }
try {
base.Int8Array = Int8Array;
}
catch { }
try {
base.Int16Array = Int16Array;
}
catch { }
try {
base.Int32Array = Int32Array;
}
catch { }
try {
base.Float32Array = Float32Array;
}
catch { }
try {
base.Float64Array = Float64Array;
}
catch { }
try {
base.ArrayBuffer = ArrayBuffer;
}
catch { }
try {
base.DataView = DataView;
}
catch { }
try {
base.isFinite = isFinite;
}
catch { }
try {
base.Promise = Promise;
}
catch { }
try {
base.Proxy = Proxy;
}
catch { }
try {
base.Symbol = Symbol;
}
catch { }
return base;
};
/* eslint-disable @typescript-eslint/ban-types */
function mergeDescriptors(a, b, c) {
const newObj = {};
const normalizedB = b == null ? {} : b;
const normalizedC = c == null ? {} : c;
[a, normalizedB, normalizedC].forEach(item => Object.defineProperties(newObj, Object.getOwnPropertyDescriptors(item)));
return newObj;
}
/* eslint-disable @typescript-eslint/ban-types */
/**
* Excludes the properties of B from A
*/
function subtract(a, b) {
const newA = {};
Object.getOwnPropertyNames(a).forEach(name => {
if (!(name in b)) {
Object.defineProperty(newA, name, Object.getOwnPropertyDescriptor(a, name));
}
});
return newA;
}
// Until import.meta.resolve becomes stable, we'll have to do this instead
const requireModule = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)));
/* eslint-disable @typescript-eslint/naming-convention */
const NODE_CJS_GLOBALS = () => {
const ecmaGlobals = ECMA_GLOBALS();
const merged = mergeDescriptors(subtract(global, ecmaGlobals), ecmaGlobals, {
require: requireModule,
process,
__dirname: (fileName) => path.native.normalize(path.native.dirname(fileName)),
__filename: (fileName) => path.native.normalize(fileName)
});
Object.defineProperties(merged, {
global: {
get() {
return merged;
}
},
globalThis: {
get() {
return merged;
}
}
});
return merged;
};
/**
* Returns an object containing the properties that are relevant to 'requestAnimationFrame' and 'requestIdleCallback'
*/
function rafImplementation(global) {
let lastTime = 0;
const _requestAnimationFrame = function requestAnimationFrame(callback) {
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = global.setTimeout(function () {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
const _cancelAnimationFrame = function cancelAnimationFrame(id) {
clearTimeout(id);
};
return {
requestAnimationFrame: _requestAnimationFrame,
cancelAnimationFrame: _cancelAnimationFrame
};
}
/**
* The jsdom module is optionally imported on-demand as needed
*/
let jsdomModule;
function loadJsdom(required = false) {
return (jsdomModule !== null && jsdomModule !== void 0 ? jsdomModule : (jsdomModule = loadModules("evaluate against a browser environment", required, "jsdom")));
}
function loadModules(description, required, moduleSpecifier = description) {
try {
return requireModule(moduleSpecifier);
}
catch (ex) {
if (required) {
throw new ReferenceError(`You must install the peer dependency '${moduleSpecifier}' in order to ${description} with ts-evaluator`);
}
return undefined;
}
}
const BROWSER_GLOBALS = () => {
const { JSDOM } = loadJsdom(true);
const { window } = new JSDOM("", { url: "https://example.com" });
const ecmaGlobals = ECMA_GLOBALS();
// Add requestAnimationFrame/cancelAnimationFrame if missing
if (window.requestAnimationFrame == null) {
const raf = rafImplementation(window);
Object.defineProperties(window, Object.getOwnPropertyDescriptors(raf));
}
// Add all missing Ecma Globals to the JSDOM window
const missingEcmaGlobals = subtract(ecmaGlobals, window);
if (Object.keys(missingEcmaGlobals).length > 0) {
Object.defineProperties(window, Object.getOwnPropertyDescriptors(ecmaGlobals));
}
return window;
};
const RETURN_SYMBOL = "[return]";
const BREAK_SYMBOL = "[break]";
const CONTINUE_SYMBOL = "[continue]";
const THIS_SYMBOL = "this";
const SUPER_SYMBOL = "super";
const NODE_ESM_GLOBALS = () => {
const ecmaGlobals = ECMA_GLOBALS();
const merged = mergeDescriptors(subtract(global, ecmaGlobals), ecmaGlobals, {
import: {
meta: {
url: (fileName) => {
const normalized = path.normalize(fileName);
return `file:///${normalized.startsWith(`/`) ? normalized.slice(1) : normalized}`;
}
}
},
process
});
Object.defineProperties(merged, {
global: {
get() {
return merged;
}
},
globalThis: {
get() {
return merged;
}
}
});
return merged;
};
/**
* Returns true if the given Node is a Declaration
* Uses an internal non-exposed Typescript helper to decide whether or not the Node is a declaration
*/
function isDeclaration(node, typescript) {
return typescript.isDeclaration(node);
}
function isNamedDeclaration(node, typescript) {
if (typescript.isPropertyAccessExpression(node))
return false;
return "name" in node && node.name != null;
}
/**
* Returns true if the given VariableDeclarationList is declared with a 'var' keyword
*/
function isVarDeclaration(declarationList, typescript) {
return declarationList.flags !== typescript.NodeFlags.Const && declarationList.flags !== typescript.NodeFlags.Let;
}
/**
* A Base class for EvaluationErrors
*/
class EvaluationError extends Error {
constructor({ node, environment, message }) {
super(message);
Error.captureStackTrace(this, this.constructor);
this.node = node;
this.environment = environment;
}
}
function isEvaluationError(item) {
return typeof item === "object" && item != null && item instanceof EvaluationError;
}
/**
* An Error that can be thrown when a moduleSpecifier couldn't be resolved
*/
class ModuleNotFoundError extends EvaluationError {
constructor({ path, node, environment, message = `Module '${path}' could not be resolved'` }) {
super({ message, environment, node });
this.path = path;
}
}
/**
* An Error that can be thrown when an unexpected node is encountered
*/
class UnexpectedNodeError extends EvaluationError {
constructor({ node, environment, typescript, message = `Unexpected Node: '${typescript.SyntaxKind[node.kind]}'` }) {
super({ message, node, environment });
}
}
/**
* Gets the name of the given declaration
*/
function getDeclarationName(options) {
var _a;
const { node, evaluate, environment, typescript, throwError } = options;
const name = typescript.getNameOfDeclaration(node);
if (name == null)
return undefined;
if (typescript.isIdentifier(name)) {
return name.text;
}
else if ((_a = typescript.isPrivateIdentifier) === null || _a === void 0 ? void 0 : _a.call(typescript, name)) {
return name.text;
}
else if (typescript.isStringLiteralLike(name)) {
return name.text;
}
else if (typescript.isNumericLiteral(name)) {
return Number(name.text);
}
else if (typescript.isComputedPropertyName(name)) {
return evaluate.expression(name.expression, options);
}
else {
return throwError(new UnexpectedNodeError({ node: name, environment, typescript }));
}
}
function getResolvedModuleName(moduleSpecifier, options) {
const { node, typescript } = options;
if (!typescript.isExternalModuleNameRelative(moduleSpecifier)) {
return moduleSpecifier;
}
const parentPath = node.getSourceFile().fileName;
return path.join(path.dirname(parentPath), moduleSpecifier);
}
/**
* Gets an implementation for the given declaration that lives within a declaration file
*/
function getImplementationForDeclarationWithinDeclarationFile(options) {
var _a, _b, _c, _d, _e;
const { node, typescript, throwError, environment } = options;
const name = getDeclarationName(options);
if (isEvaluationError(name)) {
return name;
}
if (name == null) {
return throwError(new UnexpectedNodeError({ node, environment, typescript }));
}
// First see if it lives within the lexical environment
const matchInLexicalEnvironment = getFromLexicalEnvironment(node, options.environment, name);
// If so, return it
if (matchInLexicalEnvironment != null && matchInLexicalEnvironment.literal != null) {
return matchInLexicalEnvironment.literal;
}
// Otherwise, expect it to be something that is require'd on demand
const require = getFromLexicalEnvironment(node, options.environment, "require").literal;
const moduleDeclaration = typescript.isModuleDeclaration(node)
? node
: findNearestParentNodeOfKind(node, typescript.SyntaxKind.ModuleDeclaration, typescript);
if (moduleDeclaration == null) {
return throwError(new UnexpectedNodeError({ node, environment, typescript }));
}
const moduleSpecifier = moduleDeclaration.name.text;
const resolvedModuleSpecifier = getResolvedModuleName(moduleSpecifier, options);
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
const module = (_d = (_b = (_a = options.moduleOverrides) === null || _a === void 0 ? void 0 : _a[moduleSpecifier]) !== null && _b !== void 0 ? _b : (_c = options.moduleOverrides) === null || _c === void 0 ? void 0 : _c[resolvedModuleSpecifier]) !== null && _d !== void 0 ? _d : require(resolvedModuleSpecifier);
return typescript.isModuleDeclaration(node) ? module : (_e = module[name]) !== null && _e !== void 0 ? _e : module;
}
catch (ex) {
if (isEvaluationError(ex))
return ex;
else
return throwError(new ModuleNotFoundError({ node: moduleDeclaration, environment, path: resolvedModuleSpecifier }));
}
}
function getImplementationFromExternalFile(name, moduleSpecifier, options) {
var _a, _b, _c, _d, _e, _f;
const { node, throwError, environment } = options;
const require = getFromLexicalEnvironment(node, options.environment, "require").literal;
const resolvedModuleSpecifier = getResolvedModuleName(moduleSpecifier, options);
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
const module = (_d = (_b = (_a = options.moduleOverrides) === null || _a === void 0 ? void 0 : _a[moduleSpecifier]) !== null && _b !== void 0 ? _b : (_c = options.moduleOverrides) === null || _c === void 0 ? void 0 : _c[resolvedModuleSpecifier]) !== null && _d !== void 0 ? _d : require(resolvedModuleSpecifier);
return (_f = (_e = module[name]) !== null && _e !== void 0 ? _e : module.default) !== null && _f !== void 0 ? _f : module;
}
catch (ex) {
if (isEvaluationError(ex))
return ex;
else
return throwError(new ModuleNotFoundError({ node, environment, path: resolvedModuleSpecifier }));
}
}
/**
* Finds the nearest parent node of the given kind from the given Node
*/
function findNearestParentNodeOfKind(from, kind, typescript) {
let currentParent = from;
while (true) {
currentParent = currentParent.parent;
if (currentParent == null)
return undefined;
if (currentParent.kind === kind) {
const combinedNodeFlags = typescript.getCombinedNodeFlags(currentParent);
const isNamespace = (combinedNodeFlags & typescript.NodeFlags.Namespace) !== 0 || (combinedNodeFlags & typescript.NodeFlags.NestedNamespace) !== 0;
if (!isNamespace)
return currentParent;
}
if (typescript.isSourceFile(currentParent))
return undefined;
}
}
/**
* Finds the nearest parent node with the given name from the given Node
*/
function findNearestParentNodeWithName(from, name, options, visitedRoots = new WeakSet()) {
const { typescript } = options;
let result;
function visit(nextNode, nestingLayer = 0) {
var _a, _b, _c, _d, _e;
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 = (_b = (_a = nextNode.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.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 = (_e = (_d = (_c = nextNode.parent) === null || _c === void 0 ? void 0 : _c.parent) === null || _d === void 0 ? void 0 : _d.parent) === null || _e === void 0 ? void 0 : _e.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) => visit(nextNode));
return !suceeded ? undefined : result;
}
function getStatementContext(from, typescript) {
let currentParent = from;
while (true) {
currentParent = currentParent.parent;
if (currentParent == null)
return undefined;
if (isDeclaration(currentParent, typescript) || typescript.isSourceFile(currentParent)) {
return currentParent;
}
}
}
/**
* Returns true if the provided value is ObjectLike
*
* @param value
* @returns
*/
function isObjectLike(value) {
return value != null && (typeof value === "function" || typeof value === "object");
}
/**
* Returns true if the given value can be observed
*
* @param value
* @returns
*/
function canBeObserved(value) {
return isObjectLike(value);
}
/**
* Returns true if the given function is either Function.prototype.bind, Function.prototype.call, or Function.prototype.apply
*
* @param func
* @param [environment]
* @return
*/
function isBindCallApply(func, environment) {
switch (func) {
case Function.prototype.bind:
case Function.prototype.call:
case Function.prototype.apply:
return true;
}
if (environment != null) {
const _Function = getFromLexicalEnvironment(undefined, environment, "Function").literal;
switch (func) {
case _Function.prototype.bind:
case _Function.prototype.call:
case _Function.prototype.apply:
return true;
}
}
return false;
}
var PolicyTrapKind;
(function (PolicyTrapKind) {
PolicyTrapKind["GET"] = "__$$_PROXY_GET";
PolicyTrapKind["APPLY"] = "__$$_PROXY_APPLY";
PolicyTrapKind["CONSTRUCT"] = "__$$_PROXY_CONSTRUCT";
})(PolicyTrapKind || (PolicyTrapKind = {}));
/**
* Stringifies the given PolicyTrapKind on the given path
*
* @param kind
* @param path
* @return
*/
function stringifyPolicyTrapKindOnPath(kind, path) {
switch (kind) {
case PolicyTrapKind.GET:
return `get ${path}`;
case PolicyTrapKind.APPLY:
return `${path}(...)`;
case PolicyTrapKind.CONSTRUCT:
return `new ${path}(...)`;
}
}
class EvaluationErrorIntent {
constructor(intent) {
this.intent = intent;
}
construct(node, options) {
return this.intent(node, options);
}
}
function isEvaluationErrorIntent(item) {
return typeof item === "object" && item != null && item instanceof EvaluationErrorIntent;
}
function maybeThrow(node, options, value) {
return isEvaluationErrorIntent(value) ? options.throwError(value.construct(node, options)) : value;
}
/* eslint-disable @typescript-eslint/ban-types */
/**
* Stringifies the given PropertyKey path
*/
function stringifyPath(path) {
return path.map(part => (typeof part === "symbol" ? part.description : part)).join(".");
}
/**
* Creates a proxy with hooks to check the given policy
*/
function createPolicyProxy({ hook, item, scope, policy }) {
/**
* Creates a trap that captures function invocation
*/
function createAccessTrap(inputPath, currentItem) {
const handleHookResult = (result, successCallback) => {
if (result === false)
return;
if (isEvaluationErrorIntent(result) || isEvaluationError(result))
return result;
return successCallback();
};
return !canBeObserved(currentItem) || isBindCallApply(currentItem)
? currentItem
: new Proxy(currentItem, {
/**
* Constructs a new instance of the given target
*/
construct(target, argArray, newTarget) {
return handleHookResult(hook({
kind: PolicyTrapKind.CONSTRUCT,
policy,
newTarget,
argArray,
target,
path: stringifyPath(inputPath)
}), () => Reflect.construct(target, argArray, newTarget));
},
/**
* A trap for a function call. Used to create new proxies for methods on the retrieved module objects
*/
apply(target, thisArg, argArray = []) {
return handleHookResult(hook({
kind: PolicyTrapKind.APPLY,
policy,
thisArg,
argArray,
target,
path: stringifyPath(inputPath)
}), () => Reflect.apply(target, thisArg, argArray));
},
/**
* Gets a trap for 'get' accesses
*/
get(target, property, receiver) {
const newPath = [...inputPath, property];
return handleHookResult(hook({
kind: PolicyTrapKind.GET,
policy,
path: stringifyPath(newPath),
target
}), () => {
const match = Reflect.get(target, property, receiver);
const config = Reflect.getOwnPropertyDescriptor(currentItem, property);
if (config != null && config.configurable === false && config.writable === false) {
return currentItem[property];
}
return createAccessTrap(newPath, match);
});
}
});
}
return !canBeObserved(item) ? item : createAccessTrap([scope], item);
}
/* eslint-disable @typescript-eslint/naming-convention */
/**
* A Map between built-in modules and the kind of IO operations their members performs
* @type {TrapConditionMap<NodeBuiltInsAndGlobals>}
*/
const NETWORK_MAP = {
"node:http2": "http2",
http2: {
connect: {
[PolicyTrapKind.APPLY]: true
},
createSecureServer: {
[PolicyTrapKind.APPLY]: true
},
createServer: {
[PolicyTrapKind.APPLY]: true
}
},
"node:https": "https",
https: {
createServer: {
[PolicyTrapKind.APPLY]: true
},
request: {
[PolicyTrapKind.APPLY]: true
},
get: {
[PolicyTrapKind.APPLY]: true
},
Server: {
[PolicyTrapKind.CONSTRUCT]: true
},
globalAgent: {
destroy: {
[PolicyTrapKind.APPLY]: true
}
},
Agent: {
[PolicyTrapKind.CONSTRUCT]: true
}
},
"node:http": "http",
http: {
createServer: {
[PolicyTrapKind.APPLY]: true
},
request: {
[PolicyTrapKind.APPLY]: true
},
get: {
[PolicyTrapKind.APPLY]: true
},
Server: {
[PolicyTrapKind.CONSTRUCT]: true
},
ClientRequest: {
[PolicyTrapKind.CONSTRUCT]: true
},
globalAgent: {
destroy: {
[PolicyTrapKind.APPLY]: true
}
},
Agent: {
[PolicyTrapKind.CONSTRUCT]: true
}
},
"node:dgram": "dgram",
dgram: {
createSocket: {
[PolicyTrapKind.APPLY]: true
}
},
"node:dns": "dns",
dns: {
lookup: {
[PolicyTrapKind.APPLY]: true
},
lookupService: {
[PolicyTrapKind.APPLY]: true
},
resolve: {
[PolicyTrapKind.APPLY]: true
},
resolve4: {
[PolicyTrapKind.APPLY]: true
},
resolve6: {
[PolicyTrapKind.APPLY]: true
},
resolveAny: {
[PolicyTrapKind.APPLY]: true
},
resolveCname: {
[PolicyTrapKind.APPLY]: true
},
resolveMx: {
[PolicyTrapKind.APPLY]: true
},
resolveNaptr: {
[PolicyTrapKind.APPLY]: true
},
resolveNs: {
[PolicyTrapKind.APPLY]: true
},
resolvePtr: {
[PolicyTrapKind.APPLY]: true
},
resolveSoa: {
[PolicyTrapKind.APPLY]: true
},
resolveSrv: {
[PolicyTrapKind.APPLY]: true
},
resolveTxt: {
[PolicyTrapKind.APPLY]: true
},
reverse: {
[PolicyTrapKind.APPLY]: true
},
Resolver: {
[PolicyTrapKind.CONSTRUCT]: true
}
},
"node:net": "net",
net: {
createServer: {
[PolicyTrapKind.APPLY]: true
},
createConnection: {
[PolicyTrapKind.APPLY]: true
},
connect: {
[PolicyTrapKind.APPLY]: true
},
Server: {
[PolicyTrapKind.CONSTRUCT]: true
}
},
"node:tls": "tls",
tls: {
createServer: {
[PolicyTrapKind.APPLY]: true
},
createSecureContext: {
[PolicyTrapKind.APPLY]: true
},
connect: {
[PolicyTrapKind.APPLY]: true
},
Server: {
[PolicyTrapKind.CONSTRUCT]: true
},
TLSSocket: {
[PolicyTrapKind.CONSTRUCT]: true
}
}
};
/* eslint-disable @typescript-eslint/naming-convention */
/**
* A Map between built-in identifiers and the members that produce non-deterministic results.
*/
const NONDETERMINISTIC_MAP = {
// Any network operation will always be non-deterministic
...NETWORK_MAP,
Math: {
random: {
[PolicyTrapKind.APPLY]: true
}
},
Date: {
now: {
[PolicyTrapKind.APPLY]: true
},
// Dates that receive no arguments are nondeterministic since they care about "now" and will evaluate to a new value for each invocation
[PolicyTrapKind.CONSTRUCT]: (...args) => args.length === 0 && !(args[0] instanceof Date)
}
};
/**
* Returns true if the given item is a TrapCondition
*/
function isTrapCondition(item, condition) {
// noinspection SuspiciousTypeOfGuard
return typeof item === typeof condition || typeof item === "function";
}
/**
* Returns true if the given item is a TrapCondition
*/
function isTrapConditionFunction(item) {
return typeof item === "function";
}
/**
* Returns true if the given path represents something that is nondeterministic.
*/
function isTrapConditionMet(map, condition, item) {
const atoms = item.path.split(".");
return walkAtoms(map, condition, item, atoms);
}
/**
* Walks all atoms of the given item path
*/
function walkAtoms(map, matchCondition, item, atoms) {
const [head, ...tail] = atoms;
if (head == null)
return false;
const mapEntry = map[head];
// If nothing was matched within the namespace, the trap wasn't matched
if (mapEntry == null)
return false;
if (typeof mapEntry === "string") {
return walkAtoms(map, matchCondition, item, [mapEntry, ...tail]);
}
if (isTrapCondition(mapEntry, matchCondition)) {
return handleTrapCondition(mapEntry, matchCondition, item);
}
else {
const trapMapMatch = mapEntry[item.kind];
if (trapMapMatch != null) {
return handleTrapCondition(trapMapMatch, matchCondition, item);
}
else {
return walkAtoms(mapEntry, matchCondition, item, tail);
}
}
}
/**
* Handles a TrapCondition
*/
function handleTrapCondition(trapCondition, matchCondition, item) {
// If matching the condition depends on the provided arguments, pass them in
if (isTrapConditionFunction(trapCondition)) {
const castItem = item;
return castItem.argArray != null && trapCondition(...castItem.argArray) === matchCondition;
}
// Otherwise, evaluate the truthiness of the condition
else {
return trapCondition === matchCondition;
}
}
/**
* Returns true if the given path represents something that is nondeterministic.
*/
function isNonDeterministic(item) {
return isTrapConditionMet(NONDETERMINISTIC_MAP, true, item);
}
/**
* An Error that can be thrown when a policy is violated
*/
class PolicyError extends EvaluationError {
constructor({ violation, node, environment, message }) {
super({ node, environment, message: `[${violation}]: ${message}` });
this.violation = violation;
}
}
/**
* An Error that can be thrown when something nondeterministic is attempted to be evaluated and has been disallowed to be so
*/
class NonDeterministicError extends PolicyError {
constructor({ operation, node, environment, message = `The operation: '${operation}' is nondeterministic. That is in violation of the policy` }) {
super({ violation: "deterministic", message, node, environment });
this.operation = operation;
}
}
/**
* A Map between built-in modules and the kind of IO operations their members performs
* @type {TrapConditionMap<NodeBuiltInsAndGlobals, "read"|"write">}
*/
const IO_MAP = {
"node:fs": "fs",
fs: {
readFile: {
[PolicyTrapKind.APPLY]: "read"
},
readFileSync: {
[PolicyTrapKind.APPLY]: "read"
},
readdir: {
[PolicyTrapKind.APPLY]: "read"
},
readdirSync: {
[PolicyTrapKind.APPLY]: "read"
},
read: {
[PolicyTrapKind.APPLY]: "read"
},
readSync: {
[PolicyTrapKind.APPLY]: "read"
},
exists: {
[PolicyTrapKind.APPLY]: "read"
},
existsSync: {
[PolicyTrapKind.APPLY]: "read"
},
access: {
[PolicyTrapKind.APPLY]: "read"
},
accessSync: {
[PolicyTrapKind.APPLY]: "read"
},
close: {
[PolicyTrapKind.APPLY]: "read"
},
closeSync: {
[PolicyTrapKind.APPLY]: "read"
},
createReadStream: {
[PolicyTrapKind.APPLY]: "read"
},
stat: {
[PolicyTrapKind.APPLY]: "read"
},
statSync: {
[PolicyTrapKind.APPLY]: "read"
},
watch: {
[PolicyTrapKind.APPLY]: "read"
},
watchFile: {
[PolicyTrapKind.APPLY]: "read"
},
unwatchFile: {
[PolicyTrapKind.APPLY]: "read"
},
realpath: {
[PolicyTrapKind.APPLY]: "read"
},
realpathSync: {
[PolicyTrapKind.APPLY]: "read"
},
fstat: {
[PolicyTrapKind.APPLY]: "read"
},
fstatSync: {
[PolicyTrapKind.APPLY]: "read"
},
createWriteStream: {
[PolicyTrapKind.APPLY]: "write"
},
copyFile: {
[PolicyTrapKind.APPLY]: "write"
},
copyFileSync: {
[PolicyTrapKind.APPLY]: "write"
},
unlink: {
[PolicyTrapKind.APPLY]: "write"
},
unlinkSync: {
[PolicyTrapKind.APPLY]: "write"
},
rmdir: {
[PolicyTrapKind.APPLY]: "write"
},
rmdirSync: {
[PolicyTrapKind.APPLY]: "write"
},
symlink: {
[PolicyTrapKind.APPLY]: "write"
},
symlinkSync: {
[PolicyTrapKind.APPLY]: "write"
},
truncate: {
[PolicyTrapKind.APPLY]: "write"
},
truncateSync: {
[PolicyTrapKind.APPLY]: "write"
},
utimes: {
[PolicyTrapKind.APPLY]: "write"
},
utimesSync: {
[PolicyTrapKind.APPLY]: "write"
},
appendFile: {
[PolicyTrapKind.APPLY]: "write"
},
appendFileSync: {
[PolicyTrapKind.APPLY]: "write"
},
write: {
[PolicyTrapKind.APPLY]: "write"
},
writeSync: {
[PolicyTrapKind.APPLY]: "write"
},
writeFile: {
[PolicyTrapKind.APPLY]: "write"
},
writeFileSync: {
[PolicyTrapKind.APPLY]: "write"
},
chmod: {
[PolicyTrapKind.APPLY]: "write"
},
chmodSync: {
[PolicyTrapKind.APPLY]: "write"
},
chown: {
[PolicyTrapKind.APPLY]: "write"
},
chownSync: {
[PolicyTrapKind.APPLY]: "write"
},
mkdir: {
[PolicyTrapKind.APPLY]: "write"
},
mkdirSync: {
[PolicyTrapKind.APPLY]: "write"
},
rename: {
[PolicyTrapKind.APPLY]: "write"
},
renameSync: {
[PolicyTrapKind.APPLY]: "write"
},
futimes: {
[PolicyTrapKind.APPLY]: "write"
},
futimesSync: {
[PolicyTrapKind.APPLY]: "write"
},
link: {
[PolicyTrapKind.APPLY]: "write"
},
linkSync: {
[PolicyTrapKind.APPLY]: "write"
},
mkdtemp: {
[PolicyTrapKind.APPLY]: "write"
},
open: {
[PolicyTrapKind.APPLY]: "write"
},
openSync: {
[PolicyTrapKind.APPLY]: "write"
},
fchmod: {
[PolicyTrapKind.APPLY]: "write"
},
fchmodSync: {
[PolicyTrapKind.APPLY]: "write"
},
fchown: {
[PolicyTrapKind.APPLY]: "write"
},
fchownSync: {
[PolicyTrapKind.APPLY]: "write"
},
ftruncate: {
[PolicyTrapKind.APPLY]: "write"
},
ftruncateSync: {
[PolicyTrapKind.APPLY]: "write"
},
fsync: {
[PolicyTrapKind.APPLY]: "write"
},
fsyncSync: {
[PolicyTrapKind.APPLY]: "write"
},
fdatasync: {
[PolicyTrapKind.APPLY]: "write"
},
fdatasyncSync: {
[PolicyTrapKind.APPLY]: "write"
},
lchmod: {
[PolicyTrapKind.APPLY]: "write"
},
lchmodSync: {
[PolicyTrapKind.APPLY]: "write"
}
}
};
/**
* Returns true if the given member represents a READ operation from IO
*/
function isIoRead(item) {
return isTrapConditionMet(IO_MAP, "read", item);
}
/**
* An Error that can be thrown when an IO operation is attempted to be executed that is in violation of the context policy
*/
class IoError extends PolicyError {
constructor({ node, environment, kind, message = `${kind} operations are in violation of the policy` }) {
super({ violation: "io", message, environment, node });
this.kind = kind;
}
}
/**
* Returns true if the given member represents a WRITE operation from IO
*/
function isIoWrite(item) {
return isTrapConditionMet(IO_MAP, "write", item);
}
/**
* Returns true if the given item represents a network operation
*/
function isNetworkOperation(item) {
return isTrapConditionMet(NETWORK_MAP, true, item);
}
/**
* An Error that can be thrown when a network operation is attempted to be executed that is in violation of the context policy
*/
class NetworkError extends PolicyError {
constructor({ operation, node, environment, message = `The operation: '${operation}' is performing network activity. That is in violation of the policy` }) {
super({ violation: "deterministic", message, node, environment });
this.operation = operation;
}
}
/* eslint-disable @typescript-eslint/naming-convention */
/**
* A Map between built-in modules (as well as 'process' and the kind of IO operations their members performs
*/
const PROCESS_MAP = {
"node:process": "process",
process: {
exit: {
[PolicyTrapKind.APPLY]: "exit"
}
},
// Everything inside child_process is just one big violation of this policy
"node:child_process": "child_process",
child_process: {
[PolicyTrapKind.APPLY]: "spawnChild"
},
"node:cluster": "cluster",
cluster: {
Worker: {
[PolicyTrapKind.CONSTRUCT]: "spawnChild"
}
}
};
/**
* Returns true if the given item represents a process operation that exits the process
*/
function isProcessExitOperation(item) {
return isTrapConditionMet(PROCESS_MAP, "exit", item);
}
/**
* An Error that can be thrown when a Process operation is attempted to be executed that is in violation of the context policy
*/
class ProcessError extends PolicyError {
constructor({ kind, node, environment, message = `${kind} operations are in violation of the policy` }) {
super({ violation: "process", message, node, environment });
this.kind = kind;
}
}
/**
* Returns true if the given item represents a process operation that spawns a child
*/
function isProcessSpawnChildOperation(item) {
return isTrapConditionMet(PROCESS_MAP, "spawnChild", item);
}
/* eslint-disable @typescript-eslint/naming-convention */
/**
* A Map between built-in modules (as well as 'console' and the operations that print to console
*/
const CONSOLE_MAP = {
"node:console": "console",
console: {
[PolicyTrapKind.APPLY]: true
}
};
/**
* Returns true if the given item represents an operation that prints to console
*/
function isConsoleOperation(item) {
return isTrapConditionMet(CONSOLE_MAP, true, item);
}
/**
* Creates an environment that provide hooks into policy checks
*/
function createSanitizedEnvironment({ policy, env }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hook = (item) => {
if (!policy.console && isConsoleOperation(item)) {
return false;
}
if (!policy.io.read && isIoRead(item)) {
return new EvaluationErrorIntent((node, options) => new IoError({ ...options, node, kind: "read" }));
}
if (!policy.io.write && isIoWrite(item)) {
return new EvaluationErrorIntent((node, options) => new IoError({ ...options, node, kind: "write" }));
}
if (!policy.process.exit && isProcessExitOperation(item)) {
return new EvaluationErrorIntent((node, options) => new ProcessError({ ...options, node, kind: "exit" }));
}
if (!policy.process.exit && isProcessSpawnChildOperation(item)) {
return new EvaluationErrorIntent((node, options) => new ProcessError({ ...options, node, kind: "spawnChild" }));
}
if (!policy.network && isNetworkOperation(item)) {
return new EvaluationErrorIntent((node, options) => new NetworkError({ ...options, node, operation: stringifyPolicyTrapKindOnPath(item.kind, item.path) }));
}
if (policy.deterministic && isNonDeterministic(item)) {
return new EvaluationErrorIntent((node, options) => new NonDeterministicError({ ...options, node, operation: stringifyPolicyTrapKindOnPath(item.kind, item.path) }));
}
return true;
};
const descriptors = Object.entries(Object.getOwnPropertyDescriptors(env));
const gettersAndSetters = Object.assign({}, ...descriptors.filter(([, descriptor]) => !("value" in descriptor)).map(([name, descriptor]) => ({ [name]: descriptor })));
const values = Object.assign({}, ...descriptors
.filter(([, descriptor]) => "value" in descriptor)
.map(([name, descriptor]) => ({
[name]: name === "require"
? new Proxy(descriptor.value, {
/**
* A trap for a function call. Used to create new proxies for methods on the retrieved module objects
*/
apply(target, thisArg, argArray = []) {
const [moduleName] = argArray;
return createPolicyProxy({
policy,
item: Reflect.apply(target, thisArg, argArray),
scope: moduleName,
hook
});
}
})
: createPolicyProxy({
policy,
item: descriptor.value,
scope: name,
hook
})
})));
return Object.defineProperties(values, {
...gettersAndSetters
});
}
/**
* Gets a value from a Lexical Environment
*/
function getRelevantDictFromLexicalEnvironment(env, path) {
const [firstBinding] = path.split(".");
if (objectPath.has(env.env, firstBinding))
return env.env;
if (env.parentEnv != null)
return getRelevantDictFromLexicalEnvironment(env.parentEnv, path);
return undefined;
}
/**
* Gets the EnvironmentPresetKind for the given LexicalEnvironment
*/
function getPresetForLexicalEnvironment(env) {
if (env.preset != null)
return env.preset;
else if (env.parentEnv != null)
return getPresetForLexicalEnvironment(env.parentEnv);
else
return "NONE";
}
function findLexicalEnvironmentInSameContext(from, node, typescript) {
const startingNodeContext = getStatementContext(from.startingNode, typescript);
const nodeContext = getStatementContext(node, typescript);
if ((startingNodeContext === null || startingNodeContext === void 0 ? void 0 : startingNodeContext.pos) === (nodeContext === null || nodeContext === void 0 ? void 0 : nodeContext.pos)) {
return from;
}
if (from.parentEnv == null) {
return undefined;
}
return findLexicalEnvironmentInSameContext(from.parentEnv, node, typescript);
}
/**
* Gets a value from a Lexical Environment
*/
function getFromLexicalEnvironment(node, env, path) {
const [firstBinding] = path.split(".");
if (objectPath.has(env.env, firstBinding)) {
const literal = objectPath.get(env.env, path);
switch (path) {
// If we're in a Node environment, the "__dirname" and "__filename" meta-properties should report the current directory or file of the SourceFile and not the parent process
case "__dirname":
case "__filename": {
const preset = getPresetForLexicalEnvironment(env);
return (preset === "NODE" || preset === "NODE_CJS") && typeof literal === "function" && node != null ? { literal: literal(node.getSourceFile().fileName) } : { literal };
}
case "import.meta": {
const preset = getPresetForLexicalEnvironment(env);
return (preset === "NODE_ESM" || preset === "BROWSER" || preset === "ECMA") &&
typeof literal === "object" &&
literal != null &&
typeof literal.url === "function" &&
node != null
? { literal: { url: literal.url(node.getSourceFile().fileName) } }
: { literal };
}
default:
return { literal };
}
}
if (env.parentEnv != null)
return getFromLexicalEnvironment(node, env.parentEnv, path);
return undefined;
}
/**
* Returns true if the given lexical environment contains a value on the given path that equals the given literal
*/
function pathInLexicalEnvironmentEquals(node, env, equals, ...matchPaths) {
return matchPaths.some(path => {
const match = getFromLexicalEnvironment(node, env, path);
return match == null ? false : match.literal === equals;
});
}
/**
* Returns true if the given value represents an internal symbol
*/
function isInternalSymbol(value) {
switch (value) {
case RETURN_SYMBOL:
case BREAK_SYMBOL:
case CONTINUE_SYMBOL:
case THIS_SYMBOL:
case SUPER_SYMBOL:
return true;
default:
return false;
}
}
/**
* Gets a value from a Lexical Environment
*/
function setInLexicalEnvironment({ environment, path, value, reporting, node, newBinding = false }) {
const [firstBinding] = path.split(".");
if (objectPath.has(environment.env, firstBinding) || newBinding || environment.parentEnv == null) {
// If the value didn't change, do no more
if (objectPath.has(environment.env, path) && objectPath.get(environment.env, path) === value)
return;
// Otherwise, mutate it
objectPath.set(environment.env, path, value);
// Inform reporting hooks if any is given
if (reporting.reportBindings != null && !isInternalSymbol(path)) {
reporting.reportBindings({ path, value, node });
}
}
else {
let currentParentEnv = environment.parentEnv;
while (currentParentEnv != null) {
if (objectPath.has(currentParentEnv.env, firstBinding)) {
// If the value didn't change, do no more
if (objectPath.has(currentParentEnv.env, path) && objectPath.get(currentParentEnv.env, path) === value)
return;
// Otherwise, mutate it
objectPath.set(currentParentEnv.env, path, value);
// Inform reporting hooks if any is given
if (reporting.reportBindings != null && !isInternalSymbol(path)) {
reporting.reportBindings({ path, value, node });
}
retu