// 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. import * as ts from "typescript"; import * as log from "../../log"; import * as utils from "./utils"; /** @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; // Whether or not this 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. Both the name and whether or not the property was // invoked. /** @internal */ export interface CapturedPropertyInfo { name: string; invoked: boolean; } // Information about a chain of captured properties. i.e. if you have "foo.bar.baz.quux()", we'll // say that 'foo' was captured, but that "[bar, baz, quux]" was accessed off of 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)] }. i.e. 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; // 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, 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, 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 = 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]; } function parseFunctionCode(funcString: string): [string, ParsedFunctionCode] { if (funcString.startsWith("[Function:")) { return [`the function form was not understood.`, undefined]; } // 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]"; if (funcString.indexOf(nativeCodeString) !== -1) { return [`it was a native code function.`, 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}`, undefined]; } const classDecl = file!.statements.find(x => ts.isClassDeclaration(x)); if (!classDecl) { return [`the class form was not understood:\n${funcString}`, undefined]; } const constructor = classDecl.members.find(m => ts.isConstructorDeclaration(m)); if (!constructor) { // class without explicit constructor. const isSubClass = classDecl.heritageClauses && 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.substr("async".length).trimLeft(); } if (funcString.startsWith("function get ") || funcString.startsWith("function set ")) { const trimmed = funcString.substr("function get".length); return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false); } if (funcString.startsWith("get ") || funcString.startsWith("set ")) { const trimmed = funcString.substr("get ".length); return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false); } if (funcString.startsWith("function")) { const trimmed = funcString.substr("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.substr(1).trimLeft(); prefix = "function* "; } const openParenIndex = v.indexOf("("); if (openParenIndex < 0) { return [`the function form was not understood.`, undefined]; } if (isComputed(v, openParenIndex)) { v = v.substr(openParenIndex); return ["", { funcExprWithoutName: prefix + v, funcExprWithName: prefix + "__computed" + v, functionDeclarationName: undefined, isArrowFunction: false, }]; } const nameChunk = v.substr(0, openParenIndex); const funcName = utils.isLegalMemberName(nameChunk) ? utils.isLegalFunctionName(nameChunk) ? nameChunk : "/*" + nameChunk + "*/" : ""; const commentedName = utils.isLegalMemberName(nameChunk) ? "/*" + nameChunk + "*/" : ""; v = v.substr(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, ts.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): [ts.SourceFile | undefined, string | undefined] { const file = ts.createSourceFile( "", toParse, ts.ScriptTarget.Latest, /*setParentNodes:*/ true, ts.ScriptKind.TS); const diagnostics: ts.Diagnostic[] = (file).parseDiagnostics; if (diagnostics.length) { return [undefined, `${diagnostics[0].messageText}`]; } return [file, undefined]; } function computeUsesNonLexicalThis(file: ts.SourceFile): boolean { let inTopmostFunction = false; let usesNonLexicalThis = false; ts.forEachChild(file, walk); return usesNonLexicalThis; function walk(node: ts.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(node); case ts.SyntaxKind.MethodDeclaration: case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.FunctionExpression: return visitBaseFunction(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: ts.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: ts.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 = node.arguments[3]; walk(lastFunction.body); return; } // For normal calls, just walk all arguments normally. for (const arg of node.arguments) { walk(arg); } } } /** * computeCapturedVariableNames 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: ts.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[] = []; let functionVars: Set = 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 { // The __awaiter is never considered built-in. We do this as async/await code will generate // this (so we will need it), but some libraries (like tslib) will add this to the 'global' // object. If we think this is built-in, we won't serialize it, and the function 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") { 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 { return scopes[scopes.length - 1]; } function visitIdentifier(node: ts.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: ts.Node | undefined) { if (!node) { return; } switch (node.kind) { case ts.SyntaxKind.Identifier: return visitIdentifier(node); case ts.SyntaxKind.ThisKeyword: return visitThisExpression(node); case ts.SyntaxKind.Block: return visitBlockStatement(node); case ts.SyntaxKind.CallExpression: return visitCallExpression(node); case ts.SyntaxKind.CatchClause: return visitCatchClause(node); case ts.SyntaxKind.MethodDeclaration: return visitMethodDeclaration(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(node); case ts.SyntaxKind.PropertyAccessExpression: return visitPropertyAccessExpression(node); case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.FunctionExpression: return visitFunctionDeclarationOrExpression(node); case ts.SyntaxKind.ArrowFunction: return visitBaseFunction(node, /*isArrowFunction:*/true, /*name:*/ undefined); case ts.SyntaxKind.VariableDeclaration: return visitVariableDeclaration(node); default: break; } ts.forEachChild(node, walk); } function visitThisExpression(node: ts.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: ts.Node): node is (ts.PropertyAccessExpression | ts.ElementAccessExpression) { if (ts.isPropertyAccessExpression(node)) { return true; } if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) { return true; } return false; } function determineCapturedPropertyChain(node: ts.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 && node.parent && isPropertyOrElementAccessExpression(node.parent) && node.parent.expression === node) { if (!infos) { infos = []; } const propOrElementAccess = node.parent; const name = ts.isPropertyAccessExpression(propOrElementAccess) ? propOrElementAccess.name.text : (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: ts.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: ts.FunctionDeclaration | ts.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: ts.FunctionLikeDeclarationBase, isArrowFunction: boolean, functionName: ts.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); } // 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: ts.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: ts.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( 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: ts.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: ts.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: ts.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: ts.BindingName | undefined, isVar: boolean): void { if (!n) { return; } switch (n.kind) { case ts.SyntaxKind.Identifier: return visitVariableDeclarationIdentifier(n, isVar); case ts.SyntaxKind.ObjectBindingPattern: case ts.SyntaxKind.ArrayBindingPattern: const bindingPattern = n; for (const element of bindingPattern.elements) { if (ts.isBindingElement(element)) { visitBindingElement(element, isVar); } } return; default: return; } } function visitVariableDeclaration(node: ts.VariableDeclaration): void { // tslint:disable-next-line:max-line-length const isLet = node.parent !== undefined && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Let) !== 0; // tslint:disable-next-line:max-line-length 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: ts.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: ts.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: } = 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: = 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: } // 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: ts.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; }