UNPKG

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
'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