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);
});
}