@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,218 lines (1,141 loc) • 79.7 kB
JavaScript
// 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