// Copyright 2016-2018, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // The typescript import is used for type-checking only. Do not reference it in the emitted code. // Use `ts` instead to access typescript library functions. import * as typescript from "typescript"; import * as log from "../../log"; import * as utils from "./utils"; const ts: typeof typescript = require("../../typescript-shim"); /** * @internal */ export interface ParsedFunctionCode { /** * The serialized code for the function, usable as an expression. Valid for * all functions forms (functions, lambdas, methods, etc.). */ funcExprWithoutName: string; /** * The serialized code for the function, usable as an function declaration. * Valid only for non-lambda function forms. */ funcExprWithName?: string; /** * The name of the function if it was a function declaration. This is * needed so that we can include an entry in the environment mapping this * function name to the actual function we generate for it. This is needed * so that nested recursive calls to the function see the function we're * generating. */ functionDeclarationName?: string; /** * True if the parsed code was an arrow function. */ isArrowFunction: boolean; } /** * @internal */ export interface ParsedFunction extends ParsedFunctionCode { /** * The set of variables the function attempts to capture. */ capturedVariables: CapturedVariables; /** * Whether or not the real `this` (i.e. not a lexically-captured `this`) is * used in the function. */ usesNonLexicalThis: boolean; } /** * Information about a captured property. * * @internal */ export interface CapturedPropertyInfo { /** * The name of the captured property. */ name: string; /** * True if the captured property was invoked. */ invoked: boolean; } /** * Information about a chain of captured properties. For example, if you have * `foo.bar.baz.quux()`, we'll say that `foo` was captured, but that `bar`, * `baz` and `quux` were accessed through it. We'll also note that `quux` was * invoked. * * @internal */ export interface CapturedPropertyChain { infos: CapturedPropertyInfo[]; } /** * A mapping from the names of variables we captured, to information about how * those variables were used. For example, if we see `a.b.c()` and `a` is not * declared in the function, we'll record a mapping of `{ "a": ['b', 'c' * (invoked)] }`. That is, we captured `a`, accessed the properties `b.c` off of * it, and we invoked that property access. With this information we can decide * the totality of what we need to capture for `a`. * * Note: if we want to capture everything, we just use an empty array for * `CapturedPropertyChain[]`. Otherwise, we'll use the chains to determine what * portions of the object to serialize. * * @internal */ export type CapturedVariableMap = Map<string, CapturedPropertyChain[]>; /** * The set of variables the function attempts to capture. There is a required * set an an optional set. The optional set will not block closure-serialization * if we cannot find them, while the required set will. For each variable that * is captured we also specify the list of properties of that variable we need * to serialize. An empty-list means "serialize all properties". * * @internal */ export interface CapturedVariables { required: CapturedVariableMap; optional: CapturedVariableMap; } /** * These are the special globals we've marked as ones we do not want to capture * by value. These values have a dual meaning. They mean one thing at * deployment time and one thing at cloud-execution time. By **not** * capturing-by-value we take the view that the user wants the cloud-execution * time view of things. */ const nodeModuleGlobals: { [key: string]: boolean } = { __dirname: true, __filename: true, // We definitely should not try to capture/serialize `require`. Not only // will it bottom out as a native function, but it is definitely something // the user intends to run against the right module environment at // cloud-execution time and not deployment time. require: true, }; /** * Gets the text of the provided function (using `.toString()`) and massages it * so that it is a legal function declaration. Note: this ties us heavily to V8 * and its representation for functions. In particular, it has expectations * around how functions/lambdas/methods/generators/constructors etc. are * represented. If these change, this will likely break us. * * @internal */ export function parseFunction(funcString: string): [string, ParsedFunction] { const [error, functionCode] = parseFunctionCode(funcString); if (error) { return [error, <any>undefined]; } // In practice it's not guaranteed that a function's toString is parsable by TypeScript. // V8 intrinsics are prefixed with a '%' and TypeScript does not consider that to be a valid // identifier. const [parseError, file] = createSourceFile(functionCode); if (parseError) { return [parseError, <any>undefined]; } const capturedVariables = computeCapturedVariableNames(file!); // if we're looking at an arrow function, the it is always using lexical 'this's // so we don't have to bother even examining it. const usesNonLexicalThis = !functionCode.isArrowFunction && computeUsesNonLexicalThis(file!); const result = <ParsedFunction>functionCode; result.capturedVariables = capturedVariables; result.usesNonLexicalThis = usesNonLexicalThis; if (result.capturedVariables.required.has("this")) { return [ "arrow function captured 'this'. Assign 'this' to another name outside function and capture that.", result, ]; } return ["", result]; } /** * @internal */ export function isNativeFunction(funcString: string): boolean { // Split this constant out so that if this function *itself* is closure serialized, // it will not be thought to be native code itself. const nativeCodeString = "[native " + "code]"; return funcString.indexOf(nativeCodeString) !== -1; } function parseFunctionCode(funcString: string): [string, ParsedFunctionCode] { if (funcString.startsWith("[Function:")) { return [`the function form was not understood.`, <any>undefined]; } if (isNativeFunction(funcString)) { return [`it was a native code function.`, <any>undefined]; } // There are three general forms of node toString'ed Functions we're trying to find out here. // // 1. `[mods] (...) => ... // // i.e. an arrow function. We need to ensure that arrow-functions stay arrow-functions, // and non-arrow-functions end up looking like normal `function` functions. This will make // it so that we can correctly handle 'this' properly depending on if that should be // treated as the lexical capture of 'this' or the non-lexical 'this'. // // 2. `class Foo { ... }` // // i.e. node uses the entire string of a class when toString'ing the constructor function // for it. // // 3. `[mods] function ... // // i.e. a normal function (maybe async, maybe a get/set function, but def not an arrow // function) if (tryParseAsArrowFunction(funcString)) { return ["", { funcExprWithoutName: funcString, isArrowFunction: true }]; } // First check to see if this startsWith 'class'. If so, this is definitely a class. This // works as Node does not place preceding comments on a class/function, allowing us to just // directly see if we've started with the right text. if (funcString.startsWith("class ")) { // class constructor function. We want to get the actual constructor // in the class definition (synthesizing an empty one if one does not) // exist. const [file, firstDiagnostic] = tryCreateSourceFile(funcString); if (firstDiagnostic) { return [`the class could not be parsed: ${firstDiagnostic}`, <any>undefined]; } const classDecl = <typescript.ClassDeclaration>file!.statements.find((x) => ts.isClassDeclaration(x)); if (!classDecl) { return [`the class form was not understood:\n${funcString}`, <any>undefined]; } const constructor = <typescript.ConstructorDeclaration>( classDecl.members.find((m) => ts.isConstructorDeclaration(m)) ); if (!constructor) { // class without explicit constructor. const isSubClass = classDecl.heritageClauses?.some((c) => c.token === ts.SyntaxKind.ExtendsKeyword); return isSubClass ? makeFunctionDeclaration( "constructor() { super(); }", /*isAsync:*/ false, /*isFunctionDeclaration:*/ false, ) : makeFunctionDeclaration("constructor() { }", /*isAsync:*/ false, /*isFunctionDeclaration:*/ false); } const constructorCode = funcString .substring(constructor.getStart(file, /*includeJsDocComment*/ false), constructor.end) .trim(); return makeFunctionDeclaration(constructorCode, /*isAsync:*/ false, /*isFunctionDeclaration: */ false); } let isAsync = false; if (funcString.startsWith("async ")) { isAsync = true; funcString = funcString.slice("async".length).trimLeft(); } if (funcString.startsWith("function get ") || funcString.startsWith("function set ")) { const trimmed = funcString.slice("function get".length); return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false); } if (funcString.startsWith("get ") || funcString.startsWith("set ")) { const trimmed = funcString.slice("get ".length); return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false); } if (funcString.startsWith("function")) { const trimmed = funcString.slice("function".length); return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ true); } // Add "function" (this will make methods parseable). i.e. "foo() { }" becomes // "function foo() { }" // this also does the right thing for functions with computed names. return makeFunctionDeclaration(funcString, isAsync, /*isFunctionDeclaration: */ false); } function tryParseAsArrowFunction(toParse: string): boolean { const [file] = tryCreateSourceFile(toParse); if (!file || file.statements.length !== 1) { return false; } const firstStatement = file.statements[0]; return ts.isExpressionStatement(firstStatement) && ts.isArrowFunction(firstStatement.expression); } function makeFunctionDeclaration( v: string, isAsync: boolean, isFunctionDeclaration: boolean, ): [string, ParsedFunctionCode] { let prefix = isAsync ? "async " : ""; prefix += "function "; v = v.trimLeft(); if (v.startsWith("*")) { v = v.slice(1).trimLeft(); prefix = "function* "; } const openParenIndex = v.indexOf("("); if (openParenIndex < 0) { return [`the function form was not understood.`, <any>undefined]; } if (isComputed(v, openParenIndex)) { v = v.slice(openParenIndex); return [ "", { funcExprWithoutName: prefix + v, funcExprWithName: prefix + "__computed" + v, functionDeclarationName: undefined, isArrowFunction: false, }, ]; } const nameChunk = v.slice(0, openParenIndex); const funcName = utils.isLegalMemberName(nameChunk) ? utils.isLegalFunctionName(nameChunk) ? nameChunk : "/*" + nameChunk + "*/" : ""; const commentedName = utils.isLegalMemberName(nameChunk) ? "/*" + nameChunk + "*/" : ""; v = v.slice(openParenIndex).trimLeft(); return [ "", { funcExprWithoutName: prefix + commentedName + v, funcExprWithName: prefix + funcName + v, functionDeclarationName: isFunctionDeclaration ? nameChunk : undefined, isArrowFunction: false, }, ]; } function isComputed(v: string, openParenIndex: number) { if (openParenIndex === 0) { // node 8 and lower use no name at all for computed members. return true; } if (v.length > 0 && v.charAt(0) === "[") { // node 10 actually has the name as: [expr] return true; } return false; } function createSourceFile(serializedFunction: ParsedFunctionCode): [string, typescript.SourceFile | null] { const funcstr = serializedFunction.funcExprWithName || serializedFunction.funcExprWithoutName; // Wrap with parens to make into something parseable. This is necessary as many // types of functions are valid function expressions, but not valid function // declarations. i.e. "function () { }". This is not a valid function declaration // (it's missing a name). But it's totally legal as "(function () { })". const toParse = "(" + funcstr + ")"; const [file, firstDiagnostic] = tryCreateSourceFile(toParse); if (firstDiagnostic) { return [`the function could not be parsed: ${firstDiagnostic}`, null]; } return ["", file!]; } function tryCreateSourceFile(toParse: string): [typescript.SourceFile | undefined, string | undefined] { const file = ts.createSourceFile("", toParse, ts.ScriptTarget.Latest, /*setParentNodes:*/ true, ts.ScriptKind.TS); const diagnostics: typescript.Diagnostic[] = (<any>file).parseDiagnostics; if (diagnostics.length) { return [undefined, `${diagnostics[0].messageText}`]; } return [file, undefined]; } function computeUsesNonLexicalThis(file: typescript.SourceFile): boolean { let inTopmostFunction = false; let usesNonLexicalThis = false; ts.forEachChild(file, walk); return usesNonLexicalThis; function walk(node: typescript.Node | undefined) { if (!node) { return; } switch (node.kind) { case ts.SyntaxKind.SuperKeyword: case ts.SyntaxKind.ThisKeyword: usesNonLexicalThis = true; break; case ts.SyntaxKind.CallExpression: return visitCallExpression(<typescript.CallExpression>node); case ts.SyntaxKind.MethodDeclaration: case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.FunctionExpression: return visitBaseFunction(<typescript.FunctionLikeDeclarationBase>node); // Note: it is intentional that we ignore ArrowFunction. If we use 'this' inside of it, // then that should be considered a use of the non-lexical-this from an outer function. // i.e. // function f() { var v = () => console.log(this) } // // case ts.SyntaxKind.ArrowFunction: default: break; } ts.forEachChild(node, walk); } function visitBaseFunction(node: typescript.FunctionLikeDeclarationBase): void { if (inTopmostFunction) { // we're already in the topmost function. No need to descend into any // further functions. return; } // Entering the topmost function. inTopmostFunction = true; // Now, visit its body to see if we use 'this/super'. walk(node.body); inTopmostFunction = false; } function visitCallExpression(node: typescript.CallExpression) { // Most call expressions are normal. But we must special case one kind of function: // TypeScript's __awaiter functions. They are of the form `__awaiter(this, void 0, void 0, // function* (){})`, // The first 'this' argument is passed along in case the expression awaited uses 'this'. // However, doing that can be very bad for us as in many cases the 'this' just refers to the // surrounding module, and the awaited expression won't be using that 'this' at all. walk(node.expression); if (isAwaiterCall(node)) { const lastFunction = <typescript.FunctionExpression>node.arguments[3]; walk(lastFunction.body); return; } // For normal calls, just walk all arguments normally. for (const arg of node.arguments) { walk(arg); } } } /** * Computes the set of free variables in a given function string. Note that * this string is expected to be the usual V8-serialized function expression * text. */ function computeCapturedVariableNames(file: typescript.SourceFile): CapturedVariables { // Now that we've parsed the file, compute the free variables, and return them. let required: CapturedVariableMap = new Map(); let optional: CapturedVariableMap = new Map(); const scopes: Set<string>[] = []; let functionVars: Set<string> = new Set(); // Recurse through the tree. We use typescript's AST here and generally walk the entire // tree. One subtlety to be aware of is that we generally assume that when we hit an // identifier that it either introduces a new variable, or it lexically references a // variable. This clearly doesn't make sense for *all* identifiers. For example, if you // have "console.log" then "console" tries to lexically reference a variable, but "log" does // not. So, to avoid that being an issue, we carefully decide when to recurse. For // example, for member access expressions (i.e. A.B) we do not recurse down the right side. ts.forEachChild(file, walk); // Now just return all variables whose value is true. Filter out any that are part of the built-in // Node.js global object, however, since those are implicitly availble on the other side of serialization. const result: CapturedVariables = { required: new Map(), optional: new Map() }; for (const key of required.keys()) { if (!isBuiltIn(key)) { result.required.set(key, required.get(key)!.concat(optional.has(key) ? optional.get(key)! : [])); } } for (const key of optional.keys()) { if (!isBuiltIn(key) && !required.has(key)) { result.optional.set(key, optional.get(key)!); } } log.debug(`Found free variables: ${JSON.stringify(result)}`); return result; function isBuiltIn(ident: string): boolean { // __awaiter and __rest are never considered built-in. We do this as async/await code will generate // an __awaiter (so we will need it), but some libraries (like tslib) will add this to the 'global' // object. The same is true for __rest when destructuring. // If we think these are built-in, we won't serialize them, and the functions may not // actually be available if the import that caused it to get attached isn't included in the // final serialized code. if (ident === "__awaiter" || ident === "__rest") { return false; } // crypto is never considered a built-in. Starting with nodejs 19 // there is global.crypto builtin that references // `require("crypto").webcrypto`. If we treat crypto as a builtin, we // would never serialize a require expression for it, even if the user // shadowed global.crypto by required the node:crypto module under the // name `crypto`. // // If crypto was required as a module, createClosure will add a module // entry for it, and we will correctly serialize a require expression. // Otherwise we'll pick up the entry that was added in // addEntriesForWellKnownGlobalObjectsAsync and serialize it as // `global.crypto`. if (ident === "crypto") { return false; } // Anything in the global dictionary is a built-in. So is anything that's a global Node.js object; // note that these only exist in the scope of modules, and so are not truly global in the usual sense. // See https://nodejs.org/api/globals.html for more details. return global.hasOwnProperty(ident) || nodeModuleGlobals[ident]; } function currentScope(): Set<string> { return scopes[scopes.length - 1]; } function visitIdentifier(node: typescript.Identifier): void { // Remember undeclared identifiers during the walk, as they are possibly free. const name = node.text; for (let i = scopes.length - 1; i >= 0; i--) { if (scopes[i].has(name)) { // This is currently known in the scope chain, so do not add it as free. return; } } // We reached the top of the scope chain and this wasn't found; it's captured. const capturedPropertyChain = determineCapturedPropertyChain(node); if (node.parent!.kind === ts.SyntaxKind.TypeOfExpression) { // "typeof undeclared_id" is legal in JS (and is actually used in libraries). So keep // track that we would like to capture this variable, but mark that capture as optional // so we will not throw if we aren't able to find it in scope. optional.set(name, combineProperties(optional.get(name), capturedPropertyChain)); } else { required.set(name, combineProperties(required.get(name), capturedPropertyChain)); } } function walk(node: typescript.Node | undefined) { if (!node) { return; } switch (node.kind) { case ts.SyntaxKind.Identifier: return visitIdentifier(<typescript.Identifier>node); case ts.SyntaxKind.ThisKeyword: return visitThisExpression(<typescript.ThisExpression>node); case ts.SyntaxKind.Block: return visitBlockStatement(<typescript.Block>node); case ts.SyntaxKind.CallExpression: return visitCallExpression(<typescript.CallExpression>node); case ts.SyntaxKind.CatchClause: return visitCatchClause(<typescript.CatchClause>node); case ts.SyntaxKind.MethodDeclaration: case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.SetAccessor: return visitMethodDeclaration(<typescript.MethodDeclaration>node); case ts.SyntaxKind.MetaProperty: // don't walk down an es6 metaproperty (i.e. "new.target"). It doesn't // capture anything. return; case ts.SyntaxKind.PropertyAssignment: return visitPropertyAssignment(<typescript.PropertyAssignment>node); case ts.SyntaxKind.PropertyAccessExpression: return visitPropertyAccessExpression(<typescript.PropertyAccessExpression>node); case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.FunctionExpression: return visitFunctionDeclarationOrExpression(<typescript.FunctionDeclaration>node); case ts.SyntaxKind.ArrowFunction: return visitBaseFunction( <typescript.ArrowFunction>node, /*isArrowFunction:*/ true, /*name:*/ undefined, ); case ts.SyntaxKind.VariableDeclaration: return visitVariableDeclaration(<typescript.VariableDeclaration>node); default: break; } ts.forEachChild(node, walk); } function visitThisExpression(node: typescript.ThisExpression): void { required.set("this", combineProperties(required.get("this"), determineCapturedPropertyChain(node))); } function combineProperties( existing: CapturedPropertyChain[] | undefined, current: CapturedPropertyChain | undefined, ) { if (existing && existing.length === 0) { // We already want to capture everything. Keep things that way. return existing; } if (current === undefined) { // We want to capture everything. So ignore any properties we've filtered down // to and just capture them all. return []; } // We want to capture a specific set of properties. Add this set of properties // into the existing set. const combined = existing || []; combined.push(current); return combined; } // Finds nodes of the form `(...expr...).PropName` or `(...expr...)["PropName"]` // For element access expressions, the argument must be a string literal. function isPropertyOrElementAccessExpression( node: typescript.Node, ): node is typescript.PropertyAccessExpression | typescript.ElementAccessExpression { if (ts.isPropertyAccessExpression(node)) { return true; } if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) { return true; } return false; } function determineCapturedPropertyChain(node: typescript.Node): CapturedPropertyChain | undefined { let infos: CapturedPropertyInfo[] | undefined; // Walk up a sequence of property-access'es, recording the names we hit, until we hit // something that isn't a property-access. while (node?.parent && isPropertyOrElementAccessExpression(node.parent) && node.parent.expression === node) { if (!infos) { infos = []; } const propOrElementAccess = node.parent; const name = ts.isPropertyAccessExpression(propOrElementAccess) ? propOrElementAccess.name.text : (<typescript.StringLiteral>propOrElementAccess.argumentExpression).text; const invoked = propOrElementAccess.parent !== undefined && ts.isCallExpression(propOrElementAccess.parent) && propOrElementAccess.parent.expression === propOrElementAccess; // Keep track if this name was invoked. If so, we'll have to analyze it later // to see if it captured 'this' infos.push({ name, invoked }); node = propOrElementAccess; } if (infos) { // Invariant checking. if (infos.length === 0) { throw new Error("How did we end up with an empty list?"); } for (let i = 0; i < infos.length - 1; i++) { if (infos[i].invoked) { throw new Error("Only the last item in the dotted chain is allowed to be invoked."); } } return { infos }; } // For all other cases, capture everything. return undefined; } function visitBlockStatement(node: typescript.Block): void { // Push new scope, visit all block statements, and then restore the scope. scopes.push(new Set()); ts.forEachChild(node, walk); scopes.pop(); } function visitFunctionDeclarationOrExpression( node: typescript.FunctionDeclaration | typescript.FunctionExpression, ): void { // A function declaration is special in one way: its identifier is added to the current function's // var-style variables, so that its name is in scope no matter the order of surrounding references to it. if (node.name) { functionVars.add(node.name.text); } visitBaseFunction(node, /*isArrowFunction:*/ false, node.name); } function visitBaseFunction( node: typescript.FunctionLikeDeclarationBase, isArrowFunction: boolean, functionName: typescript.Identifier | undefined, ): void { // First, push new free vars list, scope, and function vars const savedRequired = required; const savedOptional = optional; const savedFunctionVars = functionVars; required = new Map(); optional = new Map(); functionVars = new Set(); scopes.push(new Set()); // If this is a named function, it's name is in scope at the top level of itself. if (functionName) { functionVars.add(functionName.text); } // this/arguments are in scope inside any non-arrow function. if (!isArrowFunction) { functionVars.add("this"); functionVars.add("arguments"); } // The parameters of any function are in scope at the top level of the function. for (const param of node.parameters) { nameWalk(param.name, /*isVar:*/ true); // Parse default argument expressions if (param.initializer) { walk(param.initializer); } } // Next, visit the body underneath this new context. walk(node.body); // Remove any function-scoped variables that we encountered during the walk. for (const v of functionVars) { required.delete(v); optional.delete(v); } // Restore the prior context and merge our free list with the previous one. scopes.pop(); mergeMaps(savedRequired, required); mergeMaps(savedOptional, optional); functionVars = savedFunctionVars; required = savedRequired; optional = savedOptional; } function mergeMaps(target: CapturedVariableMap, source: CapturedVariableMap) { for (const key of source.keys()) { const sourcePropInfos = source.get(key)!; let targetPropInfos = target.get(key)!; if (sourcePropInfos.length === 0) { // we want to capture everything. Make sure that's reflected in the target. targetPropInfos = []; } else { // we want to capture a subet of properties. merge that subset into whatever // subset we've recorded so far. for (const sourceInfo of sourcePropInfos) { targetPropInfos = combineProperties(targetPropInfos, sourceInfo); } } target.set(key, targetPropInfos); } } function visitCatchClause(node: typescript.CatchClause): void { scopes.push(new Set()); // Add the catch pattern to the scope as a variable. Note that it is scoped to our current // fresh scope (so it can't be seen by the rest of the function). if (node.variableDeclaration) { nameWalk(node.variableDeclaration.name, /*isVar:*/ false); } // And then visit the block without adding them as free variables. walk(node.block); // Relinquish the scope so the error patterns aren't available beyond the catch. scopes.pop(); } function visitCallExpression(node: typescript.CallExpression): void { // Most call expressions are normal. But we must special case one kind of function: // TypeScript's __awaiter functions. They are of the form `__awaiter(this, void 0, void 0, function* (){})`, // The first 'this' argument is passed along in case the expression awaited uses 'this'. // However, doing that can be very bad for us as in many cases the 'this' just refers to the // surrounding module, and the awaited expression won't be using that 'this' at all. // // However, there are cases where 'this' may be legitimately lexically used in the awaited // expression and should be captured properly. We'll figure this out by actually descending // explicitly into the "function*(){}" argument, asking it to be treated as if it was // actually a lambda and not a JS function (with the standard js 'this' semantics). By // doing this, if 'this' is used inside the function* we'll act as if it's a real lexical // capture so that we pass 'this' along. walk(node.expression); if (isAwaiterCall(node)) { return visitBaseFunction( <typescript.FunctionLikeDeclarationBase>(<typescript.FunctionExpression>node.arguments[3]), /*isArrowFunction*/ true, /*name*/ undefined, ); } // For normal calls, just walk all arguments normally. for (const arg of node.arguments) { walk(arg); } } function visitMethodDeclaration(node: typescript.MethodDeclaration): void { if (ts.isComputedPropertyName(node.name)) { // Don't walk down the 'name' part of the property assignment if it is an identifier. It // does not capture any variables. However, if it is a computed property name, walk it // as it may capture variables. walk(node.name); } // Always walk the method. Pass 'undefined' for the name as a method's name is not in scope // inside itself. visitBaseFunction(node, /*isArrowFunction:*/ false, /*name:*/ undefined); } function visitPropertyAssignment(node: typescript.PropertyAssignment): void { if (ts.isComputedPropertyName(node.name)) { // Don't walk down the 'name' part of the property assignment if it is an identifier. It // is not capturing any variables. However, if it is a computed property name, walk it // as it may capture variables. walk(node.name); } // Always walk the property initializer. walk(node.initializer); } function visitPropertyAccessExpression(node: typescript.PropertyAccessExpression): void { // Don't walk down the 'name' part of the property access. It could not capture a free variable. // i.e. if you have "A.B", we should analyze the "A" part and not the "B" part. walk(node.expression); } function nameWalk(n: typescript.BindingName | undefined, isVar: boolean): void { if (!n) { return; } switch (n.kind) { case ts.SyntaxKind.Identifier: return visitVariableDeclarationIdentifier(<typescript.Identifier>n, isVar); case ts.SyntaxKind.ObjectBindingPattern: case ts.SyntaxKind.ArrayBindingPattern: const bindingPattern = <typescript.BindingPattern>n; for (const element of bindingPattern.elements) { if (ts.isBindingElement(element)) { visitBindingElement(element, isVar); } } return; default: return; } } function visitVariableDeclaration(node: typescript.VariableDeclaration): void { // eslint-disable-next-line max-len const isLet = node.parent !== undefined && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Let) !== 0; // eslint-disable-next-line max-len const isConst = node.parent !== undefined && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0; const isVar = !isLet && !isConst; // Walk the declaration's `name` property (which may be an Identifier or Pattern) placing // any variables we encounter into the right scope. nameWalk(node.name, isVar); // Also walk into the variable initializer with the original walker to make sure we see any // captures on the right hand side. walk(node.initializer); } function visitVariableDeclarationIdentifier(node: typescript.Identifier, isVar: boolean): void { // If the declaration is an identifier, it isn't a free variable, for whatever scope it // pertains to (function-wide for var and scope-wide for let/const). Track it so we can // remove any subseqeunt references to that variable, so we know it isn't free. if (isVar) { functionVars.add(node.text); } else { currentScope().add(node.text); } } function visitBindingElement(node: typescript.BindingElement, isVar: boolean): void { // array and object patterns can be quite complex. You can have: // // var {t} = val; // lookup a property in 'val' called 't' and place into a variable 't'. // var {t: m} = val; // lookup a property in 'val' called 't' and place into a variable 'm'. // var {t: <pat>} = val; // lookup a property in 'val' called 't' and decompose further into the pattern. // // And, for all of the above, you can have: // // var {t = def} = val; // var {t: m = def} = val; // var {t: <pat> = def} = val; // // These are the same as the above, except that if there is no property 't' in 'val', // then the default value will be used. // // You can also have at the end of the literal: { ...rest} // Walk the name portion, looking for names to add. for // // var {t} // this will be 't'. // // for // // var {t: m} // this will be 'm' // // and for // // var {t: <pat>} // this will recurse into the pattern. // // and for // // ...rest // this will be 'rest' nameWalk(node.name, isVar); // if there is a default value, walk it as well, looking for captures. walk(node.initializer); // importantly, we do not walk into node.propertyName // This Name defines what property will be retrieved from the value being pattern // matched against. Importantly, it does not define a new name put into scope, // nor does it reference a variable in scope. } } function isAwaiterCall(node: typescript.CallExpression) { const result = ts.isIdentifier(node.expression) && node.expression.text === "__awaiter" && node.arguments.length === 4 && node.arguments[0].kind === ts.SyntaxKind.ThisKeyword && ts.isFunctionLike(node.arguments[3]); return result; }