UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,101 lines (1,044 loc) 45.8 kB
// Consistency checker on model (XSN = augmented CSN) // Docs about XSN: internalDoc/IdeasModelChanges.md, internalDoc/Model.md (the // latter is quite outdated). The use of the XSN is PACKAGE-INTERNAL! If you // want to use it, you MUST contact us. // // The consistency check gives the consumer of XSN same safety of what values // they can expect to see at certain places in the model. It gives produces // some safety that they have produced a consistent model. That being said, // the consistency check is work-in-progress. // // The consistency check is NOT A SYNTAX CHECK: it accepts invalid CDS models, // it is usually not run in productive use, and its error message contains // property names the user is not aware of. It is considered an _internal // error_ if the consistency check throws an error. // A value in the model is one of: // // - Simple value: String, Boolean, Integer, null, undefined. // - Dictionary: object without prototype - its property names are _user // defined_ and all property values have same "type" (example: elements). // - Array: all items have same "type" (which is often a "union type"). // - Standard object: object with `Object.prototype` as prototype - its // property names are predefined (or at least their first char: `@` for // annotation assignments) and the value type depends on the property name. // - Special object: currently just for the messages. // // The CENTRAL CHARACTERISTIC in XSN (as well as plain CSN) is: in ALL standard // objects, the SAME PROPERTY NAME contains values of the SAME TYPE - we might // restrict the value space in certain contexts, though. Example: the value of // a `type` property looks the same in objects for definitions or in the object // which is the value of the `items` property. // The model is described by a schema which specifies the type for all property // names in standard objects. Such a type can be a "union type", e.g. can // allow a simple value or array. This is done by a assert function in // property `test` of a properties' specification in the schema. The test // function can use other properties in the specification. If no such function // is specified, it uses function `standard` which checks for a standard // objects with: // // - Certain required sub properties whose names are listed in the array value // of `requires` in the specification. This can be loosened: by default, no // property is required with syntax errors (overwritten by `isRequired`). // - Optional sub properties are listed in `optional`, which can also be a // function returning true if the property is allowed. // // The above mentioned restriction of the value space in certain contexts can // be specified by a property `schema` of the properties' specification in the // schema. With it, direct sub properties are checked against that // specification. Specifications can also inherit properties from other // specifications by using the name as value of `inherits` in the // specification. // The consistency check also checks the following conventions for names in // standard objects: // // - A property is non-enumerable if and only if its name starts with `_`. // This convention can be overwritten by `enumerable` of the properties' // specification in the schema. Such properties should be used for "links" // to other nodes in the model. // - A property must not be produced by a parser if its name starts with `_` or // `$`. This convention can be overwritten by `parser` of the properties' // specification in the schema. Such properties must not be used for links, // and should be used for information which does not make it into plain CSN. 'use strict'; const { Location } = require('../base/location'); const { locationString, hasErrors } = require('../base/messages'); const { XsnSource, XsnName, XsnArtifact } = require('./xsn-model'); // Properties that can appear where a type can have type arguments. const typeProperties = [ 'type', '$typeArgs', 'length', 'precision', 'scale', 'srid', '_effectiveType', '$effectiveSeqNo', ]; class InternalConsistencyError extends Error { constructor(msg) { super( `cds-compiler XSN consistency: ${ msg }` ); } } function assertConsistency( model, stage ) { const stageParser = typeof stage === 'object'; const options = stageParser && stage || model.options || { testMode: true }; if (!options.testMode || options.testMode === '$noAssertConsistency' || options.parseOnly && !stageParser) return; const schema = { ':model': { // top-level from compiler requires: [ 'options', 'definitions', 'sources' ], optional: [ 'vocabularies', 'messages', 'extensions', 'i18n', 'meta', '$magicVariables', '$builtins', '$internal', '$compositionTargets', '$collectedExtensions', '_entities', '$entity', '$blocks', '$messageFunctions', '$functions', '$assert', '_sortedSources', ], }, ':parser': { // top-level from parser requires: [ '$frontend' ], optional: [ 'messages', 'options', 'definitions', 'vocabularies', 'extensions', 'i18n', 'artifacts', 'artifacts_', 'namespace', 'usings', // CDL parser 'location', 'dirname', 'dependencies', // for USING..FROM 'kind', // TODO: remove from parser 'meta', '$withLocalized', '$sources', 'tokenStream', ], instanceOf: XsnSource, }, tokenIndex: { test: isNumber }, location: { // every thing with a $location in CSN must have a XSN location even // with syntax errors (currently even internal artifacts like $using): isRequired: parent => noSyntaxErrors() || parent && parent.kind, kind: true, instanceOf: Location, requires: [ 'file' ], // line is optional in top-level location optional: [ 'line', 'col', 'endLine', 'endCol', '$notFound', 'tokenIndex', // in parser for $lsp ], schema: { line: { test: isNumber }, col: { test: isNumber }, endLine: { test: isNumber, also: [ undefined ] }, endCol: { test: isNumber, also: [ undefined ] }, $notFound: { test: isBoolean }, }, }, sources: { test: isDictionary( isObject ), instanceOf: XsnSource }, _sortedSources: { test: isArray( isObject ), instanceOf: XsnSource }, file: { test: isString }, dirname: { test: isString }, // TODO: really necessary? realname: { test: isString }, // TODO: really necessary? dependencies: { test: isArray(), requires: [ 'literal', 'location', 'val' ], }, fileDep: { test: TODO }, // in usings $frontend: { parser: true, test: isString, enum: [ 'cdl', 'json', 'xml' ] }, messages: { enumerable: () => true, // does not matter (non-enum std), enum in CSN/XML parser test: isArray( TODO ), }, options: { test: TODO }, // TODO: check option object definitions: { test: isDictionary( definition ), requires: [ 'kind', 'location', 'name' ], optional: thoseWithKind, instanceOf: XsnArtifact, }, vocabularies: { test: isDictionary( definition ), requires: [ 'kind', 'name' ], optional: thoseWithKind, instanceOf: XsnArtifact, }, extensions: { kind: [ 'context' ], // syntax error (as opposed to HANA CDS), but still there inherits: 'definitions', test: isArray(), schema: { name: { inherits: 'name', isRequired: noSyntaxErrors } }, // name is required in parser, too }, i18n: { test: isDictionary( ( val, parent, prop, spec, lang ) => { const textValueIsString = (v, p, textProp, s, textKey) => { isString( v.val, p, textKey, s ); }; const innerDict = isDictionary( textValueIsString ); return innerDict( val, parent, lang, spec ); } ), }, $magicVariables: { // $magicVariables contains "builtin" artifacts that differ from // "normal artifacts" and therefore have a custom schema requires: [ 'kind', 'elements' ], schema: { kind: { test: isString, enum: [ '$magicVariables' ] }, elements: { // Do not use "normal" definitions spec because these artifacts // are missing the location property test: isDictionary( definition ), requires: [ 'kind', 'name' ], optional: [ 'elements', '$autoElement', '$uncheckedElements', '_origin', '_extensions', '$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps', '$calcDepElement', '$filtered', '$enclosed', '_parent', 'deprecated', '$restricted', ], schema: { kind: { test: isString, enum: [ 'builtin' ] }, name: { test: isObject, instanceOf: XsnName, requires: [ 'id', 'element' ] }, $autoElement: { test: isString }, $uncheckedElements: { test: isBoolean }, $requireElementAccess: { test: isBoolean }, deprecated: { test: isBoolean }, $restricted: { test: TODO }, // missing location for normal "elements" elements: { test: TODO }, }, }, }, }, $builtins: { test: TODO }, $blocks: { test: TODO }, builtin: { kind: true, test: builtin }, $internal: { test: standard, requires: [ '$frontend' ], schema: { $frontend: { test: isString, enum: [ '$internal' ] }, }, }, meta: { test: TODO }, // never tested due to --test-mode namespace: { test: (model.$frontend !== 'json') ? standard : TODO, // TODO: the JSON parser should augment 'namespace' correctly or better: hide it requires: [ 'location' ], optional: [ 'kind', 'name' ], }, usings: { test: isArray(), requires: [ 'kind', 'location' ], optional: [ 'name', 'extern', 'usings', 'fileDep' ], }, extern: { kind: [ 'using' ], requires: [ 'location' ], optional: [ 'location', 'path', 'id' ], schema: { path: { inherits: 'path', optional: [ '$delimited' ] } }, }, elements: { kind: true, inherits: 'definitions', also: [ 0 ] }, // 0 for cyclic expansions // specified elements in query entities (TODO: introduce real "specified elements" instead): elements$: { kind: true, enumerable: false, test: TODO }, foreignKeys$: { kind: true, enumerable: false, test: TODO }, enum$: { kind: true, enumerable: false, test: TODO }, typeProps$: { kind: true, enumerable: false, test: TODO }, // helper property for faster processing: $contains: { kind: true, test: TODO }, actions: { kind: true, inherits: 'definitions' }, enum: { kind: true, inherits: 'definitions' }, foreignKeys: { kind: true, inherits: 'definitions', instanceOf: 'ignore', also: [ false ], }, $keysNavigation: { kind: true, test: TODO }, $filtered: { kind: true, inherits: 'value' }, // for assoc+filter $enclosed: { kind: true, inherits: 'value' }, // for comp+filter params: { kind: true, inherits: 'definitions' }, _extendType: { kind: true, test: TODO }, mixin: { inherits: 'definitions' }, query: { kind: true, test: query, // properties below are "sub specifications" union: { schema: { args: { inherits: 'query', test: isArray( query ) } }, requires: [ 'op', 'location', 'args' ], optional: [ 'quantifier', 'orderBy', 'limit', 'name', '$parens', 'kind', '_origin', '$contains', // TODO tmp, see TODO in getOriginRaw() '_parent', '_main', '_leadingQuery', '_effectiveType', '$effectiveSeqNo', // in FROM '_$next', // parsing error: tableTerm with UNION on rhs. ], }, select: { // sub query requires: [ 'op', 'location', 'from' ], optional: [ 'name', '$parens', 'quantifier', 'mixin', 'excludingDict', 'columns', 'elements', '_deps', '$calcDepElement', 'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit', '$limit', '_origin', '_block', '$contains', '_projections', '_complexProjections', '_parent', '_main', '_effectiveType', '$effectiveSeqNo', '$expand', '$tableAliases', 'kind', '_$next', '_combined', '$inlines', '_status', ], }, none: { optional: () => true }, // parse error }, from: { test: from, join: { // join schema: { args: { inherits: 'from', test: isArray( from ) } }, requires: [ 'op', 'location', 'args', 'join' ], optional: [ 'on', '$parens', 'cardinality', 'kind', 'name', '_block', '_parent', '_main', '_user', '$tableAliases', '_combined', '_joinParent', '$joinArgsIndex', '_leadingQuery', '_$next', '_deps', ], }, ref: { requires: [ 'location', 'path' ], optional: [ 'kind', 'name', '$syntax', '_block', '_parent', '_main', 'elements', '_origin', '_joinParent', '$joinArgsIndex', '$parens', '_status', // TODO: only in from 'scope', '_artifact', '_originalArtifact', '$inferred', 'kind', '_effectiveType', '$effectiveSeqNo', // TODO:check this '$duplicates', // In JOIN if both sides are the same. ], }, query: { requires: [ 'query', 'location' ], optional: [ '$parens', 'kind', 'name', '_block', '_parent', '_main', 'elements', '_effectiveType', '$effectiveSeqNo', '_origin', '_joinParent', '$joinArgsIndex', '$duplicates', // duplicate query in FROM clause ], }, none: { optional: () => true }, // parse error }, columns: { kind: [ 'extend', '$column' ], test: isArray( column ), instanceOf: XsnArtifact, optional: thoseWithKind, enum: [ '*', '**' ], // '**' = duplicate wildcard requires: [ 'location' ], // schema: { kind: { isRequired: () => {} } } // kind not required }, expand: { kind: [ 'element' ], inherits: 'columns' }, inline: { kind: [ 'element' ], inherits: 'columns' }, $noOrigin: { kind: [ 'element' ], test: TODO }, excludingDict: { kind: 'element', test: isDictionary( definition ), // definition since redef requires: [ 'location', 'name' ], optional: [ '$duplicates' ], }, orderBy: { inherits: 'value', test: isArray( expression ) }, sort: { test: locationVal( isString ), enum: [ 'asc', 'desc' ] }, nulls: { test: locationVal( isString ), enum: [ 'first', 'last' ] }, $orderBy: { inherits: 'orderBy' }, groupBy: { inherits: 'value', test: isArray( expression ) }, $limit: { test: TODO }, limit: { requires: [ 'rows' ], optional: [ 'offset', 'location' ] }, rows: { inherits: 'value' }, offset: { inherits: 'value' }, _combined: { test: TODO }, $inlines: { test: TODO }, type: { kind: true, requires: [ 'location' ], optional: [ 'path', 'scope', '_artifact', '$inferred', '$parens', ], }, targetAspect: { kind: true, requires: [ 'location' ], optional: [ 'path', 'elements', '_outer', '_parent', '_main', '_block', 'kind', 'scope', '_artifact', '$inferred', '$expand', '$inCycle', '$tableAliases', '_$next', '_origin', '_effectiveType', '$effectiveSeqNo', '_extensions', '$contains', ], }, target: { kind: true, requires: [ 'location' ], optional: [ 'path', 'elements', '_outer', 'scope', '_artifact', '$inferred', ], }, path: { test: isArray( pathItem ), requires: [ 'location', 'id' ], // TODO: it can be `func` instead of `id` later optional: [ '$delimited', // TODO remove? 'args', '$syntax', 'where', 'groupBy', 'limit', 'orderBy', 'having', 'cardinality', '_artifact', '_originalArtifact', '_navigation', '_user', '$inferred', ], }, id: { test: isString }, $delimited: { parser: true, test: isBoolean }, scope: { test: isScope }, func: { test: TODO }, suffix: { test: TODO }, kind: { isRequired: !stageParser && (() => true), kind: true, // required to be set by Core Compiler even with parse errors test: isString, enum: [ 'context', 'service', 'entity', 'type', 'aspect', 'annotation', 'element', 'enum', 'action', 'function', 'param', 'key', 'event', 'annotate', 'extend', '$column', 'select', '$join', 'mixin', 'source', 'namespace', 'using', '$tableAlias', '$navElement', '$calculation', '$annotation', 'builtin', // magic variables ], }, // locations of parentheses pairs around expression: $parens: { parser: true, test: TODO }, $prefix: { test: isString }, // compiler-corrected path prefix $extended: { test: TODO, kind: [ 'element', '$inline' ] }, // `extend … with columns` $syntax: { parser: true, kind: [ 'entity', 'view', 'type', 'aspect' ], test: isString, // CSN parser should check for 'entity', 'view', 'projection' }, $tokenTexts: { parser: true, test: isStringOrBool, }, value: { optional: [ 'location', '$inferred', 'sort', 'nulls', 'param', 'scope', // for dynamic parameter '?' 'args', 'op', 'func', 'suffix', // calculated elements on-write - TODO: outside `value` 'stored', ], kind: true, test: expression, // properties below are "sub specifications" ref: { requires: [ 'location', 'path' ], optional: [ 'scope', 'variant', '_artifact', '_originalArtifact', '$inferred', '$parens', 'sort', 'nulls', '$syntax', ], }, none: { optional: () => true }, // parse error // TODO: why optional / enough in name? // TODO: "yes" instead "none": val: true, optional literal/location val: { requires: [ 'literal', 'location' ], // TODO: struct and variant only for annotation assignments optional: [ 'val', 'sym', 'name', '$inferred', '$parens', 'struct', 'variant', 'sort', 'nulls', ], }, op: { schema: { args: { inherits: 'args' } }, requires: [ 'op', 'location' ], optional: [ 'args', 'func', 'suffix', '$inferred', '$parens', '_artifact', // _artifact with "localized data"s 'coalesce' 'sort', 'nulls', // if used in GROUP BY // `elements` of type is not put into cast, many will be cds.LargeString ...typeProperties, // for CAST ], }, query: { requires: [ 'query', 'location' ], optional: [ 'stored', '$parens' ] }, }, $calc: { kind: true, test: TODO }, // TODO: rename to `value`? literal: { // TODO: check value against literal test: isString, enum: [ 'string', 'number', 'boolean', 'x', 'time', 'date', 'timestamp', 'struct', 'array', 'enum', 'null', 'token', ], }, sym: { requires: [ 'location', 'id' ], optional: [ '$delimited', '_artifact' ] }, val: { test: isVal, // the following for array/struct value requires: [ 'location' ], optional: [ 'literal', 'val', 'sym', 'struct', 'variant', 'path', 'name', '$duplicates', 'upTo', // expressions as annotation values '$tokenTexts', 'op', 'args', 'func', '_artifact', 'type', '$typeArgs', 'scale', 'srid', 'length', 'precision', 'scope', '$parens', '_block', '_outer', // for annotation assignments ], // TODO: restrict path to #simplePath }, upTo: { test: TODO }, struct: { inherits: 'val', test: isDictionary( definition ) }, // def because double @ args: { inherits: 'value', optional: [ 'name', '$duplicate', 'args', 'suffix', '$parens', 'param', 'scope', // for dynamic parameter '?' ], test: args, }, on: { kind: true, inherits: 'value', test: expression }, where: { inherits: 'value' }, having: { inherits: 'value' }, op: { test: locationVal( isString ) }, join: { test: locationVal( isString ) }, quantifier: { test: locationVal( isString ) }, stored: { test: locationVal( isBoolean ) }, // preliminary ----------------------------------------------------------- doc: { kind: true, test: TODO, }, // doc comment '@': { kind: true, inherits: 'value', optional: [ 'name', '_block', '$priority', '$inferred', '$duplicates', '$errorReported', // annotation values '$tokenTexts', 'kind', '_outer', '_effectiveType', '$effectiveSeqNo', '_origin', '_deps', // CSN parser may let these properties slip through to XSN, even if input is invalid. 'args', 'op', 'func', 'suffix', '$invalidPaths', '$parens', // for invalid CDL (parser issues) 'orderBy', ], // TODO: name requires if not in parser? }, $priority: { test: isOneOf( [ undefined, false, 'extend', 'annotate' ] ) }, $annotations: { parser: true, kind: true, test: TODO }, // deprecated, still there for cds-lsp $invalidPaths: { test: isBoolean }, name: { isRequired: stageParser && (() => false), // not required in parser kind: true, instanceOf: 'ignore', // TODO: XsnName, schema: { id: { test: isStringOrNumber }, select: { test: TODO }, // TODO: remove }, requires: [ 'location' ], optional: [ 'path', 'id', '$delimited', 'variant', // TODO: req path, opt id for main, req id for member '_artifact', '$inferred', ], }, absolute: { test: isString }, variant: { requires: [ 'location', 'path' ] }, element: { test: TODO }, // TODO: { test: isString }, action: { test: isString }, param: { test: TODO }, alias: { test: isString }, expectedKind: { kind: [ 'extend' ], test: locationVal( isString ) }, virtual: { kind: true, test: locationVal() }, key: { kind: true, test: locationVal(), also: [ null, undefined ] }, masked: { kind: true, test: locationVal() }, notNull: { kind: true, test: locationVal() }, includes: { kind: true, inherits: 'type', test: isArray() }, returns: { kind: [ 'action', 'function' ], requires: [ 'kind', 'location' ], optional: thoseWithKind, instanceOf: XsnArtifact, }, items: { kind: true, also: [ 0 ], // 0 for cyclic expansions requires: [ 'location' ], optional: [ 'enum', 'elements', 'cardinality', 'target', 'on', 'foreignKeys', 'items', '_outer', '_effectiveType', '$effectiveSeqNo', 'notNull', '_parent', '_origin', '_block', '$inferred', '$expand', '$inCycle', '_deps', 'localized', // really? see #13135 '$calcDepElement', '$syntax', '_extensions', '_status', '_redirected', ...typeProperties, ], }, // yes, also optional 'items' targetElement: { kind: true, inherits: 'type' }, // for foreign keys artifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) }, _subArtifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) }, blocks: { kind: true, test: TODO }, // TODO: make it $blocks ? length: { kind: true, test: isNumberVal }, // for number is to be checked in resolver precision: { kind: true, test: isNumberVal }, scale: { kind: true, test: isNumberVal, also: [ 'floating', 'variable' ] }, srid: { kind: true, test: isNumberVal }, localized: { kind: true, test: locationVal() }, cardinality: { kind: true, requires: [ 'location' ], optional: [ 'sourceMin', 'sourceMax', 'targetMin', 'targetMax', '$inferred' ], }, sourceMin: { test: isNumberVal }, sourceMax: { test: isNumberVal, also: [ '*' ] }, targetMin: { test: isNumberVal }, targetMax: { test: isNumberVal, also: [ '*' ] }, default: { kind: true, inherits: 'value' }, $typeArgs: { parser: true, kind: true, test: TODO }, $tableAliases: { kind: true, test: TODO }, // containing $self outside queries _block: { kind: true, test: TODO }, _parent: { kind: true, test: TODO }, _service: { kind: true, test: TODO }, _main: { kind: true, test: TODO }, _user: { kind: true, test: TODO }, // - on a path item with a filter condition to the user of the ref (not nested) // - on a JOIN node to the query (TODO: _outer?) _artifact: { test: TODO }, _originalArtifact: { test: TODO }, _navigation: { test: TODO }, _effectiveType: { kind: true, test: TODO }, $effectiveSeqNo: { kind: true, test: isNumber }, _joinParent: { test: TODO }, $joinArgsIndex: { test: isNumber }, _outer: { test: TODO }, // for items // - on an array item to the array elem/type/item (nested) // - on an anonymous aspect to the composition element // - on an annotation assignment to the annotatee $queries: { kind: [ 'entity', 'event' ], test: isArray(), requires: [ 'kind', 'location', 'name', '_parent', '_main', '_$next', '_block', // query specific 'op', 'from', 'elements', '_combined', '$tableAliases', '$inlines', ], optional: [ '_effectiveType', '$effectiveSeqNo', '$parens', '_deps', '$calcDepElement', '$expand', '$contains', // query specific 'where', 'columns', 'mixin', 'quantifier', 'offset', 'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having', '$limit', 'limit', '_status', '_origin', // via casts 'enum', ], }, _leadingQuery: { kind: true, test: TODO }, $replacement: { kind: true, test: TODO }, // for smart * in queries _origin: { kind: true, test: TODO }, _calcOrigin: { kind: true, test: TODO }, _columnParent: { kind: [ 'element', undefined ], test: TODO }, // column or * (wildcard) _from: { kind: true, test: TODO }, // TODO: not necessary anymore ? // array of $tableAlias (or includes) for explicit and implicit redirection: _redirected: { kind: true, test: TODO }, // ...array of table aliases for targets from orig to new _$next: { kind: true, test: TODO }, // next lexical search environment for values _extensions: { kind: true, test: TODO }, // for collecting extend/annotate on artifact _extend: { kind: true, test: TODO }, // for collecting extend/annotate on artifact _annotate: { kind: true, test: TODO }, // for collecting extend/annotate on artifact _extension: { kind: true, test: TODO }, // on artifact to its "super extend/annotate" statement _deps: { kind: true, test: TODO }, // for cyclic calculation // a fake element for cyclic dependency detection: e.g. dependencies to target entities. // dependants don't only depend on the calc element, but on this element as well. $calcDepElement: { kind: true, test: TODO }, _scc: { kind: true, test: TODO }, // for cyclic calculation _sccCaller: { kind: true, test: TODO }, // for cyclic calculation _status: { kind: true, test: TODO }, // TODO: $status _projections: { kind: true, test: TODO }, _complexProjections: { kind: true, test: TODO }, // for projected paths with filters $entity: { kind: true, test: TODO }, _entities: { test: TODO }, $compositionTargets: { test: isDictionary( isBoolean ) }, $collectedExtensions: { test: TODO }, _upperAspects: { kind: [ 'type', 'entity' ], test: isArray( TODO ) }, // for implicit redirection - direct and indirect query sources of simple // projections/views without @(cds.redirection.target: false): _ancestors: { kind: [ 'type', 'entity' ], test: isArray( TODO ) }, // for implicit redirection - maps service name to simple projections/views // in that service which have the current artifact in _ancestors // (it can contain the artifact itself with no/failed autoexposure): _descendants: { kind: [ 'entity' ], test: isDictionary( isArray( TODO ) ) }, $errorReported: { parser: true, kind: true, test: isString }, // to avoid duplicate messages $duplicates: { parser: true, kind: true, test: TODO }, // array of arts or true $extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status $inferred: { parser: true, kind: true, test: isOneOf( [ '', // constructed “super annotate” statement, redirected user-provided target // Uppercase values are used in logic, lowercase value are "just for us", i.e. // debugging or to add properties such as $generated in Universal CSN. // However, that is no longer true. For example, `autoexposed` is used in populate.js // as well. 'IMPLICIT', 'NULL', // from propagator 'prop', // from propagator '$autoElement', // for magicVars: $user is automatically changed to $user.id '$generated', // compiler generated annotations, e.g. @Core.Computed '$internal', // compiler internal; must not reach CSN output '*', // inferred from query wildcard 'as', // query alias name 'path-prefix', // using declaration for `entity path.prefix.E` 'aspect-composition', 'autoexposed', // for auto-exposed entities (they can't be referred to) 'cast', // type from cast() function 'composition-entity', 'copy', // only used in rewriteCondition(): On-condition is copied 'def-duplicate-autoexposed', // just like `autoexposed`, but with `duplicate` error. 'expanded', // expanded elements, items, params 'include', // through includes, e.g. `entity E : F {}` 'keys', 'localized', // e.g. compiler-generated elements for localized: `text` assoc, etc. 'localized-entity', // `.texts` entity 'localized-origin', // `.texts` entity 'nav', // only used for MASKED, TODO(v6): Remove 'query', // inferred query properties, e.g. `key` 'rewrite', // on-conditions or FKeys are rewritten 'parent-origin', // annotation/property copied from parent that does not come through // $origin and is not a direct annotation ] ), }, // Helper property for the XSN-to-CSN transformation, see function setExpandStatus(): // client, universal: render expanded elements? gensrc: produce annotate statements? // TODO: rename it to $elementsExpand ? $expand: { kind: true, // See description of `setExpandStatus()` of in `lib/compiler/utils.js`. test: isOneOf( [ 'origin', 'annotate', 'target' ] ), }, $inCycle: { kind: true, test: isBoolean }, $autoexpose: { kind: [ 'entity' ], test: isBoolean, also: [ null, 'Composition' ] }, $extra: { parser: true, test: TODO }, // for unexpected properties in CSN $withLocalized: { test: isBoolean }, $sources: { parser: true, test: isArray( isString ) }, tokenStream: { parser: true, test: TODO }, $messageFunctions: { test: TODO }, $functions: { test: TODO }, $assert: { test: TODO }, // currently just for missing Error[ref-cycle] }; let _noSyntaxErrors = null; assertProp( model, null, stageParser ? ':parser' : ':model', null, true ); return; function noSyntaxErrors() { if (_noSyntaxErrors == null) _noSyntaxErrors = !hasErrors( options.messages ); // TODO: check messageId? return _noSyntaxErrors; } function assertProp( node, parent, prop, extraSpec, noPropertyTest ) { let spec = extraSpec || schema[prop] || schema[prop.charAt(0)]; if (!spec) throw new InternalConsistencyError( `Property '${ prop }' has not been specified`); spec = inheritSpec( spec ); if (!noPropertyTest) { const char = prop.charAt(0); const parser = ('parser' in spec) ? spec.parser : char !== '_' && char !== '$'; if (stageParser && !parser) throw new InternalConsistencyError( `Non-parser property '${ prop }' set by ${ model.$frontend || '' } parser${ at( [ node, parent ] ) }` ); const enumerable = ('enumerable' in spec) ? spec.enumerable : char !== '_'; if (enumerable instanceof Function ? !enumerable( parent, prop ) : {}.propertyIsEnumerable.call( parent, prop ) !== enumerable) throw new InternalConsistencyError( `Unexpected enumerability ${ !enumerable }${ at( [ node, parent ], prop ) }` ); } if (node !== undefined) { // ignore if undefined (spec.test || standard)( node, parent, prop, spec, typeof noPropertyTest === 'string' && noPropertyTest ); } } function definition( node, parent, prop, spec, name ) { if (node.builtin) return; if (!Array.isArray( node )) node = [ node ]; // TODO: else check that there is a redefinition error for (const art of node) standard( art, parent, prop, spec, name ); } /** * `builtin` property that is set in the definer. Must only be used for `cds` * and `localized` namespaces. */ function builtin( node, parent, prop, spec, name ) { const type = typeof node; if (type !== 'string' && type !== 'boolean') throw new InternalConsistencyError(`Property '${ prop }' must be a boolean or string but was '${ typeof node }'${ at( [ node, parent ], prop, name ) }` ); if (parent.kind !== 'namespace') throw new InternalConsistencyError(`Property '${ prop }' must be inside artifact that is a namespace but was '${ parent.kind }'${ at( [ node, parent ], prop, name ) }` ); const parentName = parent.name?.id; if (parentName !== 'cds' && parentName !== 'localized') throw new InternalConsistencyError(`Property '${ prop }' must be inside namespace 'cds' or 'localized' but was '${ parentName }'${ at( [ node, parent ], prop, name ) }` ); } function column( node, ...rest ) { if (node.val) locationVal( isString )( node, ...rest ); else if (stageParser) standard( node, ...rest ); else isObject( node, ...rest ); // TODO: and inside elements } function pathItem( node, ...rest ) { if (node !== null || noSyntaxErrors()) standard( node, ...rest ); } function standard( node, parent, prop, spec, name ) { if (spec.also && spec.also.includes( node )) return; isObject( node, parent, prop, spec, name ); const names = Object.getOwnPropertyNames( node ); const requires = spec.requires || []; // Do not test 'requires' with parse errors: for (const p of requires) { if (!names.includes( p )) { const req = spec.schema && spec.schema[p] && spec.schema[p].isRequired; if ((req || schema[p] && schema[p].isRequired || noSyntaxErrors)( node )) throw new InternalConsistencyError( `Required property '${ p }' missing in object${ at( [ node, parent ], prop, name ) }` ); } } const optional = spec.optional || []; for (const n of names) { const opt = Array.isArray( optional ) ? optional.includes( n ) || optional.includes( n.charAt(0) ) : optional( n, spec ); if (node[n] !== undefined) { if (!(opt || requires.includes( n ) || n === '$extra')) throw new InternalConsistencyError( `Property '${ n }' is not expected${ at( [ node[n], node, parent ], prop, name ) }` ); assertProp( node[n], node, n, spec.schema && spec.schema[n] ); } } } function thoseWithKind( prop, spec ) { const those = spec.schema && spec.schema[prop] || schema[prop] || schema[prop.charAt(0)]; return those && those.kind; } function query( node, parent, prop, spec, idx ) { // select from <EOF> produces from: [null] if (node !== null || noSyntaxErrors()) { isObject( node, parent, prop, spec, idx ); // eslint-disable-next-line no-nested-ternary const choice = (!noSyntaxErrors()) ? 'none' : (node.from !== undefined) ? 'select' : 'union'; if (spec[choice]) assertProp( node, parent, prop, spec[choice], choice ); else throw new InternalConsistencyError( `No specification for computed variant '${ choice }'${ at( [ node, parent ], prop, idx ) }` ); } } function from( node, parent, prop, spec, idx ) { // select from <EOF> produces from: [null] if (node !== null || noSyntaxErrors()) { isObject( node, parent, prop, spec, idx ); const choice = (!noSyntaxErrors()) ? 'none' : (node.path) && 'ref' || (node.join) && 'join' || (node.query) && 'query' || 'none'; if (spec[choice]) assertProp( node, parent, prop, spec[choice], choice ); else throw new InternalConsistencyError( `No specification for computed variant '${ choice }'${ at( [ node, parent ], prop, idx ) }` ); } } function inheritSpec( spec ) { if (!spec.inherits) return spec; const chain = [ spec ]; while (spec.inherits) { spec = schema[spec.inherits]; chain.push( spec ); } chain.reverse(); return Object.assign( {}, ...chain ); } function expression( node, parent, prop, spec, idx ) { // TODO CSN parser?: { val: <token>, literal: 'token' } for keywords if (typeof node === 'string') return; while (Array.isArray( node )) { // TODO: also check getOwnPropertyNames(node) if (node.length !== 1) { node.forEach( n => expression( n, parent, prop, spec ) ); return; } [ node ] = node; } if (node == null && !noSyntaxErrors()) return; isObject( node, parent, prop, spec, idx ); const s = spec[expressionSpec( node )] || {}; const sub = Object.assign( {}, s.inherits && schema[s.inherits], s ); if (spec.requires && sub.requires) sub.requires = [ ...sub.requires, ...spec.requires ]; if (Array.isArray( spec.optional ) && Array.isArray( sub.optional )) sub.optional = [ ...sub.optional, ...spec.optional ]; // console.log(expressionSpec(node) ); (sub.test || standard)( node, parent, prop, sub, idx ); } function expressionSpec( node ) { // When a condition failure is ignored (TODO: we might test specifically // against this), an expression could have properties for query clauses: if (!noSyntaxErrors()) return 'none'; if (node.path) return 'ref'; else if (node.literal || node.val) return 'val'; else if (node.query) return 'query'; else if (node.op) return 'op'; return 'none'; // parse error } function args( node, parent, prop, spec ) { if (Array.isArray( node )) { if (parent.op && parent.op.val === 'xpr') // remove keywords for `xpr` expressions node = node.filter( a => typeof a !== 'string' ); node.forEach( (item, idx) => expression( item, parent, prop, spec, idx ) ); } else if (node && typeof node === 'object' && !Object.getPrototypeOf( node )) { for (const n in node) expression( node[n], parent, prop, spec, n ); } else { throw new InternalConsistencyError( `Expected array or dictionary${ at( [ null, parent ], prop ) }` ); } } function at( nodes, prop, name ) { const n = name && (typeof name === 'number' ? ` for index ${ name }` : ` for "${ name }"`) || ''; const loc = nodes.find( o => o && typeof o === 'object' && (o.location || o.start) ); const f = (prop) ? `${ n } in property '${ prop }'` : n; const l = locationString( loc && loc.location || loc || model.location ); return (!l) ? f : `${ f } at ${ l }`; } function isDictionary( func ) { return function dictionary( node, parent, prop, spec ) { if (spec.also && spec.also.includes( node )) return; // if (!node || typeof node !== 'object' || Object.getPrototypeOf( node )) // console.log(node,prop,model.$frontend) if (!node || typeof node !== 'object' || Object.getPrototypeOf( node )) throw new InternalConsistencyError( `Expected dictionary${ at( [ null, parent ], prop ) }` ); for (const n in node) func( node[n], parent, prop, spec, n ); }; } function isArray( func = standard ) { return function vector( node, parent, prop, spec ) { if (!Array.isArray( node )) throw new InternalConsistencyError( `Expected array${ at( [ null, parent ], prop ) }` ); node.forEach( (item, n) => func( item, parent, prop, spec, n ) ); }; } function locationVal( func = isBoolean ) { return function valWithLocation( node, parent, prop, spec, name ) { const valSchema = { val: Object.assign( {}, spec, { test: func } ) }; const requires = [ 'val', 'location' ]; const optional = [ 'literal', '$inferred', '$priority', '_columnParent' ]; standard( node, parent, prop, { schema: valSchema, requires, optional, instanceOf: spec.instanceOf, }, name ); }; } function isBoolean( node, parent, prop, spec ) { if ((spec.also) ? spec.also.includes( node ) : (node === null)) return; if (typeof node !== 'boolean') throw new InternalConsistencyError( `Expected boolean or null${ at( [ node, parent ], prop ) }` ); } function isNumberVal() { return locationVal( isNumber ); } function isNumber( node, parent, prop, spec ) { if (spec.also && spec.also.includes( node )) return; if (typeof node !== 'number') throw new InternalConsistencyError( `Expected number${ at( [ node, parent ], prop ) }` ); } function isOneOf( values ) { return function isOneOfInner( node, parent, prop ) { if (!values.includes( node ) && node !== undefined) throw new InternalConsistencyError( `Unexpected value '${ node }', expected ${ JSON.stringify( values ) }${ at( [ node, parent ], prop ) }` ); }; } function isStringOrNumber( node, parent, prop, spec ) { if (typeof node !== 'number') isString( node, parent, prop, spec ); } function isStringOrBool( node, parent, prop, spec ) { if (typeof node !== 'boolean') isString( node, parent, prop, spec ); } function isString( node, parent, prop, spec ) { if (typeof node !== 'string') throw new InternalConsistencyError( `Expected string but found ${ typeof node }${ at( [ node, parent ], prop ) }` ); // TODO: also check getOwnPropertyNames(node) if (spec.enum && !spec.enum.includes( node )) throw new InternalConsistencyError( `Unexpected value '${ node }'${ at( [ node, parent ], prop ) }` ); } function isVal( node, parent, prop, spec ) { if (Array.isArray( node )) node.forEach( (item, n) => standard( item, parent, prop, spec, n ) ); else if (node !== null && ![ 'string', 'number', 'boolean' ].includes( typeof node )) throw new InternalConsistencyError( `Expected array or simple value${ at( [ null, parent ], prop ) }` ); } function isObject( node, parent, prop, spec, name ) { if (!node || typeof node !== 'object' ) throw new InternalConsistencyError( `Expected object${ at( [ null, parent ], prop, name ) }` ); const found = Object.getPrototypeOf( node )?.constructor?.name || 'null'; if (!spec.instanceOf && Object.getPrototypeOf( node ) !== Object.prototype) throw new InternalConsistencyError( `Expected standard object but found ${ found }${ at( [ null, parent ], prop, name ) }` ); // TODO // else if (spec.instanceOf && spec.instanceOf !== 'ignore' && // Object.getPrototypeOf( node ) !== spec.instanceOf.prototype) // eslint-disable-next-line @stylistic/max-len // throw new InternalConsistencyError( `Expected object of class ${ spec.instanceOf.name } but found ${ found }${ at( [ null, parent ], prop, name ) }` ); } function inDefinitions( art, parent, prop, spec, name ) { if (Array.isArray( art )) // do not check with redefinitions return; isObject( art, parent, prop, spec, name ); if (stageParser) { if (prop === 'artifacts') standard( art, parent, prop, spec, name ); } else if (!art.name.id || !model.definitions[art.name.id] && !model.vocabularies?.[art.name.id]) { // TODO: sign ignored artifacts with $inferred = 'IGNORED' if (parent.kind === 'source' || art.kind === 'using' || art.name.id?.startsWith?.( 'localized.' )) standard( art, parent, prop, spec, name ); else throw new InternalConsistencyError( `Expected definition${ at( [ art, parent ], prop, name ) }` ); } } function isScope( node, parent, prop ) { // artifact refs in CDL have scope:0 in XSN if (Number.isInteger( node )) return; const validValues = [ 'typeOf', 'global', 'param' ]; if (!validValues.includes( node )) throw new InternalConsistencyError( `Property '${ prop }' must be either "${ validValues.join( '", "' ) }" or a number but was "${ node }"` ); } function TODO() { /* no-op */ } } module.exports = assertConsistency;