UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,324 lines (1,241 loc) 86.7 kB
// 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