UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,218 lines (1,141 loc) 79.7 kB
// Extend 'use strict'; const { weakRefLocation } = require('../base/location'); const { searchName } = require('../base/messages'); const { isDeprecatedEnabled } = require('../base/specialOptions'); const { dictAdd, pushToDict, dictForEach } = require('./dictionaries'); const { kindProperties, dictKinds } = require('./base'); const { setLink, setArtifactLink, copyExpr, setExpandStatus, linkToOrigin, initItemsLinks, setMemberParent, createAndLinkCalcDepElement, initExprAnnoBlock, initDollarSelf, initBoundSelfParam, dependsOnSilent, pathName, annotationHasEllipsis, forEachInOrder, forEachDefinition, forEachMember, forEachGeneric, isDirectComposition, targetCantBeAspect, } = require('./utils'); const layers = require('./moduleLayers'); const { CompilerAssertion } = require('../base/error'); const { Location } = require('../base/location'); const { typeParameters } = require('./builtins'); const $location = Symbol.for( 'cds.$location' ); const $inferred = Symbol.for( 'cds.$inferred' ); // TODO: no $inferred yet? // attach stupid location - TODO: remove in v7 const genLocation = new Location( '' ); const draftElements = [ 'IsActiveEntity', 'HasActiveEntity', 'HasDraftEntity', 'DraftAdministrativeData', 'SiblingEntity', ]; const draftBoundActions = [ 'draftPrepare', 'draftActivate', 'draftEdit', ]; function canBeDraftMember( name, parent, draftMembers ) { return parent?.kind === 'entity' && parent._service && draftMembers.includes( name ); } function extend( model ) { // Get simplified "resolve" functionality and the message function: const { message, error, warning, info, } = model.$messageFunctions; const { resolvePath, resolveUncheckedPath, resolveTypeArgumentsUnchecked, resolveDefinitionName, attachAndEmitValidNames, checkRedefinition, } = model.$functions; Object.assign( model.$functions, { createRemainingAnnotateStatements, extendArtifactBefore, extendArtifactAfter, extendArtifactAdd, extendForeignKeys, withLocalizedData, applyIncludes, // TODO: re-check } ); const includesNonShadowedFirst = isDeprecatedEnabled( model.options, '_includesNonShadowedFirst' ); const includeCollisions = []; forEachGeneric( model, 'definitions', tagCompositionTargets ); dictForEach( model.$collectedExtensions, e => e._extensions.forEach( tagCompositionTargets ) ); // remark: tagging on extensions works _before_ running extendArtifactBefore() on each artifact sortModelSources(); // Set annotations on user-provided artifacts, but they are not propagated yet! // Use them in the main compiler phase (and before) with extra care: forEachDefinition( model, extendArtifactBefore ); return; // Tag composition targets: --------------------------------------------------- function tagCompositionTargets( elem ) { if (elem.$inferred) // TODO: probably no $inferred yet return; if (elem.targetAspect?.elements) elem = elem.targetAspect; if (elem.elements) { forEachGeneric( elem, 'elements', tagCompositionTargets ); } else if (elem.columns) { // `elem` is query or extension elem.columns.forEach( tagCompositionTargets ); } else if (elem.$queries) { for (const query of elem.$queries) { if (query.mixin) forEachGeneric( query, 'mixin', tagCompositionTargets ); if (query.columns) query.columns.forEach( tagCompositionTargets ); // Remark: no directly published expand/inline column yet } } else if (elem.target && isDirectComposition( elem )) { const name = resolveUncheckedPath( elem.target, 'target', elem ); if (!name) return; const target = model.definitions[name]; // move target aspect in `target` to `targetAspect` (in define.js, we only // do it with anonymous aspect) if (target?.kind in { aspect: 1, type: 1 } && // type is sloppy target.elements && !target.elements[$inferred] && !targetCantBeAspect( elem, false, model.definitions )) { // tests `!elem.targetAspect` elem.targetAspect = elem.target; delete elem.target; } model.$compositionTargets[name] = true; // does not hurt if set on aspect } } //----------------------------------------------------------------------------- // Extensions: general algorithm //----------------------------------------------------------------------------- // extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements, // extendForeignKeys /** * Goes through all (applied) annotations in the given artifact and chooses one * if multiple exist according to the module layer. * TODO: update comment if extension algorithm is finished * * Called at the beginning for each main artifact, and must not call * resolvePath() then. It is later called via effectiveType(). * * @param {XSN.Artifact} art */ function extendArtifactBefore( art ) { // for main artifacts, move extensions from `$collectedExtensions` model dictionary: if (!art._main && !art._outer && art._extensions === undefined && art.name && // TODO: probably just a workaround, check with TODO in getOriginRaw() art.kind !== 'namespace') { const { id } = art.name; setLink( art, '_extensions', model.$collectedExtensions[id]?._extensions || null ); if (art._extensions && !art.builtin) { // keep extensions for builtin in $collectedExtensions delete model.$collectedExtensions[id]; // TODO: if the extension mechanism has been completed, we could uncomment: // art._extensions.forEach( ext => resolvePath( ext.name, ext.kind, ext )); // for LSP // for now, we do that at the end of createRemainingAnnotateStatements() } } if (art.kind === 'entity' && art.includes && !art._entityIncludes) setEntityIncludes( art, art ); if (art._extensions) { // TODO: the following function can now be simplified // if (art.$inferred) console.log('CAI:', art.name, art.$inferred,art._extensions) // With extensions, member appears in CSN, affects directly the rendering of // elements etc. TODO: do that more specifically on the dicts (via symbol) // Probably better: we could use the _extensions dict prop directly in to-csn if (art.$inferred) setExpandStatus( art, 'annotate' ); if (Array.isArray( art._extensions )) { checkExtensionsKind( art._extensions, art ); transformArtifactExtensions( art ); } // console.log('EA:',require('../model/revealInternalProperties') // .ref(art),Object.keys(art._extensions)) for (const prop in art._extensions) { if (Object.hasOwn( art._extensions, prop ) && // remark: if we change the array, consider whether to delete the artifact ![ 'elements', 'actions', 'params', '$gen' ].includes( prop )) applyPropertyExtensions( art, prop ); } } } /** * Push down extensions on member properties like `elements` to the individual * members (setting their `_extensions` property). * Currently, definitions in `extend` members are ignored, * because they are handled a-priori by the old-style extension mechanism. * * Outside this file: only called in effectiveType() and indirectly in * tweak-assocs.js (for foreign keys and super-annotates). */ function extendArtifactAfter( art ) { // TODO: assert that we have not yet transformed/used _extensions on sub elements // TODO necessary(?): transformArtifactExtensions must ensure that each annotate // is in either returns,items,elements,enum if (art.builtin) // builtin members handled via "super annotate" return; // If elements are added in the future via includes or extend, it should be done here const extensionsMap = art._extensions; if (!extensionsMap) // builtin members handled via "super annotate" return; // type extensions after having “populated” the artifact ($typeArgs -> length, // …, TODO: do that there) and setting an _effectiveType: if (art.$typeExts) { const { type } = art; // if the type is not inferred, it is the origin... if (type?._artifact && !type.$inferred) // ...and thus is resolved resolveTypeArgumentsUnchecked( art, type._artifact, art ); const exts = art.$typeExts; applyTypeExtensions( art, exts.length, 'length' ); const scaleDiff = applyTypeExtensions( art, exts.scale, 'scale' ); applyTypeExtensions( art, exts.precision, 'precision', scaleDiff ); applyTypeExtensions( art, exts.srid, 'srid' ); checkPrecisionScaleExtension( art, exts ); delete art.$typeExts; } moveDictExtensions( art, extensionsMap, 'actions' ); moveDictExtensions( art, extensionsMap, 'params' ); if (!extensionsMap.elements) return; // after populateArtifact, it is clear which properties the artifact has: const artProp = art.kind === 'action' || art.kind === 'function' ? 'returns' : [ 'returns', 'targetAspect', 'items' ].find( p => art[p] ); if (artProp) { const returnsDict = artProp === 'returns' && !art.returns && annotateFor( art, 'params', '' ); // create anno for non-existing returns for (const ext of extensionsMap.elements) { if (!ext.returns) { pushToDict( returnsDict || art[artProp], '_extensions', ext ); if (artProp === 'returns') extendHandleReturns( ext, art ); } else { pushToDict( returnsDict || art[artProp], '_extensions', ext.returns ); if (!art.returns) checkReturnsExtension( ext, art ); } } // TODO: what about `many many Type` (via CSN)? } else if (!art.target) { // TODO: foreign keys currently handled specially for (const ext of extensionsMap.elements) { if (ext.returns) checkReturnsExtension( ext, art ); } // if (art.elements || art.enum || art.kind === 'annotate') moveDictExtensions( art, extensionsMap, (art.enum ? 'enum' : 'elements'), false ); } } /** * Apply foreign key extensions. Because foreign keys are handled late in the compiler * (in tweak-assocs.js), we can't apply them in effectiveType(), yet. * Instead, we postpone applying them until all foreign keys were generated. * * @param art */ function extendForeignKeys( art ) { // See extendArtifactAfter() for targetAspect/items handling. if (!art._extensions || art.items || art.targetAspect?.elements) return; // push down foreign keys moveDictExtensions( art, art._extensions, 'foreignKeys', 'elements' ); if (!art.foreignKeys) return; forEachGeneric(art, 'foreignKeys', (key) => { if (!key._effectiveType) throw new CompilerAssertion('foreign key should have been processed'); extendArtifactBefore( key ); extendArtifactAfter( key ); }); } /** * Applying extensions is handled in extendArtifactAfter(). And only afterward, * an effective sequence number is set. Meaning that if a sub-artifact already * has a sequence number, then extensions would be lost. * * A special case are foreign keys, see extendForeignKeys(). */ function ensureArtifactNotProcessed( art ) { if (!model.options.testMode) return; if (art.kind !== 'key' && art.$effectiveSeqNo !== 0 && art.$effectiveSeqNo !== undefined) { // if the artifact already has a sequence number, then // extendArtifactAfter() was already called -> annotations would be lost. throw new CompilerAssertion('artifact already processed; extensions would be lost'); } } /** * Create super annotate statements for remaining extensions */ function createRemainingAnnotateStatements() { model.extensions = Object.values( model.$collectedExtensions ); // TODO: testMode sort? model.extensions.forEach( createSuperAnnotate ); // set _artifact links for “main extensions” late as it would disturb the // still existing old extend mechanism, see extendArtifactBefore(), // needed for LSP and friends: Object.values( model.sources ).forEach( setArtifactLinkForExtensions ); Object.values( model.definitions ).forEach( setArtifactLinkForExtensions ); } // TODO: delete again - if not, what about extensions in contexts/services? // Check test.lsp-api.js! Links in extensions are needed. function setArtifactLinkForExtensions( source ) { if (!source.extensions) return; for (const ext of source.extensions) { if (!ext.name?.id) continue; const { name } = ext; const { path } = name; if (name._artifact === undefined) { resolvePath( name, ext.kind, ext ); // induce error & for LSP } else if (model.options.lspMode && path?.[0]._artifact === undefined) { // we don't use resolvePath(…,'extend'), as that would add a dependency resolveDefinitionName( ext ); setArtifactLink( path[path.length - 1], name._artifact ); } } } // For extendArtifactBefore(): ------------------------------------------------ /** * Complain about invalid `extend service` and `extend context`. */ function checkExtensionsKind( extensions, art ) { for (const ext of extensions) { const kind = ext.expectedKind?.val; if (kind && kind !== art.kind) { const loc = ext.expectedKind.location; if (kind === 'context' || kind === 'service') { // We have no real artifact during the construction of a super-annotate statement: const msgArgs = { '#': (art.kind === 'service' || art.kind === 'annotate') ? art.kind : 'std', art, kind, code: 'extend … with definitions', keyword: 'extend service', }; // TODO v7: Discuss: make this an error? warning( 'ext-invalid-kind', [ loc, ext ], msgArgs, { std: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) instead', annotate: 'There is no artifact $(ART), use $(CODE) instead', // do not mention 'extend context', that is not in CAPire service: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) or $(KEYWORD) instead', } ); } // TODO: Use similar checks for EXTEND ENTITY etc - 'ext-ignoring-kind' } } } /** * Transform `art._extensions` from an array of extensions into an object * `{ prop: relevantExtensions, … }` where `relevantExtensions` are the extensions * which are relevant for the value of `art[prop]` after the application of extensions. * * Remark: it is not necessarily clear at this moment whether `art` has * `elements`, `enums`, `items`, etc. */ function transformArtifactExtensions( art ) { // TODO: if extensions has more than one of returns,items,elements,enum, delete all those props const dict = {}; for (const ext of art._extensions) { // `annotate SomeFunction with @action { r @elem };` would later be moved to // `someFunction.returns._extensions` due to `elements` → @action not relevant then: // (TODO: should we use some “already applied” flag instead?) const isAutoItemsOrReturns = art._outer || // in items or targetAspect art._parent?.returns === art && !ext._parent?.returns; for (const prop in ext) { if (!Object.hasOwn( ext, prop ) || ext[prop] === undefined) // deleted property continue; // TODO: do this check nicer (after complete move to new extensions mechanism) if (prop === 'includes') { pushToDict( dict, prop, ext ); pushTo$add( dict, ext ); } else if (prop.charAt(0) === '@' || prop === 'doc' || prop === 'columns' || prop === 'groupBy' || prop === 'where' || prop === 'having' || prop === 'orderBy' || prop === 'limit' || prop === 'length' || prop === 'scale' || prop === 'precision' || prop === 'srid') { if (!isAutoItemsOrReturns) pushToDict( dict, prop, ext ); } else if (prop === 'elements' || prop === 'enum') { if (ext.returns) { // TODO: currently, an annotate can have both // if this is a hard error, we could use syntax-unexpected-property#sibling message( 'syntax-unexpected-with-returns', [ ext[prop][$location] || ext.location, ext ], { prop, siblingprop: 'returns' }, // eslint-disable-next-line @stylistic/max-len 'Property $(PROP) of an annotate statement is ignored when it also has a property $(SIBLINGPROP)' ); } else { pushToDict( dict, 'elements', ext ); // yes, enum → elements here if (ext.kind === 'extend' && !isAutoItemsOrReturns) pushTo$add( dict, ext ); } } else if (prop === 'returns') { pushToDict( dict, 'elements', ext ); // create 'returns' for the super annotate, store in elements anyway if (!art.returns && art.kind === 'annotate') annotateCreate( art, '', art, 'returns' ); if (ext.kind === 'extend' && !isAutoItemsOrReturns) pushTo$add( dict, ext ); } else if (prop === 'actions' || prop === 'params') { pushToDict( dict, prop, ext ); if (ext.kind === 'extend' && prop === 'actions' && !isAutoItemsOrReturns) pushTo$add( dict, ext ); } } } art._extensions = dict; } /** * Sort sources according to the reversed layered extension order without * reporting any messages. * * The order of the CSN property `$sources` (from XSN `_sortedSources`) is * defined as follows: for _any_ model * * - add `type $Sources: String @(Names: []);` to one of the source files * - add `annotate $Sources with @Names: [..., ‹sourceName›]` to each source * file where ‹sourceName› is the file name of the source * - then the array value of `‹csn›.$sources` is the reverse of the array value * of `‹csn›.definitions.$Sources.@Names` */ function sortModelSources() { const scheduled = []; const layered = layeredExtensions( Object.values( model.sources ) ); for (;;) { const { highest } = extensionsOfHighestLayers( layered ); if (!highest.length) break; highest.reverse(); scheduled.push( ...highest ); } setLink( model, '_sortedSources', scheduled ); } /** * For all `prop → extensions` in `art._extensions`, apply the extensions. * Currently only for annotations, the `doc` property, `columns` and type properties, * not for `elements` and other members. */ function applyPropertyExtensions( art, prop ) { const extensions = art._extensions; // annotations, `doc`, `includes`, `columns`, `length`, ... const scheduled = []; // sort extensions according to layer (specified elements are bottom layer): const layered = layeredExtensions( extensions[prop] ); let cont = true; while (cont) { const { highest, issue } = extensionsOfHighestLayers( layered ); // console.log( 'CA:', annoName, issue, extensions) let index = highest.length; cont = !!index; // safety while (--index >= 0) { const ext = highest[index]; scheduled.push( ext ); if (extensionOverwrites( ext, prop )) { cont = false; break; } } if (issue || index > 0) reportDuplicateExtensions( highest, prop, issue, index, art ); } // Now apply the relevant extensions scheduled.reverse(); if (prop === 'includes' && !art.includes?.$original) { const $original = art.includes ?? []; art.includes = [ ...$original ]; art.includes.$original = $original; } if (prop === '$add') { extensions[prop] = scheduled; return; // the $add is applied in extendArtifactAdd() } for (const ext of scheduled) applySingleExtension( art, ext, prop ); delete extensions[prop]; } function extensionOverwrites( ext, prop ) { return (prop.charAt(0) !== '@') ? (prop === 'doc' || typeParameters.list.includes(prop)) : !annotationHasEllipsis( ext[prop] ); } // TODO: still a bit annotation assignment specific function reportDuplicateExtensions( extensions, prop, issue, index, art ) { // TODO: think about messages for these if (prop === 'elements' || prop === 'actions' || prop === 'columns' || prop === 'params' || prop === '$add' ) return; // extensions currently handled extra // TODO: columns? if (issue) { // eslint-disable-next-line no-nested-ternary let msg = (index < 0) ? 'anno-unstable-array' : (issue === true) ? 'anno-duplicate' : 'anno-duplicate-unrelated-layer'; if (prop.charAt(0) !== '@' && prop !== 'doc') { msg = (issue === true) ? 'ext-duplicate-extend-type' : 'ext-duplicate-extend-type-unrelated-layer'; // not sure whether to repeat the extended artifact in the message (we // have the semantic location, after all) // extend-repeated-intralayer / extend-unrelated-layer } const variant = prop === 'doc' ? 'doc' : 'std'; for (const ext of extensions) { const anno = ext[prop]; if (anno && !anno.$errorReported) { message( msg, [ anno.name?.location || anno.location, ext ], { '#': variant, anno: prop, type: art } ); } } } else if (index > 0) { // more than one set (not just ...) const variant = prop === 'doc' ? 'doc' : 'std'; const msgid = (prop.charAt(0) === '@' || prop === 'doc') ? 'anno-duplicate-same-file' // TODO: always ext-duplicate-… : 'ext-duplicate-same-file'; while (index >= 0) { // do not report for trailing [...] const ext = extensions[index--]; const anno = ext[prop]; warning( msgid, [ anno.name?.location || anno.location, ext ], { '#': variant, prop, anno: prop } ); } } } function applySingleExtension( art, ext, prop ) { if (prop === 'includes') { if (art.kind !== 'annotate' && !art._outer) { // TODO: why this check? // not with elem extension in targetAspect art.includes.push( ...ext.includes ); // head of ref in in ext.includes must be set in _block of ext; for // remaining path items, effectiveType() has `art` as user // (alternatively, we could set some _user on `includes` items): for (const ref of ext.includes) resolveUncheckedPath( ref, 'include', ext ); if (art.kind === 'entity') setEntityIncludes( ext, art ); // console.log( 'ASI:',prop,art.name,ext,extensionsDict[id]) } // art[prop] = (art[prop]) ? art[prop].concat( ext[prop] ) : ext[prop]; } else if (prop === 'columns') { const { query } = art; for (const col of ext.columns) col.$extended = 'columns'; if (art.kind === 'annotate' && art.$inferred === '') return; // internal super-annotate for unknown artifacts if (!query?.from?.path) { const variant = (query?.from || query)?.op?.val || 'std'; error( 'extend-columns', [ ext.columns[$location], ext ], { '#': variant, art } ); return; } if (!query.columns) query.columns = [ { location: query.from.location, val: '*' }, ...ext.columns ]; else query.columns.push( ...ext.columns ); ext.columns.forEach( col => changeParentLinks( col, query ) ); } else if (prop === 'groupBy' || prop === 'where' || prop === 'having' || prop === 'orderBy' || prop === 'limit') { applyQueryClause( prop, ext, art ); } else if (typeParameters.list.includes( prop )) { const typeExts = art.$typeExts || (art.$typeExts = {}); typeExts[prop] = ext; } else { const result = applyAssignment( art[prop], ext[prop], ext, prop ); art[prop] = (result.name) ? result : Object.assign( {}, art[prop], result ); } } function applyQueryClause( prop, ext, art ) { const { query } = art; const clause = ext[prop]; const isArray = Array.isArray( clause ); if (prop !== 'limit') { const items = isArray ? clause : [ clause ]; for (const item of items) { item.$extended = prop; setLink( item, '_block', ext._block ); setLink( item, '_outer', query ); } } if (!query?.from?.path) { const variant = (query?.from || query)?.op?.val || 'std'; const loc = isArray ? clause[$location] : clause.location; error( `extend-${ prop.toLowerCase() }`, [ loc, ext ], { '#': variant, art } ); return; } if (isArray) { if (!query[prop]) query[prop] = []; query[prop].push( ...clause ); } else { if (query[prop]) { error( 'ext-unexpected-sql-clause', [ clause.location, ext ], { art, keyword: prop } ); return; } query[prop] = clause; } } function changeParentLinks( art, queryOrMain ) { // TODO: we might also change the implicit name (if name.id is a number, // adding the previous column lenght - 1) for better error messages const parent = art._parent; if (!art._parent) return; if (parent.kind === 'extend') art._parent = queryOrMain; if (art._main.kind === 'extend') // TODO: probably always art._main = queryOrMain._main; if (art._columnParent?.kind === 'extend') art._columnParent = queryOrMain; const subColumns = art.expand || art.inline; if (subColumns) subColumns.forEach( a => changeParentLinks( a, queryOrMain ) ); forEachMember( art, a => changeParentLinks( a, queryOrMain ) ); } function applyAssignment( previousAnno, anno, art, annoName ) { const firstEllipsis = annotationHasEllipsis( anno ); if (!firstEllipsis) return anno; const hasBase = previousAnno?.literal === 'array'; if (!previousAnno) { const { location } = anno.name; if (annoName !== '@extension.code') { // Remark: we could allow that for all annotations which are not propagated message( 'anno-unexpected-ellipsis', [ firstEllipsis.location || location, art ], { code: '...' } ); } previousAnno = { kind: '$annotation', val: [], literal: 'array', name: anno.name, location, }; } else if (previousAnno.literal !== 'array') { // TODO: If we introduce sub-messages, point to the non-array base value. error( 'anno-mismatched-ellipsis', [ anno.name.location, art ], { code: '...' } ); previousAnno = { kind: '$annotation', val: [], literal: 'array', name: previousAnno.name, location: previousAnno.location, }; } const previousValue = previousAnno.val; let prevPos = 0; const result = []; for (const item of anno.val) { const ell = item && item.literal === 'token' && item.val === '...'; if (!ell) { result.push( item ); } else { let upToSpec = item.upTo && checkUpToSpec( item.upTo, art, annoName, true ); while (prevPos < previousValue.length) { const prevItem = previousValue[prevPos++]; result.push( prevItem ); if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo )) { upToSpec = false; break; } } if (upToSpec && hasBase) { // non-matched UP TO; if there is no base to apply to, there is already an error. warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' }, 'The $(CODE) value does not match any item in the base annotation $(ANNO)' ); } } } // console.log('TP:',previousValue.map(se),anno.val.map(se),'->',result.map(se)) return { kind: '$annotation', val: result, literal: 'array', name: previousAnno.name, location: previousAnno.location, }; } // function se(a) { return a.upTo ? [a.val,a.upTo.val] : a.val ; } function checkUpToSpec( upToSpec, art, annoName, isFullUpTo ) { const { literal } = upToSpec; if (!isFullUpTo) { // inside struct of UP TO if (literal !== 'struct' && literal !== 'array' ) return true; } else if (literal === 'struct') { return Object.values( upToSpec.struct ).every( v => checkUpToSpec( v, art, annoName ) ); } else if (literal !== 'array' && literal !== 'boolean' && literal !== 'null') { return true; } error( null, [ upToSpec.location, art ], { anno: annoName, code: '... up to', '#': literal }, { std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)', array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)', // eslint-disable-next-line @stylistic/max-len struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)', boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)', null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)', } ); return false; } function equalUpTo( previousItem, upToSpec ) { if (!previousItem) return false; if ('val' in upToSpec) { if (previousItem.val === upToSpec.val) // enum, struct and ref have no val return true; // TODO v6: delete the special UP TO comparison? const upToVal = upToSpec.val; const prevVal = previousItem.val; // eslint-disable-next-line eqeqeq return prevVal == upToVal && ( typeof upToVal === 'number' && stringCouldHaveBeenCdlNumber( prevVal ) || typeof prevVal === 'number' && stringCouldHaveBeenCdlNumber( upToVal ) ); } else if (upToSpec.path) { return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec ); } else if (upToSpec.sym) { return previousItem.sym && previousItem.sym.id === upToSpec.sym.id; } else if (upToSpec.struct && previousItem.struct) { return Object.entries( upToSpec.struct ) .every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) ); } return false; } // We only compare a string by number if the string is not empty, and could have // been produced for a CDL number by (a previous version of) the compiler, // i.e. having used a decimal dot, or using the scientific notation: function stringCouldHaveBeenCdlNumber( val ) { // also consider previous compiler versions return val && typeof val === 'string' && /[.eE]/.test( val ); // We do not use `!Number.isSafeInteger( Number.parseFloat( text||'0' )` // because it is unlikely that people have written a non-integer like this, // more likely is meant a digit-sequence as string } function normalizeRef( node ) { // see to-csn.js const ref = pathName( node.path ); // TODO: get rid of name.variant (induces a wrong structure anyway) return node.variant ? `${ ref }#${ pathName( node.variant.path ) }` : ref; } // For extendArtifactAfter(): ------------------------------------------------- // Remarks on messages: we allow the type extensions only if the artifact // originally had that property → any check of the kind “type prop can only be // used with FooBar” is independent from `extend … with type`. Function // checkTypeArguments() in resolve.js reports 'type-unexpected-argument', but // that is currently incomplete. // // We then report (in the future), use the first message of: // - the usual messages if a type argument is wrong, independently from `extend` // - 'ext-unexpected-type-argument' (TODO) if the artifact does not have the prop // - 'ext-invalid-type-argument' if the value is wrong for extend (no overwrite) // // TODO v6: do not allow `extend … with (precision: …)` alone if original def also has `scale` function applyTypeExtensions( art, ext, prop, scaleDiff ) { // console.log('ATE:',art?.[prop],ext?.[prop],scaleDiff) if (!ext?.[prop]) return 0; if (!art[prop]) { const isBuiltin = art._effectiveType?.builtin; if (isBuiltin && !allowsTypeArgument( art, prop )) { // Let checkTypeArguments() in resolve.js report a message, is incomplete // though, i.e. can only safely be used for scalars at the moment. But we // will improve that function and not try to do extra things here. art[prop] = ext[prop]; // enable checkTypeArguments() doing its job return 0; } // TODO: think about 'ext-unexpected-type-argument' error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': (isBuiltin ? 'indirect' : 'new-prop'), prop } ); return 0; } const artVal = art[prop].val; const extVal = ext[prop].val; if (prop === 'srid') { error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'prop', prop } ); } else if (typeof artVal !== 'number' || typeof extVal !== 'number' ) { // Users can't change from/to string value for property, // e.g. `variable`/`floating` for Decimal // TODO: Shouldn't the text distinguish between orig string and extension string? // Not sure whether to talk about strings if we have a keyword in CDL error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': 'string', prop } ); } else if (extVal < artVal + (scaleDiff || 0)) { const number = artVal + (scaleDiff || 0); error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { '#': (scaleDiff ? 'scale' : 'number'), prop, number, otherprop: 'scale', } ); } else { art[prop] = ext[prop]; return extVal - artVal; } return 0; } /** * If the target artifact has both precision and scale set, then extensions on it must also * provide both to avoid user errors for subsequent `extend` statements. * There is already a syntax error [syntax-missing-type-property] for `scale` without `precision`. * * @param {XSN.Artifact} art * @param {object} exts */ function checkPrecisionScaleExtension( art, exts ) { if (art.precision && art.scale) { if (exts.precision && !exts.scale) { error( 'ext-missing-type-property', [ exts.precision.location, exts.precision ], { art, prop: 'precision', otherprop: 'scale' } ); } } } function allowsTypeArgument( art, prop ) { const { parameters } = art._effectiveType; if (!parameters) return false; return parameters.includes( prop ) || parameters[0]?.name === prop; } function moveDictExtensions( art, extensionsMap, artProp, extProp = artProp ) { // TODO: setExpandStatus const extensions = extensionsMap[extProp || 'elements']; if (!extensions) return; for (const ext of extensions) { let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement forEachGeneric( ext, extProp || (ext.enum ? 'enum' : 'elements'), ( elemExt, name ) => { if (elemExt.kind !== 'annotate' && elemExt.kind !== 'extend') // TODO: specified elems return; // definitions inside extend, already handled dictCheck = dictCheck && checkRemainingMemberExtensions( art, elemExt, artProp, name ); const elem = art[artProp]?.[name] || annotateFor( art, extProp || 'elements', name ); setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null ) ); // TODO: why null for annotate? ensureArtifactNotProcessed( elem ); if (elem.$duplicates !== true) // TODO: re-check pushToDict( elem, '_extensions', elemExt ); }); } } function annotateFor( art, prop, name ) { const base = annotateBase( art ); if (name === '' && prop === 'params') return base.returns || annotateCreate( base, name, base, 'returns' ); const dict = base[prop] || (base[prop] = Object.create( null )); if (name == null) return dict; return dict[name] || annotateCreate( dict, name, base ); } function annotateBase( art ) { while (art._outer) // TODO: think about anonymous target aspect art = art._outer; if (art.kind === 'annotate') return art; // TODO: more to do if annotate can have `returns` property if (art.kind === 'select') art = art._parent; if (art._main) return annotateFor( art._parent, kindProperties[art.kind].dict, art.name.id ); const { id } = art.name; return model.$collectedExtensions[id] || annotateCreate( model.$collectedExtensions, id ); } function annotateCreate( dict, id, parent, prop ) { const annotate = { kind: 'annotate', name: { id, location: genLocation }, $inferred: '', location: genLocation, }; if (parent) { setLink( annotate, '_parent', parent ); setLink( annotate, '_main', parent._main || parent ); } dict[prop || id] = annotate; return annotate; } function checkReturnsExtension( ext, art ) { const msgId = hasSecurityAnno( ext.returns ) ? 'ext-unexpected-returns-sec' : 'ext-unexpected-returns'; message( msgId, [ ext.returns.location, ext ], { '#': art.kind, keyword: 'returns' }, { std: 'Unexpected $(KEYWORD); only actions and functions have return parameters', action: 'Unexpected $(KEYWORD) for action without return parameter', // function without `returns` can happen via CSN input! TODO: check in parser function: 'Unexpected $(KEYWORD) for function without return parameter', } ); // Do not put completely wrong returns into a “super annotate” statement; // this could induce consequential errors with [..., …]: return art.kind === 'action' || art.kind === 'function'; } function extendHandleReturns( ext, art ) { warning( 'ext-expecting-returns', [ ext.name.location, ext ], { '#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }', }, { std: 'Expected $(CODE)', // unused variant action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)', function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)', // eslint-disable-next-line @stylistic/max-len annotate: 'Expected $(KEYWORD) when annotating a return structure of an unknown action or function, i.e. $(CODE)', } ); } function checkRemainingMemberExtensions( parent, ext, prop, name ) { // console.log('CRME:',prop,name,parent,ext) // TODO: just use `ext-undefined-element` etc also when no elements are there // at all (but use an extra text variant and the `{…}` location). Reason: we // might allow to add new actions, and an `annotate` on an undefined action // should not lead to another message id. We would use and extra message id // if we consider this an error or such sub annotates are then ignored // (i.e. not put into the "super annotate"). const dict = parent[prop]; const securityRelevant = hasSecurityAnno( ext ) ? '-sec' : ''; if (!dict && !securityRelevant) { // TODO: check - for each name? - better locations const location = ext._parent?.[prop]?.[$location] || ext.name.location; // Remark: no `elements` dict location with `annotate Main:elem` switch (prop) { // TODO: change texts, somehow similar to checkDefinitions() ? case 'foreignKeys': case 'elements': case 'enum': // TODO: extra? warning( 'anno-unexpected-elements', [ location, ext._parent ], { '#': (parent._effectiveType?.kind === 'entity') ? 'entity' : 'std' }, { std: 'Elements only exist in entities, types or typed constructs', entity: 'Elements of entity types can\'t be annotated', // TODO: extra msg for 'entity'? → this is some other // situation, somehow similar when trying to annotate elements // of target entity } ); break; case 'params': warning( 'anno-unexpected-params', [ location, ext._parent ], {}, 'Parameters only exist for actions or functions' ); break; case 'actions': if (canBeDraftMember( name, parent, draftBoundActions )) return true; // TODO: use extra text variant and location of dictionary - no notFound( 'ext-undefined-action', ext.name.location, ext, { '#': 'action', art: parent, name } ); break; default: if (model.options.testMode) throw new CompilerAssertion(`Missing case for prop: ${ prop }`); } return false; } else if (!dict?.[name]) { // TODO: make variant `returns` an auto-variant for ($ART) ? const inReturns = parent._parent?.returns; switch (prop) { case 'elements': if (canBeDraftMember( name, parent, draftElements )) break; notFound( `ext-undefined-element${ securityRelevant }`, ext.name.location, ext, { '#': (inReturns ? 'returns' : 'element'), name }, parent.elements ); break; case 'enum': // TODO: extra msg id? notFound( `ext-undefined-element${ securityRelevant }`, ext.name.location, ext, { '#': (inReturns ? 'enum-returns' : 'enum'), name }, parent.enum ); break; case 'foreignKeys': notFound( `ext-undefined-key${ securityRelevant }`, ext.name.location, ext, { name }, parent.foreignKeys ); break; case 'params': notFound( `ext-undefined-param${ securityRelevant }`, ext.name.location, ext, { '#': 'param', name }, parent.params ); break; case 'actions': if (canBeDraftMember( name, parent, draftBoundActions )) break; notFound( `ext-undefined-action${ securityRelevant }`, ext.name.location, ext, { '#': 'action', name }, parent.actions ); break; default: if (model.options.testMode) throw new CompilerAssertion(`Missing case for prop: ${ prop }`); } } return true; } function notFound( msgId, location, address, args, validDict ) { const msg = message( msgId, [ location, address ], args ); attachAndEmitValidNames( msg, validDict ); } // For createRemainingAnnotateStatements(): ----------------------------------- function createSuperAnnotate( annotate ) { const extensions = annotate._extensions; if (extensions && !annotate._main) { const art = model.definitions[annotate.name.id]; for (const ext of extensions) checkRemainingMainExtensions( art, ext ); // induce messages for extension path if (art?.builtin && art.kind !== 'namespace') { // TODO: do not set `builtin` on cds, cds.hana setLink( annotate, '_extensions', art._extensions ); // for messages and member extensions // direct annotations on builtins or on the builtins for propagation, and // also shallow-copied to $collectedExtensions for to-csn for (const prop in art) { if (prop.charAt(0) === '@' || prop === 'doc') annotate[prop] = art[prop]; } } if (extensions.length === 1) { // i.e. no proper location if from more than one extension annotate.location = extensions[0].location; annotate.name.location = extensions[0].name.location; } } extendArtifactBefore( annotate ); extendArtifactAfter( annotate ); forEachMember( annotate, createSuperAnnotate ); } function hasSecurityAnno( ext ) { return ext['@restrict'] || ext['@requires'] || Object.keys( ext ).some( prop => prop.startsWith( '@ams.' ) ); } function checkRemainingMainExtensions( art, ext ) { const refCtx = extensionRefContext( ext ); if (resolvePath( ext.name, refCtx, ext ) && art?.builtin) { if (ext.kind === 'extend') { // extending built-ins with elements/enums already gives an error warning( 'ext-unexpected-builtin', [ ext.name.location, ext ], {}, // error v8? 'Built-in types should not be extended' ); // keep the text general const typeProp = typeParameters.list.find( p => ext[p] ); if (typeProp) { const location = ext.$typeArgs?.[$location] || ext[typeProp].location; message( 'ext-unexpected-type-property', [ location, ext ], {}, // error v7 'Built-in types can\'t be extended with type properties' ); // see also 'ext-invalid-type-property' } } else { info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location? } // TODO: remove built-ins as CC candidates via accept property of ./shared.js } } function extensionRefContext( ext ) { if (ext.kind === 'annotate') { if (hasSecurityAnno( ext )) return 'annotate-sec'; } else if (ext.artifacts || // extend … with definitions ext._block.$frontend === 'json' && !ext.elements && !ext.actions) { // TODO: not for `extend context` and `extend service` → !ext.expectedKind // TODO v7: also fully with CSN input if (!ext.doc && !Object.keys( ext ).some( a => a.charAt(0) === '@') ) return 'annotate'; // TODO: or an extra refCtx ? } return ext.kind; } // Issue messages for annotations on namespaces and builtins // (TODO: really here?, probably split main artifacts vs returns) // see also createRemainingAnnotateStatements() where similar messages are reported function checkAnnotate( construct, art ) { // TODO: Handle extend statements properly: Different message for empty extend? // --> without art._block, art not found if (construct.kind === 'annotate' && art._block?.$frontend === 'cdl') { if (construct.returns && art.kind !== 'action' && art.kind !== 'function' ) { // See moveReturnsExtensions() } else if (!construct.returns && (art.kind === 'action' || art.kind === 'function') && construct.elements) { warning( 'ext-expecting-returns', [ construct.name.location, construct ], { '#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }', }, { std: 'Expected $(CODE)', // unused variant action: 'Expected $(KEYWORD) when annotating action return structure, i.e. $(CODE)', function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)', } ); } } } // extend, mainly old-style --------------------------------------------------- function extendArtifactAdd( art ) { const { includes } = art; if (includes) { if (includes.$original) // if extensions with includes: art.includes = includes.$original; // original includes have been stored if (art.includes?.length) applyIncludes( art, art ); art.includes = includes; // early propagation of specific annotation assignments // TODO: propagate in effectiveType() ? propagateEarly( art, '@cds.autoexpose' ); propagateEarly( art, '@fiori.draft.enabled' ); } if (art._extensions?.$add) extendArtifact( art._extensions.$add, art ); checkRedefinitionThroughIncludes( art, 'elements' ); checkRedefinitionThroughIncludes( art, 'actions' ); } /** * Extend artifact `art` by `extensions`. `cyclicIncludeNames` can have values: * - falsy: try to apply include, then perform extend and annotate * - an array of include names with cyclic dependencies: includes are not applied, * extend and annotate is performed * remark: we could have applied includes without cycle * * Returns true if extend and annotate are performed. * * @param {XSN.Extension[]} extensions * @param {XSN.Definition} art * @param {String[]|false} [cyclicIncludeNames=false] */ function extendArtifact( extensions, art ) { if (!art.query && !art._main && !art._outer) { // TODO: remove _entities model._entities.push( art ); // add structure with includes in dep order } // TODO: complain if $inferred // checkExtensionsKind( extensions, art ); extendMembers( extensions, art ); reportIncludeCollisions( art ); // TODO: complain about element extensions inside projection return true; } function reportIncludeCollisions( art ) { const grouped = Object.create( null ); for (const { prop, name, existing, elem, } of includeCollisions) { const key = `${ prop }:${ name }`; if (!grouped[key]) grouped[key] = { prop, name, collisions: new Set( [ existing ] ) }; grouped[key].collisions.add( elem ); } for (const key in grouped) { const { prop, name, collisions } = grouped[key]; const member = art[prop]?.[name]; if (me