@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,418 lines (1,266 loc) • 107 kB
JavaScript
'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;
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;
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}`;
if (this.csn.namespace) {
cdlResult.namespace = `namespace ${this.renderArtifactName(this.csn.namespace, createEnv())};\n`;
cdlResult.namespace += 'using from \'./model.cds\';';
}
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 || !this.options.renderCdlCommonNamespace)
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) {
// 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
let result = this.renderAnnotationAssignmentsAndDocComment(ext, env);
// Note: Not renderDefinitionReference, because we don't care if there
// are annotations to unknown things. That's allowed!
result += `${env.indent}annotate ${this.renderArtifactName(ext.annotate, env)}`;
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.
*
* @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 += col.key ? 'key ' : '';
// Use special rendering for .expand/.inline - renderExpr cannot easily handle some cases
if (col.expand || col.i