@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,324 lines (1,241 loc) • 86.7 kB
JavaScript
// Compiler functions and utilities shared across all phases
'use strict';
const { CompilerAssertion } = require('../base/error');
const { searchName } = require('../base/messages');
const {
setLink,
setArtifactLink,
dependsOn,
pathName,
userQuery,
definedViaCdl,
targetCantBeAspect,
pathStartsWithSelf,
columnRefStartsWithSelf,
isAssocToPrimaryKeys,
artifactRefLocation,
} = require('./utils');
const $inferred = Symbol.for( 'cds.$inferred' );
const $location = Symbol.for( 'cds.$location' );
/**
* Main export function of this file. Attach "resolve" functions shared for phase
* "define" and "resolve" to `model.$functions`, where argument `model` is the XSN.
*
* Before calling `resolvePath`, make sure that the following function
* in model.$function is set:
* - `effectiveType`
*
* @param {XSN.Model} model
*/
// TODO: yes, this function will be renamed
function fns( model ) {
const { options } = model;
const {
info, error, warning, message,
} = model.$messageFunctions;
const Functions = model.$functions;
// Map `exprCtx` (is a param of traversal functions) to reference semantics
const referenceSemantics = {
// global: ------------------------------------------------------------------
using: { // only used to produce error message
isMainRef: 'all',
lexical: null,
dynamic: modelDefinitions,
notFound: undefinedDefinition,
},
// scope:'global': for cds.Association and auto-redirected targets
$global: {
isMainRef: 'all',
lexical: null,
dynamic: modelDefinitions,
notFound: undefinedDefinition,
},
// only used for the main annotate/extend statements, not inner ones:
annotate: {
isMainRef: 'all',
lexical: userBlock,
dynamic: modelDefinitions,
notFound: undefinedForAnnotate,
accept: extendableArtifact,
},
'annotate-sec': {
isMainRef: 'all',
lexical: userBlock,
dynamic: modelDefinitions,
notFound: undefinedDefinition,
messageMap: {
'ref-undefined-art': 'ext-undefined-art-sec',
'ref-undefined-def': 'ext-undefined-def-sec',
},
accept: extendableArtifact,
},
extend: {
isMainRef: 'no-generated',
lexical: userBlock,
dynamic: modelDefinitions,
notFound: undefinedForExtend,
accept: extendableArtifact,
},
_uncheckedExtension: { // to be used only with resolveUncheckedPath()
isMainRef: 'all',
lexical: userBlock,
dynamic: modelDefinitions,
notFound: () => null, // without message
},
include: {
isMainRef: 'no-generated',
lexical: userBlock,
dynamic: modelBuiltinsOrDefinitions,
notFound: undefinedDefinition,
accept: acceptStructOrBare,
},
target: {
isMainRef: 'no-autoexposed',
lexical: userBlock,
dynamic: modelBuiltinsOrDefinitions,
notFound: undefinedDefinition,
accept: acceptEntity,
noDep: true,
// special `scope`s for auto-redirections:
global: () => '$global',
},
targetAspect: {
isMainRef: 'no-autoexposed',
lexical: userBlock,
dynamic: modelBuiltinsOrDefinitions,
notFound: undefinedDefinition,
accept: acceptAspect,
},
from: {
isMainRef: 'no-autoexposed',
lexical: userBlock,
dynamic: modelBuiltinsOrDefinitions,
navigation: environment,
notFound: undefinedDefinition,
accept: acceptQuerySource,
noDep: '', // dependency special for from
args: () => 'from-args',
},
type: {
isMainRef: 'no-autoexposed',
lexical: userBlock,
dynamic: modelBuiltinsOrDefinitions,
navigation: staticTarget,
notFound: undefinedDefinition,
accept: acceptTypeOrElement,
// special `scope`s for CDL parser - TYPE OF (TODO generated?), cds.Association:
typeOf: typeOfSemantics,
global: () => '$global', // TODO: do we need `navigation: staticTarget`?
},
$typeOf: {
dynamic: typeOfParentDict,
navigation: staticTarget,
},
// element references without lexical scope (except $self/$projection): -----
targetElement: {
lexical: null,
dollar: false,
dynamic: targetElements,
navigation: targetNavigation,
notFound: undefinedTargetElement,
param: () => '$scopePar',
},
filter: {
lexical: justDollarAliases,
dollar: true,
dynamic: targetElements,
notFound: undefinedTargetElement,
param: () => '$scopePar',
nestedColumn: () => 'filter',
},
'calc-filter': { // TODO: what is so special about this?
lexical: justDollarAliases,
dollar: true,
dynamic: targetElements,
navigation: calcElemNavigation,
notFound: undefinedTargetElement,
param: paramUnsupported,
filter: () => 'calc-filter',
},
default: {
lexical: null,
dollar: true,
dynamic: () => Object.create( null ),
notFound: undefinedVariable,
param: paramUnsupported,
},
'limit-rows': {
lexical: null,
dollar: true,
dynamic: () => Object.create( null ),
notFound: undefinedVariable,
param: () => '$scopePar',
},
'limit-offset': 'limit-rows',
// general element / variable references --------------------------------------
where: {
lexical: tableAliasesAndSelf,
dollar: true,
dynamic: combinedSourcesOrParentElements,
notFound: undefinedSourceElement,
check: checkRefInQuery,
param: () => '$scopePar',
},
having: 'where',
groupBy: 'where',
column: {
lexical: tableAliasesAndSelf,
dollar: true,
dynamic: combinedSourcesOrParentElements,
notFound: undefinedSourceElement,
check: checkColumnRef,
param: () => '$scopePar',
nestedColumn: () => '$srcRefInNestedColumn',
},
'from-args': {
lexical: null,
dollar: true,
dynamic: () => Object.create( null ),
notFound: undefinedVariable,
param: () => '$scopePar',
},
calc: {
lexical: justDollarAliases,
dollar: true,
dynamic: parentElements,
navigation: calcElemNavigation,
notFound: undefinedParentElement,
param: paramUnsupported,
filter: () => 'calc-filter',
},
'join-on': {
lexical: tableAliasesAndSelf,
dollar: true,
dynamic: combinedSourcesOrParentElements,
rejectRoot: rejectOwnExceptVisibleAliases,
notFound: undefinedSourceElement,
param: () => '$scopePar',
},
on: { // unmanaged assoc: outside query, redirected or new assoc in column
lexical: justDollarAliases,
dollar: true,
dynamic: parentElements,
navigation: assocOnNavigation,
notFound: undefinedParentElement,
accept: acceptElemOrVarOrSelf,
check: checkAssocOn,
param: paramUnsupported,
rewriteProjectionToSelf: true,
nestedColumn: () => '$projRefInNestedColumn',
},
'mixin-on': {
lexical: tableAliasesAndSelf,
dollar: true,
dynamic: combinedSourcesOrParentElements,
navigation: assocOnNavigation,
notFound: undefinedSourceElement,
accept: acceptElemOrVarOrSelf,
check: checkAssocOn,
param: () => '$scopePar', // TODO: check that assocs containing param in ON is not published
},
'rewrite-on': {}, // only for traversal when rewriting on condition
'orderBy-ref': {
lexical: tableAliasesAndSelf,
dollar: true,
dynamic: parentElements,
notFound: undefinedOrderByElement,
check: checkOrderByRef,
param: () => '$scopePar',
},
'orderBy-expr': {
lexical: tableAliasesAndSelf,
dollar: true,
dynamic: combinedSourcesOrParentElements,
notFound: undefinedSourceElement,
check: checkRefInQuery,
param: () => '$scopePar',
},
'orderBy-set-ref': {
lexical: tableAliasesAndSelf, // TODO: reject own tab aliases
dollar: true,
dynamic: queryElements,
rejectRoot: rejectOwnAliasesAndMixins,
notFound: undefinedParentElement,
check: checkOrderByRef,
param: () => '$scopePar',
},
'orderBy-set-expr': {
lexical: tableAliasesAndSelf, // TODO: reject own tab aliases
dollar: true,
dynamic: () => Object.create( null ),
rejectRoot: rejectAllOwn,
notFound: undefinedVariable,
check: checkRefInQuery,
param: () => '$scopePar',
},
annotation: { // annotation assignments
lexical: justDollarAliases,
dollar: true,
dynamic: parentElementsOrKeys,
navigation: assocOnNavigation,
noDep: true,
notFound: undefinedParentElement,
accept: acceptElemOrAnyVar,
variableFilter: (dict => dict),
messageMap: {
'ref-undefined-element': 'anno-undefined-element',
'ref-undefined-param': 'anno-undefined-param',
},
param: () => '$annotationScopePar',
nestedColumn: () => '$projRefInNestedColumn',
},
// TODO: introduce some kind of inheritance
// used by xpr-rewrite.js to resolve rewritten path roots.
annoRewrite: { // annotation assignments
lexical: justDollarAliases,
dollar: true,
dynamic: parentElements,
navigation: assocOnNavigation,
noDep: true,
notFound: null, // no error, just falsy links
accept: acceptElemOrAnyVar,
param: () => '$scopePar',
nestedColumn: () => '$projRefInNestedColumn',
},
$scopePar: {
dynamic: artifactParams,
notFound: undefinedParam,
},
$annotationScopePar: {
messageMap: {
'ref-undefined-element': 'anno-undefined-element',
'ref-undefined-param': 'anno-undefined-param',
},
dynamic: artifactParams,
notFound: undefinedParam,
},
// for `nestedColumn`, these two will be merged with base semantics:
$projRefInNestedColumn: { // for assoc-`on` and annotations
lexical: justDollarAliases,
dynamic: parentElements,
navigation: assocOnNavigation, // like std `environment`, but no dependency
rewriteProjectionToSelf: true,
},
$srcRefInNestedColumn: { // for column refs
lexical: justDollarAliases,
dollar: true,
dynamic: nestedElements,
navigation: environment,
notFound: undefinedNestedElement,
},
};
Object.assign( model.$functions, {
traverseExpr,
traverseTypedExpr,
resolveUncheckedPath,
resolveTypeArgumentsUnchecked, // TODO: move to some other file
resolvePathRoot,
resolvePath,
resolveDefinitionName,
checkExpr,
checkOnCondition,
navigationEnv,
nestedElements,
attachAndEmitValidNames,
} );
traverseExpr.STOP = Symbol( 'STOP' );
traverseExpr.SKIP = Symbol( 'SKIP' );
traverseTypedExpr.STOP = traverseExpr.STOP;
traverseTypedExpr.SKIP = traverseExpr.SKIP;
return;
// Expression traversal function ----------------------------------------------
/**
* Recursively traverse the expression `expr` and call `callback` on the expression nodes.
*
* …
*
* Sub queries are not further traversed, but `callback` is called on the
* expression node having the property `query`.
*
* Callbacks can influence the traversal by returning a symbol:
*
* - `traverseExpr.STOP`: the traversal is stopped immediately
* - `traverseExpr.SKIP` on a node with a `path` property: the path items
* with its filters and arguments are not traversed
* - `traverseExpr.SKIP` on a path item: the expression in the `where`
* condition is not traversed
*/
function traverseExpr( expr, exprCtx, user, callback ) {
if (!expr || typeof expr === 'string') // parse error or keywords in {xpr:...}
return null;
let exit = null;
// `type` property for `cast, `query` for sub query
if (expr.path || expr.type || expr.query) {
exit = callback( expr, exprCtx, user );
if (exit === traverseExpr.STOP)
return exit;
}
if (expr.path && exit !== traverseExpr.SKIP) {
for (const step of expr.path) {
if (step && (step.args || step.where || step.cardinality) &&
traversePathItem( step, exprCtx, user, callback ))
return traverseExpr.STOP;
}
}
if (expr.args) {
const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
for (const arg of args) {
if (traverseExpr( arg, exprCtx, user, callback ) === traverseExpr.STOP)
return traverseExpr.STOP;
}
}
if (expr.suffix) {
for (const arg of expr.suffix) {
if (traverseExpr( arg, exprCtx, user, callback ) === traverseExpr.STOP)
return traverseExpr.STOP;
}
}
return false;
}
function traversePathItem( step, exprCtx, user, callback ) {
const exit = callback( step, exprCtx, user );
if (exit === traverseExpr.STOP)
return true;
if (step.where && exit !== traverseExpr.SKIP) {
const ctx = referenceSemantics[exprCtx].filter?.() || 'filter';
if (traverseExpr( step.where, ctx, step, callback ) === traverseExpr.STOP)
return true;
}
if (step.args) {
const ctx = referenceSemantics[exprCtx].args?.() || exprCtx;
const args = Array.isArray( step.args ) ? step.args : Object.values( step.args );
// TODO: there should be no array `args` on path item
for (const arg of args) {
if (traverseExpr( arg, ctx, user, callback ) === traverseExpr.STOP)
return true;
}
}
return false;
}
// Special expression traversal function for `resolveExpr`. Let's see
// later whether we can use this version as the general one.
// If we continue to have separate ones, remove the STOP stuff – it is not
// needed for `resolveExpr`; SKIP is used, though.
function traverseTypedExpr( expr, exprCtx, user, type, callback ) {
if (!expr || typeof expr === 'string') // parse error or keywords in {xpr:...}
return null;
let { args } = expr;
let exit = null;
// `type` property for `cast, `query` for sub query
if (expr.path || expr.type || expr.sym || expr.query) {
exit = callback( expr, exprCtx, user, type );
if (exit === traverseExpr.STOP)
return exit;
// `args` with `cast` function
}
else if (!args) {
// empty on purpose
}
else if (expr.func) {
if (!Array.isArray( args ))
args = Object.values( args );
}
else if (expr.op?.val === 'list' || args.length === 1) {
exit = type;
}
else if (expr.op?.val === '?:') {
args = traverseChoiceArgs( args, exprCtx, user, type, callback );
exit = type;
}
else {
args = traverseSpecialArgs( args, exprCtx, user, type, callback );
}
if (expr.path && exit !== traverseExpr.SKIP) {
for (const step of expr.path) {
if (step && (step.args || step.where || step.cardinality) &&
traverseTypedPathItem( step, exprCtx, user, callback ))
return traverseExpr.STOP;
}
}
if (expr.args) {
if (!args)
return traverseExpr.STOP;
for (const arg of args) {
if (traverseTypedExpr( arg, exprCtx, user, exit, callback ) === traverseExpr.STOP)
return traverseExpr.STOP;
}
}
if (expr.suffix) {
for (const arg of expr.suffix) {
if (traverseTypedExpr( arg, exprCtx, user, null, callback ) === traverseExpr.STOP)
return traverseExpr.STOP;
}
}
return exit;
}
/**
* Traverse arguments `args` if they match a specific pattern:
*
* - a (sub) expression is a comparison, i.e. uses one of the binary operators
* `=`, `<>`, '==', `!=`, `in` or `not in`,
* - one side of the comparison is a reference or a `cast` function call when
* typed with an enum type,
* - the other side is an enum reference, an enum reference in parentheses, or a
* list of enum references.
*
* Return an array of the arguments which are to be traversed normally, or
* `null` if the traversal is stopped immediately
*/
function traverseSpecialArgs( args, exprCtx, user, type, callback ) {
if (args.length <= 3) {
if (args.length === 3 && args[1].literal === 'token' &&
[ '=', '<>', '==', '!=', 'in' ].includes( args[1].val ))
return traverseComparison( args[0], args[2], exprCtx, user, callback );
}
else if (args[0].val === 'case' && args[0].literal === 'token') {
return traverseCaseWhen( args, exprCtx, user, type, callback );
}
else if (args.length === 4 && args[1].val === 'not' && args[2].val === 'in' &&
args[1].literal === 'token' && args[2].literal === 'token') {
return traverseComparison( args[0], args[3], exprCtx, user, callback );
}
return args;
}
function traverseComparison( left, right, exprCtx, user, callback ) {
if (!left || !right) // can happen in old parser
return [ left || right ];
if (left.path || left.type) { // ref or cast fn
const type = traverseTypedExpr( left, exprCtx, user, null, callback );
if (type === traverseExpr.STOP ||
traverseTypedExpr( right, exprCtx, user, type, callback ) === traverseExpr.STOP)
return null;
return [];
}
if (right.path || right.type) { // ref or cast fn
const type = traverseTypedExpr( right, exprCtx, user, null, callback );
if (type === traverseExpr.STOP ||
traverseTypedExpr( left, exprCtx, user, type, callback ) === traverseExpr.STOP)
return null;
return [];
}
return [ left, right ];
}
// for '?:' operator, only via CDL (translates to `case…when` in CSN):
function traverseChoiceArgs( args, exprCtx, user, type, callback ) {
if (traverseTypedExpr( args[0], exprCtx, user, null, callback ) === traverseExpr.STOP)
return null;
return args.slice( 1 );
}
function traverseCaseWhen( args, exprCtx, user, type, callback ) {
let idx = 1;
let when = null;
let node = args[1];
// For `CASE <expr> WHEN <…> THEN <…>`
if (node?.val !== 'when' || node.literal !== 'token') {
when = traverseTypedExpr( node, exprCtx, user, null, callback );
if (when === traverseExpr.STOP)
return null;
++idx;
}
// Remark: no need to test `literal` in the following - ensured by CDL and CSN
// parser
while (args[idx]?.val === 'when' && ++idx < args.length) {
node = args[idx];
// be robust against corrupted sources:
if ((node.literal !== 'token' || ![ 'then', 'when', 'end' ].includes( node.val )) &&
traverseTypedExpr( args[idx++], exprCtx, user, when, callback ) === traverseExpr.STOP)
return null;
if (args[idx]?.val !== 'then')
continue;
node = args[++idx];
if (node &&
(node.literal !== 'token' || node.val !== 'when' && node.val !== 'end') &&
traverseTypedExpr( args[idx++], exprCtx, user, type, callback ) === traverseExpr.STOP)
return null;
}
if (args[idx]?.val === 'else') {
if (++idx < args.length &&
traverseTypedExpr( args[idx], exprCtx, user, type, callback ) === traverseExpr.STOP)
return null;
}
return [];
}
function traverseTypedPathItem( step, exprCtx, user, callback ) {
const exit = callback( step, exprCtx, user, null );
if (exit === traverseExpr.STOP)
return true;
if (step.where && exit !== traverseExpr.SKIP) {
const ctx = referenceSemantics[exprCtx].filter?.() || 'filter';
if (traverseTypedExpr( step.where, ctx, step, null, callback ) === traverseExpr.STOP)
return true;
}
if (step.args) {
const ctx = referenceSemantics[exprCtx].args?.() || exprCtx;
const args = Array.isArray( step.args ) ? step.args : Object.values( step.args );
// TODO: there should be no array `args` on path item
for (const arg of args) {
if (traverseTypedExpr( arg, ctx, user, arg.name, callback ) === traverseExpr.STOP)
return true;
}
}
return false;
}
// Return absolute name for unchecked path `ref`. We first try searching for
// the path root starting from `env`. If it exists, return its absolute name
// appended with the name of the rest of the path. Otherwise, complain if
// `unchecked` is false, and set `ref.absolute` to the path name of `ref`.
// Used for collecting artifact extension.
//
// Return '' if the ref is good, but points to an element.
function resolveUncheckedPath( ref, refCtx, user ) {
const { path } = ref;
if (!path || path.broken) // incomplete type AST
return undefined;
const semantics = referenceSemantics[refCtx];
if (!semantics.isMainRef)
throw new CompilerAssertion( `resolveUncheckedPath() called for reference ctx '${ refCtx }'` );
if (!definedViaCdl( user ))
return (path.length === 1) ? path[0].id : '';
let art = getPathRoot( ref, semantics, user );
if (ref.scope && ref.scope !== 'global')
return ''; // TYPE OF, Main:elem
if (Array.isArray( art ))
art = art[0];
if (!art)
return (semantics.dynamic !== modelDefinitions) ? art : pathName( path );
const first = (art.kind === 'using' ? art.extern : art.name).id;
return (path.length === 1) ? first : `${ first }.${ pathName( ref.path.slice(1) ) }`;
}
/**
* Return artifact or element referred by the path in `ref`. The first
* environment we search in is `env`. If no such artifact or element exist,
* complain with message and return `undefined`. Record a dependency from
* `user` to the found artifact if `user` is provided.
*/
function resolvePath( ref, expected, user ) {
const origUser = user;
user = user._user || user;
if (ref == null) // no references -> nothing to do
return undefined;
if (ref._artifact !== undefined)
return ref._artifact;
const { path } = ref;
if (!path || path.broken || !path.length) {
// incomplete type AST or empty env (already reported)
return setArtifactLink( ref, undefined );
}
const s = referenceSemantics[expected];
const semantics = (typeof s === 'string') ? referenceSemantics[s] : s;
const r = getPathRoot( ref, semantics, origUser );
const root = r && acceptPathRoot( r, ref, semantics, origUser );
if (!root)
return setArtifactLink( ref, root );
// how many path items are for artifacts (rest: elements)
let art = getPathItem( ref, semantics, user );
if (!art)
return setArtifactLink( ref, art );
// TODO: use isMainRef string value here?
const acceptFn = semantics.accept || (semantics.isMainRef ? a => a : acceptElemOrVar);
art = setArtifactLink( ref, acceptFn( art, user, ref, semantics ) );
// TODO TMP: remove noDep: an association does not depend on the target, only
// -- on its keys/on, which depend on certain target elements
if (art && user && !semantics.noDep) {
const location = artifactRefLocation( ref );
if (semantics.noDep === '' && art._main) { // assoc in FROM
environment( art, location, user );
const target = art._effectiveType?.target?._artifact;
if (target)
dependsOn( user._main, target, location, user );
if (target?.$calcDepElement)
dependsOn( user._main, target.$calcDepElement, location, user );
}
else if (art._main && art.kind !== 'select' || path[0]._navigation?.kind !== '$self') {
// no real dependency to bare $self (or actually: the underlying query)
dependsOn( user, art, location );
if (art.$calcDepElement)
dependsOn( user, art.$calcDepElement, location );
// Without on-demand resolve, we can simply signal 'undefined "x"'
// instead of 'illegal cycle' in the following case:
// element elem: type of elem.x;
}
// TODO: really write dependency with expand/inline? write test
// (removing it is not incompatible => not urgent)
}
// TODO: follow FROM here, see csnRef - fromRef
return art;
}
/**
* Resolve the type arguments of `artifact` according to the type `typeArtifact`.
* User is used for semantic message location.
*
* For builtins, for each property name `<prop>` in `typeArtifact.parameters`, we move a value
* from `art.$typeArgs` (a vector of numbers with locations) to `artifact.<prop>`.
*
* For non-builtins, we take either one or two arguments and interpret them
* as `length` or `precision`/`scale`.
*
* Left-over arguments are errors for non-builtins and warnings for builtins.
*
* TODO: move to define.js (and probably rename), rewrite (consider syntax-unexpected-argument)
*
* @param {object} artifact
* @param {object} typeArtifact
* @param {CSN.Artifact} user
*/
function resolveTypeArgumentsUnchecked( artifact, typeArtifact, user ) {
let args = artifact.$typeArgs || [];
const parameters = typeArtifact?.parameters || [];
if (args.length > 0 && parameters.length > 0) {
// For Builtins
for (let i = 0; i < parameters.length; ++i) {
const par = parameters[i].name || parameters[i];
if (!artifact[par] && i < args.length)
artifact[par] = args[i];
}
args = args.slice( parameters.length );
// TODO: we could issue syntax-unexpected-argument here
}
else if (args.length > 0 && !typeArtifact?.builtin) {
// One or two arguments are interpreted as either length or precision/scale.
// For builtins, we know what arguments are expected, and we do not need this mapping.
// Also, we expect non-structured types.
if (args.length === 1) {
artifact.length = args[0];
args = args.slice(1);
}
else if (args.length === 2) {
artifact.precision = args[0];
artifact.scale = args[1];
args = args.slice(2);
}
}
if (!artifact.$typeArgs)
return;
// Warn about left-over arguments.
if (args.length > 0) {
const loc = [ args[0].location, user ];
if (typeArtifact?.builtin)
message( 'type-ignoring-argument', loc, { art: typeArtifact } );
// when the parser exits rule unsuccessfully/prematurely, $typeArgs might
// still have a length > 2 → no testMode dump
}
artifact.$typeArgs = undefined;
}
// Resolve the n-1 path steps before the definition name for LSP.
function resolveDefinitionName( art ) {
const path = art?.name?.path;
if (!art || art._main || !path || path.length <= 1)
return;
// Don't resolve paths in an annotation as a definition!
const definitions = art.kind === 'annotation' ? model.vocabularies : model.definitions;
let name = art.name.id;
if (art.kind === 'namespace') // namespace-statements are ref-only.
setArtifactLink( path[path.length - 1], definitions[name] || false );
for (let i = path.length - 1; i > 0; --i) {
name = name.substring(0, name.length - path[i].id.length - 1);
setArtifactLink( path[i - 1], definitions[name] || false );
}
}
function getPathRoot( { path, scope, location }, semantics, user ) {
// TODO: use string value of isMainRef?
const head = path[0];
if (!head || !head.id)
return undefined; // parse error
if (head._artifact !== undefined)
return head._artifact;
let ruser = user._user || user; // TODO: nicer name if we keep this
// TODO: re-think _user link
if (ruser._outer && !semantics.isMainRef) {
if (ruser.kind === '$annotation')
ruser = ruser._outer; // for elem refs, use elem as real "user"
else if (ruser._outer.kind === '$annotation')
ruser = ruser._outer._outer;
}
// Handle expand/inline before `type of`, :param, global (internally for CDL):
if (user._columnParent && !semantics.isMainRef) { // in expand/inline
const func = semantics.nestedColumn;
if (!func)
throw new CompilerAssertion( 'Unexpected ref context in nested column' );
const ctx = func();
semantics = (typeof ctx === 'string')
? ({ ...semantics, ...referenceSemantics[ctx] })
: ctx;
}
if (typeof scope === 'string') { // typeOf, param, global
const func = semantics[scope] || scope === 'param' && paramUnsupported;
// 'param' is a user scope → useful default (error msg ref-unexpected-param)
// 'global' and 'typeOf' are internal scopes of the compiler → dump if not provided
if (!func)
throw new CompilerAssertion( `Unexpected scope ${ scope }, no handler defined in context` );
const ctx = func( ruser, path, location, semantics );
semantics = (typeof ctx === 'string') ? referenceSemantics[ctx] : ctx;
if (!semantics)
return setArtifactLink( head, null );
}
const valid = [];
// Search in lexical environments, including $self/$projection:
const { isMainRef } = semantics;
const lexical = semantics.lexical?.( ruser ); // TODO: _columnParent?
if (lexical) {
const [ nextProp, dictProp ] = (isMainRef)
? [ '_block', 'artifacts' ]
: [ '_$next', '$tableAliases' ];
// let notApplicable = ...; // for table aliases in JOIN-ON and UNION orderBy
for (let env = lexical; env; env = env[nextProp]) {
const dict = env[dictProp] || Object.create( null );
const r = dict[head.id];
if (acceptLexical( r, path, semantics, user ))
return setArtifactLink( head, r );
valid.push( dict );
}
}
// Search in $special (excluding $self/$projection) and dynamic environment:
const dynamicDict = semantics.dynamic( ruser, user._user && user._artifact );
if (!dynamicDict) // avoid consequential errors
return setArtifactLink( head, null );
const isVar = (semantics.dollar && head.id.charAt( 0 ) === '$');
const dict = (isVar) ? model.$magicVariables.elements : dynamicDict;
const r = dict[head.id];
if (r)
return setArtifactLink( head, r );
if (!semantics.dollar) {
valid.push( dynamicDict );
if (isMainRef) // eslint-disable-next-line no-return-assign
valid.forEach( ( d, idx ) => (valid[idx] = removeGapArtifact( d )) );
}
else {
const filterFn = semantics.variableFilter || removeRestrictedVariables;
valid.push( filterFn( model.$magicVariables.elements ),
removeDollarNames( dynamicDict ) );
}
// TODO: streamline function arguments (probably: user, path, semantics )
const undef = semantics.notFound?.( user._user || user, head, valid, dynamicDict,
!isMainRef && user._user && user._artifact,
path, semantics );
return setArtifactLink( head, undef || null );
}
// Return artifact or element referred by path (array of ids) `tail`. The
// search environment (for the first path item) is `arg`. For messages about
// missing artifacts (as opposed to elements), provide the `head` (first
// element item in the path)
// TODO - think about setting _navigation for all $navElement – the
// "ref: ['tabAlias']: inline: […]" handling might be easier
// (no _columnParent consultation for key prop and renaming support)
function getPathItem( ref, semantics, user ) {
// let art = (headArt && headArt.kind === '$tableAlias') ? headArt._origin : headArt;
const { path } = ref;
let artItemsCount = 0;
const { isMainRef } = semantics;
if (isMainRef) {
artItemsCount = (typeof ref.scope === 'number' && ref.scope) ||
(ref.scope ? 1 : path.length);
}
let art = null;
const elementsEnv = semantics.navigation || environment;
let index = -1;
for (const item of path) {
++index;
--artItemsCount;
if (!item?.id) // incomplete AST due to parse error
return undefined;
if (item._artifact) { // should be there on first path element
art = item._artifact;
continue;
}
const prev = art;
const envFn = (artItemsCount >= 0) ? artifactsEnv : elementsEnv;
// TOOD: call envFn with location of last item (for dependency error)
const env = envFn( art, path[index - 1].location, user );
const found = env && env[item.id]; // not env?.[item.id] ! …we want to keep the 0
// Reject `$self.$_column_1`: TODO: necessary to do here again?
art = setArtifactLink( item, (found?.name?.$inferred === '$internal') ? undefined : found );
if (!art) {
// TODO (done?): if `env` was 0, we might set a dependency to induce an
// illegal-cycle error instead of reporting via `errorNotFound`.
const notFound = (artItemsCount >= 0) ? semantics.notFound : undefinedItemElement;
// TODO: streamline function arguments (probably: user, path, semantics, prev )
// false returned by semantics.navigation: no further error:
if (env !== false)
notFound( user, item, [ env ], null, prev, path, semantics );
return null;
}
// need to do that here, because we also need to disallow Service.AutoExposed:elem
// TODO: but Service.AutoExposed.NotAuto should be fine
if (isMainRef && isMainRef !== 'all' && artItemsCount === 0) {
if (art.kind === 'namespace') {
if (env !== false) {
semantics.notFound( user, item, [ removeGapArtifact( env ) ],
null, prev, path, semantics );
}
return null;
}
else if (art.$inferred === 'autoexposed' && !user.$inferred) {
// Depending on the processing sequence, the following could be a
// simple 'ref-undefined-art'/'ref-undefined-def' - TODO: which we
// could "change" to this message at the end of compile():
error( 'ref-unexpected-autoexposed', [ item.location, user ], { art },
'An auto-exposed entity can\'t be referred to - expose entity $(ART) explicitly' );
return null; // continuation semantics: like “not found”
}
}
}
return art;
}
/**
* Resolve the _path-root_ only. Used for rewriting annotation paths.
*
* @param ref
* @param {string} expected
* @param user
*/
function resolvePathRoot( ref, expected, user ) {
if (ref == null || !ref.path) // no references -> nothing to do
return undefined;
const s = referenceSemantics[expected];
const semantics = (typeof s === 'string') ? referenceSemantics[s] : s;
const r = getPathRoot( ref, semantics, user );
return r && acceptPathRoot( r, ref, semantics, user );
}
// Helper functions for resolve[Unchecked]Path, getPath{Root,Item}: -----------
function acceptLexical( art, path, semantics, user ) {
if (semantics.isMainRef || !art)
return !!art;
// Non-global lexical are table aliases, mixins and $self, $projection, $parameters,
// Do not accept a lonely table alias and `$projection`
// TODO: test table alias and mixin named `$projection`
if (path.length !== 1 || user.expand || user.inline) {
if (semantics.rewriteProjectionToSelf &&
art.kind === '$self' && path[0].id === '$projection') {
// Rewrite $projection to $self
path[0].id = '$self';
warning( 'ref-expecting-$self', [ path[0].location, user ],
{ code: '$projection', newcode: '$self' });
}
return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
}
// allow mixins, $self, and `up_` in anonymous target aspect (is $navElement):
return art.kind === 'mixin' ||
art.kind === '$self' && path[0].id === '$self' ||
art.kind === '$navElement';
}
function acceptPathRoot( art, ref, semantics, user ) {
const { path } = ref;
const [ head ] = path;
if (Array.isArray( art ))
return getAmbiguousRefLink( art, head, user );
if (semantics.rejectRoot?.( art, user, ref, semantics ))
return null;
switch (art.kind) {
case 'using': {
const def = model.definitions[art.extern.id];
if (!def)
return def;
if (def.$duplicates)
return false;
art = setArtifactLink( head, def ); // we do not want to see the using
if (art.kind !== 'namespace')
return art;
}
/* FALLTHROUGH */
case 'namespace': {
if (semantics.isMainRef === 'all' || path.length !== 1 && ref.scope !== 1)
return art;
const valid = [];
const lexical = userBlock( user );
if (lexical) {
for (let env = lexical; env; env = env._block)
valid.push( removeGapArtifact( env.artifacts || Object.create( null ) ) );
}
valid.push( removeGapArtifact( model.definitions ) );
semantics.notFound?.( user._user || user, head, valid, model.definitions,
null, path, semantics );
return null;
}
case 'mixin': {
// use a source element having that name if in `extend … with columns`:
const elem = (user._user || user).$extended &&
art._parent._combined[head.id];
if (elem) {
path.$prefix = elem._parent.name.id; // prepend alias name
info( 'ref-special-in-extend', [ head.location, user ],
{ '#': 'mixin', id: head.id, art: elem._origin._main } );
setLink( head, '_navigation', elem );
return setArtifactLink( head, elem._origin );
}
return setLink( head, '_navigation', art );
}
case '$navElement': {
setLink( head, '_navigation', art );
return setArtifactLink( head, art._origin );
}
case '$tableAlias': {
// use a source element having that name if in `extend … with columns`:
const { $extended } = user._user || user;
// if query source has duplicates, table alias has no elements
const elem = $extended && art.elements?.[head.id];
if (elem) {
path.$prefix = art.name.id; // prepend alias name
info( 'ref-special-in-extend', [ head.location, user ],
{ '#': 'alias', id: head.id, art: elem._origin._main } );
setLink( head, '_navigation', elem );
return setArtifactLink( head, elem._origin );
}
else if ($extended && art.elements) {
warning( 'ref-deprecated-in-extend', [ head.location, user ], { id: head.id },
// eslint-disable-next-line @stylistic/max-len
'In an added column, do not use the table alias $(ID) to refer to source elements' );
}
}
/* FALLTHROUGH */
case '$self': { // TODO: remove $projection from CC
setLink( head, '_navigation', art );
setArtifactLink( head, art._origin ); // query source or leading query in FROM
if (!art._origin)
return art._origin;
// if just table alias (with expand), mark `user` with `$noOrigin` to indicate
// that the corresponding entity should not be put as $origin into the CSN.
// TODO: remove again, should be easy enough in to-csn without.
if (path.length === 1 && art.kind === '$tableAlias')
(user._user || user).$noOrigin = true;
if (head.id === '$projection' &&
(user.kind === '$annotation' || user._outer?.kind === '$annotation')) {
error( 'ref-unsupported-projection', [ head.location, user ],
{ code: '$projection', newcode: '$self' },
'$(CODE) is not supported in annotations; replace by $(NEWCODE)' );
}
return art;
}
case '$parameters': {
// TODO: if ref.scope='param' is handled, test that here, too ?
const id = path[1]?.id;
const code = id ? `$parameters.${ id }` : '$parameters';
const newcode = id ? `:${ id }` : ':‹param›';
message( 'ref-obsolete-parameters', [ head.location, user ], { code, newcode },
'Obsolete $(CODE) - replace by $(NEWCODE)' );
return art;
}
case 'builtin': {
// TODO: use properties in builtins
if (art.name.id === '$at') {
message( 'ref-deprecated-variable', [ head.location, user ],
{ code: '$at', newcode: '$valid' },
'$(CODE) is deprecated; use $(NEWCODE) instead' );
}
else if (art.$restricted && semantics.accept !== acceptElemOrAnyVar) {
error( 'ref-unexpected-var', [ head.location, user ],
{ '#': 'annotation', name: head.id } );
return null; // no further error on `unknown` for $draft.unknown
}
return art;
}
default:
return art;
}
}
function getAmbiguousRefLink( arr, head, user ) {
if (arr[0].kind !== '$navElement' || arr.some( e => e._parent.$duplicates ))
return false;
// only complain about ambiguous source elements if we do not have
// duplicate table aliases, only mention non-ambiguous source elems
const uniqueNames = arr.filter( e => !e.$duplicates );
if (uniqueNames.length) {
const names = uniqueNames.filter( e => e._parent.name?.$inferred !== '$internal' )
.map( e => `${ e._parent.name.id }.${ e.name.id }` );
let variant = names.length === uniqueNames.length ? 'std' : 'few';
if (names.length === 0)
variant = 'none';
error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names } );
}
return false;
}
// Functions for the secondary reference semantics ----------------------------
function typeOfSemantics( user, [ head ] ) {
// `type of` is only allowed for (sub) elements of main artifacts
while (!user.kind && user._outer)
user = user._outer;
let struct = user;
while (struct.kind === 'element')
struct = struct._parent;
if (struct === user._main && struct.kind !== 'annotation')
return '$typeOf';
error( 'type-unexpected-typeof', [ head.location, user ],
{ keyword: 'type of', '#': struct.kind } );
return false;
}
function paramUnsupported( user, _path, location ) {
error( 'ref-unexpected-scope', [ location, user ], // TODO: ref-unexpected-param
// why an extra text for calculated elements? or separate for all?
{ '#': (user.$syntax === 'calc' ? 'calc' : 'std') } );
return false;
}
// Functions for semantics.lexical: -------------------------------------------
function userBlock( user ) {
return definedViaCdl( user ) && user._block;
}
function justDollarAliases( user ) {
const query = userQuery( user );
if (!query)
return user._main || user; // TODO: also contains `up_` for aspects; remove
// query.$tableAliases contains both aliases and $self/$projection
const aliases = query.$tableAliases;
const r = Object.create( null );
if (aliases.$self.kind === '$self')
r.$self = aliases.$self;
// TODO: disallow $projection for ON conditions all together
if (aliases.$projection?.kind === '$self')
r.$projection = aliases.$projection;
const { $parameters } = user._main.$tableAliases;
if ($parameters) // no need to test `kind`, just compiler-set “aliases”
r.$parameters = $parameters;
return { $tableAliases: r };
}
function tableAliasesAndSelf( user ) {
return userQuery( user ) || user._main || user;
}
// Functions called via semantics.dynamic: ------------------------------------
function modelDefinitions() {
return model.definitions;
}
function modelBuiltinsOrDefinitions( user ) {
return definedViaCdl( user ) ? model.$builtins : model.definitions;
}
function artifactParams( user ) {
// TODO: already report error here if no parameters?
return boundActionOrMain( user ).params || Object.create( null );
}
function boundActionOrMain( art ) {
while (art._main) {
if (art.kind === 'action' || art.kind === 'function')
return art;
art = art._parent;
}
return art;
}
function typeOfParentDict( user ) {
// CDL produces the following XSN representation for `type of elem`:
// { path: [{ id: 'type of'}, { id: 'elem'}], scope: 'typeOf' }
return { 'type of': user._parent };
}
function targetElements( user, pathItemArtifact ) {
// has already been computed - no further `navigationEnv` args needed
const env = navigationEnv( pathItemArtifact || user._parent );
// do not use env?.elements: a `0` should stay a `0`:
return env && env.elements;
}
function combinedSourcesOrParentElements( user ) {
const query = userQuery( user );
if (!query)
return environment( user._main ? user._parent : user );
return query._combined; // TODO: do we need query._parent._combined ?
}
function parentElements( user ) {
// Note: We could have `$self` in bound actions refer to its entity, but reject it now.
// If users request it, we can either allow it later or point them to binding parameters.
const useParent = user._main &&
user.kind !== 'select' &&
user.kind !== 'action' &&
user.kind !== 'function';
return environment( useParent ? user._parent : user );
}
function parentElementsOrKeys( user ) {
// annotations on foreign keys only ever have access to their keys (except of course via $self)
if (user.kind === 'key')
return user._parent?.foreignKeys || Object.create( null );
return parentElements( user );
}
function queryElements( user ) {
return environment( user );
}
function nestedElements( user ) {
const colParent = user._columnParent;
Functions.effectiveType( colParent ); // set _origin
const path = colParent?.value?.path;
if (!path?.length)
return undefined;
// also set dependency when navigating along assoc → provide location
return environment( colParent._origin, path[path.length - 1].location, colParent );
}
// Function called via semantics.navigation: ----------------------------------
// default is function `environment`
function artifactsEnv( art ) {
return art._subArtifacts || Object.create( null );
}
function staticTarget( prev ) {
let env = navigationEnv( prev ); // we do not write dependencies for assoc navigation
if (env === 0)
return 0;
// Last try - Composition with targetAspect only (in aspect def):
const target = env?.targetAspect;
if (target) {
if (target.elements)
return target.elements;
env = resolvePath( env.targetAspect, 'targetAspect', env );
}
return env?.elements || Object.create( null );
}
function targetNavigation( art, location, user ) {
const env = navigationEnv( art, location, user, false );
// do not use env?.elements: a `0`/false should stay a `0`/false:
return env && env.elements;
}
function assocOnNavigation( art, location, user ) {
const env = navigationEnv( art, location, user, null );
// `null` means: do not write a dependency from target of any association
// otherwise “following” own assoc would lead to cycle.
// TODO: disallow navigation other than of own assoc, and to foreign keys
// This way (not here though, but later in resolve.js)
if (env === 0)
return 0;
return env?.elements || Object.create( null );
}
function calcElemNavigation( art, location, user ) {
const env = navigationEnv( art, location, user, 'calc' );
if (env === 0)
return 0;
return env?.elements || Object.create( null );
}
// Return effective search environment provided by artifact `art`, i.e. the
// `artifacts` or `elements` dictionary. For the latter, follow the `type`
// chain and resolve the association `target`. View elements are calculated
// on demand.
// TODO: what about location/user when called from getPath ?
// TODO: think of removing `|| Object.create(null)`.
// (if not possible, move to second param position)
function environment( art, location, user ) {
const env = navigationEnv( art, location, user, 'nav' );
if (env === 0)
return 0;
return env?.elements || Object.create( null );
}
function navigationEnv( art, location, user, assocSpec ) {
// = effectiveType() on from-path, TODO: should actually already part of
// resolvePath() on FROM
if (!art)
return undefined;
let type = Functions.effectiveType( art );
while (type?.items) // TODO: disallow navigation to many sometimes
type = Functions.effectiveType( type.items );
if (!type?.target)
return type;
if (assocSpec === false) { // TODO: move to getPathItem
error( null, [ location, user ], {},
'Following an association is not allowed in an association key definition' );
return false;
} // TODO: else warning for assoc usage with falsy assocSpec
const target = type?.target._artifact;
if (!target)
return target;
// TODO: really write final dependency with expand/inline?
if (target && assocSpec && user) {
if (assocSpec !== 'calc')
dependsOn( user._main || user, target, location || user.location, user );
else
dependsOn( user.$calcDepElement, target, location || user.location, user );
}
const effectiveTarget = Functions.effectiveType( target );
// if (effectiveTarget === 0 && location)
// dependsOn( user, user, (user.target || user.type || user.value || user).location );
// console.log('NT:',assocSpec,!!user,target)
return effectiveTarget;
}
// Functions called