@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,206 lines (1,119 loc) • 50.2 kB
JavaScript
// Compiler phase 1 = "define": transform dictionary of AST-like XSNs into XSN
// The 'define' phase (function 'define' below) is the first phase of the compile
// function. In it, the compiler
//
// - collects definitions and extensions from the XSN representation of CDL and
// CSN sources (“ASTs”) into _one_ XSN model,
// - sets “structural” links between XSN nodes and completes the “name”,
// some links and names inside `extensions` are set at a later stage
// - reports errors for: “late” syntax errors (when it is more convenient to do
// it here instead of doing it in both CDL and CSN parser), “structural” errors
// and “duplicate definition errors”
// The 'define' phase is the only compile() phase which is also called for
// parse.cdl. See file ./finalize-parse-cdl.js for details.
// --------- TODO: begin in extra markdown document -----------------------------
// An XSN for a source looks like
// { kind: 'source', artifacts: <dictionary of artifact defs>, namespace: {}, ... }
//
// The property `artifacts` of a source contains the top-level definitions.
// Definitions inside a context are not listed here (as opposed to
// `definitions`, see below), but inside the property `artifacts` of that context.
// The 'define' phase (function 'define' below) enriches a dictionary of
// (file names to) AST-like XSNs and restructure them a little bit, the result
// is called XSN ("augmented CSN"):
// { sources: <dictionary of ASTs>, definitions: <dictionary of artifact defs> }
//
// The property `sources` is the input argument (dictionary of source ASTs).
//
// The property `definitions` is set by this compiler phase. It contains the
// definitions of all main artifacts (i.e. not elements) from all sources, the
// key is the absolute name of that artifact. These definitions are the same
// objects as the definitions accessible via `sources` and `artifacts` of the
// corresponding source/context.
//
// You get the compact "official" CSN format by applying the function exported
// by "../json/to-csn.js" to the XSN.
// Example 'file.cds':
// namespace A;
// context B {
// type C { elem: String(4); }
// }
// Check the augmented CSN by compiling it with
// cdsc --raw-output + file.cds
//
// ┌───────────────┐ ┌───────────────────────────────────────────┐
// │ sources │ │ definitions │
// └──┬────────────┘ └──┬────────────────────────────┬───────────┘
// │ │ │
// │ ['file.cds'] │ ['A.B'] │ ['A.B.C']
// ↓ ↓ ↓
// ┌───────────────┐ _parent ┌────────────────┐ _parent ┌──────────────┐
// │ kind:'source' │←──────────┤ kind:'context' │←──────────┤ kind: 'type' │
// │ artifacts: ───┼──────────→│ artifacts: ────┼──────────→│ ... │
// └───────────────┘ ['B'] └────────────────┘ ['C'] └──────────────┘
//
// The _parent properties are not shown in the JSON - they are used for name
// resolution, see file './resolver.js'.
// An artifact definition looks as follows (example: context "A.B" above):
// {
// kind: 'context',
// name: { path: [ { id: 'B'} ], absolute: 'A.B', location: { <for the id "B"> } },
// artifacts: <for contexts, a dictionary of artifacts defined within>,
// location: { <of the complete artifact definition> } },
// _parent: <the parent artifact, here the source 'file.cds'>
// }
// The properties `name.absolute`, `name.component` and `_parent` are set
// during this compiler phase.
// The definition of an entity or a structured type would contain an `elements`
// property instead of an `artifacts` property.
// An element definition looks as follows (example: "elem" above):
// {
// kind: 'element',
// name: { id: 'elem', component: 'elem', location: { <for the id "elem"> } }
// type: { path: [ { id: 'String', location: ... } ] },
// $typeArgs: [ { number: '4', location: ... } ]
// location: { <of the complete element definition> } },
// _parent: <the parent artifact, here the type "A.B.C">
// }
// --------- TODO: end in extra markdown document -------------------------------
// Sub phase 1 (addXYZ) - only for main artifacts
// - set _block links for main definitions, vocabulary, extensions and
// annotations on those
// - store definitions (including context extensions), NO duplicate check
// - artifact name check
// - Note: the only allow name resolving is resolveUncheckedPath(),
// TODO: make sure that _no_ _artifact link is set
// - POST: all user-written definitions are in model.definitions
// Sub Phase 2 (initXYZ)
// - set _parent, _main (later: _service?) links, and _block links of members
// - add _subArtifacts dictionary and "namespace artifacts" for name resolution
// - duplicate checks
// - structure checks ?
// - annotation assignments
// - POST: resolvePath() can be called for artifact references (if complete model)
// More sub phases...
// The main difficulty is the correct behavior concerning duplicate definitions
// - For code completion, all duplicate definitions must be further checked.
// - We need a unique object for the _subArtifacts dictionary.
// - We must have a property at the artifact whether there are duplicates in order
// to avoid consequential or repeated errors.
// - But: The same artifact is added to multiple dictionaries.
// - Solution part 1: $duplicates as property of the artifact or member
// for `definitions`, `_artifacts`, member dictionaries, `vocabulary`
// dictionary of the whole model, `$tableAliases` dictionary of queries.
// - Solution part 2: array value in dictionary for duplicates in CDL `artifacts`
// dictionary, `_combined` dictionary for query search, `$tableAliases`
// of JOIN restrictions, `vocabulary` dictionary of a CDL input source.
;
const {
forEachGeneric,
forEachInOrder,
forEachMember,
} = require('../base/model');
const { weakLocation } = require('../base/location');
const shuffleGen = require('../base/shuffle');
const {
dictAdd, dictAddArray, dictForEach, pushToDict,
} = require('../base/dictionaries');
const { kindProperties, dictKinds } = require('./base');
const {
setLink,
initItemsLinks,
setMemberParent,
createAndLinkCalcDepElement,
initExprAnnoBlock,
initDollarSelf,
initDollarParameters,
initBoundSelfParam,
dependsOnSilent,
pathName,
targetCantBeAspect,
} = require('./utils');
const { compareLayer } = require('./moduleLayers');
const { initBuiltins } = require('./builtins');
const { isInReservedNamespace } = require('../base/builtins');
const $location = Symbol.for( 'cds.$location' );
const $inferred = Symbol.for( 'cds.$inferred' );
/**
* Export function of this file. Transform argument `sources` = dictionary of
* AST-like CSNs into augmented CSN. If a vector is provided for argument
* `messages` (usually the combined messages from `parse` for all sources), do
* not throw an exception in case of an error, but push the corresponding error
* object to that vector. If at least one AST does not exist due to a parse
* error, set property `lintMode` of `options` to `true`. Then, the resolver
* does not report errors for using directives pointing to non-existing
* artifacts.
*
* @param {XSN.Model} model Model with `sources` property that contain AST-like CSNs.
*/
function define( model ) {
const { options } = model;
// Get simplified "resolve" functionality and the message function:
const {
error, warning, info, messages, message,
} = model.$messageFunctions;
const {
resolveUncheckedPath,
} = model.$functions;
const { shuffleDict, shuffleArray } = shuffleGen( options.testMode );
Object.assign( model.$functions, {
shuffleDict,
shuffleArray,
initMainArtifact,
initMembers, // for finalize-parser-cdl.js
targetIsTargetAspect,
checkRedefinition,
initSelectItems,
} );
return doDefine();
/**
* Main function of the definer.
*/
function doDefine() {
if (options.deprecated &&
messages.every( m => m.messageId !== 'api-deprecated-option' )) {
warning( 'api-deprecated-option', {}, {
prop: 'deprecated', '#': (options.beta ? 'beta' : 'std'),
}, {
std: 'With option $(PROP), recent features are disabled',
beta: 'With option $(PROP), beta features and other recent features are disabled',
} );
}
model.definitions = Object.create( null );
setLink( model, '_entities', [] ); // for entities with includes
model.$entity = 0;
model.$compositionTargets = Object.create( null );
model.$collectedExtensions = Object.create( null );
initBuiltins( model );
const sourceNames = shuffleArray( Object.keys( model.sources ) );
for (const name of sourceNames)
addSource( model.sources[name] );
// Phase 2:
for (const name of sourceNames)
initNamespaceAndUsing( model.sources[name] );
dictForEach( model.definitions, initMainArtifact );
dictForEach( model.vocabularies, initVocabulary );
dictForEach( model.$collectedExtensions, e => e._extensions.forEach( initExtension ) );
addI18nBlocks(); // TODO: part of extend.js?
const { $self } = model.definitions;
if ($self && $self.kind !== 'namespace') { // TODO v7: non-config error
message( 'name-deprecated-$self', [ $self.name.location, $self ], { name: '$self' },
'Do not use $(NAME) as name for an artifact definition' );
}
}
// Phase 1: ----------------------------------------------------------------
// Functions called from top-level: addSource()
/**
* Add definitions of the given source AST, both CDL and CSN
*
* @param {XSN.SourceAst} src
*/
function addSource( src ) {
// handle sub model from parser
if (!src.kind)
src.kind = 'source';
let namespace = src.namespace?.name;
let prefix = '';
if (namespace?.path) {
namespace.id = pathName( namespace.path );
prefix = `${ namespace.id }.`;
}
if (isInReservedNamespace( prefix )) {
error( 'reserved-namespace-cds', [ src.namespace.name.location, src.namespace.name ],
{ name: 'cds' },
'The namespace $(NAME) is reserved for CDS builtins' );
namespace = null; // TODO: reconsider
}
if (src.$frontend !== 'json') { // CDL input
// TODO: set _block to builtin
if (src.artifacts) {
// addBlockArtifact() adds usings to src.artifacts: shuffleDict must be assigned first
src.artifacts = shuffleDict( src.artifacts );
addPathPrefixes( src.artifacts, prefix ); // before addUsing
}
else if (src.usings || namespace) {
src.artifacts = Object.create( null );
}
if (src.usings)
shuffleArray( src.usings ).forEach( u => addUsing( u, src ) );
if (namespace?.id) // successfully set a full name for namespace
addNamespace( namespace, src );
if (src.artifacts) { // addBlockArtifact needs usings for context extensions
src.artifacts = shuffleDict( src.artifacts );
dictForEach( src.artifacts, a => addBlockArtifact( a, src, prefix ) );
}
}
else if (src.definitions) { // CSN input
prefix = ''; // also for addVocabulary() below
dictForEach( shuffleDict( src.definitions ), def => addMainArtifact( def, src, prefix ) );
}
if (src.vocabularies) {
if (!model.vocabularies)
model.vocabularies = Object.create( null );
dictForEach( shuffleDict( src.vocabularies ), v => addVocabulary( v, src, prefix ) );
}
if (src.extensions) { // requires using to be known!
src.extensions.forEach( e => addExtension( e, src ) );
}
}
function addMainArtifact( art, block, prefix ) {
setLink( art, '_block', block );
initExprAnnoBlock( art, block );
art.name.id ??= prefix + pathName( art.name.path );
const absolute = art.name.id;
// TODO: check reserved, see checkName()/checkLocalizedObjects() of checks.js
if (isInReservedNamespace( absolute )) {
error( 'reserved-namespace-cds', [ art.name.location, art ], { name: 'cds' },
'The namespace $(NAME) is reserved for CDS builtins' );
const builtin = model.definitions[absolute];
if (builtin && builtin.builtin) // if already a builtin...
return;
// otherwise we just define it...
}
else if (art.query && (absolute === 'localized' || absolute.startsWith( 'localized.' ))) {
// Due to recompilation, we don't emit this info message for JSON frontend.
if (block.$frontend !== 'json') {
info( 'ignored-localized-definition', [ art.name.location, art ], {},
'This definition in the namespace "localized" is ignored' );
}
return;
}
// dictAdd might set $duplicates
dictAdd( model.definitions, absolute, art );
}
// If 'A.B.C' is in 'artifacts', also add 'A' for name resolution
function addPathPrefixes( artifacts, prefix ) {
for (const name in artifacts) {
const d = artifacts[name];
const a = Array.isArray( d ) ? d[0] : d;
a.name.id ??= prefix + pathName( a.name.path );
const index = name.indexOf( '.' );
if (index < 0)
continue; // also for newly added (i.e. does not matter whether visited or not)
const using = name.substring( 0, index );
if (artifacts[using])
continue;
// TODO: enable optional locations
const location = a.name.path?.[0]?.location || a.location;
const absolute = prefix + using;
artifacts[using] = {
kind: 'using', // !, not namespace - we do not know artifact yet
name: { id: using, location, $inferred: 'as' },
extern: { location, id: absolute },
location,
$inferred: 'path-prefix',
};
}
}
/**
* Add the names of a USING declaration to the top-level search environment
* of the source, and set the absolute name referred by the USING
* declaration.
*
* @param {XSN.Using} decl Node to be expanded and added to `src`
* @param {XSN.SourceAst} src
*/
function addUsing( decl, src ) {
setLink( decl, '_block', src );
if (decl.usings) {
// e.g. `using {a,b} from 'file.cds'` -> recursive
decl.usings.forEach( u => addUsing( u, src ) );
return;
}
const path = decl.extern?.path;
if (!path || !path[0]) // syntax error
return;
decl.extern.id = pathName( path );
if (!decl.name)
decl.name = { ...path.at(-1), $inferred: 'as' };
const name = decl.name.id;
// TODO: check name: no "."
const found = src.artifacts[name];
// a real `using` declaration is “nicer” than a compiler-generated one:
if (found?.$inferred === 'path-prefix' && found.extern.id === decl.extern.id)
src.artifacts[name] = decl;
else
dictAddArray( src.artifacts, name, decl );
}
// must be called after addUsing().
function addNamespace( namespace, src ) {
// create using for own namespace:
// TODO: should we really do that (in v6)? See also initNamespaceAndUsing().
const last = namespace.path.at(-1);
const { id } = last;
if (src.artifacts[id] || last.id.includes( '.' ))
// not used as we have a definition/using with that name, or dotted last path id
return;
src.artifacts[id] = {
kind: 'using',
name: { id, location: last.location, $inferred: 'as' },
extern: namespace,
location: namespace.location,
$inferred: 'namespace',
};
}
function addBlockArtifact( art, block, prefix ) {
if (art.kind === 'using')
return;
addMainArtifact( art, block, prefix );
if (art.artifacts) {
const p = `${ art.name.id }.`;
// path prefixes (usings) must be added before extensions in artifacts:
addPathPrefixes( art.artifacts, p );
dictForEach( art.artifacts, a => addBlockArtifact( a, art, p ) );
}
if (art.extensions) { // requires using to be known!
art.extensions.forEach( e => e.name && addExtension( e, art ) );
}
}
function addExtension( ext, block ) {
setLink( ext, '_block', block );
initExprAnnoBlock( ext, block );
const absolute = ext.name && resolveUncheckedPath( ext.name, '_uncheckedExtension', ext );
if (!absolute) // broken path
return;
delete ext.name.path[0]._artifact; // might point to wrong JS object in phase 1
ext.name.id = absolute; // definition might not be there yet, no _artifact link
const location = { file: '' }; // stupid required location
const late = model.$collectedExtensions[absolute] ||
(model.$collectedExtensions[absolute] = {
kind: 'annotate',
name: { id: absolute, location },
$inferred: '',
location,
});
pushToDict( late, '_extensions', ext );
if (!ext.artifacts)
return;
// Directly add the artifacts of context and service extension:
if (!model.$blocks)
model.$blocks = Object.create( null );
// Set block number for debugging (--raw-output):
// eslint-disable-next-line no-multi-assign
ext.$effectiveSeqNo = model.$blocks[absolute] = (model.$blocks[absolute] || 0) + 1;
const prefix = `${ absolute }.`;
dictForEach( ext.artifacts, a => addBlockArtifact( a, ext, prefix ) );
}
function addVocabulary( vocab, block, prefix ) {
setLink( vocab, '_block', block );
const { name } = vocab;
name.id ??= prefix + pathName( name.path );
dictAdd( model.vocabularies, name.id, vocab );
}
// I18n code (eventually part of extend.js) -----------------------------------
/**
* Add (optional) translations into the XSN model.
*/
function addI18nBlocks() {
// TODO: the sequence should be in sync with extend / annotate / future $sources
const sortedSources = Object.keys( model.sources )
.filter( name => !!model.sources[name].i18n )
.sort( (a, b) => compareLayer( model.sources[a], model.sources[b] ) );
if (sortedSources.length === 0)
return;
if (!model.i18n)
model.i18n = Object.create( null );
for (const name of sortedSources)
addI18nFromSource( model.sources[name] );
}
/**
* Add the source's translations to the model. Warns if the source's translations
* do not match the ones from previous sources.
*
* @param {XSN.SourceAst} src
*/
function addI18nFromSource( src ) {
for (const langKey of Object.keys( src.i18n )) {
if (!model.i18n[langKey])
model.i18n[langKey] = Object.create( null );
for (const textKey of Object.keys( src.i18n[langKey] )) {
const sourceVal = src.i18n[langKey][textKey];
const modelVal = model.i18n[langKey][textKey];
if (!modelVal) {
model.i18n[langKey][textKey] = sourceVal;
}
else if (modelVal.val !== sourceVal.val) {
// TODO: behave like annotation assignments? message-id?
warning( 'i18n-different-value', sourceVal.location,
{ prop: textKey, otherprop: langKey } );
}
}
}
}
// Phase 2 ("init"), top-level & main -----------------------------------------
// Functions called from top-level: initNamespaceAndUsing(), initMainArtifact(),
// initVocabulary(), initExtension()
function initNamespaceAndUsing( src ) {
if (src.$frontend && src.$frontend !== 'cdl')
return;
if (src.namespace) {
const decl = src.namespace.name;
if (!decl.id) // parsing may have failed
return;
if (!model.definitions[decl.id]) {
// TODO: make it possible to have no location
const ns = { kind: 'namespace', name: decl, location: decl.location };
model.definitions[decl.id] = ns;
initArtifactParentLink( ns, model.definitions );
}
const last = decl.path[decl.path.length - 1];
const builtin = model.$builtins[last.id];
if (builtin && !builtin.internal &&
src.artifacts[last.id] && src.artifacts[last.id].extern === decl) {
warning( 'ref-shadowed-builtin', [ decl.location, null ], // no home artifact
{ id: last.id, art: decl.id, code: `using ${ builtin.name.id };` },
'$(ID) now refers to $(ART) - consider $(CODE)' );
}
// setArtifactLink( decl, model.definitions[absolute] ); // TODO: necessary?
}
if (!src.usings)
return;
for (const name in src.artifacts) {
const entry = src.artifacts[name];
if (!Array.isArray( entry )) // no local name duplicate
continue;
for (const decl of entry) {
if (!decl.$duplicates) { // do not have two duplicate messages
error( 'duplicate-using', [ decl.name.location, decl ], { name },
'Duplicate definition of top-level name $(NAME)' );
}
}
}
}
function initMainArtifact( art, reInit = false ) {
if (!reInit) // not for auto-exposed entity
initArtifactParentLink( art, model.definitions );
checkRedefinition( art );
initDollarSelf( art ); // $self, TODO: also for 'namespace'?
initMembers( art );
if (art.params)
initDollarParameters( art ); // $parameters
if (art.query) {
initArtifactQuery( art );
restrictToSimpleProjection( art );
}
}
function initVocabulary( art ) {
initArtifactParentLink( art, model.vocabularies );
checkRedefinition( art );
initMembers( art );
if (art.query) {
initArtifactQuery( art );
error( 'def-unsupported-projection', [ art.location, art ], null,
'Projections for annotation definitions are not supported' );
}
}
function initExtension( parent ) {
// TODO: re-think
forEachMember( parent, function initExtensionMember( sub, name, prop ) {
if (sub.kind !== 'extend' && sub.kind !== 'annotate')
return; // for defs inside, set somewhere else - TODO: rethink
if (prop === 'params' && name === '') // RETURNS
sub.name = { id: '', location: sub.location };
setLink( sub, '_block', parent._block );
initExprAnnoBlock( sub, parent._block );
setLink( sub, '_parent', parent );
setLink( sub, '_main', parent._main || parent );
initExtension( sub );
} );
if (parent.kind !== 'extend')
return;
// TODO: sub queries? expand/inline?
parent.columns?.forEach( c => setLink( c, '_block', parent._block ) );
if (parent.scale && !parent.precision) {
// TODO: where could we store the location of the name?
error( 'syntax-missing-type-property', [ parent.scale.location ],
{ prop: 'scale', otherprop: 'precision' },
'Type extension with property $(PROP) must also have property $(OTHERPROP)' );
parent.scale = undefined; // no consequential error
}
}
/**
* Make the `_parent` pointer of artifact `A.B.C` point to `A.B`. If no `A.B`
* exists in XSN.definitions, create an artifact with kind `namespace` (better
* would be `$gap`). Add `A.B.C` to the `_subArtifacts` dictionary of `A.B`.
* Do recursively if necessary, `_parent` of `A.B` is `A` in this example.
*/
function initArtifactParentLink( art, definitions, path, pathIndex ) {
setLink( art, '_parent', null );
const { id } = art.name;
const dot = id.lastIndexOf( '.' );
if (dot < 0)
return;
const prefix = id.substring( 0, dot );
let parent = definitions[prefix];
if (!parent) {
path ??= art.name.path;
pathIndex ??= path?.length - 1;
const pathItemOrName = (path && pathIndex) ? path[--pathIndex] : art.name;
const location = weakLocation( pathItemOrName.location );
parent = { kind: 'namespace', name: { id: prefix, location }, location };
definitions[prefix] = parent;
initArtifactParentLink( parent, definitions, path, pathIndex );
}
setLink( art, '_parent', parent );
if (!parent._subArtifacts)
setLink( parent, '_subArtifacts', Object.create( null ) );
if (art.$duplicates !== true) // no redef or "first def"
parent._subArtifacts[id.substring( dot + 1 )] = art; // not dictAdd()
}
// TODO: message ids
// Called by initMainArtifact() and initVocabulary() and for members
function checkRedefinition( art ) {
if (art.kind === 'annotate' || art.kind === 'extend') // move this check to call in extend.js?
return; // extensions are merged into a super-annotate; $duplicates are only kept for LSP
if (!art.$duplicates || !art.name.id ||
art.$errorReported === 'syntax-duplicate-extend')
return;
if (art._main) {
error( 'duplicate-definition', [ art.name.location, art ], {
name: art.name.id,
'#': kindProperties[art.kind].normalized || art.kind,
} );
}
else if (!art.builtin) {
// TODO: better messages with definitions with the same name as builtin,
// especially if there is just one
error( 'duplicate-definition', [ art.name.location, art ], {
name: art.name.id,
'#': (art.kind === 'annotation' ? 'annotation' : 'absolute' ),
} );
}
}
// Phase 2, init queries ------------------------------------------------------
/**
* Restrict the query of `art` to only simple projections, i.e. those without 'group by', etc.
*
* TODO v8 and for `type`: do in parsers
*
* @param {XSN.Artifact} art
*/
function restrictToSimpleProjection( art ) {
const { query } = art;
if (art.kind !== 'type')
return; // TODO(v6): Also for event
if (!query.from?.path)
return; // union, sub-select, etc. should already be rejected
const check = (prop, keyword) => (query[prop] && { prop: query[prop], keyword });
const unexpectedQueryProp = check( 'where', 'where' ) ||
check( 'groupBy', 'group by' ) ||
check( 'limit', 'limit' ) ||
check( 'having', 'having' ) ||
check( 'orderBy', 'order by' ) ||
null;
if (unexpectedQueryProp) {
error( 'query-unexpected-prop', [ unexpectedQueryProp.prop.location, query ], {
'#': art.kind,
keyword: unexpectedQueryProp.keyword,
}, {
std: 'Unexpected $(KEYWORD) for projection clause used as type expression',
type: 'Unexpected $(KEYWORD) for type definition',
event: 'Unexpected $(KEYWORD) for event definition',
} );
return;
}
const firstCondition = query.from.path.find(step => step.where)?.where;
if (firstCondition) {
error( 'query-unexpected-filter', [ firstCondition.location, query ], { '#': art.kind }, {
std: 'Unexpected filter in query source for projection clause used as type expression',
type: 'Unexpected filter in projection clause of type definition',
event: 'Unexpected filter in projection clause of event definition',
} );
}
}
function initArtifactQuery( art ) {
art.$queries = [];
setLink( art, '_from', [] ); // for sequence of resolve steps - TODO: remove
if (!setLink( art, '_leadingQuery', initQueryExpression( art.query, art ) ) )
return; // null or undefined in case of parse error
// if (art._leadingQuery !== art.$queries [0]) throw Error('FOO');
setLink( art._leadingQuery, '_$next', art );
if (art.elements) { // specified element via compilation of client-style CSN
// TODO: consider this part of a revamped on-demand 'extend' functionality
setLink( art, 'elements$', art.elements );
delete art.elements;
}
}
// art is:
// - entity/event/type for top-level queries (including UNION args)
// - $tableAlias for sub query in FROM - TODO: what about UNION there?
// - $query for real sub query (in columns, WHERE, ...), again: what about UNION there?
function initQueryExpression( query, art ) {
if (!query) // parse error
return query;
if (query.from) { // select
initQuery( query, art );
initTableExpression( query.from, query, [] );
if (query.mixin)
initMixins( query, art );
if (!query.$tableAliases.$self) { // same as $projection
const self = {
name: { id: '$self', location: query.location },
kind: '$self',
location: query.location,
};
setLink( self, '_origin', query );
setLink( self, '_parent', query );
setLink( self, '_main', query._main );
const projection = { ...self, deprecated: true }; // hide in code completion
setLink( projection, '_origin', query );
setLink( projection, '_parent', query );
setLink( projection, '_main', query._main );
query.$tableAliases.$self = self;
query.$tableAliases.$projection = projection;
}
initSubQuery( query ); // check for SELECT clauses after from / mixin
}
else if (query.args) { // UNION, INTERSECT, ..., query in parens
const leading = initQueryExpression( query.args[0], art );
for (const q of query.args.slice(1))
initQueryExpression( q, art );
setLink( query, '_leadingQuery', leading );
if (leading) {
if (query.orderBy) {
leading.$orderBy ??= [ ];
leading.$orderBy.push( ...query.orderBy );
}
if (query.limit) {
leading.$limit ??= [ ];
leading.$limit.push( query.limit );
}
}
// ORDER BY and LIMIT to be evaluated in leading query
}
else { // with parse error (`select from <EOF>`, `select from E { *, ( select }`)
return undefined;
}
return query._leadingQuery || query;
}
function initQuery( query, art ) {
setLink( query, '_block', art._block );
const main = art._main || art;
setLink( query, '_$next',
(art.kind === '$tableAlias' ? art._parent._$next : art ) );
query.kind = 'select';
query.name = { location: query.location, id: main.$queries.length + 1 };
setMemberParent( query, null, main );
// console.log(art.kind,art.name,query.name,query._$next.name)
main.$queries.push( query );
setLink( query, '_parent', art ); // _parent should point to alias/main/query
query.$tableAliases = Object.create( null ); // table aliases and mixin definitions
dependsOnSilent( main, query );
}
// table is table expression in FROM, becomes an alias
function initTableExpression( table, query, joinParents ) {
if (!table) // parse error
return;
if (table.path) { // path in FROM
if (!table.path.length)
// parse error (e.g. final ',' in FROM), projection on <eof>
return;
if (!table.name) {
const last = table.path[table.path.length - 1];
const dot = last?.id?.lastIndexOf( '.' );
const id = (dot >= 0) ? last.id.substring( dot + 1 ) : last.id || '';
// TODO: if we have too much time, we can calculate the real location with '.'
table.name = { $inferred: 'as', id, location: last.location };
}
addAsAlias( table, query, joinParents );
// _origin is set when we resolve the ref
if (query._parent.kind !== 'select')
query._main._from.push( table ); // store tabref if outside "real" subquery
// (tab refs on the right of union are unnecessary)
}
else if (table.query) {
if (!table.name?.id) {
// We don't worry about duplicate names here.
const id = `$_select_${ query._main.$queries.length + 1 }__`;
table.name = { id, location: table.location, $inferred: '$internal' };
}
addAsAlias( table, query, joinParents );
// Store _origin to leading query of table.query for name resolution
setLink( table, '_origin', initQueryExpression( table.query, table ) );
}
else if (table.join) {
if (table.on) {
setLink( table, '_$next', query ); // or query._$next?
setLink( table, '_block', query._block );
table.kind = '$join';
table.name = { location: query.location }; // param comes later
table.$tableAliases = Object.create( null ); // table aliases and mixin definitions
joinParents = [ ...joinParents, table ];
}
if (table.args) {
table.args.forEach( (tab, index) => {
// set for A2J such that for every table alias `ta`:
// ta === (ta._joinParent
// ? ta._joinParent.args[ta.$joinArgsIndex] // in JOIN
// : ta._parent.from ) // directly in FROM
// Note for --raw-output: _joinParent pointing to CROSS JOIN node has not name
if (!tab) // parse error; time for #6241
return; // (parser method to only add non-null to array)
setLink( tab, '_joinParent', table );
tab.$joinArgsIndex = index;
initTableExpression( tab, query, joinParents );
} );
}
if (table.on) { // after processing args to get the $tableAliases
setMemberParent( table, query.name.id, query ); // sets _parent,_main
initSubQuery( table ); // init sub queries in ON
const aliases = Object.keys( table.$tableAliases || {} );
// Use first table alias name on the right side of the join to name the
// (internal) query, should only be relevant for --raw-output, not for
// user messages or references - TODO: correct if join on left?
table.name.id = aliases[1] || aliases[0] || '<unknown>';
setLink( table, '_user', query ); // TODO: do not set kind/name
setLink( table, '_$next', query._$next );
// TODO: probably set this to query if we switch to name restriction in JOIN
}
}
}
function addAsAlias( table, query, joinParents ) {
table.kind = '$tableAlias';
setLink( table, '_block', query._block );
setMemberParent( table, table.name.id, query );
dictAdd( query.$tableAliases, table.name.id, table, ( name, loc, tableAlias ) => {
if (tableAlias.name.$inferred === '$internal') {
const semanticLoc = tableAlias.query?.name ? tableAlias.query : tableAlias;
// TODO: the semanticLoc query is not initialized yet, and thus cannot
// be used here
error( 'name-missing-alias', [ tableAlias.location, semanticLoc ],
{ '#': 'duplicate', code: 'as ‹alias›' } );
}
else {
error( 'duplicate-definition', [ loc, table ], { name, '#': 'alias' } );
}
} );
// also add to JOIN nodes for name restrictions:
for (const p of joinParents) {
// for JOIN alias restriction, we cannot use $duplicates, as it is
// already used for duplicate aliases of queries:
dictAddArray( p.$tableAliases, table.name.id, table );
}
if (table.name?.id[0] === '$' && table.name.$inferred !== '$internal') {
message( 'name-invalid-dollar-alias', [ table.name.location, table ], {
'#': (table.name.$inferred ? '$tableImplicit' : '$tableAlias'),
name: '$',
keyword: 'as',
} );
}
}
function initSubQuery( query ) {
if (query.on)
initExprForQuery( query.on, query );
// TODO: MIXIN with name = ...subquery (not yet supported anyway)
initSelectItems( query, query.columns, query );
if (query.where)
initExprForQuery( query.where, query );
if (query.having)
initExprForQuery( query.having, query );
initMembers( query );
}
function initExprForQuery( expr, query ) {
// TODO: use traverseExpr()
if (Array.isArray( expr )) { // TODO: old-style $parens ?
expr.forEach( e => initExprForQuery( e, query ) );
}
else if (!expr) {
return;
}
else if (expr.query) {
initQueryExpression( expr.query, query );
}
else if (expr.args) {
const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
args.forEach( e => initExprForQuery( e, query ) );
}
}
function initMixins( query, art ) {
forEachInOrder( query, 'mixin', initMixin );
function initMixin( mixin, name ) {
setLink( mixin, '_block', art._block );
setMemberParent( mixin, name, query );
checkRedefinition( mixin );
if (!(mixin.$duplicates)) {
// TODO: do some initMembers() ? If people had annotation
// assignments on the mixin... (also for future mixin definitions
// with generated values)
dictAdd( query.$tableAliases, name, query.mixin[name], ( dupName, loc ) => {
error( 'duplicate-definition', [ loc, query ], { name: dupName, '#': 'alias' } );
} );
if (mixin.name.id[0] === '$') {
message( 'name-invalid-dollar-alias', [ mixin.name.location, mixin ],
{ '#': 'mixin', name: '$' } );
}
}
}
}
function initSelectItems( parent, columns, user, inExtension = false ) {
let wildcard = !!inExtension; // no `extend … with columns { * }`
// TODO: forbid expand/inline in ref-where, outside queries (CSN), ...
let hasItems = false;
let colIndex = 0;
for (const col of columns || parent.expand || parent.inline || []) {
++colIndex;
if (!col) // parse error
continue;
hasItems = true;
if (!columns) { // expand or inline
if (parent.value)
setLink( col, '_columnParent', parent ); // TODO?: also set for '*' in expand/inline
else if (parent._columnParent)
setLink( col, '_columnParent', parent._columnParent );
// colParent not set for `{ col } as struct`, except if that is in expand/inline
}
if (col.val === '*') {
if (!wildcard) {
wildcard = col;
}
else if (wildcard === true) { // in `extend … with columns {…}`
error( 'ext-unexpected-wildcard', [ col.location, parent ], { code: '*' },
'Unexpected $(CODE) (wildcard) in an extension' );
col.val = null; // do not consider it for expandWildcard()
}
else {
// a late syntax error (this code also runs with parse-cdl), i.e.
// no semantic loc (wouldn't be available for expand/inline anyway)
// TODO: why here and not in parser?
error( 'syntax-duplicate-wildcard', [ col.location, null ], {
'#': (wildcard.location.col ? 'col' : 'std'),
prop: '*',
line: wildcard.location.line,
col: wildcard.location.col,
}, {
std: 'You have provided a $(PROP) already in line $(LINE)',
col: 'You have provided a $(PROP) already at line $(LINE), column $(COL)',
} );
// TODO: extra text variants for expand/inline? - probably not
col.val = '**'; // do not consider it for expandWildcard()
}
}
// Either expression (value), expand, new virtual or new association
else {
col.kind = 'element';
if (!col._block)
setLink( col, '_block', parent._block );
setMemberParent( col, null, parent );
// remark: _parent might be change if col is in expand of to-many assoc
ensureColumnName( col, colIndex, parent, !columns );
if (col.inline) { // `@anno elem.{ * }` does not work
if (col.doc) {
message( 'syntax-unexpected-anno', [ col.doc.location, col ],
{ '#': 'doc', code: '.{ ‹inline› }' } );
}
// col.$annotations no available for CSN input, have to search.
// Message about first annotation should be enough to avoid spam.
const firstAnno = Object.keys( col ).find( key => key.startsWith( '@' ) );
if (firstAnno) {
message( 'syntax-unexpected-anno', [ col[firstAnno].name.location, col ],
{ code: '.{ ‹inline› }' } );
}
}
// TODO: allow sub queries? at least in top-level expand without parallel ref
if (columns && !inExtension) // not (yet) in `extend … with columns {…}`
initExprForQuery( col.value, parent );
if (col.expand || col.inline)
initSelectItems( col, null, user ); // TODO: use col, remove 3rd param?
initMembers( col ); // with #13933, TODO: only for enums
checkCdlTypeCast( col );
}
}
if (hasItems && !wildcard && parent.excludingDict && !options.$recompile) {
// TODO: the SQL backend should probably delete `excluding` when expanding `*`
// TODO: use `parent` for semantic location; requires `_parent`/... links.
warning( 'query-ignoring-excluding', [ parent.excludingDict[$location], user ],
{ prop: '*' },
'Excluding elements without wildcard $(PROP) has no effect' );
}
}
/**
* Ensure that the column has a name.
*
* @param col
* @param {number} colIndex
* @param query
* @param {boolean} insideExpand
* Whether the column is inside 'expand'.
* Anonymous 'expands' don't have a column parent, hence why we need to know this explicitly.
*/
function ensureColumnName( col, colIndex, query, insideExpand ) {
if (col.name)
return col.name.id;
if (col.inline || col.val === '*' || col.val === '**') // '**' = duplicate '*'
return '';
// TODO: should we give technical name (colIndex) for error messages?
const path = col.value &&
(col.value.path || !col.value.args && col.value.func?.path);
const last = path?.length && path[path.length - 1];
if (last) {
col.name = { id: last.id || '', location: last.location, $inferred: 'as' };
return col.name.id;
}
else if ((col.expand || col.value) &&
!path && // no parse error (path without last item)
(insideExpand || query._parent.kind !== 'select')) { // not sub-selects
error( 'query-req-name',
// TODO: message function: `query` should work directly
[ (col.value || col).location, (query.name ? query : query._parent ) ],
{},
'Alias name is required for this select item' );
}
col.name = {
// NOTE: If the alias is changed, corresponding name-clash tests must be updated as well!
id: `$_column_${ colIndex }`,
location: (col.value || col).location,
$inferred: '$internal',
};
return col.name.id;
}
function checkCdlTypeCast( col ) { // with #13933:
// We don't allow CDL-style casts to anonymous structures. We reject it already here
// and not in checks.js to ensure that it's rejected in parseCdl.
// TODO: via <guard> in CdlGrammar.g4
if (col.elements) {
error( 'type-invalid-cast', [ (col.elements[$location] ?? col.location), col ],
{ '#': 'to-inline-structure' } );
}
else if (col.expand && (col.type || col.items)) {
const loc = (col.type?.location || col.elements?.[$location] ||
col.items?.location || col.location);
error( 'type-invalid-cast', [ loc, col ], { '#': 'expand' } );
}
// Remark: there are quite a few consequential error, TODO: avoid them
}
// Members (elements, enum, actions, params): ---------------------------------
/**
* Set property `_parent` for all elements in `parent` to `parent` and do so
* recursively for all sub elements.
*
* Param `initExtensions` is for parse.cdl - TODO delete
*/
function initMembers( parent ) {
const block = parent._block;
let obj = initItemsLinks( parent, block );
initExprAnnoBlock( parent, block );
if (obj.target && targetIsTargetAspect( obj )) {
obj.targetAspect = obj.target;
delete obj.target;
}
const { targetAspect } = obj;
if (targetAspect) {
if (obj.foreignKeys) {
error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], parent ] );
delete obj.foreignKeys; // continuation semantics: not specified
}
if (obj.on && !obj.target) {
error( 'type-unexpected-on-condition', [ obj.on.location, parent ] );
delete obj.on; // continuation semantics: not specified
}
if (targetAspect.elements) // eslint-disable-next-line no-multi-assign
parent = obj = initAnonymousAspect( parent, obj, targetAspect );
}
forEachInOrder( obj, 'elements', (...args) => initArtifact( parent, ...args ) );
forEachGeneric( obj, 'enum', (...args) => initArtifact( parent, ...args ) );
forEachInOrder( obj, 'foreignKeys', (...args) => initArtifact( parent, ...args ) );
forEachGeneric( parent, 'actions', (...args) => initArtifact( parent, ...args ) );
forEachInOrder( parent, 'params', (...args) => initArtifact( parent, ...args ) );
const { returns } = parent;
if (returns) {
const { kind } = parent;
returns.kind = (kind === 'extend' || kind === 'annotate') ? kind : 'param';
initArtifact( parent, returns, '' ); // '' is special name for returns parameter
}
}
function initAnonymousAspect( parent, obj, targetAspect ) {
// TODO: main?
const inEntity = parent._main?.kind === 'entity';
// TODO: also allow indirectly (component in component in entity)?
setLink( targetAspect, '_outer', obj );
setLink( targetAspect, '_parent', parent._parent );
setLink( targetAspect, '_main', null ); // for name resolution
targetAspect.kind = 'aspect'; // TODO: probably '$aspect' to detect
setLink( targetAspect, '_block', parent._block );
initDollarSelf( targetAspect );
// allow ref of up_ in anonymous aspect inside entity
// (TODO: complain if used and the managed composition is included into
// another entity - might induce auto-redirection):
if (inEntity && !targetAspect.elements.up_) {
const up = {
name: { id: 'up_' },
kind: '$navElement',
location: obj.location,
};
setLink( up, '_parent', targetAspect );
setLink( up, '_main', targetAspect ); // used on main artifact
// recompilation case: both target and targetAspect → allow up_ in that case, too:
const name = obj.target && resolveUncheckedPath( obj.target, 'target', obj );
const entity = name && model.definitions[name];
if (entity && entity.elements)
setLink( up, '_origin', entity.elements.up_ );
// processAspectComposition/expand() sets _origin to element of
// generated target entity
targetAspect.$tableAliases.up_ = up;
}
return targetAspect;
}
function initArtifact( parent, elem, name, prop ) {
if (!elem.kind) // wrong CSN input
elem.kind = dictKinds[prop];
if (!elem.name && !elem._outer) {
const ref = elem.targetElement || elem.kind === 'element' && elem.value;
if (ref && ref.path) {
elem.name = Object.assign( { $inferred: 'as' },
ref.path[ref.path.length - 1] );
}
else { // RETURNS, parser robustness
elem.name = { id: name, location: elem.location };
}
}
if (!elem._block)
setLink( elem, '_block', parent._block );
// don't dump with `entity T {}; extend T with { extend e {}; e {}; e {} };`:
setMemberParent( elem, name, parent );
checkRedefinition( elem );
initMembers( elem );
if (elem.kind === 'action' || elem.kind === 'function')
initBoundSelfParam( elem.params, elem._main );
// for a correct home path, setMemberParent needed to be called
if (!elem.value || elem.kind !== 'element' ||
elem.$syntax === 'enum' && parent.kind === 'extend') // ambiguous in parse-cdl
return;
// -> it's a calculated element
if (!elem.type && elem.value.type) { // top-level CAST( expr AS type )
if (!elem.target)
elem.type = { ...elem.value.type, $inferred: 'cast' };
}
elem.$syntax = 'calc';
// TODO: it is not just "syntax" - maybe better test for `$calcDepElement`?
createAndLinkCalcDepElement( elem );
// Special case (hack) for calculated elements that use composition+filter:
// See "Notes on `$enclosed`" in `ExposingAssocWithFilter.md` for details.
// TODO: hm, only for inferred type - then just do not infer it in that case
if (elem.target && elem.value.path?.[elem.value.path.length - 1]?.where) {
delete elem.type;
delete elem.on;
delete elem.target;
}
}
/**
* Return whether the `target` is actually a `targetAspect`
*/
function targetIsTargetAspect( elem ) {
const { target } = elem;
if (target.elements) // CSN parser ensures: has no targetAspect then
return true;
if (elem.targetAspect) {
// Ensure that a compiled CSN is parseable - not inside query, only on element
return false;
}
if (targetCantBeAspect( elem ) || options.parseCdl)
return false;
// Compare this check with check in acceptEntity() called by resolvePath()
// Remark: do not check `on` and `foreignKeys` here, we want error for those, not the aspect
const name = resolveUncheckedPath( target, 'target', elem );
const aspect = name && model.definitions[name];
return (aspect?.kind === 'aspect' || aspect?.kind === 'type') && // type is sloppy
aspect.elements && !aspect.elements[$inferred];
}
}
module.exports = define;