UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,196 lines (1,114 loc) 57.1 kB
// Populate views with elements, elements with association targets, ... // The functionality in this file is the heart of the Core Compiler and the // most complex part. It essentially implements the function `environment` // used when resolving element references: when starting a references at a // certain definition or element, which names are allowed next? // // To calculate that info, the compiler might need the same info for other // definitions. In other words: it calls itself recursively (using an iterative // algorithm where appropriate). To be able to calculate that info on demand, // the definitions need to have enough information, which must have been set in // an earlier compiler phase. It is essential to do things in the right order. // TODO: It might be that we need to call propagateKeyProps() and // addImplicitForeignKeys() in populate.js, as we might need to know the // foreign keys in populate.js (foreign key access w/o JOINs). 'use strict'; const { isDeprecatedEnabled, forEachDefinition, forEachMember, forEachGeneric, } = require('../base/model'); const { dictAdd, dictAddArray, dictFirst, dictForEach, } = require('../base/dictionaries'); const { weakLocation, weakRefLocation } = require('../base/location'); const { CompilerAssertion } = require('../base/error'); const { kindProperties } = require('./base'); const { setLink, setArtifactLink, annotationVal, annotationIsFalse, annotationLocation, linkToOrigin, setMemberParent, dependsOn, proxyCopyMembers, setExpandStatus, setExpandStatusAnnotate, dependsOnSilent, columnRefStartsWithSelf, } = require('./utils'); const { typeParameters } = require('./builtins'); const $inferred = Symbol.for( 'cds.$inferred' ); const $location = Symbol.for( 'cds.$location' ); /** * These properties are copied from specified elements. */ const typePropertiesFromSpecifiedElements = { // 'key' is special case, see setSpecifiedElementTypeProperties() // TODO: Decide on behavior if an actual key does not have "key" property in specified elements, // and another non-key is marked key in them. // key: 'if-undefined', default: 1, notNull: 1, localized: 1, ...typeParameters.expectedLiteralsFor, }; // Export function of this file. function populate( model ) { const { options } = model; // Get shared functionality and the message function: const { info, warning, error, message, } = model.$messageFunctions; const { resolvePath, nestedElements, attachAndEmitValidNames, initMainArtifact, extendArtifactBefore, extendArtifactAfter, } = model.$functions; Object.assign( model.$functions, { effectiveType, getOrigin, getInheritedProp, mergeSpecifiedForeignKeys, } ); // let depth = 100; let effectiveSeqNo = 0; // artifact number set after having set _effectiveType /** @type {any} may also be a boolean */ let newAutoExposed = []; const ignoreSpecifiedElements = isDeprecatedEnabled( model.options, 'ignoreSpecifiedQueryElements' ); forEachDefinition( model, traverseElementEnvironments ); while (newAutoExposed.length) { // console.log( newAutoExposed.map( a => a.name.id ) ) const all = newAutoExposed; newAutoExposed = []; all.forEach( traverseElementEnvironments ); } newAutoExposed = true; // internal error if auto-expose after here return; /** Make sure that effectiveType() is called on all members and items */ function traverseElementEnvironments( art ) { // We leave out foreign keys (as they are traversed via forEachMember). // Keys are handled in tweak-assocs.js if (art.kind === 'key') return; let type = effectiveType( art ); while (type?.items) type = effectiveType( type.items ); if (art.$queries) art.$queries.forEach( traverseElementEnvironments ); if (art.mixin) dictForEach( art.mixin, effectiveType ); if (art.targetAspect?.elements) effectiveType( art.targetAspect ); if (art !== art._main?._leadingQuery) // already done forEachMember( art, traverseElementEnvironments ); } //-------------------------------------------------------------------------- // The central functions for path resolution - must work on-demand //-------------------------------------------------------------------------- /** * Return the artifact having properties which are relevant for further name * resolution on `art`: `target`, `elements`, `items`, also `enum`. Make sure * that these properties actually exist, are complete and auto-corrected. Cache * the result in property `_effectiveType`. * * - actions, functions: returns `art`, might have expanded `params`/`returns` * - artifacts with direct or inherited `target`, `elements`, `items`, `enum`: * returns `art`, these properties might have been auto-redirected / expanded * - artifacts with direct or inherited scalar type: the built-in type * - other artifacts: the last artifact in the origin-chain, i.e. the one which * has neither a type nor some value path. * - returns 0 with cyclic dependencies (with recursive element expansions, we * have `elements: 0` instead). * - returns null if a relevant reference points to nothing or is corrupted * - returns false if a relevant reference points to a duplicate definition * * This function also infers type relevant properties: * * - views and queries: returns `art` with inferred query `elements` * - column with `expand`: returns `art`, usually with inferred `elements`/`items` * - more to come * * At the moment, it is assumed that includes, expansions, and localized has * been applied earlier. * * Properties which are (usually) not relevant for the name resolution, like * `length` and `cardinality`, cannot be simply accessed on the effective * artifact. The effective artifact alone is not enough to check whether an * artifact is an association or composition; it also does not give you the * information about the technical base type of an enum. * * Calculating an effective association/composition implies calculating its * target entity (including redirections), but not induce calculating the * target's elements. Calculating an effective structure (entities, …) does * not imply calculating the effective types of its elements. Calculating an * effective array does not imply calculating its effective line type. */ function effectiveType( art ) { if (!art) return art; // if (--depth) throw Error(`ET: ${ Object.keys(art) }`) if (art._effectiveType !== undefined) return art._effectiveType; // console.log(message( null, art.location, art, {}, 'Info','FT').toString()) const chain = []; // console.log( 'ET-START:', art.kind, art.name ) while (art && art._effectiveType === undefined) { setLink( art, '_effectiveType', 0 ); // initial setting in case of cycles chain.push( art ); art = getOrigin( art ); // console.log( 'ET-GO:', art?.name ) } if (art) art = art._effectiveType; if (art === 0) { model.$assert = 'cycle'; // throw Error(`CYCLE: ${ chain.length }`); return art; } chain.reverse(); for (const a of chain) { // Ensure that the _effectiveType of the parent has been calculated. This // is usually the case, but might not be for elements of anonymous target // aspects. Without it, extensions/annotations might get lost. // For a query and its parent (usually the query entity!), it is the other way // around: to calculate the _effectiveType of the query entity, we might need // to calculate the _effectiveType of a query in FROM first. if (a.kind !== 'select') effectiveType( a._outer || a._main && a._parent ); // TODO: forbid $self+$self.elem inline, see expandWildcard() // Without type, value.path or _origin at beginning, link to itself: extendArtifactBefore( a ); art = populateArtifact( a, art ) || a; setLink( a, '_effectiveType', art ); a.$effectiveSeqNo = ++effectiveSeqNo; // console.log('PE:',require('../model/revealInternalProperties').ref(a)) if (a.elements$ || a.enum$) mergeSpecifiedElementsOrEnum( a ); // console.log( 'ET-DO:', effectiveSeqNo, a?.kind, a?.name, a._extensions?.elements?.length ) extendArtifactAfter( a ); // after setting _effectiveType (for messages) if (a.typeProps$) setSpecifiedElementTypeProperties( a ); } // console.log( 'ET-END:', art?.kind, art?.name ) return art; } function populateArtifact( art, origEffective ) { // Name-resolution relevant properties directly at artifact: // ‹view›.elements of input must have been moved (to elements$) before! // console.log('Q:',art.elements,art.enum,art.items,!!art.query) // console.log('PA:',require('../model/revealInternalProperties').ref(art)) if (art.includes) // first version of includes via effectiveTpe() art.includes.forEach( i => effectiveType( i._artifact ) ); if (art.elements != null || art.enum != null || art.items != null) return art; if (art.target) { // make sure that target._artifact is set: const target = resolvePath( art.target, 'target', art ); // try to implicitly redirect explicitly provided target: if (target && !origEffective?.target && art.kind !== 'mixin') redirectImplicitly( art, art ); if (!art.expand) return art; } else if (art.targetAspect) { // target aspect in aspect return art; } // With properties to be calculated: ---------------------------------------- if (art.query && art.kind !== '$tableAlias') { // query entity const leading = art.$queries[0]; if (!leading) // parse error return null; if (leading._effectiveType !== undefined) { // You cannot refer to a query of another artifact: throw new CompilerAssertion( `Unexpected _effectiveType on leading query of ${ art.name.id }` ); } // TODO: try just return (effectiveType( leading )) === 0 ? 0 : art; setLink( leading, '_effectiveType', 0 ); // relevant to detect invalid $self.* populateQuery( leading ); setLink( leading, '_effectiveType', leading ); leading.$effectiveSeqNo = ++effectiveSeqNo; return art; } if (art.from) return populateQuery( art ); if (art.expand) { // TODO: test that there is no CDL-style cast with expand // (we could allow that later: then some basic structural check is needed) if (!art.value) { initFromColumns( art, art.expand ); if (origEffective?.target) // consider `{ … } as x: AssocType redirectImplicitly( art, origEffective ); } else if (art.value.path) { expandFromColumns( art ); } // TODO: if we allow CDL-style cast with expand in the future, we need to // redirectImplicitly when casting to assoc type return art; } if (!origEffective || origEffective.builtin) // TODO: builtin test needed? return origEffective; // With inherited auto-corrected name-resolution-relevant properties: ------- if (origEffective.target) return redirectImplicitly( art, origEffective ) ? art : origEffective; if (origEffective.elements) return expandElements( art, origEffective ) ? art : origEffective; if (origEffective.enum) return expandEnum( art, origEffective ) ? art : origEffective; if (origEffective.items) return expandItems( art, origEffective ) ? art : origEffective; if (origEffective.params || origEffective.returns) return expandParams( art, origEffective ); return origEffective; } // TODO: test it in combination with top-level CAST function // TODO: we could probably "extend" this function to all other cases where we // set an _origin in Universal CSN // TODO: add 2nd arg `considerSecondary` used in effectiveType(): prefers a // predecessor without _effectiveType (includes, joins) function getOrigin( art ) { // Be careful when using it with art.target or art.enum or art.elements if (!art) return undefined; // TODO: null? // if (--depth) throw Error(`GOR: ${ Object.keys(art) }`) if (art._origin !== undefined) return art._origin; if (art.type) // not stored in _origin return resolvePath( art.type, 'type', art ); return setLink( art, '_origin', getOriginRaw( art ) ); } function getOriginRaw( art ) { if (!art._main) { if (art.query) return getOrigin( art.$queries?.[0] ); // TODO: if we add the `includes` mechanism, use resolveUncheckedPath() for // includes here, because the accept function for includes requires the // elements to have been calculated! } else { // TODO: write checks for path in enum? if (art.value?.path) return resolvePath( art.value, (art.$syntax === 'calc' ? 'calc' : 'column'), art ); if (art.kind === 'select') { const alias = dictFirst( art.$tableAliases ); // With parse errors, the first “alias” might be $self. Using its origin // would lead to a cyclic processing dependency. return (alias.kind === '$tableAlias') ? getOrigin( alias ) : null; } // init sets _origin for alias to sub query, only need to handle ref here: if (art.kind === '$tableAlias') { // do not call effectiveType() on the source to avoid a deeper callstack const source = resolvePath( art, 'from', art._parent ); if (!source?._main) return source; // direct entity (or undefined) // Before having done the resolvePath cleanup, do not rely on resolvePath // to call effectiveType() on the last assoc of a from ref: // TODO: check this with test3/Queries/DollarSelf/CorruptedSource.err.cds const assoc = effectiveType( source ); return assoc?.target._artifact; } } return ''; } function getInheritedProp( art, prop ) { while (art?._effectiveType) { if (art[prop] !== undefined) return art[prop]; art = getOrigin( art ); } return undefined; } function userQuery( user ) { // TODO: should we set _query links in define.js? while (user._main) { if (user.kind === 'select' || user.kind === '$join') return user; user = user._parent; } return null; } // Expansions -------------------------------------------------------------- function expandItems( art, origin ) { if (art.items) return false; if (origin.items === 0 || art.$inferred === 'expanded' && isInRecursiveExpansion( art )) { art.items = 0; // circular return true; } const location = weakRefLocation( art.type || art.value ) || weakLocation( art.location ); art.items = { $inferred: 'expanded', location }; setLink( art.items, '_outer', art ); setLink( art.items, '_parent', art._parent ); setLink( art.items, '_origin', origin.items ); if (!art.$expand) art.$expand = 'origin'; // if value stays, elements won't appear in CSN return true; } function expandElements( art, struct ) { if (art.kind === '$tableAlias') { proxyCopyMembers( art, 'elements', struct.elements, art.path?.location, '$navElement' ); return true; } if (art.elements || art.kind === '$inline' || // no element expansions for "non-proper" types like // entities (as parameter types) etc: struct.kind !== 'type' && struct.kind !== 'element' && struct.kind !== 'param' && !struct._outer) return false; if (struct.elements === 0 || art.$inferred === 'expanded' && isInRecursiveExpansion( art )) { art.elements = 0; // circular return true; } const ref = art.type || art.value || art.name; const location = weakRefLocation( ref ) || weakLocation( art.location ); // console.log( message( null, location, art, {target:struct,art}, 'Info','EXPAND-ELEM') // .toString(), Object.keys(struct.elements)) proxyCopyMembers( art, 'elements', struct.elements, location, null, isDeprecatedEnabled( options, '_noKeyPropagationWithExpansions' ) ); // Set elements expansion status (the if condition is always true, as no // elements expansion will take place on artifact with existing other // member property): if (!art.$expand) art.$expand = 'origin'; // if value stays, elements won't appear in CSN // TODO: have some art.elements[SYM.$inferred] = 'expanded'; return true; } function expandEnum( art, origin ) { if (art.enum) return false; const ref = art.type || art.value || art.name; const location = weakRefLocation( ref ) || weakLocation( art.location ); proxyCopyMembers( art, 'enum', origin.enum, location ); // Set elements expansion status (the if condition is always true, as no // elements expansion will take place on artifact with existing other // member property): if (!art.$expand) art.$expand = 'origin'; // if value stays, elements won't appear in CSN art.enum[$inferred] = 'expanded'; return true; } function expandParams( art, origin ) { if (!origin._main) return origin; // not with entity (should not happen) if (origin.params) proxyCopyMembers( art, 'params', origin.params, null ); if (origin.returns) { // TODO: make linkToOrigin() work for returns, kind/name? const location = weakLocation( origin.returns.location ); art.returns = { name: Object.assign( {}, art.name, { id: '', location } ), kind: 'param', location, $inferred: 'expanded', }; setLink( art.returns, '_parent', art ); setLink( art.returns, '_main', art._main || art ); setLink( art.returns, '_origin', origin.returns ); } if (!art.$expand) art.$expand = 'origin'; // if value stays, elements won't appear in CSN return art; } /** * Return true iff `art` is from a recursive expansion, i.e. if any of its * expanded parents (including _outer) has the same non-expansion-origin. */ function isInRecursiveExpansion( art ) { const current = nonExpandedArtifact( art ); if (current.$inCycle) return true; const cycle = [ current ]; while (art.$inferred === 'expanded') { art = outerOrParent( art ); const origin = nonExpandedArtifact( art ); cycle.push( origin ); if (origin.$inCycle || origin === current) { for (const a of cycle) a.$inCycle = true; return true; } } return false; } function outerOrParent( art ) { if (art._outer) return art._outer; art = art._parent; // TODO: think about setting _parent of elements in `items` object holding // `elements`, not the most outer `items` -> return art._outer || art._parent while (art.items) art = art.items; return art; } function nonExpandedArtifact( art ) { while (art.$inferred === 'expanded') art = art._origin; return art; } //-------------------------------------------------------------------------- // Views //-------------------------------------------------------------------------- // TODO: delete XSN._entities // TODO: delete ENTITY._from - use _origin? instead _from[0] // TODO (after on-demand ext): delete XSN.$entity /** * Merge _specified_ elements with _inferred_ elements in the given view/element, * where specified elements can appear through CSN. * * We only copy annotations. * * This is important to ensure re-compilability. * * TODO: make this part of a revamped on-demand 'extend' functionality. * * @param art */ function mergeSpecifiedElementsOrEnum( art ) { let wasAnnotated = false; for (const id in (art.elements || art.enum)) { const ielem = art.elements ? art.elements[id] : art.enum[id]; // inferred element const selem = art.elements$ ? art.elements$[id] : art.enum$[id]; // specified element // TODO: the positions are very strange, at least for enums // see e.g. for test3/Queries/SpecifiedElements/SpecifiedElements.err.csn // better to complain at the end position of the enum dict if (!selem) { info( 'query-missing-element', [ ielem.name.location, art ], { '#': ielem.kind === 'enum' ? 'enum' : 'std', id, } ); } else { for (const prop in selem) { // just annotation assignments and doc comments for the moment if (prop.charAt(0) === '@' || prop === 'doc') { ielem[prop] = selem[prop]; // required for gensrc mode of to-csn.js, otherwise the annotation // may be lost during recompilation. ielem[prop].$priority = 'annotate'; wasAnnotated = true; } else if (typePropertiesFromSpecifiedElements[prop] && !ignoreSpecifiedElements) { // If ignoreSpecifiedElements is set, we ignore type properties of specified elements, // similar to how it was done in cds-compiler v3. Only annotations are copied. if (!ielem.typeProps$) setLink( ielem, 'typeProps$', Object.create( null ) ); // Note: At this point in time, effectiveType() was likely not called on the // element, yet. Setting it here, we can't compare it to its value from _origin. ielem.typeProps$[prop] = selem[prop]; } } selem.$replacement = true; if (selem.elements) setLink( ielem, 'elements$', selem.elements ); if (selem.enum) setLink( ielem, 'enum$', selem.enum ); if (selem.foreignKeys) setLink( ielem, 'foreignKeys$', selem.foreignKeys ); } } if (wasAnnotated) setExpandStatusAnnotate( art, 'annotate' ); // TODO: We don't check enum$, yet! We first need to fix expansion for // `cast(elem as EnumType)` (see #9421) for (const id in art.elements$) { const specifiedElement = art.elements$[id]; // TODO: Custom kind? specifiedElement.$isSpecifiedElement = true; if (!specifiedElement.$replacement) { const loc = [ specifiedElement.name.location, specifiedElement ]; error( 'query-unspecified-element', loc, { id } ); } } } /** * Merge _specified_ foreign keys with _inferred_ foreign keys in the given view/element, * where specified elements can appear through CSN. * * We only copy annotations. * * This is important to ensure re-compilability. * * TODO: make this part of a revamped on-demand 'extend' functionality. * * @param art */ function mergeSpecifiedForeignKeys( art ) { if (!art.foreignKeys) return; // TODO: Warn if there are no foreign keys? let wasAnnotated = false; for (const id in art.foreignKeys) { const ielem = art.foreignKeys[id]; // inferred element const selem = art.foreignKeys$[id]; // specified element if (!selem) { info( 'query-missing-element', [ ielem.name.location, art ], { '#': 'foreignKeys', id } ); } else { for (const prop in selem) { // just annotation assignments and doc comments for foreign keys if (prop.charAt(0) === '@' || prop === 'doc') { ielem[prop] = selem[prop]; // required for gensrc mode of to-csn.js, otherwise the annotation // may be lost during recompilation. ielem[prop].$priority = 'annotate'; wasAnnotated = true; } } selem.$replacement = true; } } if (wasAnnotated) setExpandStatusAnnotate( art, 'annotate' ); for (const id in art.foreignKeys$) { const specifiedElement = art.foreignKeys$[id]; if (!specifiedElement.$replacement) { const loc = [ specifiedElement.name.location, specifiedElement ]; error( 'query-unspecified-element', loc, { '#': 'foreignKeys', id } ); } } } /** * Set type properties of specified elements on the inferred artifact, but only * assign them if their values differs from the inferred ones (for better locations). * * @param {XSN.Artifact} art */ function setSpecifiedElementTypeProperties( art ) { for (const prop in art.typeProps$) { let o = art; if (o._effectiveType !== 0) { // cyclic while (!o[prop] && getOrigin( o )) o = getOrigin( o ); } if (typePropertiesFromSpecifiedElements[prop] === 'if-undefined') { if (!o[prop]) art[prop] = art.typeProps$[prop]; } else if (!o[prop] || art.typeProps$[prop].val !== o[prop]?.val) { art[prop] = art.typeProps$[prop]; } } } function populateQuery( query ) { if (query._combined || !query.from || !query.$tableAliases) // already done (TODO: re-check!) or $join query or parse error return query; setLink( query, '_combined', Object.create( null ) ); query.$inlines = []; forEachGeneric( query, '$tableAliases', resolveTabRef ); initFromColumns( query, query.columns ); if (query.excludingDict) { for (const name in query.excludingDict) resolveExcluding( name, query._combined, query.excludingDict, query ); } // TODO: should we to set some falsy values? E.g. with $self.*, cyclic from? // Yes, when element names cannot fully be determined (wrong source ref, // cyclic, ...) BTW, similar with `includes` return query; function resolveTabRef( alias ) { // effectiveType() must not be called on $self, is unnecessary for mixins: // (we might have those already) if (alias.kind === 'mixin' || alias.kind === '$self') return; if (!alias.elements) // could be false in hierarchical JOIN - TODO: necessary? effectiveType( alias ); // element → $navElement expansion for $tableAlias forEachGeneric( { elements: alias.elements }, 'elements', ( elem, name ) => { if (elem.$duplicates !== true) dictAddArray( query._combined, name, elem, null ); // not dictAdd() } ); } } function resolveExcluding( name, env, excludingDict, user ) { const found = env[name]; if (found) { // set links for LSP; if Array, then via multiple query sources ($navElement) const art = (Array.isArray(found) && found.map(f => f._origin)) || (found.kind === '$navElement' && found._origin) || found; setArtifactLink( excludingDict[name].name, art ); return; } /** @type {object} */ // console.log(name,Object.keys(env),Object.keys(excludingDict)) const compileMessageRef = info( 'ref-undefined-excluding', [ excludingDict[name].location, user ], { name }, 'Element $(NAME) has not been found' ); attachAndEmitValidNames( compileMessageRef, env ); } // query columns ----------------------------------------------------------- function expandFromColumns( elem ) { const path = elem.value?.path; if (!path || path.broken) return null; // If we allow CDL-style casts of `expand`s to associations in the future, we // need to ignore an explicit type, i.e. not getOrigin(): const assoc = resolvePath( elem.value, 'column', elem ); if (!effectiveType( assoc )?.target) return initFromColumns( elem, elem.expand ); const { targetMax } = path[path.length - 1].cardinality || getInheritedProp( assoc, 'cardinality' ) || {}; if (targetMax && (targetMax.val === '*' || targetMax.val > 1)) { elem.items = { location: elem.expand[$location] }; setLink( elem.items, '_outer', elem ); } return initFromColumns( elem, elem.expand ); } // TODO: make this function shorter // TODO: query is actually the elemParent, where the new elements are added to // top-level: ( query, query.columns ) // inline: ( queryOrColParent, col.inline, col ) // expand: ( col, col.expand ) function initFromColumns( query, columns, inlineHead = undefined ) { const elemsParent = query.items || query; if (!inlineHead) { elemsParent.elements = Object.create( null ); if (query._main._leadingQuery === query) // never the case for 'expand' query._main.elements = elemsParent.elements; } if (!columns) columns = [ { val: '*' } ]; for (let i = 0; i < columns.length; ++i) { const col = columns[i]; if (col.val === '*') { const siblings = wildcardSiblings( columns ); expandWildcard( col, siblings, inlineHead, query ); } // If neither expression (value), expand, new virtual nor new association. if (!col.value && !col.name) continue; // error should have been reported by parser if (col.inline) { const q = userQuery( query ); q.$inlines.push( col ); col.kind = '$inline'; col.name = { id: `.${ q.$inlines.length }`, $inferred: '$internal' }; // TODO: use a name already set in define.js // TODO: really use $inferred: '$internal', not '$inline' ? Re-check. // a name for this internal symtab entry (e.g. '.2' to avoid clashes // with real elements) is only relevant for `cdsc -R`/debugging // TODO: use number = column position if "top-level", negative numbers otherwise // (is also relevant for the semantic location - only use positive) dependsOnSilent( q, col ); // or use userQuery( query ) in the following, too? setMemberParent( col, null, query ); // TODO: really set _parent? initFromColumns( query, col.inline, col ); } else if (!col.$replacement) { const { id } = col.name; col.kind = 'element'; dictAdd( elemsParent.elements, id, col, ( name, location, c ) => { if (c.name.$inferred !== '$internal') error( 'duplicate-definition', [ location, query ], { name, '#': 'element' } ); } ); setMemberParent( col, id, query ); } } forEachGeneric( query, 'elements', initElem ); return true; } function initElem( elem ) { // TODO: we could share code with initMembers/init() in define.js if (elem.type && !elem.type.$inferred) return; // explicit type -> enough or getOrigin() if (elem.$inferred) { // redirectImplicitly( elem, elem._origin ); return; } if (!elem.type && elem.value?.type) { // top-level CAST( expr AS type ) if (!elem.target) { // TODO: we might issue an error if there is a target elem.type = { ...elem.value.type, $inferred: 'cast' }; // TODO: What about other direct properties in cast such as items/enum/...? } } if (elem.foreignKeys) // REDIRECTED with explicit foreign keys forEachGeneric( elem, 'foreignKeys', (key, name) => initKey( key, name, elem ) ); } function initKey( key, name, elem ) { setLink( key, '_block', elem._block ); setMemberParent( key, name, elem ); // TODO: set _block here if not present? } // col ($replacement set before *) // false if two cols have same name function wildcardSiblings( columns ) { const siblings = Object.create( null ); if (!columns) return siblings; let seenWildcard = null; for (const col of columns) { const { name } = col; if (name) { col.$replacement = !seenWildcard; siblings[name.id] = !(name.id in siblings) && col; } else if (col.val === '*') { seenWildcard = true; } } return siblings; } // TODO: disallow $self.elem.* and $self.*, toSelf.* (circular dependency) function expandWildcard( wildcard, siblingElements, colParent, query ) { const { elements } = query.items || query; let location = wildcard.location || weakRefLocation( query.from ) || weakLocation( query.location ); const inferred = query._main.$inferred; const excludingDict = (colParent || query).excludingDict || Object.create( null ); const envParent = wildcard._columnParent; const env = wildcardColumnEnv( wildcard, query ); if (!env) return; for (const name in env) { const navElem = env[name]; // TODO: remove all access to masked (use 'grep') if (excludingDict[name] || navElem.masked?.val) continue; const sibling = siblingElements[name]; if (sibling) { // is explicitly provided (without duplicate) if (!inferred && !envParent) // not yet for expand/inline reportReplacement( sibling, navElem, query ); if (!sibling.$replacement) { sibling.$replacement = true; sibling.kind = 'element'; dictAdd( elements, name, sibling, ( _name, loc ) => { // there can be a definition from a previous inline with the same name: error( 'duplicate-definition', [ loc, query ], { name, '#': 'element' } ); } ); setMemberParent( sibling, name, query ); } // else { // sibling.$inferred = 'query'; // } } else if (Array.isArray( navElem )) { const names = navElem.filter( e => !e.$duplicates ) .map( e => `${ e._parent.name.id }.${ e.name.id }` ); if (names.length) { error( 'wildcard-ambiguous', [ location, query ], { id: name, names }, 'Ambiguous wildcard, select $(ID) explicitly with $(NAMES)' ); } } else { location = weakLocation( location ); // Usually, the location of a `*`-inferred element is the location of the `*`. // For inferred entities, it is the location of the corresponding source elem // (from all generated entities, only auto-exposed are “wildcard projections”): const elemLocation = !query._main.$inferred && location; const origin = envParent ? navElem : navElem._origin; const elem = linkToOrigin( origin, name, query, null, elemLocation ); if (origin.$calcDepElement) dependsOn( elem, origin.$calcDepElement, location ); // TODO: check assocToMany { * } dictAdd( elements, name, elem, ( _name, loc ) => { // there can be a definition from a previous inline with the same name: error( 'duplicate-definition', [ loc, query ], { name, '#': 'element' } ); } ); if (!query._main.$inferred || origin.$inferred) elem.$inferred = '*'; elem.name.$inferred = '*'; // matters for A2J if (envParent) setWildcardExpandInline( elem, envParent, origin, name, location ); else setElementOrigin( elem, navElem, name, elem.location ); } } if (envParent || query.kind !== 'select') { // already done in populateQuery (TODO: change that and check whether // `*` is allowed at all in definer) if (!colParent || colParent.value._artifact) { // avoid "not found" messages if columnParent can't be found const user = colParent || query; for (const name in user.excludingDict) resolveExcluding( name, env, excludingDict, query ); } } } function wildcardColumnEnv( wildcard, query ) { // etc. wildcard._columnParent; // if (envParent) console.log( 'CE:', envParent._origin, query ); const colParent = wildcard._columnParent; if (!colParent) return userQuery( query )._combined; // see combinedSourcesOrParentElements const head = resolvePath( colParent.value, 'column', colParent ); // eslint-disable-next-line no-nested-ternary if (!head ? !columnRefStartsWithSelf( colParent ) : head._main ? userQuery( head ) !== userQuery( query ) : head._main !== query._main) return nestedElements( wildcard ); error( 'def-unexpected-wildcard', [ wildcard.location, colParent ], { code: '*' }, 'Unexpected $(CODE) (wildcard) after $self/association to self reference' ); model.$assert = null; // explains cyclic dependencies return null; } function reportReplacement( sibling, navElem, query ) { // TODO: bring this much less often = only if shadowed elem does not appear // in expr and if not projected as other name. // Probably needs to be reported at a later phase const path = sibling.value && sibling.value.path; if (!sibling.target || sibling.target.$inferred || // not explicit REDIRECTED TO path && path[path.length - 1].id !== sibling.name.id) { // or renamed const { id } = sibling.name; if (sibling.name.$inferred === '$internal') { error( 'query-req-name', // TODO: message function: `query` should work directly [ (sibling.value || sibling).location, query ], {}, 'Alias name is required for this select item' ); } else if (Array.isArray( navElem )) { // ID published! Used in stakeholder project; if renamed, add to oldMessageIds info( 'wildcard-excluding-many', [ sibling.name.location, query ], { id, keyword: 'excluding' }, // eslint-disable-next-line @stylistic/max-len 'This select item replaces $(ID) from two or more sources. Add $(ID) to $(KEYWORD) to silence this message' ); } else { // ID published! Used in stakeholder project; if renamed, add to oldMessageIds info( 'wildcard-excluding-one', [ sibling.name.location, query ], { id, alias: navElem._parent.name.id, keyword: 'excluding' }, // eslint-disable-next-line @stylistic/max-len 'This select item replaces $(ID) from table alias $(ALIAS). Add $(ID) to $(KEYWORD) to silence this message' ); } } } function setWildcardExpandInline( queryElem, columnParent, origin, name, location ) { setLink( queryElem, '_columnParent', columnParent ); const path = [ { id: name, location } ]; queryElem.value = { path, location }; // TODO: can we omit that? We have _origin setArtifactLink( path[0], origin ); setLink( queryElem, '_origin', origin ); // set _projections when inline with table alias: // const alias = columnParent?.value?.path?.[0]?._navigation; // if (alias?.kind === '$tableAlias') // pushLink( alias.elements[name], '_projections', queryElem ); } // called by expandWildcard(): function setElementOrigin( queryElem, navElem, name, location ) { const sourceElem = navElem._origin; const alias = navElem._parent; // always expand * to path with table alias (reason: columns $user etc) const path = [ { id: alias.name.id, location }, { id: name, location } ]; queryElem.value = { path, location }; setLink( path[0], '_navigation', alias ); setArtifactLink( path[0], alias._origin ); setArtifactLink( path[1], sourceElem ); // TODO: or should we set the _artifact/_effectiveType directly to the target? setArtifactLink( queryElem.value, sourceElem ); // pushLink( navElem, '_projections', queryElem ); // TODO: _effectiveType? } //-------------------------------------------------------------------------- // Auto-Redirections //-------------------------------------------------------------------------- // Conditions for redirecting target of assoc in elem // - we (the elem) are in a service // - target provided in assoc is not defined in current service // - elem is to be auto-redirected (included elem, elem from main query, ...) // - assoc is not defined in current service (or was not to be auto-redirected) function redirectImplicitly( elem, assoc ) { // PRE: elem has no target, assoc has target prop if (elem.kind === '$tableAlias') return false; // Specified elements could lead to warnings that seem unfixable by the user. // TODO: Custom kind? if (elem.$isSpecifiedElement) return false; const assocTarget = assoc.target._artifact; let target = assocTarget; // console.log( info( null, [ elem.location, elem ], {target,art:assoc,name:''+assoc.target}, // 'RED').toString()) if (!target) return false; // error in target ref const { location } = elem.value || elem.type || elem.name || elem; const service = (elem._main || elem)._service; if (service && service !== target._service && assocIsToBeRedirected( elem )) { if (service !== (assoc._main || assoc)._service || !assocIsToBeRedirected( assoc ) || elem === assoc) target = redirectImplicitlyDo( elem, assoc, target, service ); } if (elem === assoc) { // redirection of user-provided target if (assocTarget === target) // no change (due to no implicit redirection) return true; elem.target.$inferred = ''; setArtifactLink( elem.target, target ); return true; } if (target !== assocTarget) setExpandStatus( elem, 'target' ); // (might) also set in rewriteCondition elem.target = { path: [ { id: target.name.id, location } ], scope: 'global', location, $inferred: (target !== assocTarget ? 'IMPLICIT' : 'rewrite' ), }; setArtifactLink( elem.target, target ); setArtifactLink( elem.target.path[0], target ); return true; } function assocIsToBeRedirected( assoc ) { if (assoc.kind === 'mixin') return false; const query = userQuery( assoc ); return !query || query._main._leadingQuery === query; } function redirectImplicitlyDo( elem, assoc, target, service ) { // console.log('ES:',elem.name.id,elem.name.element); if (assoc._main === target && elem._main?.kind === 'entity' && elem._main?._ancestors?.includes( target )) { // source and target of the model association are the same entity, and // the current main artifact is a suitable auto-redirection target → return it return elem._main; } const elemScope = preferredElemScope( target, service, elem, assoc._main || assoc ); const exposed = minimalExposure( target, service, elemScope ); if (!exposed.length) { const origTarget = target; if (isAutoExposed( target )) target = createAutoExposed( origTarget, service, elemScope ); const desc = origTarget._descendants || setLink( origTarget, '_descendants', Object.create( null ) ); if (!desc[service.name.id]) // could be the target itself (no repeated msgs)! desc[service.name.id] = [ target ]; else desc[service.name.id].push( target ); } else if (exposed.length === 1) { return exposed[0]; } else if (elem === assoc) { // `assoc: Association to ModelEntity`: user-provided target is to be auto-redirected warning( 'type-ambiguous-target', [ elem.target.location, elem ], { target, // art: definitionScope( target ), - TODO extra debug info in message sorted_arts: exposed, }, { // eslint-disable-next-line @stylistic/max-len std: 'Replace target $(TARGET) by one of $(SORTED_ARTS); can\'t auto-redirect this association if multiple projections exist in this service', // eslint-disable-next-line @stylistic/max-len two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service', } ); // continuation semantics: no auto-redirection } else { // referred (and probably inferred) assoc (without a user-provided target at that place) // HINT: consider bin/cdsv2m.js when changing the following message text // No grouped and sub messages yet (TODO v6): mention at all target places with all assocs const withAnno = annotationVal( exposed[0]['@cds.redirection.target'] ); for (const proj of exposed) { // TODO: def-ambiguous-target (just v6, as the current is infamous and used in options), message( 'redirected-implicitly-ambiguous', [ weakLocation( proj.name.location ), proj ], { '#': withAnno && 'justOne', target, art: elem, // art: definitionScope( target ), - TODO extra debug info in message anno: 'cds.redirection.target', sorted_arts: exposed, }, { // eslint-disable-next-line @stylistic/max-len std: 'Add $(ANNO) to one of $(SORTED_ARTS) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise', // eslint-disable-next-line @stylistic/max-len two: 'Add $(ANNO) to either $(SORTED_ARTS) or $(SECOND) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise', // eslint-disable-next-line @stylistic/max-len justOne: 'Remove $(ANNO) from all but one of $(SORTED_ARTS) to have a unique redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise', } ); } // continuation semantics: no implicit redirections } return target; } // Return projections of `target` in `service`. Sorted by // - first, only consider projections with @cds.redirection.target=true // - exclude all indirect projections, i.e. those which are projection on others in list // // To avoid repeated messages: if already tried to do autoexposure, return // auto-exposed entity when successful, or `target` otherwise (no/failed autoexposure) function minimalExposure( target, service, elemScope ) { const descendants = scopedExposure( target._descendants?.[service.name.id] || [], elemScope, target ); const preferred = descendants.filter( d => annotationVal( d['@cds.redirection.target'] ) ); const exposed = preferred.length ? preferred : descendants; if (exposed.length < 2) return exposed || []; let min = []; for (const e of exposed) { if (min.every( m => m._ancestors?.includes( e ) )) { min = [ e ]; } else if (min.length !== 1 || !e._ancestors?.includes( min[0] )) { if (elemScope === '' && options.testMode) throw new CompilerAssertion( `Scope for ${ target } in service ${ service } is empty`); if (elemScope === '') return []; min.push( e ); } } return min; } // Scoped redirections ----------------------------------------------------- function preferredElemScope( target, service, elem, assocMain ) { const assocScope = definitionScope( assocMain ); const targetScope = definitionScope( target ); if (targetScope === assocScope) { // intra-scope in model const elemScope = definitionScope( elem._main || elem ); // without the if, compile.recompile.json versus expected csn.json in // test3/Redirections/AutoExposeDeepScoped would fail if (targetScope === target || // model target is scope root assocScope === assocMain || // unscoped assoc source in model elemScope !== (elem._main || elem)) // scoped assoc source in service return elemScope; // own scope, then global } if (targetScope === target) // unscoped target in model / other service return false; // all (there could be no scoped autoexposed) // scoped target in model: const exposed = minimalExposure( targetScope, service, false ); // console.log('PES:',elem.name.id,elem.name.element,exposed.map(e=>e.name.id)) if (exposed.length === 1) // unique redirection for target scope: use that return exposed[0]; // TODO: warning if exposed.length >= 2? Probably not // TODO: use excessive testing for the following // Now re-scope according to naming of auto-exposed entity: const autoScopeName = autoExposedName( targetScope, service, false ); const autoScope = model.definitions[autoScopeName]; // console.log('AEN:',autoScopeName,autoScope&&(autoScope.$inferred || autoScope.kind)) if (autoScope) return autoScope; const { location } = service.name; const nullScope = { kind: 'namespace', name: { id: autoScopeName, location }, location, }; model.definitions[autoScopeName] = nullScope; initMainArtifact( nullScope ); return nullScope; } function scopedExposure( descendants, elemScope, target ) { if (!elemScope) // no scoped redirections return descendants; // try scope as target first, even if it has @cds.redirection.target: false if (isDirectProjection( elemScope, target )) return [ elemScope ]; const scoped = descendants.filter( d => elemScope === definitionScope( d ) ); if (scoped.length) // use scoped new targets if present return scoped; // otherwise return new targets outside any scope return descendants.filter( d => d === definitionScope( d ) ); } // Return the scope of a definition. It is the last parent of the definition // which is not a context/service/namespace, or the definition itself. // If inside service, it is the direct child of the (most inner) service. function definitionScope( art ) { let base = art; while (art._parent) { if (art._parent.kind =