UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,193 lines (1,096 loc) 73.3 kB
// Compiler phase "resolve": resolve all references // The resolve phase tries to find the artifacts (and elements) for all // references in the augmented CSN. If there are unresolved references, this // compiler phase fails with an error containing a vector of corresponding // messages (alternatively, we could just store this vector in the CSN). // References are resolved according to the scoping rules of CDS specification. // That means, the first name of a reference path is not only searched in the // current environments, but also in the parent environments, with the source // as second-last, and the environment for builtins as the last search // environment. // For all type references, we set the property `type._artifact`, the latter is // the actual type definition. // If the referred type definition has a `parameters` property, we use it to // transform the `$typeArgs` property (sibling to the `type` property`) to // named properties. See function `resolveTypeExpr` below for details. // Example 'file.cds' (see './define.js' for the CSN before "resolve"): // type C { elem: String(4); } // // The corresponding definition of element "elem" looks as follows: // { // kind: 'element', // name: { id: 'elem', component: 'elem', location: ... } // type: { absolute: 'cds.String', _artifact: {...}, path: ...}, // length: { val: 4, location: <of the number literal> }, // location: ..., _parent: ... // } // Potential file names: // lookup-refs / memorize: main refs loop (phase 2) // monitor-refs: resolve-refs (not leading to new defs/elems) // repair-props: rewrite, late extensions // test-model: cycle detection, late tests (currently in checks) 'use strict'; const { forEachDefinition, forEachMember, forEachGeneric, forEachInOrder, isDeprecatedEnabled, } = require('../base/model'); const { dictAdd } = require('../base/dictionaries'); const { weakLocation } = require('../base/location'); const { combinedLocation } = require('../base/location'); const { typeParameters } = require('./builtins'); const { pushLink, setLink, setArtifactLink, setMemberParent, withAssociation, dependsOn, dependsOnSilent, testExpr, targetMaxNotOne, traverseQueryPost, linkToOrigin, compositionTextVariant, targetCantBeAspect, userParam, } = require('./utils'); const detectCycles = require('./cycle-detector'); const { CompilerAssertion } = require('../base/error'); const $location = Symbol.for( 'cds.$location' ); const $inferred = Symbol.for( 'cds.$inferred' ); // TODO: make this part of specExpected in shared.js const expWithFilter = [ 'from', 'expand', 'inline' ]; // Export function of this file. Resolve type references in augmented CSN // `model`. If the model has a property argument `messages`, do not throw // exception in case of an error, but push the corresponding error object to // that property (should be a vector). function resolve( model ) { const { options } = model; // Get shared functionality and the message function: const { info, warning, error, message, } = model.$messageFunctions; const { resolvePath, resolveDefinitionName, attachAndEmitValidNames, traverseExpr, traverseTypedExpr, effectiveType, getOrigin, getInheritedProp, hasTruthyProp, // limited inheritance resolveTypeArgumentsUnchecked, } = model.$functions; Object.assign( model.$functions, { addForeignKeyNavigations, redirectionChain, resolveExprInAnnotations, } ); const ignoreSpecifiedElements = isDeprecatedEnabled( options, 'ignoreSpecifiedQueryElements' ); forEachGeneric( model, 'sources', resolveUsings ); return doResolve(); /** * Resolve the using declarations in `using`. * Issue error message if the referenced artifact does not exist. */ function resolveUsings( src, topLevel ) { if (!src.usings) return; for (const def of src.usings) { if (def.usings) // using {...} resolveUsings( def ); if (!def.name || !def.name.id) continue; // using {...}, parse error const art = model.definitions[def.name.absolute]; if (art && art.$duplicates) continue; const ref = def.extern; const user = (topLevel ? def : src); const from = user.fileDep; if (art || !from || from.realname) // no error for non-existing ref with non-existing module resolvePath( ref, 'using', def ); // TODO: consider FROM for validNames } } function doResolve() { // Phase 1: check paths in `usings` has been moved to kick-start.js Phase 2: // calculate/init view elements & collect views in 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). // Phase 2+3: calculate keys along simple queries in collected views: model._entities = Object.values( model.definitions ) .filter( art => art.$effectiveSeqNo ) .sort( (x, y) => x.$effectiveSeqNo - y.$effectiveSeqNo ); model._entities.forEach( setNavigationProjections ); model._entities.forEach( propagateKeyProps ); // While most dependencies leading have been added at this point, new // cycles could be added later (e.g. via assocs in where conditions), // i.e. keep cycle detection with messages at the end (or after phase 4). // Phase 4: resolve all artifacts: forEachDefinition( model, resolveRefs ); forEachGeneric( model, 'vocabularies', resolveRefs ); if (options.lspMode) { for (const name in model.sources) resolveDefinitionName( model.sources[name].namespace ); } // report cyclic dependencies: detectCycles( model.definitions, ( user, art, location, semanticLoc ) => { if (location) { model.$assert = null; const msg = semanticLoc && 'target'; error( 'ref-cyclic', [ location, semanticLoc || user ], { art, '#': msg, } ); } } ); if (model.$assert) { error( '$internal-expecting-cyclic', null, {}, 'INTERNAL: the compiler should have issued an Error[ref-cyclic]' ); } return model; } //-------------------------------------------------------------------------- // Phase 2+3: calculate propagated KEYs //-------------------------------------------------------------------------- /** * Set `_projection` links in navigation elements and creates $navElement hierarchy. * * @param {XSN.Artifact} view */ function setNavigationProjections( view ) { if (!view.$queries) return; for (const query of view.$queries) { // traversing sub-elements not necessary, since we're in a view // TODO: Handle expand. forEachGeneric( query, 'elements', function navProjectionsForElement( elem ) { if (!elem._origin || elem.expand || !elem.value?.path) return; // TODO: what about elements where _origin is set without value? // TODO: or should we push elems with `expand` sibling to extra list for // better messages? (Whatever that means exactly.) if (elem._columnParent) { if (elem._columnParent?.kind !== '$inline') // we're traversing top-level elements of the query; // other _columnParent kinds can't happen throw new CompilerAssertion('found unexpected "expand", but expected "inline"'); if (!isPathBreakout( elem.value )) { const fullPath = columnParentPath( elem ); if (fullPath) setNavigationProjectionsForElementRef({ path: fullPath }, elem); } } else { setNavigationProjectionsForElementRef( elem.value, elem ); } } ); } } function setNavigationProjectionsForElementRef( ref, elem ) { const { path } = ref; const nav = pathNavigation( ref ); if (nav.navigation) { // not set for $self.… // Path could start with table alias; get start index let index = path.indexOf(nav.item); if (index === -1) return; // should not happen let navItem = nav.navigation; if (!nav.item._navigation) // first non-table-alias setLink( nav.item, '_navigation', navItem ); // We consider an element only projected if the path doesn't have // either arguments or filters; but we build up the navigation env // nonetheless, as it makes rewriting paths later on easier. let isComplexPath = !!(path[index].where || path[index].args); ++index; while (navItem && index < path.length) { const step = path[index]; if (!step?.id) break; isComplexPath ||= !!(step.where || step.args); if (!navItem.elements?.[step.id]) { const elements = navItem._origin?.elements || navItem._origin?.target?._artifact?.elements; if (!elements) break; // Only link available path steps (navigation tree). const origin = elements[step.id]; const member = linkToOrigin( origin, step.id, navItem, 'elements', navItem.path?.location, true ); member.$inferred = 'expanded'; member.kind = '$navElement'; } navItem = navItem.elements[step.id]; setLink( step, '_navigation', navItem ); ++index; } // Last path step, if found, is a projected, either complex or simple. if (index === path.length && navItem) pushLink( navItem, isComplexPath ? '_complexProjections' : '_projections', elem ); } } function columnParentPath( elem ) { if (!elem._columnParent || !elem.value?.path || isPathBreakout( elem.value )) return elem.value?.path; const fullPath = [ ...elem.value.path ]; let columnParent = elem._columnParent; while (columnParent) { if (columnParent.kind !== '$inline' || !columnParent.value?.path || isPathBreakout( columnParent.value )) { // path breakout for e.g. `$self.{ foo }`, `1 as a .{ foo }` return null; } fullPath.unshift(...columnParent.value.path); columnParent = columnParent._columnParent; } return fullPath; } function propagateKeyProps( view ) { if (view.kind === 'type') { // we don't propagate keys to type projections, see #13575 return; } // Second argument true ensure that `key` is only propagated along simple // view, i.e. ref or subquery in FROM, not UNION or JOIN. traverseQueryPost( view.query, true, ( query ) => { if (!withExplicitKeys( query ) && inheritKeyProp( query ) && withKeyPropagation( query )) // now the part with messages inheritKeyProp( query, true ); } ); } function withExplicitKeys( query ) { for (const name in query.elements) { const elem = query.elements[name]; if (elem.key && !elem.$duplicates) // also those from includes return true; } return false; } function inheritKeyProp( query, doIt ) { for (const name in query.elements) { const elem = query.elements[name]; // no key prop for duplicate elements or additional specified elements: const key = !elem.$duplicates && !elem.expand && inheritedSourceKeyProp( elem ); if (key) { if (!doIt) return true; elem.key = { location: elem.value.location, val: key.val, $inferred: 'query' }; } } return false; } function inheritedSourceKeyProp( { value, _columnParent } ) { if (!value || !value.path) return null; const nav = !_columnParent && pathNavigation( value ); const item = value.path[value.path.length - 1]; if (nav?.navigation && nav.item === item) return item._artifact?.key; if (value.path.length !== 1 || _columnParent?.kind !== '$inline') return null; const hpath = _columnParent.value?.path; const head = hpath?.length === 1 && hpath[0]._navigation; return head?.kind === '$tableAlias' && item._artifact?.key; } function primarySourceNavigation( aliases ) { for (const name in aliases) return aliases[name].elements; return undefined; } function withKeyPropagation( query ) { const { from } = query; if (!from) // parse error SELECT FROM <EOF> return false; let propagateKeys = true; // used instead early RETURN to get more messages const toMany = withAssociation( from, targetMaxNotOne, true ); if (toMany) { propagateKeys = false; info( 'query-from-many', [ toMany.location, query ], { art: toMany }, { std: 'Key properties are not propagated because a to-many association $(ART) is selected', // eslint-disable-next-line @stylistic/max-len element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected', } ); } // Check that all keys from the source are projected: const notProjected = []; // we actually push to the array const navElems = primarySourceNavigation( query.$tableAliases ); for (const name in navElems) { const nav = navElems[name]; if (nav.$duplicates) continue; const { key } = nav._origin; if (key?.val && !nav._projections?.length) notProjected.push( nav.name.id ); } if (notProjected.length) { propagateKeys = false; info( 'query-missing-keys', [ from.location, query ], { names: notProjected }, { std: 'Keys $(NAMES) have not been projected - key properties are not propagated', one: 'Key $(NAMES) has not been projected - key properties are not propagated', } ); } // Check that there is no to-many assoc used in select item: for (const name in query.elements) { const elem = query.elements[name]; if (!elem.$inferred && elem.value?.path) { const path = elem._columnParent ? columnParentPath( elem ) : elem.value.path; if (testExpr({ path }, selectTest, () => false, elem)) propagateKeys = false; } } return propagateKeys; function selectTest( expr, user ) { const art = withAssociation( expr, targetMaxNotOne ); if (art) { // ID published! Used in stakeholder project; if renamed, add to oldMessageIds info( 'query-navigate-many', [ art.location, user || query ], { art }, { std: 'Navigating along to-many association $(ART) - key properties are not propagated', // eslint-disable-next-line @stylistic/max-len element: 'Navigating along to-many association $(MEMBER) of $(ART) - key properties are not propagated', // eslint-disable-next-line @stylistic/max-len alias: 'Navigating along to-many mixin association $(MEMBER) - key properties are not propagated', } ); } return art; } } //-------------------------------------------------------------------------- // Phase 4: //-------------------------------------------------------------------------- function adHocOrMainKind( elem ) { const main = elem._main; if (main) { do { elem = elem._parent; if (elem.targetAspect) return 'aspect'; // ad-hoc composition target aspect } while (elem !== main); } return elem.kind; } // TODO: have $applied/$extension/$status on extension with the following values // - 'unknown': artifact to extend/annotate is not defined or contains unknown member // - 'referred': contains annotation for element of referred type (not yet supported) // - 'inferred': only contains extension for known member, but some inferred ones // (inferred = elements from structure includes, query elements) // - 'original': only contains extensions on non-inferred members // Resolve all references in artifact or element `art`. Do so recursively in // all sub elements. // TODO: make this function smaller function resolveRefs( art ) { if (art.builtin) return; const parent = art._parent; const allowedInMain = [ 'entity', 'aspect', 'event' ].includes( adHocOrMainKind( art ) ); const isTopLevelElement = parent && (parent.kind !== 'element' || parent.targetAspect); if (options.lspMode && art.name && !art._main) resolveDefinitionName( art ); // Check KEY (TODO: make this an extra function) const { key } = art; if (key?.val && !key.$inferred) { // With unmanaged/composition as key, we complain at the `key` keyword, not // the `on` condition / the aspect, because the easiest fix would be to // simply remove the keyword. Text and message-id are accordingly. // This fits nicely with exposing unmanaged/composition with explicit `key`. // We do not complain about unmanaged/composition inside struct keys. // (Actually, aspect compositions are not supported as sub elements anyway.) if (getInheritedProp( art, 'targetAspect' )) { error( 'def-invalid-key', [ key.location, art ], { '#': 'composition' } ); // TODO: test with managed composition exposed with explicit KEY } else if (art.target && getInheritedProp( art, 'on' )) { error( 'def-invalid-key', [ key.location, art ], { '#': 'unmanaged' } ); } else if (!allowedInMain || !isTopLevelElement) { warning( 'def-unsupported-key', [ art.key.location, art ], { '#': allowedInMain ? 'sub' : 'kind', keyword: 'key' } ); } } if (art.targetAspect && targetCantBeAspect( art, true )) { // If not for anonymous aspect, this message can only occur for CSN input → // we are more CSN specific (we could add more text variants, but this is // CSN input with an undocumented CSN property…) For an anonymous target // aspect, we could have more text variants, though… const msg = art.targetAspect.elements ? 'anonymous' : (art.target || !art._parent?.query && art._parent?.kind !== 'event') && 'std'; error( 'type-unexpected-target-aspect', [ art.targetAspect.location, art ], { '#': msg || 'target', prop: 'targetAspect', otherprop: 'target' }, { std: 'Unexpected property $(PROP)', anonymous: 'Unexpected anonymous target aspect', target: 'Unexpected property $(PROP), adding property $(OTHERPROP) might help', } ); } // TODO: else resolvePath() + test for cds.Composition? if (art.includes && !allowedInMain) { // TODO: make this a check function for shared.js / or make it part of extend.js for (const include of art.includes) { const struct = include._artifact; if (struct && struct.kind !== 'type' && struct.elements && Object.values( struct.elements ).some( e => e.targetAspect )) { error( 'type-managed-composition', [ include.location, art ], { '#': struct.kind, art: struct } ); } } } let obj = art; if (obj.type) // TODO: && !obj.type.$inferred ? resolveTypeExpr( obj, art ); const type = effectiveType( obj ); // make sure implicitly redirected target exists if (!obj.items && type && type.items) { // TODO: shouldn't be this part of populate.js ? const items = { location: weakLocation( (obj.type || obj).location ), $inferred: 'expanded', }; setLink( items, '_outer', obj ); setLink( items, '_parent', obj._parent ); setLink( items, '_origin', type.items ); obj.items = items; obj.$expand = 'origin'; } if (obj.items) { // TODO: make this a while in v6 (also items proxy) obj = obj.items || obj; // the object which has type properties effectiveType( obj ); } if (obj.type) { // TODO: && !obj.type.$inferred ? if (obj !== (art.returns || art)) // not already checked resolveTypeExpr( obj, art ); // typeOf unmanaged assoc? TODO: is this the right place to check this? // (probably better in rewriteAssociations) const elemType = obj.type._artifact; if (elemType && effectiveType( elemType )) { const assocType = getAssocSpec( elemType ) || {}; if ((assocType.on || assocType.$assocFilter) && !obj.on) obj.on = { $inferred: 'rewrite' }; // TODO: no extra rewrite here if (assocType.targetAspect) { error( 'composition-as-type-of', [ obj.type.location, art ], {}, 'A managed aspect composition element can\'t be used as type' ); return; } else if (assocType.on || assocType.$assocFilter) { error( 'type-unexpected-assoc', [ obj.type.location, art ] ); return; } // Check if relational type is missing its target or if it's used directly. if (elemType.category === 'relation' && !obj.target && !obj.targetAspect) { const isCsn = (obj._block && obj._block.$frontend === 'json'); error( 'type-missing-target', [ obj.type.location, obj ], { '#': isCsn ? 'csn' : 'std', type: elemType }, { // We don't say "use 'association to <target>" because the type could be used // in action parameters, etc. as well. std: 'The type $(TYPE) can\'t be used directly because it\'s compiler internal', csn: 'Type $(TYPE) is missing a target', } ); } } } if (obj.target) { if (!obj.target.$inferred || obj.target.$inferred === 'aspect-composition') resolveTarget( art, obj ); else // TODO: better write when inferred target must be redirected resolveRedirected( obj, obj.target._artifact ); } else if (obj.kind === 'mixin') { // TODO: also check that the type is cds.Association or cds.Composition error( 'non-assoc-in-mixin', [ (obj.type || obj.name).location, art ], {}, 'Only unmanaged associations are allowed in mixin clauses' ); } if (art.targetElement) // in foreign keys resolvePath( art.targetElement, 'targetElement', art ); // Resolve projections/views if (art.$queries) art.$queries.forEach( resolveQuery ); // TODO: or should we set silent dependencies in init()? if (obj.elements) { // silent dependencies forEachGeneric( obj, 'elements', elem => dependsOnSilent( art, elem ) ); } else if (obj.targetAspect && obj.targetAspect.elements) { // silent dependencies forEachGeneric( obj.targetAspect, 'elements', elem => dependsOnSilent( art, elem ) ); } if (obj.foreignKeys) { // silent dependencies // Avoid strange ref-cyclic if managed composition is key (check comes later) // Done by addImplicitForeignKeys() for implicit keys. if (!art.foreignKeys?.[$inferred] && obj.$inferred !== 'aspect-composition') forEachGeneric( obj, 'foreignKeys', elem => dependsOnSilent( art, elem ) ); addForeignKeyNavigations( art ); } resolveExpr( art.default, 'default', art, art ); // TODO: distinguish not by $syntax (it is semantics), but whether in query const valueCtx = (art.$syntax === 'calc') ? 'calc' : 'column'; resolveExpr( art.value, valueCtx, art, art ); if (art.type?.$inferred === 'cast') inferTypePropertiesFromCast( art ); if (art.value) { if (art.$syntax === 'calc') checkCalculatedElement( art ); } resolveExprInAnnotations( art ); forEachMember( art, resolveRefs, art.targetAspect ); // After the resolving of foreign keys (and adding implicit ones): if (obj.target?.$inferred === '') checkRedirectedUserTarget( art ); if (!ignoreSpecifiedElements && art.elements$ && art.elements) { for (const id in art.elements$) { resolveRefs( art.elements$[id] ); checkSpecifiedElement( art.elements[id], art.elements$[id] ); } } // Set '@Core.Computed' in the Core Compiler to have it propagated... if (art.kind !== 'element' || art['@Core.Computed']) return; // For events and types, elements can't be @Core.Computed, as values are only used // to infer the element signature. For virtual, we keep @Core.Computed, as it's // always been that way, even before type projections. const elementsCanBeComputed = art._main?.kind !== 'type' && art._main?.kind !== 'event'; if (art.virtual?.val || elementsCanBeComputed && art.value && (!art.value._artifact || !art.value.path || // in localization view: _artifact, but no path art.value.stored?.val || // calculated elements on-write are always computed art.value._artifact.kind === 'builtin' || art.value._artifact.kind === 'param' || art.value.scope === 'param' )) { art['@Core.Computed'] = { name: { path: [ { id: 'Core.Computed', location: art.location } ], location: art.location, }, $inferred: '$generated', }; } if (art.kind === 'element' && art._effectiveType) checkLocalizedElement( art ); return; /** * Check whether the signature of the specified element matches that of the inferred one. * * TODO: resolveRefs() is already too long → do not add sub functions * * TODO: * - This function has a lot of quite similar code blocks; it should be refactored to * combine them. * - Some checks are not performed because of to.sql() backend "bugs", that affect the * recompilation, such as flattening removing/not setting "key" where required. * * @param {XSN.Element} inferredElement * @param {XSN.Element} specifiedElement * @param {XSN.Element} user Only used for if specifiedElement is actually an `items` */ function checkSpecifiedElement( inferredElement, specifiedElement, user = specifiedElement ) { if (!inferredElement || !specifiedElement) return; // Check explicit types: If either side has one, so must the other. const sType = specifiedElement.type?._artifact; const iTypeArt = getInheritedProp( inferredElement, 'type' )?._artifact; const iType = iTypeArt || inferredElement; // FIXME: The coding above returns incorrect iType for expand on associations // $enclosed: maybe composition was changed to association; we allow that change here. const compToAssoc = sType === model.definitions['cds.Association'] && inferredElement.target; // xor: could be missing a type; if (!specifiedElement.type && inferredElement.type) { error( 'query-mismatched-element', [ specifiedElement.location, user ], { '#': !specifiedElement.type ? 'missing' : 'extra', name: user.name.id, prop: 'type', } ); return; } // If specified type is `null`, type could not be resolved. else if (!compToAssoc && sType && sType !== iType && // Special case for $recompilation: allow one level of type indirection. See #12113. (!options.$recompile || sType !== iType.type?._artifact)) { const typeName = !iTypeArt && 'typeExtra' || // no inferred type prop iType?.name && sType?.name && 'typeName' || // both types are named 'type'; // unknown type names const othertype = typeName !== 'type' && iType || ''; error( 'query-mismatched-element', [ specifiedElement.type.location || specifiedElement.location, user, ], { '#': typeName, name: user.name.id, type: sType, othertype, } ); return; } // This relies on (element) expansion! Check that both sides have the following properties. // On the inferred side, they are likely expanded. if (!hasXorPropMismatch( 'elements' ) && !hasXorPropMismatch( 'items' ) && !hasXorPropMismatch( 'target' ) && !hasXorPropMismatch( 'enum' )) { // Element are already traversed via elements$ merging. // only check items, if the specified one is not expanded/inferred if (specifiedElement.items && !specifiedElement.items.$inferred) checkSpecifiedElement( inferredElement.items, specifiedElement.items, specifiedElement ); if (specifiedElement.target?._artifact && inferredElement.target?._artifact && specifiedElement.target._artifact !== inferredElement.target._artifact) { error( 'query-mismatched-element', [ specifiedElement.target.location || specifiedElement.location, user, ], { '#': 'target', name: user.name.id, target: specifiedElement.target, art: inferredElement.target, } ); } if (specifiedElement.foreignKeys) { const sKeys = Object.keys( specifiedElement.foreignKeys ); /** @type {any} */ let iAssoc = inferredElement; if (inferredElement._effectiveType !== 0) { while (iAssoc._origin && !iAssoc.foreignKeys && !iAssoc.on) iAssoc = iAssoc._origin; } const iKeys = Object.keys( iAssoc.foreignKeys || {} ); const loc = [ specifiedElement.foreignKeys[$location] || specifiedElement.location, user, ]; if (iAssoc.on) { error( 'query-mismatched-element', loc, { '#': 'unmanagedToManaged', name: user.name.id, } ); } else if (sKeys.length !== iKeys.length || sKeys.some( fkey => !iKeys.includes( fkey ) )) { error( 'query-mismatched-element', loc, { '#': 'foreignKeys', name: user.name.id, } ); } } if (specifiedElement.virtual) { const iVirtual = getInheritedProp( inferredElement, 'virtual' )?.val || false; if (!specifiedElement.virtual.val !== !iVirtual) { error( 'query-mismatched-element', [ specifiedElement.virtual.location || specifiedElement.location, user, ], { '#': 'prop', prop: 'virtual', name: user.name.id, } ); } } // If cardinality is not specified, the compiler uses the inferred one. if (specifiedElement.cardinality) { // Users can change the origin's cardinality via filter: We can't rely on the origin. const ref = inferredElement.value?.path; const assocFilterCardinality = ref?.[ref.length - 1]?.cardinality; const sCardinality = specifiedElement.cardinality; const iCardinality = assocFilterCardinality || getInferredCardinality(); if (!iCardinality) { error( 'query-mismatched-element', [ sCardinality.location || specifiedElement.location, user, ], { '#': 'extra', prop: 'cardinality', name: user.name.id, } ); } else { // Note: Cardinality does not have sourceMin (CSN "srcmin"). const props = { targetMax: 'max', targetMin: 'min', sourceMax: 'src', }; for (const prop in props) { if (sCardinality[prop]?.val === iCardinality[prop]?.val) continue; error( 'query-mismatched-element', [ sCardinality[prop]?.location || sCardinality.location || specifiedElement.location, user, ], { // eslint-disable-next-line no-nested-ternary '#': !sCardinality[prop] ? 'missing' : (iCardinality[prop] ? 'prop' : 'extra'), prop: `cardinality.${ props[prop] }`, name: user.name.id, } ); } } } if (specifiedElement.value) { error( 'query-unexpected-property', [ specifiedElement.value.location || specifiedElement.location, user, ], { '#': 'calculatedElement', prop: 'value', name: user.name.id, } ); } if (specifiedElement.key) { // TODO: `|| inferredElement.key?.val`, once to.sql is fixed // TODO: Do not use _origin chain for key; has been propagated in propagateKeyProps(). const iKey = getInheritedProp( inferredElement, 'key' )?.val; // If "key" is specified or truthy in the inferred element, the values must match. if (!iKey !== !specifiedElement.key?.val) { error( 'query-mismatched-element', [ specifiedElement.key?.location || specifiedElement.location, user, ], { '#': specifiedElement.key ? 'prop' : 'missing', prop: 'key', name: user.name.id, } ); } } if (specifiedElement.enum && !specifiedElement.$expand) { // TODO: ".value" is necessary due to recompilation: The compiler does not copy // "enum" out of ".value", i.e. casts, only "type", changing the _effectiveType. const iEnumValues = inferredElement.enum || inferredElement.value?.enum; const sEnumValues = specifiedElement.enum; for (const name in specifiedElement.enum) { // TODO: See TODO above; issue is cast() const sEnumEntry = sEnumValues[name]; const iEnumEntry = iEnumValues[name]?._effectiveType || iEnumValues[name]; if (!iEnumEntry) { error( 'query-mismatched-element', [ specifiedElement.location, user ], { '#': 'enumExtra', name: user.name.id, id: name, } ); break; } else { // We allow implicit `val: "<name>"`. const iVal = iEnumEntry.value?.val || iEnumEntry.value?.['#'] || name; const sVal = sEnumEntry.value?.val || sEnumEntry.value?.['#'] || name; if (iVal !== sVal) { error( 'query-mismatched-element', [ specifiedElement.location, user ], { '#': 'enumVal', name: user.name.id, id: name, } ); break; } } } } } function hasXorPropMismatch( prop ) { // FIXME: `.value` check should be removed after #11183 // It appears the SQL backends expand a type in cast and add an `enum` property there. // This property is directly in the `value` property, but not part of `inferredElement` // which has a `type` property, but no `enum`. if (!inferredElement[prop] !== !specifiedElement[prop] && !inferredElement.value?.[prop] !== !specifiedElement[prop]) { error( 'query-mismatched-element', [ specifiedElement.location, specifiedElement ], { '#': specifiedElement[prop] ? 'extra' : 'missing', name: user.name.id, prop, } ); return true; } return false; } function getInferredCardinality() { let element = inferredElement; if (element._effectiveType !== 0) { while (getOrigin( element )) { const ref = element.value?.path; if (element.cardinality || ref?.[ref.length - 1]?.cardinality) break; element = getOrigin( element ); } } const ref = element.value?.path; return element.cardinality || ref?.[ref.length - 1]?.cardinality; } } } /** * Issue warnings for restrictions concerning `localized`, i.e. for situations * where a (later) inherited `localized` does not lead to the texts entity * (element) being created, because the inherited info was not available then, * or would involve more work (localized sub elements). */ function checkLocalizedElement( art ) { const parent = art._parent; if (!parent) return; // with duplicate defs const isSubElem = (parent.kind === 'element' || art._outer); if (isSubElem) { // sub element or in MANY // Localized sub elements in types, aspects, parameters and non-query // entities are not problematic. They are just not really useful there → // just report direct (not inherited) `localized` usage in non-inferred // elements then. For non-query entities, always report. if (art._main?.kind !== 'entity' || art._main?.query || userParam( parent ) ? !art.$inferred && art.localized?.val && art._main?.kind !== 'annotation' : getInheritedProp( art, 'localized' )) { const loc = (art.localized || art.type || art.value)?.location || art.location; warning( 'type-unsupported-localized', [ loc, art ], {}, 'Localized sub elements are not supported' ); } } else if (parent.kind === 'entity' && !art._main?.query && art.$syntax !== 'calc' && getInheritedProp( art, 'localized' )?.val && // no inherited `localized` which wasn't known in generate.js // TODO: should we set `localized` to null otherwise? !hasTruthyProp( art, 'localized' )) { const loc = (art.localized || art.type)?.location || art.location; warning( 'type-missing-localized', [ loc, art ], { keyword: 'localized' }, 'Add keyword $(KEYWORD), can\'t derive early enough that the element is localized' ); } } function checkCalculatedElement( art ) { const loc = [ art.value.location, art ]; if (art._parent.kind === 'element') { // TODO: Support calculated elements in structures. // The checks below are already aware of those. message( 'def-unsupported-calc-elem', loc, { '#': 'nested' } ); } const allowedInKind = [ 'entity', 'aspect', 'element' ]; let parent = art._parent; while (parent.kind === 'element') parent = parent._parent; if (!allowedInKind.includes( art._main.kind )) { if (art.$inferred === 'include') { // even for include-chains, we find the correct ref due to element-expansion. const include = art._main.includes.find( i => i._artifact === art._origin._main ); error( 'ref-invalid-calc-elem', [ include.location || art.value.location, art ], { '#': art._main.kind } ); } else { error( 'def-invalid-calc-elem', loc, { '#': art._main.kind } ); } } else if (!allowedInKind.includes( parent.kind )) { error( 'def-invalid-calc-elem', loc, { '#': parent.kind } ); } else if (effectiveType( art )?.elements && !art.$inferred) { // For inferred (e.g. included) calc elements, this error is already emitted at the origin. if (art.type) { error( 'type-unexpected-structure', [ art.type.location, art ], { '#': 'calc' } ); } else { error( 'ref-unexpected-structured', [ art.value.location, art ], { '#': 'struct-expr', elemref: art.value } ); } } else if (effectiveType( art )?.items && !art.$inferred) { // For inferred (e.g. included) calc elements, this error is already emitted at the origin. const isCast = art.type?.$inferred === 'cast'; error( 'type-unexpected-many', [ (art.type || art.value).location, art ], { '#': (!art.type && 'calc-implicit') || (isCast && 'calc-cast') || 'calc', elemref: art.type ? undefined : { ref: art.value.path }, } ); } else { const noTruthyAllowed = [ 'key', 'virtual' ]; for (const prop of noTruthyAllowed) { if (art[prop]?.val) { // probably better than a parse error (which is good for DEFAULT vs calc), // also appears with parse-cdl: error( 'def-invalid-calc-elem', loc, { '#': prop } ); return; // one error is enough } } } } /** * Return type containing the assoc spec (keys, on); note that no * propagation/rewrite has been done yet, cyclic dependency must have been * checked before! */ function getAssocSpec( type ) { let unmanaged = null; while (type) { if (type.on) // if unmanaged, continue trying to find targetAspect unmanaged = type; else if (type.foreignKeys || type.targetAspect) return type; else if (type.value?.path?.[type.value.path.length - 1]?.where) return { $assocFilter: true }; // filter -> always unmanaged type = getOrigin( type ); } return unmanaged; } function inferTypePropertiesFromCast( elem ) { for (const prop of typeParameters.list) { if (elem.value[prop]) elem[prop] = { ...elem.value[prop], $inferred: 'cast' }; } } // Phase 4 - queries and associations -------------------------------------- function resolveQuery( query ) { if (!query._main || !query._effectiveType) // parse error return; // TODO: or set silent dependencies in init? forEachGeneric( query, 'elements', elem => dependsOnSilent( query, elem ) ); forEachGeneric( query, '$tableAliases', ( alias ) => { if (alias.kind === 'mixin') resolveRefs( alias ); // mixin element else if (alias.kind !== '$self') // pure path has been resolved, resolve args and filter now: resolveExpr( alias, 'from', query._parent ); } ); for (const col of query.$inlines) resolveExpr( col.value, 'column', col ); // for (const col of query.$inlines) // if (!col.value.path) throw new CompilerAssertion(col.name.element) if (query !== query._main._leadingQuery) // will be done later forEachGeneric( query, 'elements', resolveRefs ); if (query.from) resolveJoinOn( query.from ); if (query.where) resolveExpr( query.where, 'where', query ); if (query.groupBy) resolveBy( query.groupBy, 'groupBy', 'groupBy' ); resolveExpr( query.having, 'having', query ); if (query.$orderBy) // ORDER BY from UNION: // TODO clarify: can I access the tab alias of outer queries? If not: // 4th arg query._main instead query._parent. resolveBy( query.$orderBy, 'orderBy-set-ref', 'orderBy-set-expr' ); if (query.orderBy) { // ORDER BY // search in `query.elements` after having checked table aliases of the current query resolveBy( query.orderBy, 'orderBy-ref', 'orderBy-expr' ); // TODO: disallow resulting element ref if in expression! // Necessary to check it in the compiler as it might work with other semantics on DB! // (we could downgrade it to a warning if name is equal to unique source element name) // TODO: Some helping text mentioning an alias name would be useful } for (const limit of query.$limit || []) // LIMIT from UNION: resolveLimit( limit ); if (query.limit) resolveLimit( query.limit ); return; function resolveJoinOn( join ) { if (join && join.args) { // JOIN for (const j of join.args) resolveJoinOn( j ); if (join.on) resolveExpr( join.on, 'join-on', join ); } } /** * Note the strange name resolution (dynamic part) for ORDER BY: the same * as for select items if it is an expression, but first look at select * item alias (i.e. like `$projection.NAME` if it is a path. If it is an * ORDER BY of an UNION, do not allow any dynamic path in an expression, * and only allow the elements of the leading query if it is a path. * * This seems to be similar, but different in SQLite 3.22.0: ORDER BY seems * to bind stronger than UNION (see <SQLite>/src/parse.y), and the name * resolution seems to use select item aliases from all SELECTs of the * UNION (see <SQLite>/test/tkt2822.test). */ function resolveBy( array, refMode, exprMode ) { for (const value of array ) { if (value) resolveExpr( value, (value.path ? refMode : exprMode), query ); } } function resolveLimit( limit ) { if (limit.rows) resolveExpr( limit.rows, 'limit-rows', query ); if (limit.offset) resolveExpr( limit.offset, 'limit-offset', query ); } } function resolveTarget( art, obj ) { if (art !== obj && obj.on) { // Unmanaged assoc inside items. Unmanaged assoc in param handled in resolveRefs() message( 'type-invalid-items', [ obj.on.location, art ], { '#': 'assoc', prop: 'items' } ); setArtifactLink( obj.target, undefined ); return; } const target = resolvePath( obj.target, 'target', art ); if (obj._columnParent && obj.type && !obj.type.$inferred && art._main && art._main.query) { // New association inside expand/inline: The on-condition can't be properly checked, // so abort early. See #8797 error( 'query-unexpected-assoc', [ obj.name.location, art ], {}, 'Unexpected new association in expand/inline' ); return; // avoid subsequent errors } if (obj.on) { if (!art._main || !art._parent.elements && !art._parent.items && !art._parent.targetAspect) { // TODO: test of .items a bit unclear - we should somehow restrict the // use of unmanaged assocs in MANY, at least with $self // TODO: $self usage in anonymous aspects to be corrected in Core Compiler message( 'assoc-as-type', [ obj.on.location, art ], { '#': compositionTextVariant( obj, 'comp' ) }, { std: 'An unmanaged association can\'t be defined as type', comp: 'An unmanaged composition can\'t be defined as type', } ); // TODO: also warning if inside structure } else { // if (obj.target._artifact) // TODO: extra with $inferred (to avoid messages)? resolveExpr( obj.on, art.kind === 'mixin' ? 'mixin-on' : 'on', art ); } } else if (art.kind === 'mixin') { error( 'assoc-in-mixin', [ obj.target.location, art ], {}, 'Managed associations are not allowed for MIXIN elements' ); return; // avoid subsequent errors } else if (obj.type && !obj.type.$inferred && art._parent && art._parent.kind === 'select') { // New association in views, i.e. parent is a query. error( 'query-expected-on-condition', [ obj.target.location, art ], {}, 'Expected ON-condition for published association' ); return; // avoid subsequent errors } else if (target && !obj.foreignKeys && target.kind === 'entity') { // redirected or explicit type cds.Association, ... if (obj.type?._artifact?.internal) addImplicitForeignKeys( art, obj, target ); } if (target && !target.$inferred) { if (!obj.type || obj.type.$inferred || obj.target.$inferred) { // REDIRECTED resolveRedirected( art, target ); } } } function checkRedirectedUserTarget( art ) { const issue = { target: art.target._artifact }; const tgtPath = art.target.path; const modelTarget = tgtPath[tgtPath.length - 1]._artifact; // Array#at comes with node-16.6 // Check ON condition: no renamed target element traverseExpr( art.on, 'on-check', art, (expr) => { const { path } = expr; if (!expr?._artifact || path?.length < 2 || issue['#']) return traverseExpr.SKIP; // no path or with error or already found issue const head = (path[0]._navigation?.kind === '$self') ? 1 : 0; if (path[head]._artifact === art) checkAutoRedirectedPathItem( path[head + 1], modelTarget, issue ); return traverseExpr.SKIP; } ); // Check explicit+implicit foreign keys: no renamed target element const implicit = art.foreignKeys?.[$inferred]; forEachGeneric( art, 'foreignKeys', (fkey) => { const { targetElement } = fkey; if (targetElement._artifact && !issue['#']) checkAutoRedirectedPathItem( targetElement.path[0], modelTarget, issue, implicit ); } ); // Check implicit foreign keys: same keys in same order if (implicit && !issue['#']) { const serviceKeys = keyElementNames( issue.target.elements ); const modelKeys = keyElementNames( modelTarget.elements ); if (modelKeys.length !== serviceKeys.length) { issue.id = modelKeys.find( id => !serviceKeys.includes( id ) ); issue['#'] = 'missing'; } else if (!modelKeys.every( (id, index) => id === serviceKeys[index] )) { issue['#'] = 'order'; } } if (issue['#']) message( 'type-expecting-service-target', [ art.target.location, art ], issue ); } function keyElementNames( elements ) { const names = []; for (const name in elements) { if (elements[name].key?.val) names.push( name ); } return names; } function checkAutoRedirectedPathItem( pathItem, modelTarget, issue, isKey = false ) { if (!pathItem) // $self.assoc return; let targetElem = pathItem._artifact; while (targetElem && targetElem._main !== modelTarget) targetElem = directOrigin( targetElem ); if (targetElem?.name.id === pathItem.id && (!isKey || targetElem.key?.val)) return; issue.id = pathItem.id; issue.line = pathItem.location.line; issue.col = pathItem.location.col; issue['#'] = (isKey ? 'key' : 'ref'); } function directOrigin( elem ) { if (!elem._main.query || !elem._origin) return elem._origin; // included element const { path } = elem.value; const kind = path[0]._navigation?.kind; // TODO: expand/inline (also Alias.*) return [ null, '$navElement', '$tableAlias' ][path.length] === (kind || true) && elem._origin; } fu