@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
1,512 lines (1,433 loc) • 71.7 kB
JavaScript
// CSN frontend - transform CSN into XSN
// TODO: re-check extensions handling - set kind early!, ...
// TODO: restrict 'actions' etc better in annotate statements - also/only consider parent property!
// TODO: extend E { extend elem { extend sub } }
'use strict';
/**
* Overview of properties in schema specifications (values in 'schema' dictionary):
*
* @typedef {object} SchemaSpec
*
* @property {TransformerFunction} [type]
* Transformation and test function (i.e. type). The first four arguments are the same for
* all functions. Further ones may be accepted as well.
*
* @property {string} [class]
* A schemaClass. Possible values are keys of the variable "schemaClasses".
* Essentially all properties of the class are copied.
*
* @property {Function} [arrayOf]
* Alternative to "type". The property should be an array. Value is passed to arrayOf().
* Value is ignored if "type" is set. Then it is only used for better error messages.
*
* @property {Function} [dictionaryOf]
* Alternative to "type". The property should be an object in dictionary form (i.e.
* Object.<string, type>). Value is passed to dictionaryOf(). Value is ignored if "type"
* is set. Then it is only used for better error messages.
*
* @property {Object.<string, SchemaSpec>} [schema]
* If some sub-properties have a different semantic in this property than the default then
* switch the currently used spec to this value.
*
* @property {string} [prop]
* Name of the property. compileSchema() sets it to the dictionary key by default.
*
* @property {string} [msgProp]
* Display name of the property. compileSchema() sets it to the dictionary key
* (+ optional '[]') by default.
*
* @property {string} [msgVariant]
* Use this message variant instead of the default one.
* Allows more precise and detailed error messages.
*
* @property {string|string[]|Function|false} [requires]
* If the value is a(n array of) string, then (one of) the given sub-property is required.
* If a function, that function issues its own message.
* If `undefined` (default), then at least one property is required.
* If false, then no sub-properties are required.
*
* @property {boolean} [noPrefix]
* Only used for '#' at the moment. Signals that the entry should not be used for keys
* like '#key'. `getSchema(...)` normally checks if `schema[prop]` exists and if not, checks
* for `schema[prop.charAt(0)]`.
* This is intended for annotations and similar (which start with special characters).
*
* @property {boolean} [ignore]
* Don't issue warnings.
*
* @property {string[]} [optional]
* Optional sub-properties that may be used. Warnings are issued if unknown properties are set.
*
* @property {string} [defaultKind]
* Default kind for sub-elements, e.g. objects in "elements".
*
* @property {string[]|Function} [inKind]
* Specifies in what definition type this property may be used, e.g. "virtual" may only be
* used for elements. If it is a function then it takes two arguments "kind" and "parentSpec"
* should return a boolean.
*
* @property {string[]} [validKinds]
* What "kind" values are possible in a definition. The root "definitions" properties allows
* more kinds than e.g. definitions inside "elements".
*
* @property {string|string[]|Object} [onlyWith]
* Defines that the property *must* be used with one of these properties.
* If an object, it maps the kind value to a string or array of strings.
*
* @property {number} [minLength]
* Minimum number of elements that an array must have.
*
* @property {boolean} [inValue]
* Puts the value into an XSN property "value", e.g. { value: ... }
*
* @property {string[]} [xorGroups]
* Corresponding xor groups. It references a value of xorGroups. If set then only one property
* of the xorGroup may be set, e.g. if target is set, elements may not.
* If you are looking for a `notWith` (which should be symmetric), this is your property.
*
* @property {string} [vZeroFor]
* Marks the property as a CSN 0.1.0 property. It is replaced by this CSN 1.0
* property (value of vZeroFor).
*
* @property {string} [vZeroIgnore]
* Marks the property as a CSN 0.1.0 property. The property is ignored and a warning may be
* issues about it.
*
* @property {string} [xorException]
* A property name that is allowed besides another property of an xorGroup (as an exception
* to the rule).
*
* @property {boolean} [ignoreExtra]
* Whether extra properties are ignored and not put into $extra.
*/
/**
* @typedef {Function} TransformerFunction
* @param {object} obj
* @param {object} xsn
* @param {object} csn
* @param {object} prop
* @param {...any} any Further arguments.
* @returns {any} XSN property (e.g. string, object, ...)
*/
const { dictAdd } = require('../base/dictionaries');
const { quotedLiteralPatterns } = require('../compiler/builtins');
const { isAnnotationExpression } = require('../base/builtins');
const { CompilerAssertion } = require('../base/error');
const { Location } = require('../base/location');
const { XsnSource } = require('../compiler/xsn-model');
const { xsnAsTree, splitClauses } = require('../parsers/XprTree');
const $location = Symbol.for('cds.$location');
let inExtensions = null;
let vocabInDefinitions = null; // must be reset!
// CSN property names reserved for CAP
const ourpropsRegex = /^(?:[_$=#@][a-zA-Z]*[0-9]*|[a-zA-Z]+[0-9]*)$/;
// Sync with definition in to-csn.js:
const typeProperties = [
// do not include CSN v0.1.0 properties here:
'target', 'elements', 'enum', 'items',
'cardinality', // for association publishing in views
'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull', 'default',
'keys', 'on', // only with 'target'
];
const exprProperties = [
// do not include CSN v0.1.0 properties here:
'ref', 'xpr', 'list', 'val', '#', 'func', 'SELECT', 'SET', // Core Compiler checks SELECT/SET
'param', 'literal', 'args', 'cast', // only with 'ref'/'ref'/'val'/'func'
];
// Groups of properties which cannot be used together:
const xorGroups = {
// include CSN v0.1.0 properties here:
':type': [
'target', 'targetAspect', 'elements', 'items', // xorException: target+targetAspect
'length', 'precision', 'scale', 'srid', // xorException: precision+scale
],
':enum': [ 'target', 'targetAspect', 'elements', 'enum', 'items' ],
':expr': [ // see also xorException property in schema
'ref', 'xpr', 'list', 'val', '#', 'func', 'SELECT', 'SET', 'expand',
'=', 'path', 'value', 'op', // '='/'path' is CSN v0.1.0 here
],
':col': [ 'expand', 'inline' ],
':ext': [ 'annotate', 'extend' ], // TODO: better msg for test/negative/UnexpectedProperties.csn
':assoc': [
'on', 'keys',
'foreignKeys', 'onCond', // 'foreignKeys'/'onCond' is CSN v0.1.0
],
':on': [ 'on', 'default' ],
// TODO - improve consequential errors: assume no name given with `join` or `inline`?
as: [ 'as', 'join', 'inline' ],
scope: [ 'param' ],
quantifier: [ 'some', 'any', 'distinct', 'all' ],
// quantifiers 'some' and 'any are 'xpr' token strings in CSN v1.0
};
// Functions reading properties which do not count for the message
// 'Object in $(PROP) must have at least one property'
const functionsOfIrrelevantProps = [ ignore, extra, explicitName ];
const schemaClasses = {
condition: {
arrayOf: exprOrString,
type: condition,
msgVariant: 'or-string', // for 'syntax-expecting-object'
// TODO: also specify requires here, and adapt onlyWith()
optional: exprProperties,
},
expression: {
type: expr,
optional: exprProperties,
},
natnumOrStar: {
type: natnumOrStar,
msgVariant: 'or-asterisk', // for 'syntax-expecting-unsigned-int'
},
columns: {
arrayOf: selectItem,
msgVariant: 'or-asterisk', // for 'syntax-expecting-object'
defaultKind: '$column',
validKinds: [], // pseudo kind '$column'
// A column with only as+cast.type is a new association
requires: [ 'ref', 'cast', 'xpr', 'val', '#', 'func', 'list',
'SELECT', 'SET', 'expand', 'virtual' ],
schema: {
xpr: {
class: 'condition',
type: xprInValue,
xorException: 'func', // see xorGroup :expr; for window functions
inKind: [ '$column' ],
inValue: true,
},
cast: { // CDL-style type cast
// see “global” `cast` schema for SQL `cast` function
type: embed,
inValue: false,
optional: typeProperties,
inKind: [ '$column' ],
},
'=': {
// by not setting `vZeroFor`, we disallow `=` in `columns`.
// CSN v0.1 didn't have columns, so this isn't breaking v0.1 compatibility.
type: ignore,
},
},
},
};
// TODO: also have stricter tests for strings in in xpr/args, join, op, sort, nulls ?
const schema = compileSchema( {
requires: {
type: renameTo( 'dependencies', arrayOf( stringVal, val => (val.literal === 'string') ) ),
},
i18n: {
dictionaryOf: i18nLang,
},
// definitions: ------------------------------------------------------------
definitions: {
dictionaryOf: definition,
defaultKind: 'type',
validKinds: [
'entity', 'type', 'aspect', 'action', 'function', 'context', 'service', 'event', 'annotation',
],
// requires: { entity: ['elements', 'query', 'includes'] } - not, make it work w/o elements
},
vocabularies: {
dictionaryOf: definition,
defaultKind: 'annotation',
validKinds: [],
},
extensions: {
arrayOf: definition,
defaultKind: 'annotate',
validKinds: [], // use annotate/extend instead of kind
requires: [ 'extend', 'annotate' ],
},
enum: {
type: enumDict,
dictionaryOf: definition,
defaultKind: 'enum',
validKinds: [ 'enum' ],
inKind: [ 'element', 'type', 'param', 'annotation', 'annotate', 'extend' ],
},
elements: {
type: elementsDict,
dictionaryOf: definition,
defaultKind: 'element',
validKinds: [ 'element' ],
requires: requiresOnWithBothTargetProps,
inKind: [
'element',
'type',
'aspect',
'entity',
'param',
'annotation',
'event',
'annotate',
'extend',
],
},
actions: {
dictionaryOf: actions,
defaultKind: 'action',
validKinds: [ 'action', 'function' ],
onlyWith: { aspect: 'elements' },
inKind: [ 'entity', 'aspect', 'annotate', 'extend' ],
},
params: {
dictionaryOf: definition,
defaultKind: 'param',
validKinds: [ 'param' ],
inKind: [ 'entity', 'action', 'function', 'annotate' ], // TODO: 'extend'?
},
mixin: {
dictionaryOf: definition,
defaultKind: 'mixin',
validKinds: [],
},
columns: {
class: 'columns',
inKind: [ 'extend' ], // only valid in extend and SELECT/projection
},
expand: {
class: 'columns',
xorException: 'ref', // see xorGroup :expr
inKind: [ '$column' ], // only valid in $column
},
inline: {
class: 'columns',
onlyWith: 'ref',
inKind: [ '$column' ], // only valid in $column
},
keys: {
arrayOf: definition,
type: keys,
defaultKind: 'key',
validKinds: [],
requires: 'ref',
onlyWith: 'target',
inKind: [ 'element', 'type', 'param' ],
},
foreignKeys: { // CSN v0.1.0 property -> use 'keys'
vZeroFor: 'keys',
inKind: [],
dictionaryOf: definition,
defaultKind: 'key',
validKinds: [],
},
// kind and name: ----------------------------------------------------------
kind: {
type: validKind,
inKind: (( kind, parentSpec ) => !inExtensions && parentSpec.validKinds.length),
},
annotate: {
type: kindAndName,
inKind: [ 'annotate' ],
},
extend: {
type: kindAndName,
inKind: [ 'extend' ],
},
as: {
// remark: 'as' does not count as "relevant" property in standard check that
// an object has >0 props, see const functionsOfIrrelevantProps.
type: explicitName,
inKind: [ '$column', 'key' ],
},
// type properties (except: elements, enum, keys, on): ---------------------
type: {
type: typeArtifactRef,
msgVariant: 'or-object', // for 'syntax-expecting-string',
optional: [ 'ref' ],
inKind: [ 'element', 'type', 'param', 'mixin', 'event', 'annotation', 'extend' ],
schema: {
ref: {
arrayOf: typeRefItem,
type: renameTo( 'path', typeRef ),
minLength: 1,
requires: 'id',
optional: [ 'id' ],
ignoreExtra: true, // custom properties inside `ref` ignored.
},
},
},
targetAspect: {
type: artifactRef,
xorException: inferredTargetEntityForAspect, // usually allows `target`
msgVariant: 'or-object', // for 'syntax-expecting-string',
requires: 'elements',
optional: [ 'elements' ], // 'elements' for ad-hoc aspect compositions
inKind: [ 'element' ],
},
target: {
type: artifactRef,
xorException: inferredTargetEntityForAspect, // usually allows `targetAspect`
msgVariant: 'or-object', // for 'syntax-expecting-string',
requires: 'elements',
optional: [ 'elements' ], // 'elements' for ad-hoc COMPOSITION OF (gensrc style CSN)
inKind: [ 'element', 'type', 'mixin', 'param' ],
},
cardinality: { // there is an extra def for 'from'
type: object,
optional: [ 'src', 'min', 'max' ],
inKind: [ 'element', 'type', 'mixin' ],
onlyWith: [ 'target', 'targetAspect', 'where' ], // also in 'ref[]'
},
items: {
type: object,
optional: typeProperties, // TODO: think of items: {}, then requires: false
inKind: [ 'element', 'type', 'param', 'annotation' ],
},
localized: {
type: boolOrNull,
inKind: [ 'element', 'type', 'param', 'annotation' ],
},
length: {
type: natnum,
inKind: [ 'element', 'type', 'param', 'annotation', 'extend' ],
// we do not require a 'type', too - could be useful alone in a 'cast'
},
precision: {
type: natnum,
xorException: 'scale', // see xorGroup :type
inKind: [ 'element', 'type', 'param', 'annotation', 'extend' ],
},
scale: {
type: scalenum,
xorException: 'precision', // see xorGroup :type
inKind: [ 'element', 'type', 'param', 'annotation', 'extend' ],
},
srid: {
type: natnum,
inKind: [ 'element', 'type', 'param', 'annotation' ], // no 'extend'!
},
srcmin: { // in 'cardinality'
type: renameTo( 'sourceMin', natnum ),
},
src: { // in 'cardinality'
class: 'natnumOrStar',
type: renameTo( 'sourceMax', natnumOrStar ),
},
min: { // in 'cardinality'
type: renameTo( 'targetMin', natnum ),
},
max: { // in 'cardinality'
class: 'natnumOrStar',
type: renameTo( 'targetMax', natnumOrStar ),
},
sourceMax: {
class: 'natnumOrStar',
vZeroFor: 'src',
},
targetMin: {
vZeroFor: 'min',
type: natnum,
},
targetMax: {
class: 'natnumOrStar',
vZeroFor: 'max',
},
// expression properties (except: SELECT, SET): ----------------------------
ref: {
arrayOf: refItem,
type: renameTo( 'path', arrayOf( refItem ) ),
msgVariant: 'or-object', // for 'syntax-expecting-string',
minLength: 1,
requires: 'id',
optional: [
'id', 'args', 'cardinality', 'where',
// Support once we allow them in non-parse-only CDL.
// 'groupBy', 'having', 'orderBy', 'limit',
],
xorException: 'expand', // see xorGroup :expr
inKind: [ '$column', 'key' ],
},
id: { // in 'ref' item
type: string,
},
param: {
type: asScope, // is bool, stored as string in XSN property 'scope'
onlyWith: 'ref',
inKind: [ '$column' ],
},
func: {
type: func,
xorException: 'xpr', // see xorGroup :expr
inKind: [ '$column' ],
},
args: {
class: 'condition',
type: args,
schema: { // named arguments cannot directly have a string
'-named': { // '-named' and '-' must not exist top-level
prop: 'args', dictionaryOf: expr, optional: exprProperties,
},
},
onlyWith: [ 'func', 'id', 'op' ],
inKind: [ '$column' ],
},
xpr: {
class: 'condition',
type: xpr,
xorException: 'func', // see xorGroup :expr
// special treatment in $column
},
list: {
class: 'condition',
type: list,
inKind: [ '$column' ],
},
val: {
type: value,
inKind: [ '$column', 'enum' ],
xorException: '#', // see xorGroup :expr
// see also extra handling for 'element' in extension, see definition()
},
literal: {
type: literal,
onlyWith: 'val',
inKind: [ '$column', 'enum' ], // 'element' sometimes in extension
},
'#': {
noPrefix: true, // schema spec for '#', not for '#whatever'
type: symbol,
// Note: We emit a warning if '#' is used in enums. Because the compiler
// can generate CSN like this, we need to be able to parse it.
inKind: [ '$column', 'enum' ],
xorException: 'val', // see xorGroup :expr
// see also extra handling for 'element' in extension, see definition()
},
path: { // in CSN v0.1.0 'foreignKeys'
vZeroFor: 'ref',
inKind: [],
inValue: true,
type: vZeroRef,
},
'=': { // v0.1.0 { "=": "A.B" } for v1.0 { "ref": ["A", "B"] }
noPrefix: true, // schema spec for '=', not for '=whatever'
vZeroFor: 'ref',
inKind: [], // still used in annotation assignments...
type: vZeroRef, // ...see property '@' / function annotation()
},
// primary query properties: -----------------------------------------------
query: {
type: embed,
optional: [ 'SELECT', 'SET' ],
inKind: [ 'entity', 'event' ],
},
projection: {
type: queryTerm,
requires: 'from',
optional: [
'from', 'all', 'distinct', 'columns', 'excluding', // no 'mixin'
'where', 'groupBy', 'having', 'orderBy', 'limit',
],
inKind: [ 'entity', 'event', 'type' ],
},
SELECT: {
type: queryTerm,
requires: 'from',
optional: [
'from', 'mixin', 'all', 'distinct', 'columns', 'excluding',
'where', 'groupBy', 'having', 'orderBy', 'limit', 'elements',
],
inKind: [ '$column' ],
schema: {
elements: {
dictionaryOf: definition,
type: ( ...a ) => {
dictionaryOf( definition )( ...a );
}, // ignore, but test
defaultKind: 'element',
validKinds: [ 'element' ],
},
},
},
SET: {
type: querySet,
requires: 'args',
optional: [ 'op', 'all', 'distinct', 'args', 'orderBy', 'limit' ],
schema: {
args: {
arrayOf: embed, // like query
type: queryArgs,
minLength: csn => (csn.op ? 2 : 1),
optional: [ 'SELECT', 'SET' ],
},
},
inKind: [ '$column' ],
},
op: { // used for UNION etc. in CSN v1.0
vZeroFor: 'xpr',
vZeroIgnore: 'call', // is also used in CSN v0.1.0 for "normal" expressions
type: setOp,
onlyWith: 'args',
},
join: {
type: join, // string like 'cross' - TODO: test for valid ones?
},
from: {
type: fromObject,
optional: [ 'ref', 'join', 'cardinality', 'args', 'on', 'SELECT', 'SET', 'as' ],
schema: {
cardinality: {
type: object,
optional: [ 'srcmin', 'src', 'min', 'max' ],
onlyWith: 'join',
},
args: {
arrayOf: fromObject,
minLength: 2,
optional: [ 'ref', 'join', 'cardinality', 'args', 'on', 'SELECT', 'SET', 'as' ],
onlyWith: 'join',
schema: {}, // 'args' in 'args' in 'from' is same as 'args' in 'from'
},
},
},
some: { type: asQuantifier }, // probably just CSN v0.1.0
any: { type: asQuantifier }, // probably just CSN v0.1.0
distinct: { type: asQuantifier },
all: { type: asQuantifier },
// further query properties: -----------------------------------------------
excluding: {
inKind: [ '$column' ],
arrayOf: string,
type: excluding,
},
on: {
class: 'condition',
onlyWith: [ 'target', 'join' ],
inKind: [ 'element', 'mixin' ],
},
onCond: {
vZeroFor: 'on',
inKind: [],
type: renameTo( 'on', expr ),
optional: exprProperties,
},
where: {
class: 'condition',
},
groupBy: {
arrayOf: expr, optional: exprProperties,
},
having: {
class: 'condition',
},
orderBy: {
arrayOf: expr, optional: [ 'sort', 'nulls', ...exprProperties ],
},
sort: {
type: stringVal,
},
nulls: {
type: stringVal, // TODO: test for valid ones?
},
limit: {
type: object, requires: 'rows', optional: [ 'rows', 'offset' ],
},
rows: {
class: 'expression',
},
offset: {
class: 'expression',
},
// miscellaneous properties in definitions: --------------------------------
doc: {
type: stringValOrNull,
msgVariant: 'or-null', // for 'syntax-expecting-string'
inKind: () => true, // allowed in all definitions (including columns and extensions)
},
'@': { // for all properties starting with '@'
noPrefix: false, // just '@' is no CSN property
prop: '@‹anno›', // which property name do messages use for annotation assignments?
type: annotation,
// allowed in all definitions except mixins (including columns and extensions)
inKind: kind => (kind !== 'mixin'),
schema: {
'-expr': { // '-expr' and '-' must not exist top-level
prop: '@‹anno›',
type: object,
optional: [
'=', '#', 'xpr', 'ref', 'val', 'list',
'literal', 'func', 'args', 'param',
'cast',
],
schema: {
'=': {
type: renameTo( '$tokenTexts', stringOrBool ),
xorGroups: null, // reset xorGroup; allow '=' for all :expr
},
},
},
},
},
abstract: { // v1: with 'abstract', an entity becomes an aspect
type: abstract,
inKind: [ 'entity', 'aspect' ], // 'aspect' because 'entity' is replaced by 'aspect' early
},
key: {
type: boolOrNull,
inKind: [ 'element', '$column' ],
},
masked: {
type: masked,
inKind: [ 'element' ],
},
notNull: {
type: boolOrNull,
inKind: [ 'element', 'param', 'type' ], // TODO: $column - or if so: in 'cast'?
},
virtual: {
type: boolOrNull,
inKind: [ 'element', '$column' ],
},
cast: { // SQL `cast` function
// see `cast` sub schema inside `columns` for CDL-style type cast
type: embed,
// see also call of eventualCast() for every (expression) object
inValue: false, // should have no relevance
optional: [ 'type', 'length', 'precision', 'scale', 'srid' ],
},
default: {
class: 'expression',
inKind: [ 'element', 'param', 'type' ],
},
includes: {
arrayOf: stringRef,
inKind: [ 'entity', 'type', 'aspect', 'event', 'extend' ],
},
returns: {
type: returnsDefinition,
defaultKind: 'param',
validKinds: [ 'param' ],
inKind: [ 'action', 'function', 'annotate' ],
},
technicalConfig: { // treat it like external_property
type: extra,
inKind: [ 'entity' ],
},
$syntax: {
type: dollarSyntax,
ignore: true,
inKind: [ 'entity', 'type', 'aspect' ],
},
origin: { // old-style CSN
type: vZeroDelete, ignore: true,
},
source: { // CSN v0.1.0 query not supported (is error)
type: ignore,
},
value: {
class: 'expression', // calculated elements
vZeroFor: 'val', // CSN v0.1.0 property for `val` in enum def
// type: annoValue,
inKind: [ 'element', 'enum' ], // TODO: Remove "enum" again; currently for extensions
optional: exprProperties.concat([ 'stored' ]),
},
stored: {
type: boolOrNull,
},
// ignored: ----------------------------------------------------------------
$location: { // special
ignore: true, type: ignore,
},
$generatedFieldName: {
ignore: true, type: ignore, // TODO: do we need to do something?
},
namespace: {
type: namespace,
},
meta: { // meta information
type: ignore, // TODO: should we test s/th here?
},
version: { // deprecated top-level property
type: ignore,
},
messages: { // deprecated top-level property
type: ignore,
},
options: { // deprecated top-level property
type: ignore,
},
csnInteropEffective: {
type: ignore, // by https://github.com/SAP/csn-interop-specification
},
indexNo: { // CSN v0.1.0, but ignored without message
ignore: true, type: ignore,
},
// TODO: should we keep $parens ?
$generated: {
type: string,
},
$: {
noPrefix: false, // just '$' is no CSN property
type: ignore,
ignore: true,
}, // including $origin
_: {
noPrefix: false, // just '_' is no CSN property
type: ignore,
ignore: true,
},
} );
const topLevelSpec = {
msgProp: '', // falsy '' for top-level
type: object,
optional: [
'requires', 'definitions', 'vocabularies', 'extensions', 'i18n',
'namespace', 'version', 'messages', 'meta', 'options', '@', '$location',
'csnInteropEffective',
],
requires: false, // empty object OK
schema,
};
// Module variables, schema compilation, and functors ------------------------
/** @type {(id, location, textOrArguments, texts?) => void} */
// eslint-disable-next-line no-unused-vars
let message = (_id, loc, textOrArguments, texts) => undefined;
/** @type {(id, location, textOrArguments, texts?) => void} */
// eslint-disable-next-line no-unused-vars
let error = (id, loc, textOrArguments, texts) => undefined;
/** @type {(id, location, textOrArguments, texts?) => void} */
// eslint-disable-next-line no-unused-vars
let warning = (id, loc, textOrArguments, texts) => undefined;
/** @type {(id, location, textOrArguments, texts?) => void} */
// eslint-disable-next-line no-unused-vars
let info = (id, loc, textOrArguments, texts) => undefined;
let csnVersionZero = false;
let csnFilename = '';
let virtualLine = 1;
/** @type {CSN.Location[]} */
let dollarLocations = [];
let arrayLevelCount = 0;
/**
* @param {Object.<string, SchemaSpec>} specs
* @param {object} [proto]
* @returns {Object.<string, SchemaSpec>}
*/
function compileSchema( specs, proto = null ) {
// no prototype to protect against evil-CSN properties 'toString' etc.
const r = Object.assign( Object.create( proto ), specs );
for (const p of Object.keys( specs )) {
const s = r[p];
if (s.class) {
const scs = schemaClasses[s.class];
for (const c of Object.keys( scs )) {
if (s[c] == null)
s[c] = scs[c];
}
}
if (s.prop == null)
s.prop = p;
if (s.msgProp == null)
s.msgProp = (s.arrayOf || s.dictionaryOf) ? `${ s.prop }[]` : s.prop;
if (s.schema)
s.schema = compileSchema( s.schema, r );
if (!s.type) {
if (s.arrayOf)
s.type = arrayOf( s.arrayOf );
else if (s.dictionaryOf)
s.type = dictionaryOf( s.dictionaryOf );
else
throw new CompilerAssertion( `Missing type specification for property "${ p }"` );
}
if (s.xorGroups === undefined) {
// Only set xorGroup once. Could already be set through shared sub-schema
// of schemaClasses or be explicitly set.
s.xorGroups = [];
for (const group in xorGroups) {
if (xorGroups[group].includes(p))
s.xorGroups.push(group);
}
}
}
if (proto)
return r;
// Set property 'inValue' in main schema only:
for (const prop of exprProperties) {
if (r[prop].inValue === undefined)
r[prop].inValue = true;
}
return r;
}
function renameTo( xsnProp, fn ) {
return function renamed( val, spec, xsn, csn ) {
const r = fn( val, spec, xsn, csn );
if (r !== undefined)
xsn[xsnProp] = r;
};
}
function arrayOf( fn, filter = undefined ) {
return function arrayMap( val, spec, xsn, csn ) {
if (!isArray( val, spec ))
return undefined;
const r = val.map( (v) => {
++virtualLine;
return fn( v, spec, xsn, csn ) || { location: location() };
} );
if (val.length)
++virtualLine; // [] in one JSON line
const minLength = (typeof spec.minLength === 'function')
? spec.minLength( csn )
: spec.minLength || 0;
if (minLength > val.length) {
error( 'syntax-incomplete-array', location(true),
{ prop: spec.prop, n: minLength, '#': minLength === 1 ? 'one' : 'std' });
}
if (filter)
return r.filter(filter);
return r;
};
}
// Generic functions, objects (std signature) --------------------------------
function ignore( obj ) {
if (obj && typeof obj === 'object') {
const array = (Array.isArray( obj )) ? obj : Object.values( obj );
if (!array.length)
return; // {}, [] in one JSON line
virtualLine += 1 + array.length;
array.forEach( ignore );
}
}
function embed( obj, spec, xsn ) {
Object.assign( xsn, object( obj, spec ) ); // TODO: $location?
}
function extra( node, spec, xsn ) {
if (!xsn.$extra)
xsn.$extra = Object.create(null);
xsn.$extra[spec.prop] = node;
return ignore( node );
}
function eventualCast( obj, spec, xsn ) {
if (!obj.cast || spec.optional && !spec.optional.includes('cast'))
return xsn;
xsn.op = { val: 'cast', location: xsn.location };
const r = { location: xsn.location };
xsn.args = [ r ];
return r;
}
function object( obj, spec ) {
if (!isObject( obj, spec ))
return undefined;
pushLocation( obj );
const r = { location: location() };
const xor = {};
const csnProps = Object.keys( obj );
const o = eventualCast( obj, spec, r ); // do s/th special for CAST
let relevantProps = 0;
if (csnProps.length) {
++virtualLine;
const expected = (p => spec.optional.includes(p));
for (const p of csnProps) {
const s = getSpec( spec, obj, p, xor, expected );
// TODO: count illegal properties with Error msg as relevant to avoid 2nd error
if (!functionsOfIrrelevantProps.includes( s.type ))
++relevantProps;
const v = (s.inValue) ? o : r;
const val = s.type( obj[p], s, v, obj, p );
if (val !== undefined)
v[p] = val;
++virtualLine;
}
}
const { requires } = spec;
if (requires === undefined || requires === true) {
// console.log(csnProps,JSON.stringify(spec))
if (!relevantProps) {
error( 'syntax-incomplete-object', location(true),
{ '#': (obj.as != null ? 'as' : 'std'), prop: spec.msgProp, otherprop: 'as' } );
}
}
else if (requires) {
// console.log(csnProps,JSON.stringify(spec))
onlyWith( spec, requires, obj, null, xor, () => true );
}
popLocation( obj );
return r;
}
function vZeroDelete( o, spec ) { // for old-CSN property 'origin'
if (!csnVersionZero) {
message( 'syntax-deprecated-property', location(true),
{ '#': 'zero', prop: spec.msgProp } );
}
ignore( o );
}
// Definitions, dictionaries and arrays of definitions (std signature) -------
function definition( def, spec, xsn, csn, name ) {
if (!isObject( def, spec )) {
return {
kind: (inExtensions ? 'annotate' : spec.defaultKind),
name: { id: '', location: location() },
location: location(),
};
}
pushLocation( def );
const savedInExtensions = inExtensions;
let kind = calculateKind( def, spec ); // might set inExtensions
const r = (kind === '$column') ? { location: location() } : { location: location(), kind };
const xor = {};
const { prop } = spec;
const kind0 = (spec.validKinds.length || spec.prop === 'extensions') && kind;
const csnProps = Object.keys( def );
// For compatibility, extension property `elements` could actually be an `enum`:
if (savedInExtensions === '' && prop === 'elements' && // in extend property `elements`
!Object.keys( def ).some( couldNotBeEnumProperty )) {
r.$syntax = 'enum'; // could be an enum
if (def.val !== undefined || def['#'] !== undefined)
kind = 'enum'; // for function expected(), i.e. allow property `val`/`#`
}
if (csnProps.length) {
const valueName = (prop === 'keys' || prop === 'foreignKeys' ? 'targetElement' : 'value');
// the next is basically object() + the inValue handling
++virtualLine;
for (const p of csnProps) {
const s = getSpec( spec, def, p, xor, expected, kind0 );
const v = !s.inValue && r || r[valueName] || (r[valueName] = { location: location() });
const val = s.type( def[p], s, v, def, p );
if (val !== undefined)
v[p] = val;
++virtualLine;
}
}
if (!r.name && name != null) {
r.name = { id: name, location: r.location };
if (prop === 'columns' || prop === 'keys' || prop === 'foreignKeys')
r.name.$inferred = 'as';
}
if (spec.requires)
onlyWith( spec, spec.requires, def, null, xor, () => true );
inExtensions = savedInExtensions;
popLocation( def );
if (kind !== 'annotation' || prop === 'vocabularies')
return r;
if (!vocabInDefinitions) {
vocabInDefinitions = Object.create(null);
vocabInDefinitions[$location] = location();
}
vocabInDefinitions[name] = r; // deprecated: anno def in 'definitions'
return undefined;
function expected( p, s ) {
if (!Array.isArray(s.inKind))
return s.inKind && s.inKind( kind, spec );
return s.inKind.includes( kind ) &&
// for an 'annotate', both 'annotate' and the "host" kind must be expected
(!inExtensions || s.inKind.includes( inExtensions ) ||
// extending elements in returns can be without 'returns' in CSN
// see function elementsDict() for detail, TODO: remove finally
inExtensions === 'action' && p === 'elements');
}
}
function namespace( ref, spec ) {
const ns = stringRef(ref, spec);
return ns ? { kind: 'namespace', name: ns } : null;
}
function couldNotBeEnumProperty( prop ) {
// returns true for `value` (which we allow with warning when extending an enum with `elements`)
const inKind = schema[prop]?.inKind; // undefined for annotations, $location, …
// inKind for annotation assignments is function -> can be for enum
return Array.isArray( inKind ) && inKind.includes( 'element' );
}
function actions( def, spec, xsn, csn, name ) {
if (def.kind === 'extend' && (def.elements || def.enum)) {
// TODO: Handle this case in extend.js; already done for `returns`
// See message ext-expecting-returns
error( 'syntax-unexpected-property', location(true), {
'#': def.kind,
prop: def.enum ? 'enum' : 'elements',
parentprop: spec.msgProp,
kind: def.kind,
} );
}
return definition( def, spec, xsn, csn, name );
}
// A dictionary is expected. Uses spec.dictionaryOf. If unset, default is "definition".
function dictionaryOf( elementFct ) {
return function dictionary( dict, spec ) {
if (!dict || typeof dict !== 'object' || Array.isArray( dict )) {
error( 'syntax-expecting-object', location(true),
{ prop: spec.prop }); // spec.prop, not spec.msgProp!
return ignore( dict );
}
const r = Object.create(null);
r[$location] = location();
const allNames = Object.keys( dict );
if (!allNames.length)
return r; // {} in one JSON line
++virtualLine;
for (const name of allNames) {
if (!name) {
message( 'syntax-invalid-name', location(true),
{ '#': 'dict', parentprop: spec.prop } );
}
const val = elementFct( dict[name], spec, r, dict, name );
if (val !== undefined)
r[name] = val;
++virtualLine;
}
return r;
};
}
function keys( array, spec, xsn ) {
if (!isArray( array, spec ))
return;
const r = Object.create(null);
r[$location] = location();
if (array.length)
++virtualLine; // possibly empty array
for (const def of array) {
const id = def.as || implicitName( def.ref );
const name = (typeof id === 'string') ? id : '';
// definer will complain about repeated names
dictAdd( r, name, definition( def, spec, r, array, name ) );
++virtualLine;
}
xsn.foreignKeys = r;
}
// Use with spec.msgVariant: 'or-asterisk'
function selectItem( def, spec, xsn, csn ) {
if (def === '*') // compile() will complain about repeated '*'s
return { val: '*', location: location() };
return definition( def, spec, xsn, csn, null ); // definer sets name
}
function returnsDefinition( def, spec, xsn, csn ) {
return definition( def, spec, xsn, csn, '' );
}
// Temporary function as long as the message below is not a hard error
function elementsDict( def, spec, xsn ) {
const elements = dictionaryOf( definition )( def, spec );
if (inExtensions !== 'action')
return elements;
warning( 'syntax-expecting-returns', elements[$location],
{ prop: 'elements', parentprop: 'returns' },
// eslint-disable-next-line @stylistic/max-len
'Expecting property $(PROP) to be put into an object for property $(PARENTPROP) when annotating action return structures' );
xsn.returns = { kind: 'annotate', elements, location: elements[$location] };
return undefined;
}
function enumDict( def, spec, xsn ) {
const dict = dictionaryOf( definition )( def, spec );
if (!inExtensions)
return dict;
xsn.elements = dict; // normalize to `elements` for `annotate`
return undefined;
}
// For v1 CSNs with annotation definitions
function attachVocabInDefinitions( csn ) {
if (!csn.vocabularies) {
csn.vocabularies = vocabInDefinitions;
}
else {
for (const name in vocabInDefinitions)
dictAdd( csn.vocabularies, name, vocabInDefinitions[name] );
}
}
// Kind, names and references (std signature) --------------------------------
function kindAndName( id, spec, xsn ) {
const { prop } = spec;
xsn.kind = prop; // TODO: set this in definition
if (!string( id, spec ))
return;
xsn.name = { path: [ { id, location: location() } ], location: location() };
}
function explicitName( id, spec, xsn ) {
if (string( id, spec ))
xsn.name = { id, location: location() };
}
function abstract( val, spec, xsn, csn ) {
const strange = csn.kind !== 'entity';
if (strange || !csnVersionZero) {
warning( 'syntax-deprecated-abstract', location(true),
{ '#': strange ? 'strange-kind' : 'std', prop: 'abstract', kind: 'entity' } );
}
boolOrNull( val, spec );
}
function dollarSyntax( val, spec, xsn, csn ) {
if (csn.kind === 'type' && val === 'aspect') {
warning( 'syntax-deprecated-dollar-syntax', location(true),
{ '#': 'aspect', prop: '$syntax', kind: 'aspect' } );
return ignore( val );
}
else if (xsn.kind === 'entity') {
if (val === 'projection') {
warning( 'syntax-deprecated-dollar-syntax', location(true),
{
'#': 'projection',
prop: '$syntax',
siblingprop: 'projection',
otherprop: 'query',
} );
return string( val, spec );
}
if (val === 'entity' || val === 'view')
return string( val, spec );
}
warning( 'syntax-deprecated-dollar-syntax', location(true), { prop: '$syntax' } );
return ignore( val );
}
function validKind( val, spec, xsn ) {
if (val === xsn.kind) // has been set in definition - the same = ok!
return undefined; // already set in definition
if (val === 'view' && xsn.kind === 'entity') {
warning( 'syntax-deprecated-kind', location(true),
{ prop: spec.msgProp, value: 'entity' },
'Replace value in $(PROP) by $(VALUE)' );
}
else if (val !== 'entity' && val !== 'type' || xsn.kind !== 'aspect') {
error( 'syntax-invalid-kind', location(true), { prop: spec.msgProp },
'Invalid value for property $(PROP)' );
}
return ignore( val );
}
function typeArtifactRef( ref, spec ) {
if (ref && typeof ref === 'object' && !Array.isArray( ref )) {
if (ref.ref?.length === 1)
return artifactRef( ref, { ...spec, ignoreExtra: true } );
}
return artifactRef( ref, spec );
}
function fromObject( ref, spec ) {
const r = object( ref, spec );
if (r?.path?.length > 1)
r.scope = 1; // `type`/`from` ref in CSN: elements start after definitions name
return r;
}
// Use with spec.msgVariant: 'or-object'
function artifactRef( ref, spec ) {
if (!ref || typeof ref !== 'string') {
if (!ref || typeof ref !== 'object' || Array.isArray( ref ))
return string( ref, spec );
// use error message 'syntax-expecting-string' (string more likely than object):
return (!ref || typeof ref !== 'object' || Array.isArray( ref ))
? string( ref, spec )
: fromObject( ref, spec );
}
if (spec.prop !== 'type')
return stringRef( ref, spec );
// now the CSN v0.1.0 type of: 'Artifact..e1.e2'; error if not csnVersionZero
const idx = ref.indexOf('..');
if (idx < 0)
return stringRef( ref, spec );
if (!csnVersionZero) {
message( 'syntax-deprecated-value', location(true),
{ '#': 'zero-replace', prop: spec.msgProp, value: '{ ref: […] }' } );
}
const r = refSplit( ref.substring( idx + 2 ), 'type' );
r.path.unshift( { id: ref.substring( 0, idx ), location: location() } );
r.scope = 1;
return r;
}
function stringRef( ref, spec ) {
return string( ref, spec ) &&
{ path: [ { id: ref, location: location() } ], location: location() };
}
// with spec.msgVariant: 'or-object'
function refItem( item, spec ) {
if (typeof item === 'string' && item)
return { id: item, location: location() };
if (item && typeof item === 'object' && !Array.isArray( item ))
return object( item, spec );
// use error message 'syntax-expecting-string' (string more likely than object):
return string( item, spec );
}
function asScope( scope, spec, xsn ) {
if (scope)
xsn.scope = spec.prop;
boolOrNull( scope, spec );
}
function vZeroRef( name, spec, xsn ) {
if (!string( name, spec ))
return;
const path = name.split('.');
if (!path.every( id => id)) { // TODO: why just warning?
warning( 'syntax-invalid-zero-ref', location(true), { prop: spec.msgProp },
'Invalid string reference in property $(PROP)' );
}
xsn.path = path.map( id => ({ id, location: location() }) );
}
// Specific values and annotations (std signature) ---------------------------
function boolOrNull( val, spec ) {
if ([ true, false, null ].includes( val ))
return { val, location: location() };
warning( 'syntax-expecting-boolean', location(true), { prop: spec.msgProp },
'Expecting boolean or null for property $(PROP)' );
ignore( val );
return { val: !!val, location: location() };
}
function string( val, spec ) {
if (typeof val === 'string' && val)
return val;
error( 'syntax-expecting-string', location(true),
{ '#': spec.msgVariant, prop: spec.msgProp } );
return ignore( val );
}
function stringOrBool( val, spec ) {
if (typeof val === 'string' && val || typeof val === 'boolean')
return val;
error( 'syntax-expecting-string', location(true),
{ '#': spec.msgVariant || 'or-bool', prop: spec.msgProp } );
return ignore( val );
}
function stringVal( val, spec ) {
if (typeof val === 'string' && val)
// XSN TODO: do not require literal
return { val, literal: 'string', location: location() };
error( 'syntax-expecting-string', location(true), { prop: spec.msgProp },
'Expecting non-empty string for property $(PROP)' );
return ignore( val );
}
function stringValOrNull( val, spec ) {
if (val === null)
return { val, location: location() };
return stringVal(val, spec);
}
function scalenum( val, spec ) {
if ([ 'floating', 'variable' ].includes(val))
return { val, literal: 'string', location: location() }; // XSN TODO: remove `literal`
return natnum(val, spec );
}
function natnum( val, spec ) {
if (typeof val === 'number' && val >= 0 && Number.isSafeInteger( val ))
return { val, location: location() };
const loc = location(true);
error( 'syntax-expecting-unsigned-int', loc,
{ '#': spec.msgVariant || 'csn', prop: spec.msgProp, op: '*' } );
return ignore( val );
}
// Use with spec.msgVariant !
function natnumOrStar( val, spec ) {
return (val === '*')
? { val, location: location() }
: natnum( val, spec );
}
function symbol( id, spec, xsn ) { // for CSN property '#'
if (!string( id, spec ))
return;
xsn.literal = 'enum'; // CSN cannot have both '#' and 'literal'
xsn.sym = { id, location: location() };
}
/**
* Wrapper around the default `ref` spec: Don't allow references of length 1 for types.
*/
function typeRef( val, spec, xsn, csn ) {
// e.g. { ref: [ 'T' ] }
if (Array.isArray(val) && val.length <= 1)
warning( 'syntax-deprecated-type-ref', location(true), { '#': 'std', prop: 'type' });
return arrayOf(spec.arrayOf)(val, spec, xsn, csn);
}
/**
* Similar to refItem(), but warns that the item should be a string if `id` is the only CSN
* property inside the ref-item.
*/
function typeRefItem( val, spec, xsn, csn ) {
// e.g. [ 'T', { id: 'elem', other_prop: true } ]
// avoid duplicate messages for single-item reference, see typeRef()
if (val && csn.ref?.length > 1 && typeof val === 'object' && val.id) {
const ownKeysCount = Object.keys(val).filter(key => ourpropsRegex.test(key)).length;
if (ownKeysCount === 1)
warning('syntax-deprecated-type-ref', location(true), { '#': 'ref-item', prop: 'ref[]' });
}
return refItem(val, spec);
}
/**
* returns:
* - false = no "...",
* - true = "..." without UP TO,
* - 'upTo' = "..." with UP TO
*
* @returns {string|boolean}
*/
function isEllipsis( val ) {
return val && typeof val === 'object' && '...' in val && Object.keys(val).length === 1 &&
(val['...'] === true || 'upTo');
}
function annoValue( val, spec ) {
if (val == null) // TODO: reject undefined
return { val, literal: 'null', location: location() };
const lit = typeof val;
if (lit !== 'object')
return { val, literal: lit, location: location() };
if (Array.isArray( val )) {
/** @type {string|boolean} */
let seenEllipsis = false;
if (arrayLevelCount > 0) { // TODO: also inside structure (possible in CSN!)
if (val.some( isEllipsis )) { // remark: check is via parsing rules in CDL
error( 'syntax-unexpected-ellipsis', location(true),
{ '#': 'csn-nested', prop: '...' } );
}
}
else {
for (const item of val) {
if (seenEllipsis !== true) { // no `...` yet, or only `... up to`
seenEllipsis = isEllipsis( item ) || seenEllipsis;
}
else if (isEllipsis( item )) { // `...`with or without UP TO
error( 'syntax-unexpected-ellipsis', location(true),
{ '#': 'csn-duplicate', prop: '...', code: '{ "...": true }' } );
break;
}
}
}
arrayLevelCount++;
const retval = {
location: location(),
val: arrayOf( annoValue )( val, spec ),
literal: 'array',
};
arrayLevelCount--;
if (seenEllipsis === 'upTo') {
error( 'syntax-missing-ellipsis', location(true), // at closing bracket
{ code: '{ "...": ‹up to value› }', newcode: '{ "...": true }' } );
}
return retval;
}
else if (typeof val['='] === 'string' || val['='] === true) {
// An object with `=` is an expression if and only if:
// - there is exactly one property ('=')
// - there is at least one other expression property (e.g. "xpr")
const valKeys = Object.keys( val );
if (valKeys.length === 1 && typeof val['='] === 'string') {
++virtualLine;
const r = refSplit( val['='], '=' ); // i.e. no extra `variant` stuff
++virtualLine;
return r;
}
else if (isAnnotationExpression( val )) {
const s = schema['@'].schema['-expr'];
const r = { location: location() };
Object.assign( r, object( val, s ) );
return r;
}
// fallthrough -> unchecked structure
}
if (typeof val['#'] === 'string') {
if (Object.keys( val ).length === 1) {
++virtualLine;
const xsn = { location: location() };
symbol( val['#'], schema['#'], xsn );
++virtualLine;
return xsn;
}
}
else if (val['...'] !== undefined && Object.keys( val ).length === 1) {
// TODO: only if not nested - see error above
++virtualLine;
const ell = val['...'];
const r = {
val: '...',
literal: 'token',
location: location(),
};
if (ell !== true)
r.upTo = annoValue( ell, schema['@'] );
++virtualLine;
return r;
}
const r = { struct: Object.create(null), literal: 'struct', location: location() };
++virtualLine;
for (const name of Object.keys( val )) {
r.struct[name] = annotation( val[name], schema['@'], null, val, name );
++virtualLine;
}
return r;
}
function annotation( val, spec, xsn, csn, name ) {
// not used for the value
const id = (xsn ? name.substring(1) : name);
if (!id) // `"@": …` is already syntax-unknown-property
message( 'syntax-inval