UNPKG

@azure-tools/typespec-apiview

Version:

Library for emitting APIView token files from TypeSpec

1,223 lines (1,222 loc) 50 kB
import { getNamespaceFullName, getSourceLocation, navigateProgram } from "@typespec/compiler"; import { SyntaxKind } from "@typespec/compiler/ast"; import { generateId, NamespaceModel } from "./namespace-model.js"; import { LIB_VERSION } from "./version.js"; import { NavigationItem, TokenKind } from "./schemas.js"; import { NamespaceStack, reviewLineText } from "./util.js"; export class ApiView { constructor(name, packageName, includeGlobalNamespace) { this.reviewLines = []; this.navigationItems = []; this.diagnostics = []; this.namespaceStack = new NamespaceStack(); this.typeDeclarations = new Set(); this.name = name; this.packageName = packageName; this.packageVersion = "ALL"; this.includeGlobalNamespace = includeGlobalNamespace !== null && includeGlobalNamespace !== void 0 ? includeGlobalNamespace : false; this.crossLanguagePackageId = packageName; this.currentLine = { LineId: "", CrossLanguageId: "", Tokens: [], Children: [], }; this.parentStack = []; this.currentParent = undefined; this.emitHeader(); } /** Compiles the APIView model for output. */ compile(program) { let allNamespaces = new Map(); // collect namespaces in program navigateProgram(program, { namespace(obj) { const name = getNamespaceFullName(obj); allNamespaces.set(name, obj); }, }); allNamespaces = new Map([...allNamespaces].sort()); for (const [name, ns] of allNamespaces.entries()) { if (!this.shouldEmitNamespace(name)) { continue; } // use a fake name to make the global namespace clear const namespaceName = name === "" ? "::GLOBAL::" : name; const nsModel = new NamespaceModel(namespaceName, ns, program); if (nsModel.shouldEmit()) { this.tokenizeNamespaceModel(nsModel); this.buildNavigation(nsModel); } } // Enable this if desired to debug the output //this.appendDebugInfo(); } appendDebugInfo() { function processLines(lines) { for (const line of lines) { const lineId = line.LineId; const relatedLineId = line.RelatedToLine; const isContextEnd = line.IsContextEndLine; let string = ""; if (lineId && lineId !== "") { string += ` LINE_ID: ${lineId} `; } if (relatedLineId && relatedLineId !== "") { string += ` RELATED_TO: ${relatedLineId} `; } if (isContextEnd) { string += " IS_CONTEXT_END TRUE "; } if (string !== "") { line.Tokens.push({ Kind: TokenKind.Comment, Value: `// ${string.trim()}`, }); } processLines(line.Children); } } processLines(this.reviewLines); } /** Attempts to resolve any type references marked as __MISSING__. */ resolveMissingTypeReferences() { for (const token of this.currentLine.Tokens) { if (token.Kind === TokenKind.TypeName && token.NavigateToId === "__MISSING__") { token.NavigateToId = this.definitionIdFor(token.Value, this.packageName); } } } /** Apply workarounds to the model before output */ adjustLines(lines) { let currentContext = undefined; let contextMatchFound = false; for (const line of lines) { // run the normal adjust line logic this.adjustLine(line); const lineId = line.LineId; if (lineId === "Azure.Test.ConstrainedComplex") { let test = "best"; } const relatedTo = line.RelatedToLine; const isContextEnd = line.IsContextEndLine; if (isContextEnd && relatedTo) { throw new Error("Context end line should not have a relatedTo line."); } if (currentContext) { if (lineId === currentContext) { contextMatchFound = true; line.RelatedToLine = undefined; line.IsContextEndLine = false; } if (relatedTo && currentContext !== relatedTo) { if (!contextMatchFound) { // catches the scenario where the relatedTo line is different within what should be the current context throw new Error("Mismatched contexts. Expected ${currentContext}, got ${lineId}"); } else { // covers the instance where there never is an IsContextEndLine set, which happens if there's no closing brace // on a separate line currentContext = relatedTo; } } else { // key to this whole method. This copies RelatedToLine to all lines between the start and end of a context line.RelatedToLine = currentContext; } if (isContextEnd) { currentContext = undefined; contextMatchFound = false; line.RelatedToLine = undefined; } } else { // if currentContext isn't set and we encounter a relatedTo line, set the current context if (relatedTo) { currentContext = relatedTo; // if currentContext isn't set but there's a lineId with childrent, set the current context } else if (lineId && line.Children.length > 0) { currentContext = lineId; contextMatchFound = true; // If a context end is found without a start, then ignore the contextEnd } else if (isContextEnd) { line.IsContextEndLine = false; } } } } adjustLine(line) { for (const token of line.Tokens) { this.adjustToken(token); } this.adjustLines(line.Children); } adjustToken(token) { var _a; // the server has a bizarre "true" default for HasSuffixSpace that we // need to account for. Also, we can delete the property if it's true // since that's the server default. token.HasSuffixSpace = (_a = token.HasSuffixSpace) !== null && _a !== void 0 ? _a : false; if (token.HasSuffixSpace) { delete token.HasSuffixSpace; } } /** Output the APIView model to the CodeFile JSON format. */ asCodeFile() { this.adjustLines(this.reviewLines); return { Name: this.name, PackageName: this.packageName, PackageVersion: this.packageVersion, ParserVersion: LIB_VERSION, Language: "TypeSpec", LanguageVariant: undefined, CrossLanguagePackageId: this.crossLanguagePackageId, ReviewLines: this.reviewLines, Diagnostics: this.diagnostics, Navigation: this.navigationItems, }; } /** Outputs the APIView model to a string approximation of what will display on the web. */ asString() { return this.reviewLines.map(l => reviewLineText(l, 0)).join("\n"); } token(kind, value, options) { this.currentLine.Tokens.push({ Kind: kind, Value: value, ...options, }); } indent() { // ensure no trailing space at the end of the line try { const lastToken = this.currentLine.Tokens[this.currentLine.Tokens.length - 1]; lastToken.HasSuffixSpace = false; } catch (e) { // no tokens, so nothing to do return; } if (this.currentParent) { this.currentParent.Children.push(this.currentLine); this.parentStack.push(this.currentParent); } else { this.reviewLines.push(this.currentLine); } this.currentParent = this.currentLine; this.currentLine = { LineId: "", CrossLanguageId: "", Tokens: [], Children: [], }; } deindent() { if (!this.currentParent) { return; } // Ensure that the last line before the deindent has no blank lines const lastChild = this.currentParent.Children.pop(); if (lastChild && lastChild.Tokens.length > 0) { this.currentParent.Children.push(lastChild); } this.currentParent = this.parentStack.pop(); this.currentLine = { LineId: "", CrossLanguageId: "", Tokens: [], Children: [], }; } /** Set the exact number of desired newlines. */ blankLines(count = 0) { this.newline(); const parentLines = this.currentParent ? this.currentParent.Children : this.reviewLines; let newlineCount = 0; if (parentLines.length) { for (let i = parentLines.length - 1; i >= 0; i--) { if (parentLines[i].Tokens.length === 0) { newlineCount++; } else { break; } } } if (newlineCount == count) { return; } else if (newlineCount > count) { const toRemove = newlineCount - count; for (let i = 0; i < toRemove; i++) { parentLines.pop(); } } else { for (let i = newlineCount; i < count; i++) { this.newline(); } } } newline() { // ensure no trailing space at the end of the line if (this.currentLine.Tokens.length > 0) { const lastToken = this.currentLine.Tokens[this.currentLine.Tokens.length - 1]; lastToken.HasSuffixSpace = false; const firstToken = this.currentLine.Tokens[0]; firstToken.HasPrefixSpace = false; } if (this.currentParent) { this.currentParent.Children.push(this.currentLine); } else { this.reviewLines.push(this.currentLine); } this.currentLine = { LineId: "", CrossLanguageId: "", Tokens: [], Children: [], }; } getLastLine() { if (!this.currentParent) { return undefined; } const lastChild = this.currentParent.Children[this.currentParent.Children.length - 1]; const lastGrandchild = lastChild === null || lastChild === void 0 ? void 0 : lastChild.Children[lastChild.Children.length - 1]; if ((lastGrandchild === null || lastGrandchild === void 0 ? void 0 : lastGrandchild.Children.length) > 0) { throw new Error("Unexpected great-grandchild in getLastLine()!"); } return lastGrandchild !== null && lastGrandchild !== void 0 ? lastGrandchild : lastChild; } /** * Places the provided token in the tree based on the provided characters. * param token The token to snap. * param characters The characters to snap to. */ snapToken(punctuationToken, characters) { var _a; const allowed = new Set(characters.split("")); const lastLine = (_a = this.getLastLine()) !== null && _a !== void 0 ? _a : this.currentLine; // iterate through tokens in reverse order for (let i = lastLine.Tokens.length - 1; i >= 0; i--) { const token = lastLine.Tokens[i]; if (token.Kind === TokenKind.Text) { // skip blank whitespace tokens const value = token.Value.trim(); if (value.length === 0) { continue; } else { // no snapping, so render in place this.currentLine.Tokens.push(punctuationToken); return; } } else if (token.Kind === TokenKind.Punctuation) { // ensure no whitespace after the trim character if (allowed.has(token.Value)) { token.HasSuffixSpace = false; punctuationToken.HasSuffixSpace = false; lastLine.Tokens.push(punctuationToken); } else { // no snapping, so render in place this.currentLine.Tokens.push(punctuationToken); return; } } else { // no snapping, so render in place this.currentLine.Tokens.push(punctuationToken); return; } } } lineMarker(options) { var _a, _b; this.currentLine.LineId = (_a = options === null || options === void 0 ? void 0 : options.value) !== null && _a !== void 0 ? _a : this.namespaceStack.value(); this.currentLine.CrossLanguageId = (options === null || options === void 0 ? void 0 : options.addCrossLanguageId) ? ((_b = options === null || options === void 0 ? void 0 : options.value) !== null && _b !== void 0 ? _b : this.namespaceStack.value()) : undefined; this.currentLine.RelatedToLine = options === null || options === void 0 ? void 0 : options.relatedLineId; } punctuation(value, options) { const snapTo = options === null || options === void 0 ? void 0 : options.snapTo; options === null || options === void 0 ? true : delete options.snapTo; const isContextEndLine = options === null || options === void 0 ? void 0 : options.isContextEndLine; options === null || options === void 0 ? true : delete options.isContextEndLine; const token = { Kind: TokenKind.Punctuation, Value: value, ...options, }; if (snapTo) { this.snapToken(token, snapTo); } else { this.currentLine.Tokens.push(token); } if (isContextEndLine) { this.currentLine.IsContextEndLine = true; } } text(text, options) { this.token(TokenKind.Text, text, options); } keyword(keyword, options) { this.token(TokenKind.Keyword, keyword, options); } typeDeclaration(typeName, typeId, addCrossLanguageId, options) { if (typeId) { if (this.typeDeclarations.has(typeId)) { throw new Error(`Duplication ID "${typeId}" for declaration will result in bugs.`); } this.typeDeclarations.add(typeId); } this.lineMarker({ value: typeId, addCrossLanguageId: true }); this.token(TokenKind.TypeName, typeName, options); } typeReference(typeName, options) { var _a; options = options !== null && options !== void 0 ? options : {}; options.NavigateToId = (_a = options.NavigateToId) !== null && _a !== void 0 ? _a : "__MISSING__"; this.token(TokenKind.TypeName, typeName, { ...options }); } member(name, options) { this.token(TokenKind.MemberName, name, options); } stringLiteral(value, options) { const lines = value.split("\n"); if (lines.length === 1) { this.currentLine.Tokens.push({ Kind: TokenKind.StringLiteral, Value: `\u0022${value}\u0022`, ...options }); } else { this.punctuation(`"""`, options); this.newline(); for (const line of lines) { this.literal(line, options); this.newline(); } this.punctuation(`"""`, options); } } literal(value, options) { this.token(TokenKind.StringLiteral, value, options); } diagnostic(message, targetId, level) { this.diagnostics.push({ Text: message, TargetId: targetId, Level: level, }); } shouldEmitNamespace(name) { if (name === "" && this.includeGlobalNamespace) { return true; } if (name === this.packageName) { return true; } if (!name.startsWith(this.packageName)) { return false; } const suffix = name.substring(this.packageName.length); return suffix.startsWith("."); } emitHeader() { const toolVersion = LIB_VERSION; const headerText = `// Package parsed using @azure-tools/typespec-apiview (version:${toolVersion})`; this.literal(headerText, { SkipDiff: true }); this.namespaceStack.push("GLOBAL"); this.lineMarker(); this.namespaceStack.pop(); // TODO: Source URL? this.blankLines(2); } tokenize(node) { let obj; let last = 0; // track the final index of an array let parentNamespace; switch (node.kind) { case SyntaxKind.AliasStatement: obj = node; this.namespaceStack.push(obj.id.sv); this.keyword("alias", { HasSuffixSpace: true }); this.typeDeclaration(obj.id.sv, this.namespaceStack.value(), true); this.tokenizeTemplateParameters(obj.templateParameters); this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.value); this.namespaceStack.pop(); break; case SyntaxKind.ArrayExpression: obj = node; this.tokenize(obj.elementType); this.punctuation("[]"); break; case SyntaxKind.ArrayLiteral: obj = node; this.punctuation("#["); last = obj.values.length - 1; obj.values.forEach((val, i) => { this.tokenize(val); if (i !== last) { this.punctuation(",", { HasSuffixSpace: true }); } }); this.punctuation("]"); break; case SyntaxKind.AugmentDecoratorStatement: obj = node; const decoratorName = this.getNameForNode(obj.target); this.namespaceStack.push(decoratorName); this.punctuation("@@"); this.tokenizeIdentifier(obj.target, "keyword"); this.lineMarker(); if (obj.arguments.length) { const last = obj.arguments.length - 1; this.punctuation("("); this.tokenize(obj.targetType); if (obj.arguments.length) { this.punctuation(",", { HasSuffixSpace: true }); } for (let x = 0; x < obj.arguments.length; x++) { const arg = obj.arguments[x]; this.tokenize(arg); if (x !== last) { this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation(")"); this.namespaceStack.pop(); } break; case SyntaxKind.BooleanLiteral: obj = node; this.literal(obj.value.toString()); break; case SyntaxKind.BlockComment: throw new Error(`Case "BlockComment" not implemented`); case SyntaxKind.TypeSpecScript: throw new Error(`Case "TypeSpecScript" not implemented`); case SyntaxKind.ConstStatement: obj = node; this.namespaceStack.push(obj.id.sv); this.keyword("const", { HasSuffixSpace: true }); this.tokenizeIdentifier(obj.id, "declaration"); this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.value); this.namespaceStack.pop(); break; case SyntaxKind.DecoratorExpression: obj = node; parentNamespace = this.namespaceStack.value(); this.namespaceStack.push(generateId(obj)); this.punctuation("@"); this.tokenizeIdentifier(obj.target, "keyword"); this.lineMarker({ relatedLineId: parentNamespace }); if (obj.arguments.length) { last = obj.arguments.length - 1; this.punctuation("("); for (let x = 0; x < obj.arguments.length; x++) { const arg = obj.arguments[x]; this.tokenize(arg); if (x !== last) { this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation(")"); } this.namespaceStack.pop(); break; case SyntaxKind.DirectiveExpression: obj = node; parentNamespace = this.namespaceStack.value(); this.namespaceStack.push(generateId(node)); this.lineMarker({ relatedLineId: parentNamespace }); this.keyword(`#${obj.target.sv}`, { HasSuffixSpace: true }); for (const arg of obj.arguments) { switch (arg.kind) { case SyntaxKind.StringLiteral: this.stringLiteral(arg.value, { HasSuffixSpace: true }); break; case SyntaxKind.Identifier: this.stringLiteral(arg.sv, { HasSuffixSpace: true }); break; } } this.newline(); this.namespaceStack.pop(); break; case SyntaxKind.EmptyStatement: throw new Error(`Case "EmptyStatement" not implemented`); case SyntaxKind.EnumMember: obj = node; this.tokenizeDecoratorsAndDirectives(obj.decorators, obj.directives, false); this.tokenizeIdentifier(obj.id, "member"); this.lineMarker({ addCrossLanguageId: true }); if (obj.value) { this.punctuation(":", { HasSuffixSpace: true }); this.tokenize(obj.value); } break; case SyntaxKind.EnumSpreadMember: obj = node; this.punctuation("..."); this.tokenize(obj.target); this.lineMarker(); break; case SyntaxKind.EnumStatement: this.tokenizeEnumStatement(node); break; case SyntaxKind.JsNamespaceDeclaration: throw new Error(`Case "JsNamespaceDeclaration" not implemented`); case SyntaxKind.JsSourceFile: throw new Error(`Case "JsSourceFile" not implemented`); case SyntaxKind.Identifier: obj = node; const id = this.namespaceStack.value(); this.typeReference(obj.sv, { NavigateToId: id }); break; case SyntaxKind.ImportStatement: throw new Error(`Case "ImportStatement" not implemented`); case SyntaxKind.IntersectionExpression: obj = node; for (let x = 0; x < obj.options.length; x++) { const opt = obj.options[x]; this.tokenize(opt); if (x !== obj.options.length - 1) { this.punctuation("&", { HasPrefixSpace: true, HasSuffixSpace: true }); } } break; case SyntaxKind.InterfaceStatement: this.tokenizeInterfaceStatement(node); break; case SyntaxKind.InvalidStatement: throw new Error(`Case "InvalidStatement" not implemented`); case SyntaxKind.LineComment: throw new Error(`Case "LineComment" not implemented`); case SyntaxKind.MemberExpression: this.tokenizeIdentifier(node, "reference"); break; case SyntaxKind.ModelExpression: this.indent(); this.tokenizeModelExpression(node, { isOperationSignature: false }); this.deindent(); break; case SyntaxKind.ModelProperty: this.tokenizeModelProperty(node, false); break; case SyntaxKind.ModelSpreadProperty: obj = node; this.punctuation("..."); this.tokenize(obj.target); this.lineMarker(); break; case SyntaxKind.ModelStatement: obj = node; this.tokenizeModelStatement(obj); break; case SyntaxKind.NamespaceStatement: throw new Error(`Case "NamespaceStatement" not implemented`); case SyntaxKind.NeverKeyword: this.keyword("never"); break; case SyntaxKind.NumericLiteral: obj = node; this.literal(obj.value.toString()); break; case SyntaxKind.ObjectLiteral: obj = node; this.punctuation("#{"); this.indent(); last = obj.properties.length - 1; obj.properties.forEach((prop, i) => { this.tokenize(prop); if (i !== last) { this.punctuation(",", { HasSuffixSpace: false }); } this.newline(); }); this.deindent(); this.punctuation("}"); break; case SyntaxKind.ObjectLiteralProperty: obj = node; this.tokenizeIdentifier(obj.id, "member"); this.punctuation(":", { HasSuffixSpace: true }); this.tokenize(obj.value); break; case SyntaxKind.ObjectLiteralSpreadProperty: obj = node; // TODO: Whenever there is an example? throw new Error(`Case "ObjectLiteralSpreadProperty" not implemented`); case SyntaxKind.OperationStatement: this.tokenizeOperationStatement(node); break; case SyntaxKind.OperationSignatureDeclaration: obj = node; this.punctuation("("); if (obj.parameters.properties.length) { this.indent(); this.tokenizeModelExpression(obj.parameters, { isOperationSignature: true }); this.deindent(); } this.punctuation("):", { HasSuffixSpace: true }); this.tokenizeReturnType(obj, { isExpanded: true }); break; case SyntaxKind.OperationSignatureReference: obj = node; this.keyword("is", { HasPrefixSpace: true, HasSuffixSpace: true }); this.tokenize(obj.baseOperation); break; case SyntaxKind.Return: throw new Error(`Case "Return" not implemented`); case SyntaxKind.StringLiteral: obj = node; this.stringLiteral(obj.value); break; case SyntaxKind.ScalarStatement: this.tokenizeScalarStatement(node); break; case SyntaxKind.TemplateParameterDeclaration: obj = node; this.tokenize(obj.id); if (obj.constraint) { this.keyword("extends", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.constraint); } if (obj.default) { this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.default); } break; case SyntaxKind.TupleExpression: obj = node; this.punctuation("[", { HasSuffixSpace: true }); for (let x = 0; x < obj.values.length; x++) { const val = obj.values[x]; this.tokenize(val); if (x !== obj.values.length - 1) { this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation("]"); break; case SyntaxKind.TypeReference: obj = node; this.tokenizeIdentifier(obj.target, "reference"); this.tokenizeTemplateInstantiation(obj); break; case SyntaxKind.UnionExpression: obj = node; for (let x = 0; x < obj.options.length; x++) { const opt = obj.options[x]; this.tokenize(opt); if (x !== obj.options.length - 1) { this.punctuation("|", { HasPrefixSpace: true, HasSuffixSpace: true }); } } break; case SyntaxKind.UnionStatement: this.tokenizeUnionStatement(node); break; case SyntaxKind.UnionVariant: this.tokenizeUnionVariant(node); break; case SyntaxKind.UnknownKeyword: this.keyword("unknown"); break; case SyntaxKind.UsingStatement: throw new Error(`Case "UsingStatement" not implemented`); case SyntaxKind.ValueOfExpression: this.keyword("valueof", { HasSuffixSpace: true }); this.tokenize(node.target); break; case SyntaxKind.VoidKeyword: this.keyword("void"); break; case SyntaxKind.TemplateArgument: case SyntaxKind.StringTemplateExpression: obj = node; const stringValue = this.buildTemplateString(obj); const multiLine = stringValue.includes("\n"); // single line case if (!multiLine) { this.stringLiteral(stringValue); break; } // otherwise multiline case const lines = stringValue.split("\n"); this.punctuation(`"""`); this.indent(); for (const line of lines) { this.literal(line); this.newline(); } this.deindent(); this.punctuation(`"""`); break; case SyntaxKind.StringTemplateSpan: obj = node; this.punctuation("${"); this.tokenize(obj.expression); this.punctuation("}"); this.tokenize(obj.literal); break; case SyntaxKind.StringTemplateHead: case SyntaxKind.StringTemplateMiddle: case SyntaxKind.StringTemplateTail: obj = node; this.literal(obj.value); break; case SyntaxKind.CallExpression: obj = node; this.tokenize(obj.target); this.punctuation("(", { HasSuffixSpace: false }); for (let x = 0; x < obj.arguments.length; x++) { const arg = obj.arguments[x]; this.tokenize(arg); if (x !== obj.arguments.length - 1) { this.punctuation(",", { HasSuffixSpace: true, snapTo: "}" }); } } this.punctuation(")"); break; default: // All Projection* cases should fail here... throw new Error(`Case "${SyntaxKind[node.kind].toString()}" not implemented`); } } tokenizeTemplateInstantiation(obj) { if (!obj.arguments.length) { return; } // if any argument is a ModelExpression, then we need to expand the template to multiple lines const isExpanded = obj.arguments.some(arg => arg.argument.kind === SyntaxKind.ModelExpression); this.punctuation("<"); if (isExpanded) { this.indent(); } for (let x = 0; x < obj.arguments.length; x++) { const arg = obj.arguments[x]; this.tokenizeTemplateArgument(arg); if (x !== obj.arguments.length - 1) { this.punctuation(",", { HasSuffixSpace: true, snapTo: "}" }); if (isExpanded && arg.argument.kind) { this.blankLines(0); } } } if (isExpanded) { this.newline(); this.deindent(); } this.punctuation(">"); } buildExpressionString(node) { switch (node.kind) { case SyntaxKind.StringLiteral: return `"${node.value}"`; case SyntaxKind.NumericLiteral: return node.value.toString(); case SyntaxKind.BooleanLiteral: return node.value.toString(); case SyntaxKind.StringTemplateExpression: return this.buildTemplateString(node); case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.NeverKeyword: return "never"; case SyntaxKind.TypeReference: const obj = node; switch (obj.target.kind) { case SyntaxKind.Identifier: return obj.target.sv; case SyntaxKind.MemberExpression: return this.getFullyQualifiedIdentifier(obj.target); } default: throw new Error(`Unsupported expression kind: ${SyntaxKind[node.kind]}`); //unsupported ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode | AnyKeywordNode; } } /** Constructs a single string with template markers. */ buildTemplateString(node) { let result = node.head.value; for (const span of node.spans) { result += "${" + this.buildExpressionString(span.expression) + "}"; result += span.literal.value; } return result; } tokenizeModelStatement(node) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); this.keyword("model", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); if (node.extends) { this.keyword("extends", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(node.extends); } if (node.is) { this.keyword("is", { HasPrefixSpace: true, HasSuffixSpace: true }); this.tokenize(node.is); } this.tokenizeTemplateParameters(node.templateParameters); if (node.properties.length) { this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (const prop of node.properties) { const propName = this.getNameForNode(prop); this.namespaceStack.push(propName); this.tokenize(prop); this.punctuation(";"); this.namespaceStack.pop(); this.newline(); } this.deindent(); this.punctuation("}", { isContextEndLine: true }); } else { this.punctuation("{}", { HasPrefixSpace: true }); } this.namespaceStack.pop(); } tokenizeScalarStatement(node) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); this.keyword("scalar", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); if (node.extends) { this.keyword("extends", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(node.extends); } this.tokenizeTemplateParameters(node.templateParameters); this.newline(); this.namespaceStack.pop(); } tokenizeInterfaceStatement(node) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); this.keyword("interface", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); this.tokenizeTemplateParameters(node.templateParameters); this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (let x = 0; x < node.operations.length; x++) { const op = node.operations[x]; this.tokenizeOperationStatement(op, true); this.blankLines(1); } this.deindent(); this.punctuation("}", { isContextEndLine: true }); this.namespaceStack.pop(); } tokenizeEnumStatement(node) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); this.keyword("enum", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (const member of node.members) { const memberName = this.getNameForNode(member); this.namespaceStack.push(memberName); this.tokenize(member); this.punctuation(","); this.namespaceStack.pop(); this.newline(); } this.deindent(); this.punctuation("}", { isContextEndLine: true }); this.namespaceStack.pop(); } tokenizeUnionStatement(node) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); this.keyword("union", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (let x = 0; x < node.options.length; x++) { const variant = node.options[x]; const variantName = this.getNameForNode(variant); this.namespaceStack.push(variantName); this.tokenize(variant); this.namespaceStack.pop(); if (x !== node.options.length - 1) { this.punctuation(","); } this.newline(); } this.deindent(); this.punctuation("}", { isContextEndLine: true }); this.namespaceStack.pop(); } tokenizeUnionVariant(node) { this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); if (node.id !== undefined) { this.tokenizeIdentifier(node.id, "member"); this.punctuation(":", { HasSuffixSpace: true }); } this.lineMarker({ addCrossLanguageId: true }); this.tokenize(node.value); } tokenizeModelProperty(node, inline) { this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, inline); this.tokenizeIdentifier(node.id, "member"); this.lineMarker(); this.punctuation(node.optional ? "?:" : ":", { HasSuffixSpace: true }); this.tokenize(node.value); if (node.default) { this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(node.default); } } /** Expands and tokenizes a model expression (anonymous model) */ tokenizeModelExpression(node, options) { const isOperationSignature = options.isOperationSignature; // display {} for empty model or nothing for empty operation signature if (!node.properties.length) { if (!isOperationSignature) { this.punctuation("{}", { HasPrefixSpace: true }); } return; } if (!isOperationSignature) { this.punctuation("{"); this.indent(); } this.namespaceStack.push("anonymous"); for (let x = 0; x < node.properties.length; x++) { const prop = node.properties[x]; const propName = this.getNameForNode(prop); this.namespaceStack.push(propName); switch (prop.kind) { case SyntaxKind.ModelProperty: this.tokenizeModelProperty(prop, false); break; case SyntaxKind.ModelSpreadProperty: this.tokenize(prop); } this.namespaceStack.pop(); if (isOperationSignature) { if (x !== node.properties.length - 1) { this.punctuation(",", { HasSuffixSpace: true, snapTo: "}" }); } } else { this.punctuation(";", { HasSuffixSpace: true, snapTo: "}" }); } this.blankLines(0); } if (!isOperationSignature) { this.deindent(); this.punctuation("}"); this.newline(); } this.namespaceStack.pop(); } tokenizeOperationStatement(node, suppressOpKeyword = false) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); if (!suppressOpKeyword) { this.keyword("op", { HasSuffixSpace: true }); } this.tokenizeIdentifier(node.id, "declaration"); this.tokenizeTemplateParameters(node.templateParameters); this.tokenize(node.signature); this.punctuation(";", { isContextEndLine: true }); this.namespaceStack.pop(); } tokenizeNamespaceModel(model) { this.namespaceStack.push(model.name); if (model.node.kind === SyntaxKind.NamespaceStatement) { this.tokenizeDecoratorsAndDirectives(model.node.decorators, model.node.directives, false); } this.keyword("namespace", { HasSuffixSpace: true }); this.typeDeclaration(model.name, this.namespaceStack.value(), true, { HasSuffixSpace: true }); this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (const node of model.augmentDecorators) { this.tokenize(node); this.blankLines(1); } for (const node of model.operations.values()) { this.tokenize(node); this.blankLines(1); } for (const node of model.resources.values()) { this.tokenize(node); this.blankLines(1); } for (const node of model.models.values()) { this.tokenize(node); this.blankLines(1); } for (const node of model.aliases.values()) { this.tokenize(node); this.punctuation(";"); this.blankLines(1); } for (const node of model.constants.values()) { this.tokenize(node); this.punctuation(";"); this.blankLines(0); } this.deindent(); this.punctuation("}", { isContextEndLine: true }); this.blankLines(1); this.namespaceStack.pop(); } tokenizeDecoratorsAndDirectives(decorators, directives, inline) { const docDecorators = ["doc", "summary", "example"]; if ((directives || []).length === 0 && (decorators || []).length === 0) { return; } for (const directive of directives !== null && directives !== void 0 ? directives : []) { this.tokenize(directive); } // render each decorator for (const node of decorators || []) { const isDoc = docDecorators.includes(node.target.sv); this.tokenize(node); if (isDoc) { // if any token in a line is documentation, then the whole line is for (const token of this.currentLine.Tokens) { token.IsDocumentation = true; } } if (!inline) { this.newline(); } } } getFullyQualifiedIdentifier(node, suffix) { switch (node.base.kind) { case SyntaxKind.Identifier: return `${node.base.sv}.${suffix}`; case SyntaxKind.MemberExpression: return this.getFullyQualifiedIdentifier(node.base, `${node.base.id.sv}.${suffix}`); } } tokenizeIdentifier(node, style) { switch (node.kind) { case SyntaxKind.MemberExpression: const defId = this.getFullyQualifiedIdentifier(node, node.id.sv); switch (style) { case "reference": this.typeReference(defId); break; case "member": this.member(defId); break; case "keyword": this.keyword(defId); break; case "declaration": throw new Error(`MemberExpression cannot be a "declaration".`); } break; case SyntaxKind.StringLiteral: if (style !== "member") { throw new Error(`StringLiteral type can only be a member name. Unexpectedly "${style}"`); } this.stringLiteral(node.value); break; case SyntaxKind.Identifier: switch (style) { case "declaration": this.typeDeclaration(node.sv, this.namespaceStack.value(), true, { HasSuffixSpace: false }); break; case "reference": const defId = this.definitionIdFor(node.sv, this.packageName); this.typeReference(node.sv, { NavigateToId: defId }); break; case "member": this.member(this.getRawText(node)); break; case "keyword": this.keyword(node.sv); break; } } } getRawText(node) { return getSourceLocation(node).file.text.slice(node.pos, node.end); } tokenizeTemplateParameters(nodes) { if (nodes.length) { this.punctuation("<"); for (let x = 0; x < nodes.length; x++) { const param = nodes[x]; this.tokenize(param); if (x !== nodes.length - 1) { this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation(">"); } } tokenizeTemplateArgument(obj) { if (obj.name) { this.text(obj.name.sv); this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); } if (obj.argument.kind === SyntaxKind.ModelExpression) { this.tokenizeModelExpression(obj.argument, { isOperationSignature: false }); } else { this.tokenize(obj.argument); } } tokenizeReturnType(node, options) { if (options.isExpanded && node.parameters.properties.length) { const offset = this.currentLine.Tokens.length; this.tokenize(node.returnType); const returnTokens = this.currentLine.Tokens.slice(offset); const returnTypeString = returnTokens .filter((x) => x.Value) .flatMap((x) => x.Value) .join(""); this.namespaceStack.push(returnTypeString); this.lineMarker(); this.namespaceStack.pop(); } else { this.tokenize(node.returnType); } } buildNavigation(ns) { this.namespaceStack.reset(); this.navigationItems.push(new NavigationItem(ns, this.namespaceStack)); } getNameForNode(node) { const id = generateId(node); if (id) { return id.split(".").splice(-1)[0]; } else { throw new Error("Unable to get name for node."); } } definitionIdFor(value, prefix) { if (value.includes(".")) { const fullName = `${prefix}.${value}`; return this.typeDeclarations.has(fullName) ? fullName : undefined; } for (const item of this.typeDeclarations) { if (item.split(".").splice(-1)[0] === value) { return item; } } return undefined;