private static walkAST()

in Clients/AmbrosiaJS/Ambrosia-Node/src/Meta.ts [3927:4200]


    private static walkAST(nodeToWalk: TS.Node, haltOnError: boolean = true): void
    {
        let nodes: TS.Node[] = nodeToWalk.getChildren();
        
        nodes.forEach(node =>
        {
            try
            {
                const isStatic: boolean = (node.modifiers !== undefined) && (node.modifiers.filter(m => m.kind === TS.SyntaxKind.StaticKeyword).length === 1);
                const isPrivate: boolean = (node.modifiers !== undefined) && (node.modifiers.filter(m => m.kind === TS.SyntaxKind.PrivateKeyword).length === 1);
                let isExported: boolean = (node.modifiers !== undefined) && (node.modifiers.filter(m => m.kind === TS.SyntaxKind.ExportKeyword).length === 1);
                const isStaticMethod: boolean = isStatic && (node.kind === TS.SyntaxKind.MethodDeclaration);
                const isNestedFunction: boolean = (node.kind === TS.SyntaxKind.FunctionDeclaration) && (AST._functionEndPos > 0);
                let isWellKnownFunction: boolean = false;

                // Check for an @ambrosia tag on unsupported nodes
                if (AST._targetNodeKinds.indexOf(node.kind) === -1)
                {
                    const attrs: AmbrosiaAttrs = AST.getAmbrosiaAttrs(node);
                    if (attrs["hasAmbrosiaTag"] === true)
                    {
                        const targetNames: string = (this._targetNodeKinds
                            .slice(0, this._targetNodeKinds.length - 1)
                            .map(kind => AST.getNodeKindName(kind))
                            .join(", ") + ", and " + AST.getNodeKindName(this._targetNodeKinds[this._targetNodeKinds.length - 1]))
                            .replace("method", "static method");
                        throw new Error(`The ${CODEGEN_TAG_NAME} tag is not valid on a ${AST.getNodeKindName(node.kind)} (at ${attrs["location"]}); valid targets are: ${targetNames}`);
                    }
                }
                // Checks for an @ambrosia tag on a method
                if (node.kind === TS.SyntaxKind.MethodDeclaration)
                {
                    const attrs: AmbrosiaAttrs = AST.getAmbrosiaAttrs(node);
                    if (attrs["hasAmbrosiaTag"] === true)
                    {
                        // Check for an @ambrosia tag on a non-static method
                        if (!isStatic)
                        {
                            throw new Error(`The ${CODEGEN_TAG_NAME} tag is not valid on a non-static method (at ${attrs["location"]})`);
                        }
                        // A static method has to directly belong to an exported class: it cannot belong to a class expression [because there is no way to reference the method]
                        if (TS.isClassExpression(node.parent))
                        {
                            throw new Error(`The ${CODEGEN_TAG_NAME} tag is not valid on a static method of a class expression (at ${attrs["location"]})`);
                        }
                        // Check for an @ambrosia tag on a private static method
                        if (isPrivate && isStatic) 
                        {
                            throw new Error(`The ${CODEGEN_TAG_NAME} tag is not valid on a private static method (at ${attrs["location"]})`);
                        }
                    }
                }
                // Check for an @ambrosia tag on a local function
                if (isNestedFunction)
                {
                    const attrs: AmbrosiaAttrs = AST.getAmbrosiaAttrs(node);
                    if (attrs["hasAmbrosiaTag"] === true)
                    {
                        throw new Error(`The ${CODEGEN_TAG_NAME} tag is not valid on a local function (at ${attrs["location"]})`);
                    }
                }

                // Look for the first exported variable whose type is a class that extends AmbrosiaAppState
                // TODO: This is brittle, since the target variable may not be the first matching variable we find (it may be declared later in the input file).
                if ((AST._appStateVar === "") && (node.kind === TS.SyntaxKind.VariableDeclaration))
                {
                    const result: { varName: string, varType: TS.Type } | null = AST.getVarOfBaseType(node as TS.VariableDeclaration, "AmbrosiaAppState", "AmbrosiaRoot.ts");
                    if (result)
                    {
                        // Note: The app state variable may no longer be in the same namespace as the AppState class (as they were in the generated code). So we must explicitly
                        //       determine the namespace path for the type (class) of the app state variable rather than assuming it's the same as AST.getNamespacePath(node).
                        const varClassSymbol: TS.Symbol = result.varType.symbol;
                        AST._appStateVar = AST.getNamespacePath(node) ? (AST.getNamespacePath(node) + "." + result.varName) : result.varName;
                        AST._appStateVarClassName = AST.getNamespacePathOfSymbol(varClassSymbol) ? (AST.getNamespacePathOfSymbol(varClassSymbol) + "." + varClassSymbol.name) : varClassSymbol.name;
                        // Utils.log(`DEBUG: Exported variable ${AST._appStateVar} (of type ${AST._appStateVarClassName}) extends AmbrosiaAppState`);
                    }
                }

                // Keep track of JSDoc comments for namespaces/classes [although we won't need all of these since not all namespaces/classes contain published entities]
                if ((node.kind === TS.SyntaxKind.ModuleDeclaration) || (node.kind === TS.SyntaxKind.ClassDeclaration))
                {
                    const jsDocComment: TS.JSDoc | null = AST.getJSDocComment(node);
                    if (jsDocComment)
                    {
                        const namespaceName: string = node.getChildren().filter(c => TS.isIdentifier(c))[0].getText();
                        const namespacePath: string = AST.getNamespacePath(node);
                        const pathedNamespace: string = namespacePath ? `${namespacePath}.${namespaceName}` : namespaceName;
                        AST._namespaceJSDocComments[pathedNamespace] = jsDocComment.getText();
                    }
                }

                // Keep track of entering/leaving namespaces and classes [so that we can track the "path" to published entities]
                if ((node.kind === TS.SyntaxKind.ModuleDeclaration) || (node.kind === TS.SyntaxKind.ClassDeclaration))
                {
                    const moduleOrClassDecl: TS.ModuleDeclaration = (node as TS.ModuleDeclaration);
                    const namespaceName: string = moduleOrClassDecl.name.getText();
                    const namespaceEndPos: number = moduleOrClassDecl.getStart() + moduleOrClassDecl.getWidth() - 1;
                    AST._namespaces.push(`${namespaceName}:${namespaceEndPos}`);
                    AST._currentNamespaceEndPos = namespaceEndPos;
                    Utils.log(`Entering namespace '${namespaceName}' (now in '${AST.getCurrentNamespacePath()}') at ${AST.getLocation(node.getStart())}`, null, Utils.LoggingLevel.Debug);
                }
                if ((node.getStart() >= AST._currentNamespaceEndPos) && (AST._namespaces.length > 0))
                {
                    const leavingNamespaceName: string = assertDefined(AST._namespaces.pop()).split(":")[0];
                    const enteringNamespaceEndPos = (AST._namespaces.length > 0) ? parseInt(AST._namespaces[AST._namespaces.length - 1].split(":")[1]) : AST._sourceFile.getWidth();
                    AST._currentNamespaceEndPos = enteringNamespaceEndPos;
                    Utils.log(`Leaving namespace '${leavingNamespaceName}' (now in '${AST.getCurrentNamespacePath() || "[Root]"}') at ${AST.getLocation(node.getStart())}`, null, Utils.LoggingLevel.Debug);
                }

                // Keep track of entering/leaving a function (or static method) so that we can we can detect nested (local) functions (which are never candidates to be published)
                if (((node.kind === TS.SyntaxKind.FunctionDeclaration) || isStaticMethod) && (AST._functionEndPos === 0))
                {
                    const functionDecl: TS.FunctionDeclaration = (node as TS.FunctionDeclaration);
                    AST._functionEndPos = functionDecl.getStart() + functionDecl.getWidth() - 1;
                    Utils.log(`Entering ${isStaticMethod ? "static method" : "function"} '${functionDecl.name?.getText() || "N/A"}' at ${AST.getLocation(node.getStart())}`, null, Utils.LoggingLevel.Debug);
                }
                if ((AST._functionEndPos > 0) && (node.getStart() >= AST._functionEndPos))
                {
                    Utils.log(`Leaving function (or static method) at ${AST.getLocation(node.getStart())}`, null, Utils.LoggingLevel.Debug);
                    AST._functionEndPos = 0;
                }

                // Keep track of whether we have found any of the "well known" Ambrosia AppEvent handlers
                if (node.kind === TS.SyntaxKind.FunctionDeclaration)
                {
                    const functionDecl: TS.FunctionDeclaration = node as TS.FunctionDeclaration;
                    const isAsync: boolean = node.modifiers ? (node.modifiers.filter(m => m.kind === TS.SyntaxKind.AsyncKeyword).length === 1) : false;
                    const fnName: string = functionDecl.name?.getText() || "N/A";
                    const location: string = AST.getLocation(node.getStart());

                    if (isExported && (Object.keys(_appEventHandlerFunctions).indexOf(fnName) !== -1))
                    {
                        const fnDetails: AppEventHandlerFunctionDetails = _appEventHandlerFunctions[fnName];
                        const ambrosiaAttrs: AmbrosiaAttrs = AST.getAmbrosiaAttrs(functionDecl, AST._supportedAttrs[functionDecl.kind]);

                        if (ambrosiaAttrs["hasAmbrosiaTag"])
                        {
                            throw new Error(`The ${CODEGEN_TAG_NAME} tag is not valid on an AppEvent handler ('${fnName}') at ${ambrosiaAttrs["location"]}`);
                        }

                        if (isAsync)
                        {
                            throw new Error(`The AppEvent handler '${fnName}' (at ${location}) cannot be async`);
                        }

                        if (fnDetails.foundInInputSource)
                        {
                            throw new Error(`The AppEvent handler '${fnName}' (at ${location}) has already been defined (at ${fnDetails.location})`);
                        }
                        else
                        {
                            const parameters: string = functionDecl.parameters.map(p => p.getText()).join(", ");
                            const expectedParameters: string = fnDetails.expectedParameters;
                            const returnType: string = functionDecl.type?.getText() || "void";
                            const expectedReturnType: string = fnDetails.expectedReturnType;

                            if (parameters.replace(/\s*/, "") !== expectedParameters.replace(/\s*/, ""))
                            {
                                Utils.log(`Warning: Skipping Ambrosia AppEvent handler function '${fnName}' (at ${location}) because it has different parameters (${parameters.replace(/\s/g, "").replace(":", ": ")}) than expected (${expectedParameters})`);
                            }
                            else
                            {
                                if (returnType.replace(/\s*/, "") !== expectedReturnType.replace(/\s*/, ""))
                                {
                                    Utils.log(`Warning: Skipping Ambrosia AppEvent handler function '${fnName}' (at ${location}) because it has a different return type (${returnType}) than expected (${expectedReturnType})`);
                                }
                                else
                                {
                                    fnDetails.foundInInputSource = true;
                                    fnDetails.nsPath = AST.getNamespacePath(functionDecl);
                                    fnDetails.location = location;
                                }
                            }
                            isWellKnownFunction = true;
                        }
                    }
                }
                
                // Publish functions/types/enums and static methods marked with an @ambrosia JSDoc tag, and which are exported
                if (!isWellKnownFunction && (AST._targetNodeKinds.indexOf(node.kind) >= 0))
                {
                    let location: string = AST.getLocation(node.getStart());
                    let entityName: string = (node as TS.DeclarationStatement).name?.getText() || "N/A";
                    let nodeName: string = `${isStatic ? "static " : ""}${AST.getNodeKindName(node.kind)} '${entityName}'`;
                    let skipSilently: boolean = ((node.kind === TS.SyntaxKind.MethodDeclaration) && !isStatic) || isNestedFunction || isPrivate;

                    if (!skipSilently)
                    {
                        // Static methods are not explicitly exported, rather they have to [directly] belong to an exported class
                        if (!isExported && (node.kind === TS.SyntaxKind.MethodDeclaration))
                        {
                            if (TS.isClassDeclaration(node.parent) && node.parent.modifiers && (node.parent.modifiers.filter(m => m.kind === TS.SyntaxKind.ExportKeyword).length === 1))
                            {
                                isExported = true;
                            }
                        }

                        if (AST._supportedAttrs[node.kind] === undefined)
                        {
                            throw new Error(`Internal error: No _supportedAttrs defined for a ${TS.SyntaxKind[node.kind]}`);
                        }
                        let ambrosiaAttrs: AmbrosiaAttrs = AST.getAmbrosiaAttrs(node, AST._supportedAttrs[node.kind]);
                        let hasAmbrosiaTag: boolean = ambrosiaAttrs["hasAmbrosiaTag"] as boolean;
                        let isPublished: boolean = ambrosiaAttrs["publish"] as boolean;

                        if (hasAmbrosiaTag)
                        {
                            if (isPublished)
                            {
                                if (isExported)
                                {
                                    let publishedEntity: string = "";
                                    switch (node.kind)
                                    {
                                        case TS.SyntaxKind.FunctionDeclaration:
                                            publishedEntity = AST.publishFunction(node as TS.FunctionDeclaration, nodeName, location, ambrosiaAttrs);
                                            break;
                                        case TS.SyntaxKind.MethodDeclaration: // Note: Static methods only
                                            publishedEntity = AST.publishFunction(node as TS.MethodDeclaration, nodeName, location, ambrosiaAttrs);
                                            break;
                                        case TS.SyntaxKind.TypeAliasDeclaration:
                                            publishedEntity = AST.publishTypeAlias(node as TS.TypeAliasDeclaration, nodeName, location, ambrosiaAttrs);
                                            break;
                                        case TS.SyntaxKind.EnumDeclaration:
                                            publishedEntity = AST.publishEnum(node as TS.EnumDeclaration, nodeName, location, ambrosiaAttrs);
                                            // To check the result:
                                            // Utils.log(getPublishedType(entityName).makeTSType());
                                            break;
                                        default:
                                            throw new Error(`Unsupported AST node type '${nodeName}'`);
                                    }
                                    Utils.log(`Successfully published ${nodeName} as a ${publishedEntity}`);
                                    AST._publishedEntityCount++;
                                }
                                else
                                {
                                    Utils.log(`Warning: Skipping ${nodeName} at ${location} because it is not exported`);
                                }
                            }
                            else
                            {
                                Utils.log(`Warning: Skipping ${nodeName} at ${location} because its ${CODEGEN_TAG_NAME} 'publish' attribute is missing or 'false'`);
                            }
                        }
                        else
                        {
                            // We don't support the @ambrosia tag on the declaration of an overloaded function, so it's expected to be missing in this case,
                            // therefore we don't report the warning. However, this will also be a common "mistake", so publishFunction() checks for this too.
                            let isOverloadFunctionDeclaration: boolean = (node.kind === TS.SyntaxKind.FunctionDeclaration) && ((node as TS.FunctionDeclaration).body === undefined);
                            if (!isOverloadFunctionDeclaration) 
                            {
                                Utils.log(`Warning: Skipping ${nodeName} at ${location} because it has no ${CODEGEN_TAG_NAME} JSDoc tag`);
                            }
                        }
                    }
                }
            }
            catch (error: unknown)
            {
                if (haltOnError)
                {
                    // Halt the walk
                    throw error;
                }
                else
                {
                    // Log the error and continue [even though continuing is potentially unsafe]
                    Utils.log(`Error: ${Utils.makeError(error).message}`);
                    AST._ignoredErrorCount++;
                }
            }
            this.walkAST(node, haltOnError);
        });
    }