UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,304 lines (1,166 loc) 53.6 kB
// Checks on XSN performed during compile() that are useful for the user // but not necessary for the compiler to work. // TODO: Major issues so far: // * Different ad-hoc value/type checks (associations, enum, ...) - // specify a proper one and use consistently // * Using name comparisons instead proper object comparisons. // * Often forgot to consider CSN input 'use strict'; const { forEachGeneric, forEachDefinition, forEachMember, forEachMemberRecursively, isDeprecatedEnabled, } = require('../base/model'); const { typeParameters } = require('./builtins'); const { propagationRules, acceptsExprValues } = require('../base/builtins'); const { annotationVal } = require('./utils'); const $location = Symbol.for( 'cds.$location' ); /** * Run compiler checks on the given XSN model. * * @param {XSN.Model} model */ function check( model ) { const { error, warning, info, message, } = model.$messageFunctions; const { getOrigin, getInheritedProp, } = model.$functions; checkSapCommonLocale( model ); checkSapCommonTextsAspects( model ); forEachDefinition( model, checkDefinition ); forEachGeneric( model, 'vocabularies', checkAnnotationDefinition ); return; function checkDefinition( def ) { checkEvent( def ); checkGenericConstruct( def ); if (def.includes && def.elements) checkElementIncludeOverride( def ); forEachMember( def, member => checkMember( member ) ); if (def.$queries) def.$queries.forEach( checkQuery ); } function checkEvent( def ) { // Ensure that events are structured. Up to compiler v4, we allowed non-structured events, // because when we introduced them, it was not fully specified what they are. if (def.kind === 'event' && def._effectiveType && !def._effectiveType.elements && !def._effectiveType.projection) message( 'def-expected-structured', [ (def.type || def.name).location, def ] ); } function checkAnnotationDefinition( art ) { // TODO: Should we check elements similar to definition-elements as well? checkEnumType( art ); forEachMemberRecursively( art, (member) => { if (member.localized?.val) warning( 'def-unexpected-localized-anno', [ member.localized.location, member ] ); } ); } function* iterateAnnotations( art ) { for (const prop in art) { if (prop.charAt(0) === '@') yield prop; } } function checkGenericConstruct( art ) { checkName( art ); checkTypeArguments( art ); if (art.value && !art.$calcDepElement && (art.type || art.elements || art.items)) checkTypeCast( art.value, art ); for (const anno of iterateAnnotations( art )) checkAnnotationAssignment1( art, art[anno] ); checkTypeStructure( art ); checkAssociation( art ); // type def could be assoc checkDefaultValue( art ); checkEnumType( art ); } function checkMember( member, parentProps = { key: false, virtual: false } ) { // To avoid "bubble-up" checks, store required parent properties. if (member.key?.val === true) parentProps.key = member.key; if (member.virtual?.val === true) parentProps.virtual = member.virtual; checkGenericConstruct( member ); if (member.kind === 'element') checkElement( member, parentProps ); forEachMember( member, m => checkMember( m, parentProps ) ); } function checkKey( elem, parentProps ) { const key = parentProps.key || elem.key; if (!key?.val || key?.$inferred) return; const isVirtual = parentProps.virtual?.val || elem.virtual?.val; const typeName = elem._effectiveType?.name?.id; if (isVirtual) { error( 'def-unexpected-key', [ (parentProps.key || elem.key).location, elem ], { '#': 'virtual', keyword: 'key' } ); } else if (typeName === 'cds.Map') { error( 'def-unexpected-key', [ elem.type?.location || elem.location, elem ], { '#': 'invalidType', keyword: 'key', type: typeName } ); } else if (typeName === 'cds.LargeString' || typeName === 'cds.Vector' || typeName === 'cds.hana.CLOB' || typeName === 'cds.LargeBinary') { warning( 'def-unsupported-key', [ elem.type?.location || elem.location, elem ], { '#': 'type', keyword: 'key', type: typeName } ); } } function checkElement( elem, parentProps ) { checkKey( elem, parentProps ); checkLocalizedElement( elem ); if (elem.value) { if (elem._main?.query) checkSelectItemValue( elem ); else if (elem.$syntax === 'calc') checkCalculatedElementValue( elem ); } checkCardinality( elem ); // TODO: also for assoc types } function checkName( construct ) { // TODO: move to define.js if (model.options.$skipNameCheck || !construct._main) return; // TODO: Move a corrected version of this check to definer (but do not rely on it!): // The code below misses to consider CSN input! // Maybe remove the check? But consider runtimes that rely on '.' as element separator. if (construct.kind === 'element' || construct.kind === 'action' || construct.kind === 'param') { if (construct.name.id?.includes( '.' )) { error( 'def-invalid-name', [ construct.name.location, construct ], { '#': construct.kind || 'std' } ); } } } /** * Check the type arguments on `art`, e.g. cds.Decimal can't have a `length`, structures * can't have `precision`, etc. * * @param {XSN.Artifact} art * @param {XSN.Artifact} user */ function checkTypeArguments( art, user = art ) { if (art.builtin || art.kind === 'context' || art.kind === 'service') return; if (art.items) checkTypeArguments( art.items, art ); const actualParams = typeParameters.list.filter( param => art[param] !== undefined ); if (actualParams.length === 0) return; const typeArt = art.type?._artifact || art; // Note: `_effectiveType` points to `art` itself, if it is an enum type, // descend to the origin in this case. let effectiveType = typeArt._effectiveType; while (effectiveType?.enum) effectiveType = (effectiveType._origin || effectiveType.type?._artifact)?._effectiveType; if (!effectiveType || (effectiveType.type && !effectiveType.type._artifact)) { return; // e.g. illegal definition references, cycles, unknown artifacts, … } else if (!art.type && !effectiveType.type && !effectiveType?.builtin) { // Special case for deprecated flag "ignore specified elements": The `type` property // is lost in columns, but `length`,… are kept -> mismatch. This behavior is the // same as in cds-compiler v3. See #12169 for details. if (!isDeprecatedEnabled( model.options, 'ignoreSpecifiedQueryElements' )) { error( 'type-missing-type', [ art.location, user ], { otherprop: 'type', prop: actualParams[0] }, 'Missing $(OTHERPROP) property next to $(PROP)' ); } return; } const expectedParams = effectiveType.parameters && effectiveType.parameters.map( p => p.name || p ) || []; for (const param of actualParams) { if (!expectedParams.includes( param )) { // Whether the type ref itself is a builtin or a custom type with a builtin as base. let variant; if ((art.type?._artifact || art._effectiveType).builtin) variant = 'builtin'; else if (effectiveType.builtin) variant = 'type'; else // effectiveType is not a builtin -> array or structured variant = 'non-scalar'; error( 'type-unexpected-argument', [ art[param].location, user ], { '#': variant, prop: param, art: art.type || art._effectiveType, type: effectiveType, } ); break; // Avoid spam: Only emit the first error. } else if (!typeParameters.expectedLiteralsFor[param].includes( typeof art[param].val )) { // TODO: this could be probably better done via syntax check (already for CSN input) error( 'type-unexpected-argument', [ art[param].location, user ], { '#': 'incorrect-type', prop: param, code: typeof art[param].val, names: typeParameters.expectedLiteralsFor[param], // TODO: no double quote via $(NAMES), but see TODO above } ); break; // Avoid spam: Only emit the first error. } } } /** * Check the type in an SQL cast expression. * * @param xpr * @param {XSN.Artifact} user */ function requireExplicitTypeInSqlCast( xpr, user ) { if (!xpr.type) { error( 'expr-missing-type', [ xpr.location, user ], { }, 'Missing type in SQL cast function' ); } } function checkTypeCast( xpr, user ) { const isSqlCast = (xpr.op?.val === 'cast'); const elem = isSqlCast ? xpr.args?.[0]?._artifact : xpr._artifact; const typeArt = isSqlCast ? xpr : user; if (!elem || typeArt.$inferred || !isSqlCast && typeArt.type?.$inferred) return; // e.g. $inferred: 'generated' const typeItems = typeArt.items ?? typeArt; // explicit type, elements, or items -> has type cast const hasCast = typeItems.type || (typeItems.elements && !typeArt.expand && !typeArt.$expand && !elem.$expand); if (!hasCast) return; const { type } = typeItems; const loc = [ (type || typeItems).location, user ]; if (type?._artifact?._effectiveType?.name.id === 'cds.Map') { error( 'type-invalid-cast', loc, { '#': 'std', type: 'cds.Map' } ); } else if (elem.elements && !typeArt.$expand) { // TODO: calc elements error( 'type-invalid-cast', loc, { '#': 'from-structure' } ); } else if (elem.target && (typeArt.items || !type?._artifact?.target)) { error( 'type-invalid-cast', loc, { '#': 'from-assoc' } ); } else if ((typeArt.elements && !typeArt.$expand) || type?._artifact?.elements) { error( 'type-invalid-cast', loc, { '#': 'to-structure' } ); } else if (!elem.target && // referenced element is not association !user.type?.$inferred && // $inferred types already reported in resolve.js. ( // assoc used in SQL cast type?._artifact?.target && isSqlCast || // there is a target and the type is a direct `cds.Association`; // other types handled by resolver already. typeItems.target && type?._artifact?.category === 'relation' ) ) { // - redirection-check in resolve.js already checks this for CDL-casts // - `"cast": { "target": "…", "type": "cds.Association", … }` via CSN input. error('type-invalid-cast', loc, { '#': 'assoc' }); } } function checkLocalizedElement( elem ) { if (elem.localized?.val) { const type = elem._effectiveType; if (type?.category === 'map') { error( 'def-unexpected-localized', [ elem.localized.location, elem ], { keyword: 'localized', '#': 'map' } ); } else if (type?.elements) { // warning only, as we want to support it in the future warning( 'def-unexpected-localized-struct', [ elem.localized.location, elem ], { keyword: 'localized' } ); } else if (!type || !type.builtin || type.category !== 'string') { // See discussion issue #6520: should we allow all scalar types? info( 'ref-expecting-localized-string', [ elem.type?.location, elem ], { keyword: 'localized' }, 'Expecting a string type in combination with keyword $(KEYWORD)' ); } } // TODO: This check should be moved to localized.js - WHY? // "key" keyword at localized element in SELECT list. // TODO: not in inferred elements, but also inside aspects // TODO: `localized` is not necessarily at _origin, but the _origin chain if (elem.key?.val && elem._main?.query) { // either the element was casted to localized (no `_origin`) or // original element is localized but not key, as that would have // already resulted in a warning by localized.js if ((!elem._origin && elem.localized?.val) || (elem._origin?.localized?.val && !elem._origin.key?.val)) { warning( 'def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' }, 'Keyword $(KEYWORD) is ignored for primary keys' ); } } } function checkQuery( query ) { // TODO: check too simple (just one source), as most of those in this file // Check expressions in the various places where they may occur if (query.from) visitSubExpression( query.from, query, checkGenericExpression ); if (query.where) visitExpression( query.where, query, checkGenericExpression ); if (query.groupBy) { for (const groupByEntry of query.groupBy) visitExpression( groupByEntry, query, checkGenericExpression ); } if (query.having) visitExpression( query.having, query, checkGenericExpression ); if (query.orderBy) { for (const orderByEntry of query.orderBy) visitExpression( orderByEntry, query, checkGenericExpression ); } if (query.mixin) { for (const mixinName in query.mixin) checkAssociation( query.mixin[mixinName] ); } } function checkEnumType( enumNode ) { // Either the type is an enum or an arrayed enum. We are only interested in // the enum and don't care whether the enum is arrayed. enumNode = enumNode.enum ? enumNode : enumNode.items; if (!enumNode || !enumNode.enum) return; const type = enumNode?.type?._artifact?._effectiveType; // We can't distinguish (in CSN) between these two cases: // type Base : String enum { b;a = 'abc'; }; // type ThroughRef : Base; (1) // type NotAllowed : Base enum { a } (2) // (2) should not be allowed but (1) should be. That's why we allow (2). if (!type || type.enum) return; // All builtin types are allowed except binary, structured (Map), and relational types. // The latter are "internal" types. // Structures/Arrays are not allowed. // TODO(v6): Reverse coding: use allow-list approach; don't forget about geo, etc. const invalidEnumBuiltins = { __proto__: null, structure: 'struct', binary: 'binary', relation: 'relation', vector: 'vector', map: 'map', }; if (!type.builtin || type.internal || type.category in invalidEnumBuiltins) { let typeClass = 'std'; if (type.category in invalidEnumBuiltins) typeClass = invalidEnumBuiltins[type.category]; else if (type.elements) typeClass = 'struct'; else if (type.items) typeClass = 'items'; error( 'type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, { std: 'Only builtin types are allowed as enums', binary: 'Binary types are not allowed as enums', relation: 'Relational types are not allowed as enums', struct: 'Structured types are not allowed as enums', vector: 'Vector types are not allowed as enums', items: 'Arrayed types are not allowed as enums', map: 'Map types are not allowed as enums', } ); return; } checkEnumValue( enumNode ); } /** * Check the given enum's elements and their values. For example, * whether the value types are valid for the used enum type. * `enumNode` can be also be `type.items` if the type is an arrayed enum. * * @param {XSN.Definition} enumNode */ function checkEnumValue( enumNode ) { const type = enumNode.type?._artifact?._effectiveType; if (!type || !enumNode.enum || !type.builtin) return; const isNumeric = type.category === 'decimal' || type.category === 'integer'; const isString = type.category === 'string'; if (!isString) { // Non-string enums MUST have a value as the value is only deducted for string types. const emptyValue = Object.keys( enumNode.enum ) .find( name => !enumNode.enum[name].value ); if (emptyValue) { const failedEnum = enumNode.enum[emptyValue]; message( 'type-missing-enum-value', [ failedEnum.location, failedEnum ], { '#': isNumeric ? 'numeric' : 'std', name: emptyValue, } ); } } // We only check string and numeric value types. // TODO: share value-type check with that of annotation assignments if (!isString && !isNumeric) return; const expectedType = isNumeric ? 'number' : 'string'; let art = enumNode; while (art?._effectiveType && art.length === undefined) art = getOrigin( art ); const maxLength = art.length?.val ?? model.options.defaultStringLength; // Do not check elements that don't have a value at all or are // references to other enum elements. There are other checks for that. const hasWrongType = element => element.value && (element.value.literal !== expectedType) && (element.value.literal !== 'enum'); for (const key in enumNode.enum) { const element = enumNode.enum[key]; if (hasWrongType( element )) { const actualType = element.value.literal; warning( 'type-unexpected-value', [ element.value.location, element ], { '#': expectedType, name: key, prop: actualType || 'unknown', }, { std: 'Incorrect value type $(PROP) for enum element $(NAME)', // Not used number: 'Expected numeric value for enum element $(NAME) but was $(PROP)', string: 'Expected string value for enum element $(NAME) but was $(PROP)', } ); } else if (isString && maxLength !== undefined) { const value = element.value?.val ?? element.name.id; if (value.length > maxLength) { const loc = element.value?.location ?? element.name.location; warning( 'def-invalid-value', [ loc, element ], { '#': element.value ? 'std' : 'implicit', name: element.name.id, value: maxLength, }, { std: 'Enum value $(NAME) exceeds specified length $(VALUE)', implicit: 'Implicit enum value $(NAME) exceeds specified length $(VALUE)', } ); } } } } /** * Check that min and max cardinalities of 'art' have legal values * * TODO: move to define.js or parsers * * @param {XSN.Artifact} art */ function checkCardinality( art ) { if (!art.cardinality) return; // Max cardinalities must be a positive number or '*' for (const prop of [ 'sourceMax', 'targetMax' ]) { if (art.cardinality[prop]) { const { val, location } = art.cardinality[prop]; if (val !== '*' && val <= 0) { error( 'type-invalid-cardinality', [ location, art ], { '#': prop, prop: val, otherprop: '*' } ); } } } // If provided, min cardinality must not exceed max cardinality (note that // '*' is considered to be >= any number) const pair = [ [ 'sourceMin', 'sourceMax', 'sourceVal' ], [ 'targetMin', 'targetMax', 'targetVal' ], ]; pair.forEach( ([ lhs, rhs, variant ]) => { if (art.cardinality[lhs] && art.cardinality[rhs] && art.cardinality[rhs].literal === 'number' && art.cardinality[lhs].val > art.cardinality[rhs].val) error( 'type-invalid-cardinality', [ art.cardinality.location, art ], { '#': variant } ); } ); } function checkAssociation( elem ) { if (!elem.target && !elem.targetAspect) return; // TODO: yes, a check similar to this could make it into the compiler) // when virtual element is part of association let fkCount = 0; if (elem.foreignKeys) { for (const k in elem.foreignKeys) { ++fkCount; // Note: If the foreign key is structured, we don't check its elements! const key = elem.foreignKeys[k].targetElement; if (key && isVirtualElement( key._artifact )) error( 'ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' } ); else if (key._artifact?.$syntax === 'calc' && !key._artifact.value.stored?.val) error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } ); else if (key._artifact?._effectiveType?.name.id === 'cds.Map') error( 'ref-unexpected-map', [ key.location, elem ], { '#': 'keys', type: 'cds.Map' } ); } } if (elem.default?.val !== undefined) { if (elem.targetAspect || elem.on || fkCount !== 1) { const variant = (elem.targetAspect && 'targetAspect') || (elem.on && 'onCond') || 'multi'; error( 'type-unexpected-default', [ elem.default.location, elem ], { '#': variant, keyword: 'default', count: fkCount, } ); } else { const fkName = Object.keys( elem.foreignKeys )[0]; if (elem.foreignKeys[fkName].targetElement._artifact?._effectiveType?.elements) { error( 'type-unexpected-default', [ elem.default.location, elem ], { '#': 'structuredKey', keyword: 'default', name: fkName, } ); } } } checkOnCondition( elem ); } function checkDefaultValue( art ) { if (!art._effectiveType) return; if (art.kind !== 'element' && art.kind !== 'type' && art.kind !== 'param') return; const defaultValue = getInheritedProp( art, 'default' ); if (defaultValue?.val === undefined) return; // Check that "not null" artifacts don't have `null` default values. // At least one property must be written explicitly to avoid reporting on inferred elements. if (art.default?.val === null || art.notNull?.val) { const notNullValue = getInheritedProp( art, 'notNull' ); if (notNullValue?.val && defaultValue?.val === null) { const loc = (art.default || art.notNull)?.location || art.location; const variant = art.kind + (!art.default && art.notNull ? 'NotNull' : 'DefaultNull'); message( 'type-unexpected-null', [ loc, art ], { '#': variant, art, keyword: 'not null', value: 'null', } ); } } const isMap = art._effectiveType?.name.id === 'cds.Map'; if (isMap) { error( 'type-unexpected-default', [ defaultValue.location, art ], { '#': 'map', keyword: 'default', type: 'cds.Map', } ); } else if (art._effectiveType?.elements) { // TODO: error for v7 warning( 'type-unexpected-default-struct', [ defaultValue.location, art ], { '#': art.kind, keyword: 'default', }, { std: 'Unexpected $(KEYWORD) for a structure', param: 'Unexpected $(KEYWORD) for a structured parameter', type: 'Unexpected $(KEYWORD) for a structured type definition', element: 'Unexpected $(KEYWORD) for a structured element', } ); } } function getBinaryOp( cond ) { const { op, args } = cond; return op?.val === 'ixpr' && args?.length === 3 && args[1].literal === 'token' && args[1] || op; } /** * TODO: A function like this could be part of the compiler * * Check that the given type has no conflicts between its `type` property * and its `elements` or `items` property. For example if `type` is not * structured but the artifact has an `elements` property then the user * made a mistake. This scenario can only happen through CSN and not CDL. * * @param {XSN.Artifact} artifact */ function checkTypeStructure( artifact ) { // Just a basic check. We do not check that the inner structure of `items` // is the same as the type but only that all are arrayed or structured. if (artifact.type?._artifact) { const finalType = artifact.type._artifact._effectiveType || artifact.type._artifact; if (artifact.items && !finalType.items) { warning( 'type-items-mismatch', [ artifact.type.location, artifact ], { type: artifact.type, prop: 'items' }, 'Used type $(TYPE) is not arrayed and conflicts with $(PROP) property' ); } else if (artifact.elements && !finalType.elements) { // TODO: Handle cds.Map! warning( 'type-elements-mismatch', [ artifact.type.location, artifact ], { type: artifact.type, prop: 'elements' }, 'Used type $(TYPE) is not structured and conflicts with $(PROP) property' ); } } if (artifact.items) checkTypeStructure( artifact.items ); } /** * Report issues when an entity overrides structured elements of an included entity * with a scalar one or vice versa. * * NOTE: Relies on element expansion. */ function checkElementIncludeOverride( def ) { for (const name in def.elements) { const element = def.elements[name]; // Element is new in `art`, not expanded; we can't check for !element._origin, due // to calculated elements such as `a = b`. if (element.$inferred !== 'include' && element.$inferred !== 'aspect-composition') { for (const include of def.includes) { if (include._artifact?.elements?.[name] !== undefined) checkElementOverride( element, include._artifact.elements[name] ); } } } return; function checkElementOverride( elem, original ) { const xorElements = !elem.elements !== !original.elements; if (xorElements) { // one of the two elements is not structured const prop = !elem.elements ? 'new-not-structured' : 'old-not-structured'; // Position at type/struct, not name const loc = elem.type?.location || elem.elements?.[$location] || elem.location; error( 'ref-invalid-override', [ loc, elem ], { '#': prop, art: original._main, name: elem.name.id } ); return false; } else if (original.elements && !checkSubStructureOverride( elem, elem.elements, original.elements )) { return false; } const xorTarget = !(elem.target || elem.targetAspect) !== !(original.target || original.targetAspect); if (xorTarget) { // one of the two elements is not an association const prop = !elem.target ? 'new-not-target' : 'old-not-target'; // Position at type/assoc, not name const loc = elem.target?.location || elem.type?.location || elem.location; error( 'ref-invalid-override', [ loc, elem ], { '#': prop, art: original._main, name: elem.name.id } ); return false; } return true; } /** * Ensure the new one has at least as many elements as the original. */ function checkSubStructureOverride( user, elements, originals ) { for (const element in originals) { const elem = elements[element]; const orig = originals[element]; if (elem === undefined) { const loc = [ elements[$location], user ]; error( 'ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element } ); return false; // only report once } else if (!checkElementOverride( elem, orig )) { return false; } } return true; } } /** * Check a generic expression (or condition) for semantic validity. * * @param {any} xpr The expression to check * @param {XSN.Artifact} user User for semantic location * @param {any} _parentExpr * @param {string} [context] where the expression is used, e.g. 'anno' */ function checkGenericExpression( xpr, user, _parentExpr, context ) { if (context !== 'anno') checkExpressionNotVirtual( xpr, user ); checkExpressionAssociationUsage( xpr, user, { context, rejectManaged: context === 'anno', rejectUnmanaged: true, } ); if (xpr.op?.val === 'cast') { requireExplicitTypeInSqlCast( xpr, user ); checkTypeCast( xpr, user ); checkTypeArguments( xpr, user ); } } function checkExpressionNotVirtual( xpr, user ) { if (xpr._artifact && isVirtualElement( xpr._artifact )) error( 'ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' } ); } function checkOnCondition( elem ) { if (elem.$inferred === 'localized') return; // ignore if (!elem.on || elem.on.$inferred) return; visitExpression( elem.on, elem, (xpr, user) => { checkExpressionNotVirtual( xpr, user ); checkExpressionAssociationUsage( xpr, user, null ); if (xpr._artifact?._effectiveType?.name.id === 'cds.Map') { error( 'ref-unexpected-map', [ xpr.location, user ], { '#': 'onCond', type: 'cds.Map' } ); } else if (xpr._artifact?.$syntax === 'calc' && !xpr._artifact.value.stored?.val) { // Essential check. Dependency handling for `on` conditions must change if // this is allowed. See test3/Associations/Dependencies/. error('ref-unexpected-calculated', [ xpr.location, user ], { '#': 'on' }); } } ); } function checkSelectItemValue( elem ) { checkExpressionAssociationUsage( elem.value, elem, { context: 'query', rejectManaged: false, rejectUnmanaged: true, } ); checkVirtualSelectItemChangeForV6( elem ); // To avoid duplicate messages, only run this check if the type wasn't inferred from // the cast, as otherwise we will check it twice (once here, once via element). if (elem.value?.op?.val === 'cast' && elem.type?.$inferred !== 'cast') { requireExplicitTypeInSqlCast( elem.value, elem ); checkTypeCast( elem.value, elem ); checkTypeArguments( elem.value, elem ); } visitSubExpression( elem.value, elem, (xpr, user, parentExpr) => { checkGenericExpression( xpr, elem, parentExpr, 'query' ); } ); } /** * In v6, there will be a change in semantics for following example: * * ```cds * view V as select from E { * virtual b, // -> warning: will be new element, not reference in v6 * } * ``` * * We allow users to define _new_ elements using `virtual`. But all such references * in v5 are valid, resolvable references. Hence, the semantics will change in v6. * Let users know about it. * * @param elem * * TODO: simplify if the old parser is gone (query-invalid-virtual-struct stays) */ function checkVirtualSelectItemChangeForV6( elem ) { if ( !elem.virtual?.val || elem.virtual.$inferred || // not explicitly marked virtual !elem.name.$inferred || // has explicit alias elem._columnParent || // virtual inside expand/inline is already error elem._parent.kind === 'element' || // dito (expand without ref) !elem.value?.path && !elem.value?.func || // neither ref nor function call elem.value.args || // arguments (with function call) elem.value.path?.length > 1 || // multi-path step, i.e. no new definition elem.value.path?.some( ps => ps.args || ps.where ) // not a simple reference ) return; if (elem.expand) { warning( 'query-invalid-virtual-struct', [ elem.expand[$location], elem ], { code: `as ${ elem.name.id }` } ); } else { error( 'def-upcoming-virtual-change', [ elem.virtual.location, elem ] ); } } function checkCalculatedElementValue( elem ) { const isStored = elem.value.stored?.val; visitExpression( elem.value, elem, (xpr, user) => { // We only need to check artifact references. To avoid false positives and conflicts // with $self comparison-checks, ignore bare $self. const isArtRef = xpr._artifact && !(xpr.path?.length === 1 && xpr.path[0]._navigation?.kind === '$self'); if (isArtRef) { const lastStep = xpr.path?.[xpr.path.length - 1]; const sourceLoc = lastStep.location || xpr.location; checkExpressionNotVirtual( xpr, user ); // For inferred (e.g. included) calc elements, this error is already emitted at the origin. // And users can't change structured to non-structured elements. if (!elem.$inferred && xpr._artifact._effectiveType?.elements) { error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'struct-expr', elemref: xpr } ); } else if (xpr._artifact.target !== undefined && (!lastStep.where || lastStep.where.args?.length === 0 || isStored)) { // Allow using an association _with non-empty filter_, but only for on-read // calculated elements. // TODO: Also allow bare unmanaged association references and remove beta. const variant = (isStored && lastStep.where && 'assoc-stored') || (isComposition( model, xpr._artifact ) && 'expr-comp') || 'expr'; error( 'ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant } ); } else if (xpr._artifact.localized?.val && isStored) { error( 'ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' } ); } } } ); // Calculated elements must not refer to keys, because that may lead to another // key in an SQL view, which is missing in OData (for on-read). // Following associations does not lead to this issue. if (elem.value.path && isKeyElement( elem.value._artifact ) && !followsAnAssociation( elem.value.path )) { error( 'ref-unexpected-key', [ elem.value.location, elem ], {}, 'Calculated elements can\'t refer directly to key elements' ); } } /** * Returns true if any of the path steps follows an association. * * @param path * @return {boolean} */ function followsAnAssociation( path ) { for (const step of path) { if (step._artifact?.target) return true; } return false; } function isKeyElement( elem ) { let parent = elem; while (parent) { if (parent.key?.val === true) return true; parent = parent._parent; } return false; } /** * Check whether the supplied argument is a virtual element * * TO CLARIFY: do we want the "no virtual element" check for virtual elements/columns, too? * * @param {any} elem Element to check (part of an expression) * @returns {Boolean} */ function isVirtualElement( elem ) { let parent = elem?._origin || elem; while (parent) { if (parent.virtual?.val === true) return true; parent = parent._parent; } return false; } /** * Check a tree-like expression for semantic validity * * @param {any} xpr The expression to check * @param {XSN.Artifact} user * @param {{context: string, rejectUnmanaged, rejectManaged}|null} [rejectAssocTail] * Context where association tails are not allowed. * @returns {void} */ function checkExpressionAssociationUsage( xpr, user, rejectAssocTail = null ) { if (!xpr.args) return; // Only check associations and $self if this is not a backlink-like // expression (a comparison of $self with an assoc). // We don't check token-stream-like 'xpr's. const args = Array.isArray( xpr.args ) ? xpr.args : Object.values( xpr.args ); const isNotSelfComparison = args.length > 0 && xpr.op?.val !== 'xpr' && !isBinaryDollarSelfComparisonWithAssoc( xpr ); if (isNotSelfComparison) { const op = getBinaryOp( xpr ); for (const arg of args) { if (arg && !(op?.val !== '=' && isDollarSelfOrProjectionOperand( arg ))) checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail ); } } } /** * @param arg * @param {XSN.Artifact} user * @param {{context: string, rejectUnmanaged, rejectManaged}|null} [rejectAssocTail] * Context where association tails are not allowed. */ function checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail ) { // Arg must not be an association and not $self // Only if path is not after an `exists` if (arg.$syntax !== 'after-exists' && rejectAssocTail && isAssociationOperand( arg )) { if (rejectAssocTail.rejectManaged && rejectAssocTail.rejectUnmanaged || rejectAssocTail.rejectManaged && arg._artifact.keys || rejectAssocTail.rejectUnmanaged && arg._artifact.on) { // only a few contexts have special message const context = rejectAssocTail.context === 'query' && 'query-' || rejectAssocTail.context === 'anno' && 'anno-' || ''; // Hm, in other message context about associations or compositions, we do // not have separate text variants for association or composition… const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr'; error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': `${ context }${ variant }` } ); } } } /** * Return true if 'arg' is an expression argument of type association or composition. */ function isAssociationOperand( arg ) { // If it has a target, it is an association or composition return !!arg._artifact?._effectiveType?.target; } /** * Return true if 'arg' is an expression argument denoting "$self" || "$projection" */ function isDollarSelfOrProjectionOperand( arg ) { return arg.path?.length === 1 && (arg.path[0].id === '$self' || arg.path[0].id === '$projection'); } /** * Return true if 'xpr' is backlink-like expression (a comparison of "$self" with an assoc) * * @param {any} xpr The expression to check * @returns {Boolean} */ function isBinaryDollarSelfComparisonWithAssoc( xpr ) { // Must be an expression with arguments if (!xpr.op || !xpr.args) return false; // One argument must be "$self" and the other an assoc if (xpr.op.val === '=' && xpr.args.length === 2) { // Tree-ish expression from the compiler (not augmented) // eslint-disable-next-line @stylistic/max-len return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[1] ) || // eslint-disable-next-line @stylistic/max-len isAssociationOperand( xpr.args[1] ) && isDollarSelfOrProjectionOperand( xpr.args[0] )); } else if (xpr.args.length === 3 && xpr.args[1].val === '=') { // Tree-ish expression from the compiler (not augmented) // eslint-disable-next-line @stylistic/max-len return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[2] ) || // eslint-disable-next-line @stylistic/max-len isAssociationOperand( xpr.args[2] ) && isDollarSelfOrProjectionOperand( xpr.args[0] )); } // Nothing else qualifies return false; } /** * Returns true if the given annotation accepts expressions as values. * * @param {object} anno * @param {XSN.Artifact} art * @returns {boolean} */ function checkAnnotationAcceptsExpressions( anno, art ) { const name = anno.name?.id; if (!name) return true; if (!propagationRules[`@${ name }`] || acceptsExprValues[`@${ name }`]) return true; error( 'anno-unexpected-expr', [ anno.location, art ], { anno: name }, 'Unexpected expression as value for $(ANNO)' ); return false; } function checkAnnotationAssignment1( art, anno ) { const name = anno.name?.id; if (art.$contains?.$annotation && anno.kind === '$annotation' && anno._outer) { if (checkAnnotationAcceptsExpressions( anno, art )) checkAnnotationExpressions( anno, art ); } // Has been slightly adapted for model.vocabularies but comments need to be // adapted, etc. // TODO: rework completely! // TODO: if we have such a check, consider #variant, anno.@anno, anno@anno // Sanity checks (ignore broken assignments) if (!name) return; // Compiler specific annotation validation. const annotationChecks = { __proto__: null, '@cds.redirection.target': checkAnnoRedirectionTarget, }; const annoName = `@${ name }`; annotationChecks[annoName]?.(anno, art); if (!model.vocabularies) return; // Just a little workaround to adapt to changed `name`s, not nice coding: const hashIndex = anno.name.id.indexOf( '#' ); const path = (hashIndex > 0 ? anno.name.id.substring( 0, hashIndex ) : anno.name.id) .split( '.' ).map( id => ({ id }) ); // Annotation artifact for longest path step of annotation path let fromArtifact = null; let pathStepsFound = 0; for (let i = path.length; i > 0; i--) { const absoluteName = path.slice( 0, i ).map( p => p.id ).join( '.' ); if (model.vocabularies[absoluteName]) { fromArtifact = model.vocabularies[absoluteName]; pathStepsFound = i; break; } } if (!fromArtifact) { // Unchecked annotation => nothing to check return; } const { artifact, endOfPath } = resolvePathFrom( path.slice( pathStepsFound ), fromArtifact ); // Check what we actually want to check checkAnnotationAssignment( anno, artifact, endOfPath, art ); } function checkAnnoRedirectionTarget( anno, art ) { if (anno.$inferred) return; if (!annotationVal( anno )) return; // ignore falsey values, including 'null' // Non-entities can't have this annotation, nor can non-service entities, // nor can complex queries such as joins, unions, or selecting from an association. const isIgnored = isComplexView( art ) || (art.kind !== 'entity') || !art._service; if (isIgnored) { const loc = anno.val ? anno.location : anno.name.location; info( 'anno-ignoring-redirection-target', [ loc, art ], { anno: anno.name.id }, '$(ANNO) has no effect here; use it on simple projections inside services' ); } } // Perform checks for annotation assignment 'anno', using corresponding annotation declaration, // made of 'annoDecl' (artifact or undefined) and 'elementDecl' (annotation or element // or undefined). Report errors on 'options.messages. function checkAnnotationAssignment( anno, annoDecl, elementDecl, art ) { // Nothing to check if no actual annotation declaration was found if (!annoDecl || annoDecl.artifacts && !elementDecl) return; // Must be an annotation if found if (annoDecl.kind !== 'annotation') // i.e namespace return; // Element must exist in annotation if (!elementDecl) { warning( null, [ anno.location || anno.name.location, art, anno ], { name: anno.name.id, anno: annoDecl.name.id }, 'Element $(NAME) not found for annotation $(ANNO)' ); return; } if (!elementDecl._effectiveType) return; // type resolution error // Must have literal or path unless it is a boolean if (!anno.literal && !anno.path && elementDecl._effectiveType.category !== 'boolean') { if (elementDecl.type?._artifact) { warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ], { '#': 'type', type: elementDecl.type._artifact } ); } else { warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ], { '#': 'std', anno: anno.name.id } ); } return; } // Value must be assignable to type checkValueAssignableTo( anno, anno, elementDecl, art ); } /** * Check the expressions inside annotations. */ function checkAnnotationExpressions( anno, art ) { if (anno.$tokenTexts) { checkGenericExpression( anno, art, null, 'anno' ); } else if (anno.literal === 'array') { anno.val.forEach( val => checkAnnotationExpressions( val, art ) ); } else if (anno.literal === 'struct') { const struct = Object.values(anno.struct); struct.forEach(val => checkAnnotationExpressions( val, art )); } } // Check that annotation assignment 'value' (having 'path or 'literal' and // 'val') is potentially assignable to element 'element'. Complain on 'loc' // if not function checkValueAssignableTo( annoDef, value, elementDecl, art ) { // FIXME: We currently do not have any element declaration that could match // a 'path' value, so we simply leave those alone if (value.path) return; const anno = annoDef.name.id; const loc = [ value.location || value.name.location, art, annoDef ]; // Array expected? if (elementDecl._effectiveType.items) { // Make sure we have an array value if (value.literal !== 'array') { warning( null, loc, { anno }, 'An array value is required for annotation $(ANNO)' ); return; } // Check each element for (const valueItem of value.val) checkValueAssignableTo( value, valueItem, elementDecl._effectiveType.items, art ); return; } // Struct expected (can only happen within arrays, or CSN input)? if (elementDecl._effectiveType.elements) { if (value.literal !== 'struct') { warning( null, loc, { anno }, 'A struct value is required here for annotation $(ANNO)' ); return; } // FIXME: Should check each element return; } // Handle each (primitive) expected element type separately // TODO: Don't rely on name; use actual type const type = elementDecl._effectiveType; if (!type) return; if (type.category === 'string') { if (value.literal !== 'string' && value.literal !== 'enum' && !elementDecl._effectiveType.enum) { warning( null, loc, { type, anno }, 'A string value is required for type $(TYPE) for annotation $(ANNO)' ); } } else if (type.category === 'binary') { if (value.literal !== 'string' && value.literal !== 'x') { warning( null, loc, { type, anno }, 'A hexadecimal string value is required for type $(TYPE) for annotation $(ANNO)' ); } } else if (type.category === 'decimal' || type.category === 'integer') { if (value.literal !== 'number' && value.literal !== 'enum' && !elementDecl._effectiveType.enum) { warning( null, loc, { type, anno }, 'A numerical value is required for type $(TYPE) for annotation $(ANNO)' ); } } else if (type.category === 'dateTime') { if (value.literal !== 'date' && value.literal !== 'time' && value.literal !== 'timestamp' && value.literal !== 'string') { // Hm, actually date and time cannot be mixed warning( null, loc, { type, anno }, // eslint-disable-next-line @stylistic/max-len 'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)' ); } } else if (type.category === 'boolean') { if (value.literal && value.literal !== 'boolean') { warning( null, loc, { type, anno }, 'A boolean value is required for type $(TYPE) for annotation $(ANNO)' ); } } else if (type.target || type.category === 'geo') { warning( null, loc, { type: (type.target ? 'cds.Association' : type), anno }, 'Type $(TYPE) can\'t be assigned a value for annotation $(ANNO)' ); // TODO: complain at definition instead } else if (!type.enum) { // type error somewhere; ignore return; } // Check enums const expectedEnum = elementDecl._effectiveType.enum; if (value.literal === 'enum') { if (expectedEnum) { // Enum symbol provided and expected if (!expectedEnum[value.sym.id]) { // ... but no such constant warning( null, loc, { id: `#${ value.sym.id }`, anno }, 'Enum symbol $(ID) not found in enum for annotation $(ANNO)' ); } } else { // Enum symbol provided but not expected warning( null, loc, { id: `#${ value.sym.id }`, type, anno }, 'Can\'t use enum symbol $(ID) for non-enum type $(TYPE) for annotation $(ANNO)' ); } } else if (expectedEnum) { // Enum symbol not provided but expected const hasValidValue = Object.keys( expectedEnum ) .some( symbol => getEnumValue( expectedEnum[symbol] ) === value.val ); if (!hasValidValue) { // ... and none of the valid enum symbols matches the value warning( null, loc, { anno }, 'An enum value is required for annotation $(ANNO)' ); } } } function getEnumValue( enumSymbol ) { if (enumSymbol.value) return enumSymbol.value?.val; if (enumSymbol._effectiveType) return enumSymbol._effectiveType?.value?.val; return null; } // TODO: remove // Return the artifact (and possibly, its element) found by following 'path' // starting at 'from'. The return value is an object { artifact, endOfPath } // with 'artifact' being the last artifact encountered on 'path' (or // 'undefined' if none found), and 'endOfPath' being the element or artifact // represented by the full path (or 'undefined' if not found). Note that // only elements and artifacts are considered for path traversal (no actions, // functions, parameters etc.) function resolvePathFrom( path, from, result = {} ) { // Keep last encountered artifacts if (from && !from._main) result.artifact = from; // Always keep current path end result.endOfPath = from; // Stop if found or failed if (path.length === 0 || !from) return result; // Continue search with next path step const nextStepEnv = (from._effectiveType || from).artifacts || from._effectiveType?.elements || []; return resolvePathFrom( path.slice(1), nextStepEnv[path[0].id], result ); } } /** * Ensure that the `locale` element of `sap.common.TextsAspects` * is