UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,241 lines (1,160 loc) 73 kB
// Extend 'use strict'; const { weakRefLocation } = require('../base/location'); const { searchName } = require('../base/messages'); const { forEachInOrder, forEachDefinition, forEachMember, forEachGeneric, isDeprecatedEnabled, } = require('../base/model'); const { dictAdd, pushToDict } = require('../base/dictionaries'); const { kindProperties, dictKinds } = require('./base'); const { setLink, setArtifactLink, copyExpr, setExpandStatusAnnotate, linkToOrigin, initItemsLinks, setMemberParent, createAndLinkCalcDepElement, initExprAnnoBlock, initDollarSelf, initBoundSelfParam, dependsOnSilent, storeExtension, pathName, annotationHasEllipsis, } = 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' ); // attach stupid location - TODO: remove in v6 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, targetIsTargetAspect, checkRedefinition, initSelectItems, } = model.$functions; Object.assign( model.$functions, { createRemainingAnnotateStatements, extendArtifactBefore, extendArtifactAfter, extendForeignKeys, withLocalizedData, applyIncludes, // TODO: re-check } ); const includesNonShadowedFirst = isDeprecatedEnabled( model.options, '_includesNonShadowedFirst' ); sortModelSources(); const extensionsDict = Object.create( null ); // TODO TMP forEachDefinition( model, tagIncludes ); // TODO TMP forEachDefinition( model, extendArtifactBefore ); applyExtensions(); // old-style return; // TMP: function tagIncludes( art ) { if (art.includes) extensionsDict[art.name.id] = []; } //----------------------------------------------------------------------------- // 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 * * @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._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) setExpandStatusAnnotate( art, 'annotate' ); if (Array.isArray( art._extensions )) { checkExtensionsKind( art._extensions, art ); // TODO: check with builtins transformArtifactExtensions( art ); } applyAllExtensions( 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 function extendArtifactAfter( art ) { const extensionsMap = art._extensions; if (!extensionsMap || art.builtin) // 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; } if (art.kind === 'annotate' && !art.returns && extensionsMap.returns && !art._parent?.returns) annotateCreate( art, '', art, 'returns' ); moveDictExtensions( art, extensionsMap, 'actions' ); moveDictExtensions( art, extensionsMap, 'params' ); moveReturnsExtensions( art, extensionsMap ); if (art.returns) { ensureArtifactNotProcessed( art.returns ); pushToDict( art.returns, '_extensions', ...extensionsMap.elements || [] ); pushToDict( art.returns, '_extensions', ...extensionsMap.enum || [] ); if (art.kind !== 'annotate') { extendHandleReturns( extensionsMap.elements, art ); extendHandleReturns( extensionsMap.enum, art ); return; } } const sub = art.items || art.targetAspect?.elements && art.targetAspect; if (sub) { ensureArtifactNotProcessed( sub ); pushToDict( sub, '_extensions', ...extensionsMap.elements || [] ); pushToDict( sub, '_extensions', ...extensionsMap.enum || [] ); } else { let elementsProp = 'elements'; if (art.kind !== 'annotate') elementsProp = art.enum && 'enum' || art.target && 'foreignKeys' || 'elements'; // keys are handled in tweak-assocs.js; don't push them down; see extendForeignKeys() if (elementsProp !== 'foreignKeys') moveDictExtensions( art, extensionsMap, elementsProp, 'elements' ); moveDictExtensions( art, extensionsMap, 'enum' ); } } /** * 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(): ------------------------------------------------ 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(v6): 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' } } } // TODO: if extensions has more than one of returns,items,elements,enum, delete all those props function transformArtifactExtensions( art ) { const hasOnlySubExtensions = art._outer; // items, anonymous aspects const dict = Object.create( null ); for (const ext of art._extensions) { for (const prop in ext) { if (ext[prop] === undefined) // deleted property continue; // TODO: do this check nicer (after complete move to new extensions mechanism) if (prop.charAt(0) === '@' || prop === 'doc' || prop === 'includes' || prop === 'columns' || prop === 'length' || prop === 'scale' || prop === 'precision' || prop === 'srid') { if (!hasOnlySubExtensions) pushToDict( dict, prop, ext ); } else if (prop === 'elements' || prop === 'enum' || prop === 'actions' || prop === 'params' || prop === 'returns') { if (ext.kind === 'extend') pushToDict( dict, 'includes', ext ); pushToDict( dict, prop, 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 ); } function applyAllExtensions( art ) { const extensions = art._extensions; for (const prop in extensions) { // TODO: do the following `if` in a nicer way if ([ 'elements', 'enum', 'actions', 'params', 'returns' ].includes( prop )) continue; // currently just annotates on sub elements - TODO: error here // 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(); 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 === 'enum' || prop === 'actions' || prop === 'columns' || prop === 'params' || prop === 'returns' || prop === 'includes' ) return; // extensions currently handled extra 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) } 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 (ext.kind === 'extend' && art.$inferred) { error( 'extend-for-generated', [ ext.name.location, ext ], { art, keyword: 'extend' }, 'You can\'t use $(KEYWORD) on the generated $(ART)' ); } else if (art.kind !== 'annotate' && !art._outer) { // not with elem extension in targetAspect const { id } = art.name; const dict = extensionsDict[id] || (extensionsDict[id] = []); dict.push( ext ); // TODO: change // 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 = true; 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 ); initSelectItems( query, ext.columns, query, true ); } 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 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. * * @param {XSN.Artifact} art * @param {object} exts */ function checkPrecisionScaleExtension( art, exts ) { if (art.precision && art.scale) { if ((exts.precision || exts.scale) && !(exts.precision && exts.scale)) { const missing = exts.precision ? 'scale' : 'precision'; const prop = exts.precision ? 'precision' : 'scale'; error( 'ext-missing-type-property', [ exts[prop].location, exts[prop] ], { art, prop, otherprop: missing } ); } } } 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: setExpandStatusAnnotate const extensions = extensionsMap[extProp]; if (!extensions) return; const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate for (const ext of extensions) { let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement forEachGeneric(ext, extProp, (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 = artDict[name] || annotateFor( art, extProp, name ); setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null ) ); ensureArtifactNotProcessed( elem ); if (elem.$duplicates !== true) pushToDict( elem, '_extensions', elemExt ); }); } } function moveReturnsExtensions( art, extensionsMap ) { const extensions = extensionsMap.returns; if (!extensions) return; const artReturns = art.returns; let extReturns = artReturns; const isAction = art.kind === 'action' || art.kind === 'function'; for (const ext of extensions) { if (!artReturns && art.kind !== 'annotate') { const msgId = ext.returns && hasSecurityAnno( ext.returns ) ? 'ext-unexpected-returns-sec' : 'ext-unexpected-returns'; message( msgId, [ ext.returns.location, ext ], { '#': (isAction ? art.kind : 'std'), 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 [..., …]: if (!isAction) continue; // do not put into 'extensions' // add to 'extensions' for action/function without returns: extReturns ??= annotateFor( art, 'params', '' ); } if (extReturns) { setLink( ext.name, '_artifact', (isAction ? artReturns : null ) ); pushToDict( extReturns, '_extensions', ext.returns ); } } } 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 extendHandleReturns( extensions, art ) { for (const ext of extensions || []) { 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)', } ); } } 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 ); 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 )) // error for extend, info for annotate return; if (art?.builtin) { // TODO: do via accept info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location? } } 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 --------------------------------------------------- /** * Apply the extensions inside the extensionsDict on the model. * * First try normally: extends with structure includes; with remaining cyclic * includes, do so without includes. */ function applyExtensions() { let cyclicIncludeNames = false; let extNames = Object.keys( extensionsDict ).sort(); while (extNames.length) { const { length } = extNames; for (const name of extNames) { const art = model.definitions[name]; if (art && art.kind !== 'namespace' && extendArtifact( extensionsDict[name], art, cyclicIncludeNames )) delete extensionsDict[name]; } extNames = Object.keys( extensionsDict ); // no sort() required anymore if (extNames.length >= length) cyclicIncludeNames = Object.keys( extensionsDict ); // = no includes } } /** * 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, cyclicIncludeNames = null ) { if (!cyclicIncludeNames && !(canApplyIncludes( art, art ) && extensions.every( ext => canApplyIncludes( ext, art ) ))) return false; if (cyclicIncludeNames) { canApplyIncludes( art, art, cyclicIncludeNames ); extensions.forEach( ext => canApplyIncludes( ext, art, cyclicIncludeNames ) ); } else if (!(canApplyIncludes( art, art ) && extensions.every( ext => canApplyIncludes( ext, art ) ))) { // console.log( 'FALSE:',art.name, extensions.map( e => e.name ) ) return false; } if (!art.query) { model._entities.push( art ); // add structure with includes in dep order art.$entity = ++model.$entity; } if (art.includes) { if (!cyclicIncludeNames) { applyIncludes( art, art ); } else { // resolve artifacts to induce errors: either ref-invalid-include or ref-cyclic for (const ref of art.includes) resolvePath( ref, 'include', art ); } } // checkExtensionsKind( extensions, art ); extendMembers( extensions, art ); if (!cyclicIncludeNames && art.includes) { // early propagation of specific annotation assignments propagateEarly( art, '@cds.autoexpose' ); propagateEarly( art, '@fiori.draft.enabled' ); } // TODO: complain about element extensions inside projection return true; } function extendMembers( extensions, art ) { // TODO: do the whole extension stuff lazily if the elements are requested const elemExtensions = []; if (art._main) // extensions already sorted for main artifacts extensions.sort( layers.compareLayer ); // TODO: use same sequence as in chooseAssignment() - better: use common code with that fn // console.log('EM:',art.name,extensions,art._extensions) for (const ext of extensions) { // those in extMap.includes // console.log(message( 'id', [ext.location, ext], { art: ext.name._artifact }, // 'Info', 'EXT').toString()) if (ext.name._artifact === undefined) { // not already applied setArtifactLink( ext.name, art ); if (ext.includes) { // TODO: currently, re-compiling from gensrc does not give the exact // element sequence - we need something like // includes = ['Base1',3,'Base2'] // where 3 means adding the next 3 elements before applying include 'Base2' if (art.includes) art.includes.push( ...ext.includes ); else art.includes = [ ...ext.includes ]; applyIncludes( ext, art ); } // console.log(ext,art) checkAnnotate( ext, art ); // TODO: do we allow to add elements with array of {...}? If yes, adapt initMembers( ext, art, ext._block ); // might set _extend, _annotate dependsOnSilent( art, ext ); // art depends silently on ext (inverse to normal dep!) } for (const name in ext.elements) { const elem = ext.elements[name]; if (elem.kind === 'element') { // i.e. not extend or annotate elemExtensions.push( elem ); break; // more than one elem in same EXTEND is fine } } } if (elemExtensions.length > 1) reportUnstableExtensions( elemExtensions ); // This whole function will be removed with a next change - no need to have nice code here: const dict = Object.create( null ); // actions cannot be extended anyway. TODO: there should be a message // (possible with CSN input), but that was missing before this change, too. for (const e of extensions) { if (!e.elements) continue; for (const n in e.elements) { if (e.elements[n].kind === 'extend') pushToDict( dict, n, e.elements[n] ); } } for (const name in dict) { let obj = art; if (obj.targetAspect) obj = obj.targetAspect; while (obj.items) obj = obj.items; const validDict = obj.elements || obj.enum; const member = validDict && validDict[name]; if (!member) extendNothing( dict[name], 'elements', name, art, validDict ); else if (!(member.$duplicates)) extendMembers( dict[name], member ); } } /** * Report 'Warning: Unstable element order due to repeated extensions' * except if all extensions are in the same file. * * @param {XSN.Extension[]} extensions */ function reportUnstableExtensions( extensions ) { // No message if all extensions are in the same file: const file = layers.realname( extensions[0] ); if (extensions.every( ( ext, i ) => !i || file === layers.realname( ext ) )) return; // Similar to chooseAssignment(), TODO there: also extra intralayer message // as this is a modeling error let lastExt = null; let open = []; // the "highest" layers for (const ext of extensions) { const extLayer = layers.layer( ext ) || { realname: '', _layerExtends: Object.create( null ) }; if (!open.length) { lastExt = ext; open = [ extLayer.realname ]; } else if (extLayer.realname === open[open.length - 1]) { // in same layer if (lastExt) { message( 'extend-repeated-intralayer', [ lastExt.location, lastExt ] ); lastExt = null; } message( 'extend-repeated-intralayer', [ ext.location, ext ] ); } else { if (lastExt && (open.length > 1 || !extLayer._layerExtends[open[0]])) { // report for lastExt if that is unrelated to other open exts or current ext message( 'extend-unrelated-layer', [ lastExt.location, lastExt ], {}, 'Unstable element order due to other extension in unrelated layer' ); } lastExt = ext; open = open.filter( name => !extLayer._layerExtends[name] ); open.push( extLayer.realname ); } } } /** * @param {XSN.Extension[]} extensions * @param {string} prop * @param {string} name * @param {XSN.Artifact} art * @param {object} validDict */ function extendNothing( extensions, prop, name, art, validDict ) { // TODO: probably too much magic in the creation of artName… const extMain = { ...(art._main || art) }; const artName = searchName( art, name, dictKinds[prop] ); setLink( artName, '_main', extMain ); for (const ext of extensions) { // TODO: use shared functionality with notFound in resolver.js const { location } = ext.name; extMain.kind = ext.kind; const msg = error( 'extend-undefined', [ location, artName ], { art: artName }, { std: 'Unknown $(ART) - nothing to extend', element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend', action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend', } ); attachAndEmitValidNames( msg, validDict ); } } // TODO TMP: copied from ./define.js: ----------------------------------------- /** * Set property `_parent` for all elements in `parent` to `parent` and do so * recursively for all sub elements. * * If not for extensions: construct === parent * * TODO: separate extension! */ function initMembers( construct, parent, block ) { // TODO: split extend from init const main = parent._main || parent; const isQueryExtension = construct.kind === 'extend' && main.query; let obj = initItemsLinks( construct, block ); initExprAnnoBlock( construct, block ); if (obj.target && targetIsTargetAspect( obj )) { obj.targetAspect = obj.target; delete obj.target; } const { targetAspect } = obj; if (targetAspect) { if (obj.foreignKeys) { error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ] ); delete obj.foreignKeys; // continuation semantics: not specified } if (obj.on && !obj.target) { error( 'type-unexpected-on-condition', [ obj.on.location, construct ] ); delete obj.on; // continuation semantics: not specified } if (targetAspect.elements) initAnonymousAspect(); } if (obj !== parent && obj.elements && parent.enum) { // applying the extension initElementsAsEnum(); } else { if (checkDefinitions( construct, parent, 'elements', obj.elements