UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,415 lines (1,262 loc) 110 kB
'use strict'; // to.cdl() renderer // // This file contains the whole to.cdl(), which takes CSN and outputs CDL. // It used e.g. by `cds import`. // // // # Development Notes // // ## Abbreviations used // - fqn : fully qualified name, i.e. a name that is a global definition reference // const keywords = require('../base/keywords'); const { cdlNewLineRegEx } = require('../language/textUtils'); const { findElement, createExpressionRenderer, withoutCast } = require('./utils/common'); const { escapeString, hasUnpairedUnicodeSurrogate } = require('./utils/stringEscapes'); const { checkCSNVersion } = require('../json/csnVersion'); const { normalizeTypeRef, forEachDefinition } = require('../model/csnUtils'); const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnricher'); const { isBetaEnabled } = require('../base/model'); const { ModelError, CompilerAssertion } = require('../base/error'); const { typeParameters, specialFunctions } = require('../compiler/builtins'); const { isAnnotationExpression } = require('../base/builtins'); const { forEach } = require('../utils/objectUtils'); const { isBuiltinType } = require('../base/builtins'); const { cloneFullCsn } = require('../model/cloneCsn'); const { getKeysDict, implicitAs } = require('../model/csnRefs'); const { undelimitedIdentifierRegex } = require('../parsers/identifiers'); const { getNormalizedQuery } = require('../model/csnUtils'); const { line, pretty, nestBy, bracketBlock, joinDocuments, } = require('./utils/pretty'); const specialFunctionKeywords = Object.create(null); const MAX_LINE_WIDTH = 72; const INDENT_SIZE = 2; function format( document ) { return pretty(document, MAX_LINE_WIDTH); } /** * @param {string} path * @returns {string} */ function rootPathSegment( path ) { // RegEx is at least twice as fast as .split()[0] return path.match(/^[^.]+/)[0]; } /** * Path alias to be rendered as a USING statement. */ class UsingAlias { path; alias; /** * @param {string} path * @param {string} alias */ constructor(path, alias) { this.path = path; this.alias = alias; } requiresExplicitAlias() { return this.alias && implicitAs(this.path) !== this.alias; } } class NameScopeStack { /** @type {DefinitionPathTree[]} */ #scopes = []; /** @type {Record<string, UsingAlias>} */ #aliasToFqn = Object.create(null); /** @type {Record<string, UsingAlias>} */ #fqnToAlias = Object.create(null); /** @type {string|null} */ #namespaceAlias = null; /** @param {DefinitionPathTree} root @param {CSN.Model} csn */ setRootScope( root, csn ) { root.availableRootPaths = Object.assign(Object.create(null), root.children); this.#scopes = [ root ]; this.#prepareUniqueUsingsForRootPaths(csn); } /** * @param {DefinitionPathTree} scope */ pushNameEnv(scope) { const outerScope = this.#scopes.at(-1); const isNamespace = this.#scopes.length === 1 && !scope.definition; if (isNamespace) this.#namespaceAlias = implicitAs(scope.name); // Own children are always available. // Root paths of the outer scope are also available in the inner scope. scope.availableRootPaths = Object.assign(Object.create(null), outerScope.availableRootPaths, scope.children); this.#scopes.push(scope); } popNameEnv() { const popped = this.#scopes.pop(); const wasNamespace = this.#scopes.length === 1 && !popped.definition; if (wasNamespace) this.#namespaceAlias = null; } /** * To be able to refer to definitions outside the current scope, we need to have * unique USING statements. The most stable way is to create a USING statement for * root path-segments on-demand and give it an alias by having it unique in the set * of all path segments of all definitions. * * While this will still lead to some long paths here and there, it is the most * secure way to avoid ambiguities due to shadowing names. * * @param {CSN.Model} csn */ #prepareUniqueUsingsForRootPaths(csn) { // We include vocabularies here, too, because their names are affected by a global "namespace". const names = [ ...Object.keys(csn.definitions || {}), ...Object.keys(csn.vocabularies || {}), ...(csn.extensions || []).map(ext => ext.extend || ext.annotate || ''), ]; const segmentedNames = names.map(name => name.split('.')); this.nonRootSegments = new Set(segmentedNames.map(segments => segments.slice(1)).flat(1)); // Don't use `this.#scopes[0].availableRootPaths`, as that will contain unreachable paths, // e.g. for a file that contains `namespace a.b`, `a` is not reachable. this.rootSegments = new Set(segmentedNames.map(name => name[0])); this.rootSegments.add('cds'); // builtin namespaces this.rootSegments.add('hana'); } /** * @param {string} fqn Path for which we want to add an alias. */ #addUsingAlias( fqn ) { const segments = fqn.split('.'); let aliasName = segments.at(-1); // An explicit alias only needs to be used if the implicit one has the possibility of // being shadowed in any scope or if there is already an alias of that name. if (this.nonRootSegments.has(aliasName) || this.#aliasToFqn[aliasName]) { // There is a non-root segment of the same root name, hence the need for aliases. let counter = 0; aliasName += '_'; const baseAlias = aliasName; while (this.nonRootSegments.has(aliasName) || this.rootSegments.has(aliasName) || this.#aliasToFqn[aliasName]) { // Alias must be unique among _all_ segments and existing USINGs. aliasName = `${ baseAlias }${ ++counter }`; } } // Always add an alias, even if unnecessary, as we'd otherwise try to create // it in #useAliasForPath() again if the same rootName is seen again. if (this.#aliasToFqn[aliasName]) throw new CompilerAssertion(`to.cdl: Alias "${ aliasName }" already exists; collision for ${ fqn } and ${ this.#aliasToFqn[aliasName].path }`); const alias = new UsingAlias(fqn, aliasName); this.#aliasToFqn[aliasName] = alias; this.#fqnToAlias[fqn] = alias; } /** * We assume that definition names, when rendered, are always relative * to the current name environment. * * This function must only be used for statements that _create_ definitions * and not for references _to_ definitions. * * @param {string} fqn * @returns {string} */ definitionName(fqn) { const leaf = this.#scopes.at(-1); if (!leaf?.name) return fqn; if (isBuiltinType(fqn)) { // For e.g. `annotate` statements: // - `annotate String;` is invalid // - `annotate cds.String;` works return fqn; } if (fqn.startsWith(`${ leaf.name }.`)) return fqn.substring(leaf.name.length + 1); // '+1' => also remove '.' throw new CompilerAssertion('to.cdl: Definition to be rendered is not in current name scope!'); } /** * Get a relative reference to the given definition name in the current name environment. * * This function must only be used for references _to_ definitions and not * for statements that _create_ definitions, i.e _introduce_ a new name. * * @param {string} fqn * @returns {string} */ definitionReference(fqn) { if (isBuiltinType(fqn)) { const ref = this.builtinShorthandReference(fqn); if (ref !== null) return ref; } const name = rootPathSegment(fqn); // Go through all scopes except the root one, since in there, paths are always absolute. for (let i = this.#scopes.length - 1; i >= 1; i--) { const tree = this.#scopes[i]; if (tree.name && fqn.startsWith(`${ tree.name }.`)) { // FQN is in current scope. const relativeName = fqn.substring(tree.name.length + 1); const relativeRoot = rootPathSegment(relativeName); // Since CDS requires root path segments to be _known within a CDL document_, we // need to check if the root path is _known_. If not, we need a USING statement. // Example: `namespace ns; entity C : ns.D {};` -> must render alias, as 'D' would // be invalid! Required for parseCdl. if (!tree.children[relativeRoot]) return this.#useAliasForPathInScope(fqn, tree); // Name can be used relative to scope 'tree'. We now need to check if the relative // name does not collide with more inner scopes by checking for direct children. for (let j = this.#scopes.length - 1; j > i; j--) { if (this.#scopes[j].children[relativeRoot]) { // collision; requires alias return this.#useAliasForPathInScope(fqn, tree); } } return relativeName; } else if (name in tree.children) { // Name is in current scope, but it is not the artifact we're looking for. // Use a global alias to avoid confusing it. return this.#useAliasForPathInScope(fqn, null); } } // At this point, the path is unknown and outside any non-root scope. if (this.#namespaceAlias && (name !== 'cds' || this.#namespaceAlias === 'cds')) { // There is a namespace. We need a USING for all non-builtin paths, but also for // builtins if the namespace alias collides. Builtin collision, e.g. // `type my.cds.String : cds.String;` with common namespace "my.cds". return this.#useAliasForPathInScope(fqn, null); } if (name !== 'cds' && !this.#scopes[0].availableRootPaths?.[name]) { // In case the non-builtin path is unknown, add a using statement. Required for parseCdl. // Completely unknown: -> alias return this.#useAliasForPathInScope(fqn, null); } // Builtin or root path is known. return fqn; } /** * Adapt the FQN to use a global alias. The alias is created for either * the scope in which the FQN resides or the root path segment. * * @param {string} fqn * @param {DefinitionPathTree} [scope] * @returns {string} */ #useAliasForPathInScope( fqn, scope ) { const path = scope?.name ? scope.name : rootPathSegment(fqn); if (!this.#fqnToAlias[path]?.path) this.#addUsingAlias(path); if (this.#fqnToAlias[path].alias === path) return fqn; // shortcut to avoid substring() return this.#fqnToAlias[path].alias + fqn.substring(path.length); } /** * Returns a shorthand reference to the builtin type if possible or * null otherwise, in which case the caller must ensure that the full type * can be used. * * Example: * cds.Integer -> Integer * cds.hana.NCHAR -> hana.NCHAR * * @param {string} type * @returns {string|null} */ builtinShorthandReference(type) { const shortHand = type.slice(4); // remove 'cds.' const root = rootPathSegment(shortHand); if (this.#scopes.at(-1).availableRootPaths[root]) return null; // there is already an artifact of the same name if (this.#namespaceAlias === root) return null; // alias collides with shorthand return shortHand; } /** * Get a list of objects meant to be rendered as USING statements. * * @returns {UsingAlias[]} */ getUsings() { const result = []; for (const alias in this.#aliasToFqn) result.push(this.#aliasToFqn[alias]); return result; } } /** * @see createDefinitionPathTree() */ class DefinitionPathTree { name = null; /** @type {Record<string, DefinitionPathTree>} */ children = Object.create(null); definition = null; /** @type {Record<string, DefinitionPathTree>} */ availableRootPaths = null; // used in NameScopeStack /** * @param {string} fqn */ constructor(fqn) { this.name = fqn; } } /** * For a CSN model, constructs a tree of all path segments of all definitions, e.g. * definitions `a.b.c.d` and `a.b.e.f` will end up in: * ``` * a * └─ b * ├─ c * │ └─ d (link to definition) * └─ e * └─ f (link to definition) * ``` * * @param {CSN.Model} csn * @param {CdlOptions} options * @returns {DefinitionPathTree} */ function createDefinitionPathTree( csn, options ) { const tree = new DefinitionPathTree(''); if (!csn.definitions) return tree; const useNesting = options.renderCdlDefinitionNesting !== false; for (const defName in csn.definitions) { const segments = defName.split('.'); if (!useNesting) { // If we don't want nesting, don't do more work than necessary: // only the first path step is relevant segments.length = 1; } let leaf = tree; for (let i = 0; i < segments.length; i++) { const level = segments[i]; const fqn = segments.slice(0, i + 1).join('.'); leaf.children[level] ??= new DefinitionPathTree(fqn); leaf = leaf.children[level]; } leaf.definition = csn.definitions[defName]; } return tree; } class CsnToCdl { /** * @param {CSN.Model} csn * @param {CdlOptions} options * @param {object} msg */ constructor(csn, options, msg) { this.csn = csn; this.options = options; this.msg = msg; if (this.options.csnFlavor === 'universal' && isBetaEnabled(this.options, 'enableUniversalCsn')) { // Since the expander modifies the CSN, we need to clone it first or // toCdl can't guarantee that the input CSN is not modified. this.csn = cloneFullCsn(this.csn, this.options); enrichUniversalCsn(this.csn, this.options); } checkCSNVersion(this.csn, this.options); this.exprRenderer = this.createCdlExpressionRenderer(); this.subelementAnnotates = []; } render() { const cdlResult = Object.create(null); cdlResult.model = ''; const env = createEnv(); const useNesting = this.options.renderCdlDefinitionNesting !== false; this.definitionTree = createDefinitionPathTree(this.csn, this.options); this.commonNamespace = this.getCommonNamespace(); env.nameEnvStack.setRootScope(this.definitionTree, this.csn); const useNamespace = this.commonNamespace !== this.definitionTree; if (useNamespace) env.nameEnvStack.pushNameEnv(this.commonNamespace); cdlResult.model += useNesting ? this.renderNestedDefinitions(env) : this.renderDefinitions(env); // sub-element annotations that can't be written directly. cdlResult.model += this.renderExtensions(this.subelementAnnotates, env); if (this.csn.vocabularies) cdlResult.model += this.renderVocabularies(this.csn.vocabularies, env); if (this.csn.extensions) cdlResult.model += this.renderExtensions(this.csn.extensions, env); if (useNamespace) env.nameEnvStack.popNameEnv(); cdlResult.model = this.renderUsingAliases(env.nameEnvStack.getUsings(), env) + cdlResult.model; if (this.csn.requires) { let usingsStr = this.csn.requires.map(req => `using from '${ req }';`).join('\n'); usingsStr += '\n\n'; cdlResult.model = usingsStr + cdlResult.model; } if (this.commonNamespace.name) cdlResult.model = `namespace ${ this.renderArtifactName(this.commonNamespace.name, env) };\n\n${ cdlResult.model }`; this.msg.throwWithError(); return cdlResult; } /** * Determine a common namespace along all definitions. * Returns this.definitionTree if there is no common namespace. * * @returns {DefinitionPathTree} */ getCommonNamespace() { let root = this.definitionTree; if (this.options.renderCdlDefinitionNesting === false || this.options.renderCdlCommonNamespace === false) return root; // User does not want common namespace. if (this.csn.vocabularies) { // TODO: With vocabularies, we don't search for a common namespace. // Reason being that `namespace` statements affect vocabularies, but // we don't create definition trees for them. return root; } if (this.csn.extensions?.length > 0) { // TODO: Check for the case of `entity Unknown.E {}; annotate Unknown;` // by going through all extensions. return root; } while (root) { const keys = Object.keys(root.children); if (keys.length !== 1 || root.children[keys[0]].definition) { // There is either more than one sibling path, or the path is a definition. // We MUST NOT create a common namespace for `entity A {}; entity A.A {}`! break; } if (keys[0] === 'cds') { // Don't use 'cds' as common namespace _anywhere_, not even in `namespace foo.cds.bar;` // While our code _does_ handle such cases, as it also needs to do so for `String`, etc., // it would make reading to.cdl() output worse. return this.definitionTree; } root = root.children[keys[0]]; } return root; } /** * @param {UsingAlias[]} aliases * @param {CdlRenderEnvironment} env * @returns {string} */ renderUsingAliases(aliases, env) { if (this.options.renderCdlDefinitionNesting !== false) { // openAPI importer searches for a single USING statement and replaces it. // Let's try to be backward compatible. return aliases.length > 0 ? `using { ${ aliases.map(entry => (entry.requiresExplicitAlias() ? `${ this.quotePathIfRequired(entry.path, env) } as ${ this.quoteNonIdentifierOrKeyword(entry.alias, env) }` : entry.path)).join(', ') } };\n\n` : ''; } let result = ''; for (const entry of aliases) { if (entry.requiresExplicitAlias()) result += `using { ${ this.quotePathIfRequired(entry.path, env) } as ${ this.quoteNonIdentifierOrKeyword(entry.alias, env) } };\n`; else result += `using { ${ entry.path } };\n`; } return result !== '' ? `${ result }\n` : result; } /** * Render definitions in a flat list, i.e. without nesting. * * @param {CdlRenderEnvironment} env * @returns {string} */ renderDefinitions(env) { let result = ''; forEachDefinition(this.csn, (artifact, artifactName) => { const sourceStr = this.renderDefinition(artifactName, artifact, env); if (sourceStr !== '') result += `${ sourceStr }\n`; }); return result; } /** * Render entries from the `csn.definitions` dictionary. * Returns an empty string if nothing is rendered. * * @return {string} */ renderNestedDefinitions(env) { const that = this; let result = ''; renderTree(this.definitionTree); return result; /** * @param {DefinitionPathTree} tree */ function renderTree( tree ) { for (const name in tree.children) { const entry = tree.children[name]; const def = entry.definition; if (def?.kind === 'service' || def?.kind === 'context') { // Render service/context with nested definitions. env.path = [ 'definitions', entry.name ]; result += that.renderAnnotationAssignmentsAndDocComment(def, env); result += `${ env.indent }${ def.kind } ${ that.renderArtifactName(entry.name, env) } {\n`; env.increaseIndent(); env.nameEnvStack.pushNameEnv(entry); if (entry.children) renderTree(entry); env.nameEnvStack.popNameEnv(); env.decreaseIndent(); if (result.at(-1) === '\n' && result.at(-2) === '\n') result = result.substring(0, result.length - 1); // to get the closing brace on the next line after a definition, remove one linebreak result += `${ env.indent }};\n\n`; } else if (def) { const sourceStr = that.renderDefinition(entry.name, def, env); if (sourceStr !== '') result += `${ sourceStr }\n`; if (entry.children) renderTree(entry); } else if (entry.children) { renderTree(entry); } } } } /** * Render annotation definitions, i.e. entries from csn.vocabularies. * Returns an empty string if there isn't anything to render. * * @param {object} vocabularies * @param {CdlRenderEnvironment} env * @return {string} */ renderVocabularies( vocabularies, env ) { let result = ''; for (const key in vocabularies) result += this.renderVocabulariesEntry(key, vocabularies[key], env); return result; } /** * @param {string} name * @param anno * @param {CdlRenderEnvironment} env * @returns {string} */ renderVocabulariesEntry( name, anno, env ) { if (anno.$ignore) return ''; // This environment is passed down the call hierarchy, for dealing with // indentation and name resolution issues env.path = [ 'vocabularies', name ]; const sourceStr = this.renderArtifact(name, anno, env, 'annotation'); return `${ sourceStr }\n`; } /** * Render 'extend' and 'annotate' statements from the `extensions` array. * Could be annotate-statements for sub-elements annotations or from parseCdl's * extensions array or just unapplied extensions. * * @param {CSN.Extension[]} extensions * @param {CdlRenderEnvironment} env * @return {string} */ renderExtensions( extensions, env ) { if (!env.path) env = env.cloneWith({ path: [ 'extensions' ] }); return extensions.map((ext, index) => this.renderExtension(ext, env.withSubPath([ index ]))).join('\n'); } /** * Render an 'extend' and 'annotate' statement. * * @param {CSN.Extension} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderExtension( ext, env ) { if (ext.extend) return this.renderExtendStatement(ext.extend, ext, env); return this.renderAnnotateStatement(ext, env); } /** * Render an 'extend' statement. * `extName` is the extension's artifact's name, most likely `ext.extend`. * This function is recursive, which is why you need to pass it explicitly. * * @param {string} extName * @param {object} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderExtendStatement( extName, ext, env ) { // Element extensions have `kind` set. Don't use for enum extension. const isElementExtend = (ext.kind === 'extend' && !ext.enum); let result = this.renderAnnotationAssignmentsAndDocComment(ext, env); extName = this.renderArtifactName(extName, env); if (ext.includes && ext.includes.length > 0) { // Includes can't be combined with anything in braces {}. const affix = isElementExtend ? 'element ' : ''; const includes = ext.includes.map((inc, i) => this.renderDefinitionReference(inc, env.withSubPath([ 'includes', i ]))).join(', '); result += `${ env.indent }extend ${ affix }${ extName } with ${ includes };\n`; return result; } const typeParams = this.renderTypeParameters(ext, true); if (typeParams) { result += `${ env.indent }extend ${ extName } with ${ typeParams };\n`; return result; } // If there is nothing to extend, e.g. only annotations, don't render an // empty element list. This would end up in diffs with parseCdl CSN. if (!ext.elements && !ext.columns && !ext.actions && !ext.enum) { result += `${ env.indent }extend ${ extName };\n`; return result; } // We have the "old-style" prefix syntax and the "new-style" postfix "with <type>" syntax. // The former one can not only extend (sub-)elements but also actions in the same statement whereas // the latter cannot. // If there are actions, check if there are also elements/columns, and if so, use the prefix notation. const usePrefixNotation = ext.actions && (ext.columns || ext.elements); if (usePrefixNotation) result += `${ env.indent }extend ${ this.getExtendPrefixVariant(ext) } ${ extName } with {\n`; else result += `${ env.indent }extend ${ extName } with ${ this.getExtendPostfixVariant(ext) }{\n`; if (ext.columns) result += this.renderViewColumns(ext, env.withIncreasedIndent()); else if (ext.elements || ext.enum) result += this.renderExtendStatementElements(ext, env); // Not part of if/else cascade, because it may be in postfix notation. if (ext.actions) { const childEnv = env.withIncreasedIndent(); let actions = ''; forEach(ext.actions, (actionName, action) => { actions += this.renderActionOrFunction(actionName, action, childEnv.withSubPath([ 'actions', actionName ]), true); }); if (!usePrefixNotation) result += actions; else if (actions !== '') result += `${ env.indent }} actions {\n${ actions }`; } result += `${ env.indent }};\n`; return result; } /** * What <extend> prefix type to use. Used to render `extend <type> <ref>` statements. * * @param {object} ext * @return {string} */ getExtendPrefixVariant( ext ) { if (ext.kind === 'extend') return 'element'; // element extensions inside an `extend` if (ext.columns) return 'projection'; if (ext.elements) return 'entity'; return ''; } /** * What <extend> postfix type to use. Used to render `extend <ref> with <type>` statements. * * @param {CSN.Extension} ext * @return {string} */ getExtendPostfixVariant( ext ) { if (ext.columns) return 'columns '; if (ext.actions) return 'actions '; if (ext.enum) return 'enum '; if (ext.elements) { // enum/elements ambiguity -> look into elements const isLikelyElement = Object.keys(ext.elements) .find(name => ext.elements[name].value !== undefined); if (isLikelyElement) return 'elements '; } // ambiguity; no postfix, i.e. `extend … with { … }`.s return ''; } /** * Render the elements inside an `extend` statement. They may themselves be `extend` statements. * * @param {CSN.Extension} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderExtendStatementElements( ext, env ) { let result = ''; const prop = ext.elements ? 'elements' : 'enum'; forEach(ext[prop] || {}, (elemName, element) => { const childEnv = env.withIncreasedIndent().withSubPath([ 'elements', elemName ]); if (element.kind === 'extend') result += this.renderExtendStatement(elemName, element, childEnv); else // As soon as we are inside an element, nested `extend` are not possible, // since we can't extend an existing element of a new one. result += this.renderElement(elemName, element, childEnv.withSubPath([ prop, elemName ])); }); return result; } /** * Render an 'annotate' statement. * * @param {CSN.Extension} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderAnnotateStatement( ext, env ) { // Special case: Super annotate has both "returns" and "elements". // Render as separate `annotate`s, but keep the order. if (ext.elements && ext.returns) { const [ , second ] = Object.keys(ext).filter(key => key === 'elements' || key === 'returns'); // The first of 'elements' or 'returns' gets all other properties as well. // The second only gets one property (itself). let result = this.renderAnnotateStatement({ ...ext, [second]: undefined }, env); result += this.renderAnnotateStatement({ annotate: ext.annotate, [second]: ext[second] }, env); return result; } // Top-level annotations of the artifact const topLevelAnnotations = this.renderAnnotationAssignmentsAndDocComment(ext, env.withIncreasedIndent()); // Note: Not renderDefinitionReference, because we don't care if there // are annotations to unknown things. That's allowed! let result = `${ env.indent }annotate ${ this.renderArtifactName(ext.annotate, env) }`; if (topLevelAnnotations) result += ` with\n${ topLevelAnnotations }`; if (ext.params) result += this.renderAnnotateParamsInParentheses(ext, env); // Element extensions and annotations (possibly nested) if (ext.elements || ext.enum) result += ` ${ this.renderAnnotateStatementElements(ext, env) }`; else if (ext.returns) result += this.renderAnnotateReturns(ext, env); if (ext.actions) { // Bound action annotations result += ' actions {\n'; env.increaseIndent(); env.path.push('actions', ''); for (const name in ext.actions) { env.path[env.path.length - 1] = name; const action = ext.actions[name]; result += this.renderAnnotationAssignmentsAndDocComment(action, env) + env.indent + this.quoteNonIdentifierOrKeyword(name, env); // Action parameter annotations if (action.params) result += this.renderAnnotateParamsInParentheses(action, env); if (action.returns) result += this.renderAnnotateReturns(action, env); result = removeTrailingNewline(result); result += ';\n'; } env.decreaseIndent(); result += `${ env.indent }}`; } result = removeTrailingNewline(result); result += ';\n'; return result; } /** * Render the elements-specific part of an 'annotate' statement for an element dictionary * 'ext.elements' (assuming that the surrounding parent has just been rendered, without trailing newline). * Returns the resulting source string, ending without a trailing newline. * * @param {object} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderAnnotateStatementElements( ext, env ) { const elements = ext.enum ? ext.enum : ext.elements; let result = '{\n'; env.increaseIndent(); env.path.push(ext.enum ? 'enum' : 'elements', ''); for (const name in elements) { env.path[env.path.length - 1] = name; const elem = elements[name]; result += this.renderAnnotationAssignmentsAndDocComment(elem, env); result += env.indent + this.quoteNonIdentifierOrKeyword(name, env); if (elem.elements) { env.path.push('elements'); result += ` ${ this.renderAnnotateStatementElements(elem, env) }`; env.path.pop(); } else if (elem.enum) { env.path.push('enum'); result += ` ${ this.renderAnnotateStatementElements(elem, env) }`; env.path.pop(); } result += ';\n'; } env.path.length -= 2; env.decreaseIndent(); result += `${ env.indent }}`; return result; } /** * Renders the `returns` part of an `annotate` statement for (bound) actions. * `ext` must be an object with a `returns` property. * * @param {CSN.Extension} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderAnnotateReturns( ext, env ) { env = env.withSubPath([ 'returns', 'elements' ]); let result = ' returns'; const returnAnnos = this.renderAnnotationAssignmentsAndDocComment(ext.returns, env.withIncreasedIndent()); if (returnAnnos) result += `\n${ returnAnnos }`; if (ext.returns.elements) { // Annotations are on separate lines: Have it aligned nicely result += returnAnnos ? `${ env.indent }` : ' '; result += this.renderAnnotateStatementElements(ext.returns, env); } return result; } /** * Render a parameter list for `annotate` statements, in parentheses `()`. * * @param {CSN.Artifact} ext * @param {CdlRenderEnvironment} env * @return {string} */ renderAnnotateParamsInParentheses( ext, env ) { const childEnv = env.withIncreasedIndent(); let result = '(\n'; const paramAnnotations = []; forEach(ext.params, (paramName, param) => { const annos = this.renderAnnotationAssignmentsAndDocComment(param, childEnv); const name = this.quoteNonIdentifierOrKeyword(paramName, childEnv); // Not supported, yet (#13052) // const sub = (param.elements || param.enum) ? ` ${renderAnnotateStatementElements(param, childEnv)}` : ''; paramAnnotations.push( annos + childEnv.indent + name); }); result += `${ paramAnnotations.join(',\n') }\n${ env.indent })`; return result; } /** * Render an artifact. Return the resulting source string. * * @param {string} artifactName * @param {CSN.Artifact} art * @param {CdlRenderEnvironment} env */ renderDefinition( artifactName, art, env ) { env = env.cloneWith({ path: [ 'definitions', artifactName ] }); const kind = art.kind || 'type'; // the default kind is "type". switch (kind) { case 'entity': if (art.query || art.projection) return this.renderView(artifactName, art, env); return this.renderArtifact(artifactName, art, env); case 'aspect': return this.renderAspect(artifactName, art, env); case 'context': case 'service': return this.renderContextOrService(artifactName, art, env); case 'annotation': // annotation in 'csn.definitions' for compiler v1 compatibility return this.renderArtifact(artifactName, art, env, 'annotation'); case 'action': case 'function': return this.renderActionOrFunction(artifactName, art, env, false); case 'type': case 'event': return this.renderArtifact(artifactName, art, env); default: throw new ModelError(`to.cdl: Unknown artifact kind: '${ art.kind }' at ${ JSON.stringify(env.path) }`); } } /** * @param {string} artifactName * @param {CSN.Artifact} art * @param {CdlRenderEnvironment} env * @param {string} [overrideKind] If set, override the artifact kind. */ renderArtifact( artifactName, art, env, overrideKind ) { let result = this.renderAnnotationAssignmentsAndDocComment(art, env); let kind = overrideKind || art.$syntax === 'aspect' && 'aspect' || art.kind; if (art.abstract) kind = `abstract ${ kind }`; // Vocabularies are in a separate name environment. We can't shorten them. const normalizedArtifactName = kind !== 'annotation' ? this.renderArtifactName(artifactName, env) : this.quotePathIfRequired(artifactName, env); result += `${ env.indent }${ kind } ${ normalizedArtifactName }`; if (art.params) result += this.renderParameters(art, env); let isDirectStruct = false; const isQuery = art.query || art.projection; if (isQuery) { result += ' : '; // types/events (should) only support "projections" result += this.renderQuery(getNormalizedQuery(art).query, true, 'projection', env.withSubPath([ art.projection ? 'projection' : 'query' ])); } else { const type = this.renderTypeReferenceAndProps(art, env); if (type) { isDirectStruct = type.startsWith('{'); if (art.includes?.length && isDirectStruct) { // We can only render includes, if the type is directly structured. Otherwise, we would // render e.g. `type T : Include : T2;`, which is invalid. We use `extend` in such cases. result += this.renderIncludes(art.includes, env); } // For nicer output, no colon if unnamed structure is used. result += (!art.type && art.elements) ? ` ${ type }` : ` : ${ type }`; } else { this.msg.warning('syntax-missing-type', env.path, { '#': art.kind, name: artifactName }, { std: 'Missing type for definition $(NAME); can\'t be represented in CDL', entity: 'Missing elements for entity $(NAME); can\'t be represented in CDL', }); } } if (art.actions) { if (!isQuery && !isDirectStruct) { // If there are no elements nor query, but actions, CDL syntax requires braces. result += ' { }'; } result += this.renderBoundActionsAndFunctions(art, env); } result += ';\n'; if (art.includes?.length && !isDirectStruct) { // If we're not a directly structured type, render the `includes` as `extend` // statements directly below the type definition. result += this.renderExtendStatement(artifactName, { includes: art.includes }, env); } return result; } /** * @param {string} artifactName * @param {CSN.Artifact} art * @param {CdlRenderEnvironment} env * @returns {string} */ renderContextOrService( artifactName, art, env ) { let result = this.renderAnnotationAssignmentsAndDocComment(art, env); result += `${ env.indent }${ art.kind } ${ this.renderArtifactName(artifactName, env) }`; return `${ result } {};\n`; } /** * Render an aspect. Return the resulting source string. * Behaves very similar to renderEntity, _except_ that aspects are * allowed to _not_ have elements, e.g. `aspect A;`. * * @param {string} artifactName * @param {CSN.Artifact} art * @param {CdlRenderEnvironment} env * @return {string} */ renderAspect( artifactName, art, env ) { let result = this.renderAnnotationAssignmentsAndDocComment(art, env); result += `${ env.indent }aspect ${ this.renderArtifactName(artifactName, env) }`; if (art.includes) result += this.renderIncludes(art.includes, env); if (art.elements) result += ` ${ this.renderElements(art, env) }`; else if (art.actions) // if there are no elements, but actions, CDL syntax requires braces. result += ' { }'; result += `${ this.renderBoundActionsAndFunctions(art, env) };\n`; return result; } /** * Render a list of elements enclosed in braces. If the list is empty, returns `{ }`. * * @param {object} artifact Artifact with `elements` property. * @param {CdlRenderEnvironment} env * @return {string} */ renderElements( artifact, env ) { let elements = ''; const childEnv = env.withIncreasedIndent(); for (const name in artifact.elements) elements += this.renderElement(name, artifact.elements[name], childEnv.withSubPath([ 'elements', name ])); return (elements === '') ? '{ }' : `{\n${ elements }${ env.indent }}`; } /** * Render an element (of an entity, type or annotation, not a projection or view) * or an enum symbol. * Returns the resulting source string. * * @param {string} elementName * @param {CSN.Element} element * @param {CdlRenderEnvironment} env */ renderElement( elementName, element, env ) { const isCalcElement = (element.value !== undefined); let result = this.renderAnnotationAssignmentsAndDocComment(element, env); result += env.indent; result += element.virtual ? 'virtual ' : ''; result += element.key ? 'key ' : ''; result += element.masked ? 'masked ' : ''; result += this.quoteNonIdentifierOrKeyword(elementName, env); if (element['#'] !== undefined) { // enum symbol reference result += ` = #${ element['#'] }`; } else if (element.val !== undefined) { // enum value result += ` = ${ this.exprRenderer.renderExpr(element, env) }`; } else if (!isCalcElement || !isDirectAssocOrComp(element.type) && !element.$filtered && !element.$enclosed) { // If the element is a calculated element _and_ a direct association or // composition, we'd render `Association to F on (cond) = calcValue;` which // would alter the ON-condition. // If it is a calculated element _and_ an indirect association (via type chain), // we'd get a cast to an association. const props = this.renderTypeReferenceAndProps(element, env); if (props !== '') result += ` : ${ props }`; } if (isCalcElement) { // calculated element // @ts-ignore result += ' = '; env.path.push('value'); const isSubExpr = (element.value.xpr && xprContainsCondition(element.value.xpr)); result += isSubExpr ? this.exprRenderer.renderSubExpr(element.value, env) : this.exprRenderer.renderExpr(element.value, env); if (element.value.stored === true) result += ' stored'; env.path.length -= 1; } return `${ result };\n`; } /** * Render annotations that were extended to a query element of a view or projection (they only * appear in the view's 'elements', not in their 'columns' for client CSN, because the element * itself may not even be in 'columns', e.g. if it was expanded from a '*'). Return the * resulting rendered 'annotate' statement or an empty string if none required. * * Note: In the past, we checked if the annotation also exists in the respective column, * however, in client CSN, annotations are not part of the column and in parseCdl CSN, * no `elements` exist. * * @param {CSN.Artifact} art * @param {CdlRenderEnvironment} env * @return {string} */ renderQueryElementAndEnumAnnotations( art, env ) { const annotate = this.collectAnnotationsOfElementsAndEnum(art, env); if (annotate) return this.renderExtensions([ annotate ], env); return ''; } /** * Create an "annotate" statement as a CSN extension for all annotations of (sub-)elements. * If no annotation was found, we return `null`. * * @param {CSN.Artifact} artifact * @param {CdlRenderEnvironment} env * @return {CSN.Extension|null} */ collectAnnotationsOfElementsAndEnum( artifact, env ) { // Array, which may be annotated as well. if (artifact.items) { env = env.withSubPath([ 'items' ]); artifact = artifact.items; } if (!artifact.elements && !artifact.enum && !artifact.keys) return null; const annotate = { annotate: env.path[1] }; // Based on the current path, create a correctly nested structure // of elements for which we collect annotations. let obj = annotate; for (let i = 2; i < env.path.length; ++i) { const key = env.path[i]; if (key === 'elements' || key === 'actions' || key === 'params') { obj[key] = Object.create(null); const elem = env.path[i + 1]; obj[key][elem] = {}; obj = obj[key][elem]; } else if (key === 'returns') { obj.returns = {}; obj = obj.returns; } else { // ignore others, e.g. 'items' } } return collectAnnos(obj, artifact) ? annotate : null; /** * Recursive function to collect annotations. `annotateObj` will get an `elements` * object with annotations only if there are annotations on `art`'s (sub-)elements or * enums. Returned object will use "elements" even for enums, since that is * expected in extensions. * * @return {boolean} True, if there were annotations, false otherwise. */ function collectAnnos( annotateObj, art ) { if (!Object.hasOwnProperty.call(art, 'elements') && !Object.hasOwnProperty.call(art, 'enum') && !Object.hasOwnProperty.call(art, 'keys')) return false; const dict = art.enum || art.keys && getKeysDict(art) || art.elements; // Use "elements" for all. This is allowed in extensions. const collected = { elements: Object.create(null) }; let hasAnnotation = false; forEach(dict, (elemName, element) => { if (!collected.elements[elemName]) collected.elements[elemName] = { }; let hasElementAnnotations = false; for (const name in element) { if (name.startsWith('@')) { collected.elements[elemName][name] = element[name]; hasElementAnnotations = true; hasAnnotation = true; } } const hasSubAnnotations = collectAnnos(collected.elements[elemName], element); if (!hasElementAnnotations && !hasSubAnnotations) delete collected.elements[elemName]; // delete if no annotations exist hasAnnotation = hasAnnotation || hasSubAnnotations; }); if (hasAnnotation) annotateObj.elements = collected.elements; return hasAnnotation; } } /** * Render the source of a query, which may be a path reference, possibly with an alias, * or a subselect, or a join operation, as seen from artifact 'art'. * Returns the source as a string. * * @param {object} source * @param {CdlRenderEnvironment} env * @return {string} */ renderViewSource( source, env ) { // Sub-SELECT if (source.SELECT || source.SET) { const subEnv = env.withIncreasedIndent(); let result = `(\n${ subEnv.indent }${ this.renderQuery(source, false, 'view', subEnv) }\n${ env.indent })`; if (source.as) result += this.renderAlias(source.as, env); return result; } // JOIN else if (source.join) { // One join operation, possibly with ON-condition env.path.push('args', 0); let result = `(${ this.renderViewSource(source.args[0], env) }`; for (let i = 1; i < source.args.length; i++) { env.path[env.path.length - 1] = i; result += ` ${ source.join } `; result += this.renderJoinCardinality(source.cardinality); result += `join ${ this.renderViewSource(source.args[i], env) }`; } env.path.length -= 2; if (source.on) { env.path.push('on'); result += ` on ${ this.exprRenderer.renderExpr(source.on, env.withSubPath([ 'on' ])) }`; env.path.length -= 1; } result += ')'; return result; } // Ordinary path, possibly with an alias return this.renderAbsolutePathWithAlias(source, env); } renderJoinCardinality( card ) { let result = ''; if (card) { if (card.srcmin && card.srcmin === 1) result += 'exact '; result += card.src && card.src === 1 ? 'one ' : 'many '; result += 'to '; if (card.min && card.min === 1) result += 'exact '; if (card.max) result += (card.max === 1) ? 'one ' : 'many '; } return result; } /** * Render a path that starts with an absolute name (as used e.g. for the source of a query), * with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'. * Returns the name as a string. * * @param {object} path * @param {CdlRenderEnvironment} env * @return {string} */ renderAbsolutePath( path, env ) { // Sanity checks if (!path.ref) throw new ModelError(`Expecting ref in path: ${ JSON.stringify(path) }`); // Determine the absolute name of the first artifact on the path (before any associations or element traversals) const firstArtifactName = path.ref[0].id || path.ref[0]; // Render the first path step (absolute name, with different quoting/naming ..) let result = this.renderDefinitionReference(firstArtifactName, env); // Even the first step might have parameters and/or a filter env.path.push('ref', 0); if (path.ref[0].args) result += `(${ this.renderArguments(path.ref[0], ':', env) })`; if (path.ref[0].where) result += this.renderFilterAndCardinality(path.ref[0], env); env.path.length -= 2; // Add any path steps (possibly with parameters and filters) that may follow after that if (path.ref.length > 1) result += `:${ this.exprRenderer.renderExpr({ ref: path.ref.slice(1) }, env) }`; return result; } /** * Render a path that starts with an absolute name (as used for the source of a query), * possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a * 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias * is created to the original implicit name. * Returns the name and alias as a string. * * @param {object} path * @param {CdlRenderEnvironment} env * @return {string} */ renderAbsolutePathWithAlias( path, env ) { // We may have changed the implicit alias due to renderAbsolutePath() and renderDefinitionReference() // introducing USING statements. We need to ensure that the implicit alias stays the same. const isElementRef = path.ref.length > 1; const alias = path.as || implicitAs(path.ref); let result = this.renderAbsolutePath(path, env); if (path.as) { // Source had an alias - render it result += this.renderAlias(path.as, env); } else if (!isElementRef) { const defName = path.ref[0].id || path.ref[0]; const sourcePath = env.nameEnvStack.definitionReference(defName); // Source did not have an alias, but we add one as we'd // otherwise have a different implicit alias. if (sourcePath.split('.').at(-1) !== alias) result += this.renderAlias(alias, env); } return result; } /** * Render the given columns / select items. * * @param {CSN.Extension | CSN.QuerySelect} art * @param {object} elements * @param {CdlRenderEnvironment} env * @return {string} */ renderViewColumns( art, env, elements = Object.create(null) ) { env.path.push( 'columns', -1 ); const result = art.columns.map((col, i) => { env.path[env.path.length - 1] = i; return this.renderViewColumn(col, env, findElement(elements, col)); }).join(',\n'); env.path.length -= 2; return `${ result }\n`; } /** * Render a single view or projection column 'col', as it occurs in a select list or * projection list within 'art', possibly with annotations. * Return the resulting source string (no trailing LF). * * @param {object} col * @param {CdlRenderEnvironment} env * @param {CSN.Element} element Element corresponding to the column. Generated by the compiler. */ renderViewColumn( col, env, element ) { // Annotations and column let result = ''; if (!col.doc) { // TODO: In contrast to annotations, we do not render the doc comment as part // of an `annotate` statement. That may change in the future. result += this.renderDocComment(element, env); } result += this.renderAnnotationAssignmentsAndDocComment(col, env); result += env.indent; // only if column is virtual, keyword virtual was present in the source text result += col.virtual ? 'virtual ' : ''; result +=