@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,415 lines (1,262 loc) • 110 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 !== 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 +=