@sap/cds-compiler
Advanced tools
Comparing version 5.2.0 to 5.3.0
@@ -220,2 +220,7 @@ #!/usr/bin/env node | ||
if (cmdLine.options[cmdLine.command]?.transitiveLocalizedViews) { | ||
cmdLine.options.fewerLocalizedViews = !cmdLine.options[cmdLine.command].transitiveLocalizedViews | ||
delete cmdLine.options[cmdLine.command].transitiveLocalizedViews; | ||
} | ||
parseSeverityOptions(cmdLine); | ||
@@ -222,0 +227,0 @@ |
@@ -50,16 +50,16 @@ #!/usr/bin/env node | ||
} | ||
const parser = compiler.parseX( buf, 'hi.cds', options ).tokenStream; | ||
// ts is parser with new parser | ||
const { tokens, lexer } = parser; | ||
const { tokenStream } = compiler.parseX( buf, 'hi.cds', options ); | ||
const { tokens, lexer } = tokenStream; | ||
if (!buf.length || !tokens || !tokens.length) | ||
return; | ||
const chars = [ ...buf ]; | ||
for (const tok of tokens) { | ||
const { location } = tok; | ||
const start = lexer.characterPos( location.line, location.col ); | ||
if (tok.type === 'Comment') // but interpret DocComment! | ||
continue; | ||
const { location, start } = tok; | ||
if (start < 0) | ||
continue; | ||
const stop = lexer.characterPos( location.endLine, location.endCol ) - 1; | ||
const cat = tok.parsed; | ||
// console.log(tok.location.toString(),tok.text,tok.parsed,stop > start) | ||
const cat = tok.parsedAs; | ||
if (!cat) { | ||
@@ -77,3 +77,3 @@ if (stop > start) { | ||
chars[start] = categoryChars[cat] || cat.charAt(0); | ||
if (stop > start) // stop in ANTLR at last char, not behind | ||
if (stop > start) | ||
chars[start + 1] = '_'; | ||
@@ -80,0 +80,0 @@ } |
@@ -11,3 +11,8 @@ # ChangeLog of Beta Features for cdx compiler and backends | ||
## Version 5.3.0 - 2024-09-25 | ||
### Removed `optionalActionFunctionParameters` | ||
It is now enabled by default. | ||
## Version 5.0.0 - 2024-05-29 | ||
@@ -21,15 +26,15 @@ | ||
## Removed `odataAnnotationExpressions` | ||
### Removed `odataAnnotationExpressions` | ||
It is now enabled by default. | ||
## Removed `odataPathsInAnnotationExpressions` | ||
### Removed `odataPathsInAnnotationExpressions` | ||
It is now enabled by default. | ||
## Removed `annotationExpressions` | ||
### Removed `annotationExpressions` | ||
It is now enabled by default. | ||
## Added `temporalRawProjection` | ||
### Added `temporalRawProjection` | ||
@@ -36,0 +41,0 @@ Enables revocation of temporal where clause in projections of temporal entities if the |
@@ -121,2 +121,7 @@ 'use strict'; | ||
}, | ||
withLocations: { | ||
validate: val => typeof val === 'boolean' || val === 'withEndPosition', | ||
expected: () => 'type boolean|"withEndPosition"', | ||
found: val => `type ${ typeof val }`, | ||
}, | ||
dictionaryPrototype: { | ||
@@ -123,0 +128,0 @@ validate: () => true, |
@@ -24,3 +24,2 @@ // module- and csn/XSN-independent definitions | ||
odataTerms: true, | ||
optionalActionFunctionParameters: true, // not supported by runtime, yet. | ||
effectiveCsn: true, | ||
@@ -27,0 +26,0 @@ tenantVariable: true, |
@@ -125,3 +125,2 @@ // Consistency checker on model (XSN = augmented CSN) | ||
'meta', | ||
'@sql_mapping', // TODO: it is time that a 'header' attribute replaces 'version' | ||
'$withLocalized', | ||
@@ -582,2 +581,3 @@ '$sources', | ||
'_origin', '_block', '$inferred', '$expand', '$inCycle', '_deps', | ||
'localized', // really? see #13135 | ||
'$calcDepElement', | ||
@@ -1046,3 +1046,3 @@ '$syntax', '_extensions', | ||
// Object.getPrototypeOf( node ) !== spec.instanceOf.prototype) | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
// throw new InternalConsistencyError( `Expected object of class ${ spec.instanceOf.name } but found ${ found }${ at( [ null, parent ], prop, name ) }` ); | ||
@@ -1049,0 +1049,0 @@ } |
@@ -222,3 +222,3 @@ // The builtin artifacts of CDS | ||
// T HH : mm : ss TZD | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
const timestampRegEx = /^(-?\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,7})?)?(?:Z|[+-]\d{2}(?::\d{2})?)?$/; | ||
@@ -225,0 +225,0 @@ // YYYY - MM - dd T HH : mm : ss . fraction TZD |
@@ -36,2 +36,4 @@ // Checks on XSN performed during compile() that are useful for the user | ||
const { getOrigin } = model.$functions; | ||
checkSapCommonLocale( model ); | ||
@@ -404,2 +406,7 @@ checkSapCommonTextsAspects( model ); | ||
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 | ||
@@ -411,3 +418,3 @@ // references to other enum elements. There are other checks for that. | ||
for (const key of Object.keys( enumNode.enum )) { | ||
for (const key in enumNode.enum) { | ||
const element = enumNode.enum[key]; | ||
@@ -424,2 +431,14 @@ if (hasWrongType( element )) { | ||
} | ||
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)', | ||
} ); | ||
} | ||
} | ||
} | ||
@@ -857,5 +876,5 @@ } | ||
// Tree-ish expression from the compiler (not augmented) | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[1] ) || | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
isAssociationOperand( xpr.args[1] ) && isDollarSelfOrProjectionOperand( xpr.args[0] )); | ||
@@ -865,5 +884,5 @@ } | ||
// Tree-ish expression from the compiler (not augmented) | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[2] ) || | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
isAssociationOperand( xpr.args[2] ) && isDollarSelfOrProjectionOperand( xpr.args[0] )); | ||
@@ -1063,3 +1082,3 @@ } | ||
warning( null, loc, { type, anno }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)' ); | ||
@@ -1066,0 +1085,0 @@ } |
@@ -177,3 +177,2 @@ // Compiler phase 1 = "define": transform dictionary of AST-like XSNs into XSN | ||
initMembers, | ||
checkDefinitions, // TODO: remove | ||
initSelectItems, | ||
@@ -192,9 +191,8 @@ } ); | ||
messages.every( m => m.messageId !== 'api-deprecated-option' )) { | ||
warning( 'api-deprecated-option', {}, | ||
{ prop: 'deprecated', '#': (options.beta ? 'beta' : 'std') }, { | ||
// TODO: make the text scarier in future versions | ||
std: 'With option $(PROP), many newer features are disabled', | ||
// eslint-disable-next-line max-len | ||
beta: 'With option $(PROP), beta features and many other newer features are disabled', | ||
} ); | ||
warning( 'api-deprecated-option', {}, { | ||
prop: 'deprecated', '#': (options.beta ? 'beta' : 'std'), | ||
}, { | ||
std: 'With option $(PROP), recent features are disabled', | ||
beta: 'With option $(PROP), beta features and other recent features are disabled', | ||
} ); | ||
} | ||
@@ -322,3 +320,3 @@ model.definitions = Object.create( null ); | ||
// TODO: enable optional locations | ||
const location = a.name.path && a.name.path[0].location || a.location; | ||
const location = a.name.path?.[0]?.location || a.location; | ||
const absolute = prefix + using; | ||
@@ -356,3 +354,3 @@ artifacts[using] = { | ||
if (!decl.name) | ||
decl.name = { ...path[path.length - 1], $inferred: 'as' }; | ||
decl.name = { ...path.at(-1), $inferred: 'as' }; | ||
const name = decl.name.id; | ||
@@ -372,3 +370,3 @@ // TODO: check name: no "." | ||
// TODO: should we really do that (in v6)? See also initNamespaceAndUsing(). | ||
const last = namespace.path[namespace.path.length - 1]; | ||
const last = namespace.path.at(-1); | ||
const { id } = last; | ||
@@ -445,4 +443,4 @@ if (src.artifacts[id] || last.id.includes( '.' )) | ||
return; | ||
if (parent.columns) // TODO: sub queries? expand/inline? | ||
parent.columns.forEach( c => setLink( c, '_block', parent._block ) ); | ||
// TODO: sub queries? expand/inline? | ||
parent.columns?.forEach( c => setLink( c, '_block', parent._block ) ); | ||
if (parent.scale && !parent.precision) { | ||
@@ -803,3 +801,3 @@ // TODO: where could we store the location of the name? | ||
const aliases = Object.keys( table.$tableAliases || {} ); | ||
// Use first tabalias name on the right side of the join to name the | ||
// Use first table alias name on the right side of the join to name the | ||
// (internal) query, should only be relevant for --raw-output, not for | ||
@@ -929,12 +927,11 @@ // user messages or references - TODO: correct if join on left? | ||
// no semantic loc (wouldn't be available for expand/inline anyway) | ||
error( 'syntax-duplicate-wildcard', [ col.location, null ], | ||
{ | ||
'#': (wildcard.location.col ? 'col' : 'std'), | ||
prop: '*', | ||
line: wildcard.location.line, | ||
col: wildcard.location.col, | ||
}, { | ||
std: 'You have provided a $(PROP) already in line $(LINE)', | ||
col: 'You have provided a $(PROP) already at line $(LINE), column $(COL)', | ||
} ); | ||
error( 'syntax-duplicate-wildcard', [ col.location, null ], { | ||
'#': (wildcard.location.col ? 'col' : 'std'), | ||
prop: '*', | ||
line: wildcard.location.line, | ||
col: wildcard.location.col, | ||
}, { | ||
std: 'You have provided a $(PROP) already in line $(LINE)', | ||
col: 'You have provided a $(PROP) already at line $(LINE), column $(COL)', | ||
} ); | ||
// TODO: extra text variants for expand/inline? - probably not | ||
@@ -1084,3 +1081,3 @@ col.val = null; // do not consider it for expandWildcard() | ||
{ name: e.name.id, code: 'extend … with enum' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Unexpected elements like $(NAME) in an extension for an enum. Additionally, use $(CODE) when extending enums' ); | ||
@@ -1106,3 +1103,3 @@ // Don't emit 'ext-expecting-enum' if this error is emitted. | ||
// TODO: main? | ||
const inEntity = parent._main && parent._main.kind === 'entity'; | ||
const inEntity = parent._main?.kind === 'entity'; | ||
// TODO: also allow indirectly (component in component in entity)? | ||
@@ -1218,3 +1215,3 @@ setLink( targetAspect, '_outer', obj ); | ||
main.kind === 'extend' && { name: { id: '$self' } }; | ||
// remark: an extend has no "table alias" `$self` (relevant for parse-cdl) | ||
// remark: an 'extend' has no "table alias" `$self` (relevant for parse-cdl) | ||
setLink( type, '_artifact', $self ); | ||
@@ -1272,3 +1269,2 @@ setLink( path[0], '_artifact', $self ); | ||
} | ||
// | ||
else if (parent.kind === 'action' || parent.kind === 'function') { | ||
@@ -1275,0 +1271,0 @@ error( 'ext-unexpected-action', [ construct.location, construct ], { '#': parent.kind }, { |
@@ -546,3 +546,3 @@ // Extend | ||
array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)', | ||
@@ -644,5 +644,5 @@ boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)', | ||
const number = artVal + (scaleDiff || 0); | ||
error( 'ext-invalid-type-property', [ ext[prop].location, ext ], | ||
// eslint-disable-next-line object-curly-newline | ||
{ '#': (scaleDiff ? 'scale' : 'number'), prop, number, otherprop: 'scale' } ); | ||
error( 'ext-invalid-type-property', [ ext[prop].location, ext ], { | ||
'#': (scaleDiff ? 'scale' : 'number'), prop, number, otherprop: 'scale', | ||
} ); | ||
} | ||
@@ -1175,10 +1175,7 @@ else { | ||
extMain.kind = ext.kind; | ||
const msg | ||
= error( 'extend-undefined', [ location, artName ], | ||
{ art: artName }, | ||
{ | ||
std: 'Unknown $(ART) - nothing to extend', | ||
element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend', | ||
action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend', | ||
} ); | ||
const msg = error( 'extend-undefined', [ location, artName ], { art: artName }, { | ||
std: 'Unknown $(ART) - nothing to extend', | ||
element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend', | ||
action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend', | ||
} ); | ||
attachAndEmitValidNames( msg, validDict ); | ||
@@ -1198,2 +1195,3 @@ } | ||
* @param {XSN.Artifact} target | ||
* @param {string[]} [justResolveCyclic] | ||
* @returns {boolean} | ||
@@ -1279,3 +1277,3 @@ */ | ||
* @param {XSN.Artifact} art | ||
* @param {string} prop: 'elements' or 'actions' | ||
* @param {string} prop 'elements' or 'actions' | ||
*/ | ||
@@ -1282,0 +1280,0 @@ function includeMembers( ext, art, prop ) { |
@@ -106,3 +106,3 @@ // Generate: localized data and managed compositions | ||
{ option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' ); | ||
@@ -248,3 +248,3 @@ hasError = true; | ||
warning( null, [ art.name.location, art ], { art: textsEntity }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Texts entity $(ART) can\'t be created as there is another definition with that name' ); | ||
@@ -644,3 +644,3 @@ info( null, [ textsEntity.name.location, textsEntity ], { art }, | ||
error( null, [ location, elem ], { art: entityName }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Target entity $(ART) can\'t be created as there is another definition with this name' ); | ||
@@ -647,0 +647,0 @@ return false; |
@@ -635,2 +635,8 @@ // Populate views with elements, elements with association targets, ... | ||
/** | ||
* Set type properties of specified elements on the inferred artifact, but only | ||
* assign them if their values differs from the inferred ones (for better locations). | ||
* | ||
* @param {XSN.Artifact} art | ||
*/ | ||
function setSpecifiedElementTypeProperties( art ) { | ||
@@ -976,3 +982,3 @@ for (const prop in art.typeProps$) { | ||
{ id, keyword: 'excluding' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'This select item replaces $(ID) from two or more sources. Add $(ID) to $(KEYWORD) to silence this message' ); | ||
@@ -984,3 +990,3 @@ } | ||
{ id, alias: navElem._parent.name.id, keyword: 'excluding' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'This select item replaces $(ID) from table alias $(ALIAS). Add $(ID) to $(KEYWORD) to silence this message' ); | ||
@@ -1112,5 +1118,5 @@ } | ||
}, { | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
std: 'Replace target $(TARGET) by one of $(SORTED_ARTS); can\'t auto-redirect this association if multiple projections exist in this service', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service', | ||
@@ -1137,7 +1143,7 @@ } ); | ||
}, { | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
std: 'Add $(ANNO) to one of $(SORTED_ARTS) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
two: 'Add $(ANNO) to either $(SORTED_ARTS) or $(SECOND) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
justOne: 'Remove $(ANNO) from all but one of $(SORTED_ARTS) to have a unique redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise', | ||
@@ -1144,0 +1150,0 @@ } ); |
@@ -77,3 +77,3 @@ // Propagate properties in XSN | ||
const { rewriteAnnotationsRefs } = xprRewriteFns( model ); | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
const oldVirtualNotNullPropagation = isDeprecatedEnabled( options, '_oldVirtualNotNullPropagation' ); | ||
@@ -372,3 +372,3 @@ const { warning, throwWithError } = model.$messageFunctions; | ||
warning( 'def-missing-virtual', [ item.location, elem ], { art, keyword: 'virtual' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Prepend $(KEYWORD) to current select item - referred element $(ART) is virtual which is not inherited' ); | ||
@@ -375,0 +375,0 @@ return; |
@@ -44,6 +44,9 @@ // Tweak associations: rewrite keys and on conditions | ||
mergeSpecifiedForeignKeys, | ||
navigationEnv, | ||
redirectionChain, | ||
} = model.$functions; | ||
Object.assign(model.$functions, { | ||
firstProjectionForPath, | ||
findRewriteTarget, | ||
cachedRedirectionChain, | ||
}); | ||
@@ -129,3 +132,3 @@ | ||
std: 'Association target $(TARGET) is outside any service', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
exposed: 'If association is published in service $(SERVICE), its target $(TARGET) is outside any service', | ||
@@ -147,3 +150,3 @@ } ); | ||
{ keyword: 'on', art: assocWithExplicitSpec( assoc ) }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Do not specify an $(KEYWORD) condition when redirecting the managed association $(ART)' ); | ||
@@ -404,2 +407,8 @@ } | ||
setExpandStatus( elem, 'target' ); | ||
// There were previous issues in resolving the target artifact. | ||
// Avoid further compiler messages. | ||
if (!elem.target._artifact) | ||
return; | ||
if (elem._parent?.kind === 'element') { | ||
@@ -410,2 +419,3 @@ // managed association as sub element not supported yet | ||
error( 'type-unsupported-rewrite', [ elem.location, elem ], { '#': 'sub-element' } ); | ||
removeArtifactLinks(); | ||
return; | ||
@@ -423,15 +433,8 @@ } | ||
if (!navigation) { // TODO: what about $projection.assoc as myAssoc ? | ||
if (elem._columnParent) | ||
if (elem._columnParent) { | ||
error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } ); | ||
removeArtifactLinks(); | ||
} | ||
return; // should not happen: $projection, $magic, or ref to const | ||
} | ||
const isAssocInStruct = (navigation !== assoc && navigation._origin !== assoc); | ||
if (isAssocInStruct) { | ||
// For "[sub.]assoc1.assoc2": not supported, yet (#3977) | ||
const multipleAssoc = elem.value.path.slice(0, -1).some(segment => segment._artifact?.target); | ||
if (multipleAssoc && elem._redirected !== null) { // null = already reported | ||
error('rewrite-not-supported', [ elem.target.location, elem ], { '#': 'secondary' }); | ||
return; | ||
} | ||
} | ||
@@ -445,2 +448,3 @@ if (!nav.tableAlias || nav.tableAlias.path) { | ||
error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } ); | ||
removeArtifactLinks(); | ||
return; | ||
@@ -452,2 +456,3 @@ } | ||
'Selecting unmanaged associations from a sub query is not supported' ); | ||
removeArtifactLinks(); | ||
return; | ||
@@ -458,2 +463,13 @@ } | ||
elem.on.$inferred = 'rewrite'; | ||
/** | ||
* Clear all `_artifact` links in the ON-condition to avoid follow-up | ||
* issues during ON-condition rewriting of associations that inherit | ||
* the ON-condition. | ||
*/ | ||
function removeArtifactLinks() { | ||
traverseExpr( elem.on, 'rewrite-on', elem, (expr) => { | ||
setArtifactLink( expr, null ); | ||
} ); | ||
} | ||
} | ||
@@ -549,2 +565,3 @@ | ||
cond.$parens = [ assocPathStep.location ]; | ||
const navEnv = nav && followNavigationPath( elem.value?.path, nav ) || nav?.tableAlias; | ||
traverseExpr( cond, 'rewrite-filter', elem, (expr) => { | ||
@@ -573,3 +590,3 @@ if (!expr.path || expr.path.length === 0) | ||
// up to here, filter is relative to original association | ||
rewriteExpr( expr, elem, nav?.tableAlias ); | ||
rewriteExpr( expr, elem, nav?.tableAlias, navEnv ); | ||
} | ||
@@ -678,4 +695,2 @@ } ); | ||
// TODO: complain about $self (unclear semantics) | ||
// console.log( info(null, [assoc.name.location, assoc], | ||
// { art: expr._artifact, names: expr.path.map(i=>i.id) }, 'A').toString(), expr.path) | ||
@@ -687,16 +702,11 @@ if (!expr.path || !expr._artifact) | ||
if (navEnv) { // from ON cond of element in source ref/d by table alias | ||
const source = tableAlias._origin; | ||
const root = expr.path[0]._navigation || expr.path[0]._artifact; | ||
if (!root || root._main !== source) | ||
return; // not $self or source element | ||
if (!root || root.kind === 'builtin') | ||
return; // not $self or source element, e.g. builtin | ||
// parameters are not allowed in ON-conditions; error emitted elsewhere already | ||
if (expr.scope === 'param' || root.kind === '$parameters') | ||
return; // are not allowed anyway - there was an error before | ||
const startIndex = (root.kind === '$self' ? 1 : 0); | ||
const exprNavigation = (root.kind === '$self' ? tableAlias : navEnv); | ||
const result = firstProjectionForPath( expr.path, startIndex, exprNavigation, assoc ); | ||
// For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`. | ||
if (result.item && assoc._origin === result.item._artifact) | ||
result.elem = assoc; | ||
return; | ||
rewritePath( expr, result.item, assoc, result.elem, assoc.value.location ); | ||
rewritePathForEnv( expr, navEnv, assoc ); | ||
} | ||
@@ -709,3 +719,3 @@ else if (assoc._main.query) { // from ON cond of mixin element in query | ||
{ id: assoc.value._artifact.name.id }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Association $(ID) can\'t be projected because its ON-condition refers to a parameter' ); | ||
@@ -719,2 +729,3 @@ assoc.$errorReported = 'assoc-unexpected-scope'; | ||
const elem = (assoc._origin === root) ? assoc : navProjection( nav.navigation, assoc ); | ||
// TODO: Use rewritePathForEnv(); make it handle mixins | ||
rewritePath( expr, nav.item, assoc, elem, | ||
@@ -743,7 +754,7 @@ nav.item ? nav.item.location : expr.path[0].location ); | ||
const art = assoc._origin; | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
warning( 'rewrite-shadowed', [ elem.name.location, elem ], { art: art && effectiveType( art ) }, { | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
std: 'This element is not originally referred to in the ON-condition of association $(ART)', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)', | ||
@@ -756,2 +767,134 @@ } ); | ||
/** | ||
* Rewrite the given reference by using projected elements of the given | ||
* navigation environment. | ||
* | ||
* @param {XSN.Expression} ref | ||
* @param {object} navEnv | ||
* @param {XSN.Artifact} user | ||
*/ | ||
function rewritePathForEnv( ref, navEnv, user ) { | ||
// TODO: combine with rewriteGenericAnnoPath() of xpr-rewrite | ||
// reset artifact link; we'll set it again if there are no errors | ||
setArtifactLink( ref, null ); | ||
const rootItem = ref.path[0]; | ||
const root = ref.path[0]._navigation || ref.path[0]._artifact; | ||
const startIndex = (root.kind === '$self' ? 1 : 0); | ||
if (root.kind === '$self') { | ||
let rootEnv = navEnv; | ||
while (rootEnv?.kind === '$navElement') { | ||
if (rootEnv._origin?.target?._artifact === root._origin) | ||
break; | ||
rootEnv = rootEnv._parent; | ||
} | ||
navEnv = rootEnv; | ||
} | ||
// Store the original artifact, so that we can use it to | ||
// calculate a redirection chain later on. | ||
ref.path.forEach((item) => { | ||
if (item._artifact) | ||
setLink( item, '_originalArtifact', item._artifact ); | ||
}); | ||
let env = navEnv; | ||
let art = rootItem._artifact; | ||
let isTargetSide = null; | ||
for (let i = startIndex; i < ref.path.length; ++i) { | ||
if (i > startIndex && art.target) { | ||
// if the current artifact is an association, we need to respect the redirection | ||
// chain from original target to new one. | ||
// FIXME: Won't work with associations in projected structures. | ||
const origTarget = ref.path[i - 1]?._originalArtifact?.target?._artifact; | ||
const chain = cachedRedirectionChain( art, origTarget ); | ||
if (!chain) { | ||
missingProjection( ref, i, user, false ); | ||
return; | ||
} | ||
for (const alias of chain) { | ||
art = rewritePathItemForEnv( ref, alias, i, user ); | ||
isTargetSide ??= (art === user); | ||
if (!art) { | ||
missingProjection( ref, i, user, isTargetSide ); | ||
return; | ||
} | ||
} | ||
} | ||
art = rewritePathItemForEnv( ref, env, i, user ); | ||
isTargetSide ??= (art === user); | ||
if (!art) { | ||
missingProjection( ref, i, user, isTargetSide ); | ||
return; | ||
} | ||
env = navigationEnv( art, null, null, 'nav' ); | ||
} | ||
setArtifactLink( ref, art ); | ||
if (startIndex === 0 && rootItem.id.startsWith('$')) { | ||
// TODO: What about filters? Also rewritten there? | ||
// After rewriting, if an element starts with `$` -> add root prefix | ||
// FIXME: "user" not correct for association inside sub-element, | ||
// because `user._parent` is assumed to be the query | ||
prependSelfToPath( ref.path, user ); | ||
} | ||
} | ||
function rewritePathItemForEnv( ref, navEnv, index, user ) { | ||
const rewriteTarget = findRewriteTarget( ref, index, navEnv, user ); | ||
const found = rewriteTarget[0]; | ||
if (!found) { | ||
setArtifactLink( ref.path[index], found ); | ||
return found; | ||
} | ||
if (rewriteTarget[1] > index) { | ||
// we keep the last segment, in case it has non-enumerable properties | ||
ref.path[index] = ref.path[rewriteTarget[1]]; | ||
ref.path.splice(index + 1, rewriteTarget[1] - index); | ||
} | ||
const item = ref.path[index]; | ||
if (item.id !== found.name.id || (rewriteTarget[1] - index) !== 0) | ||
item.id = found.name.id; | ||
return setArtifactLink( ref.path[index], found ); | ||
} | ||
/** | ||
* @param {XSN.Path} ref | ||
* @param {number} index | ||
* @param {XSN.Artifact} user | ||
* @param {boolean} isTargetSide | ||
*/ | ||
function missingProjection( ref, index, user, isTargetSide ) { | ||
const item = ref.path[index]; | ||
if (!isTargetSide) { | ||
const { location } = user.value; | ||
const rootItem = ref.path[0]; | ||
const elemref = rootItem._navigation?.kind === '$self' ? ref.path.slice(1) : ref.path; | ||
// TODO: Fix message for sub-elements: `s: { a: Association on x=1, x: Integer};` for x | ||
error( 'rewrite-not-projected', [ location, user ], { | ||
name: user.name.id, | ||
art: item._artifact || item._originalArtifact, | ||
elemref: { ref: elemref }, | ||
} ); | ||
} | ||
else { | ||
const isExplicit = user.target && !user.target.$inferred; | ||
const loc = isExplicit ? user.target.location : item.location; | ||
error( 'query-undefined-element', [ loc, user ], { | ||
'#': isExplicit ? 'redirected' : 'std', | ||
id: item.id, | ||
name: user.name.id, | ||
target: user.target._artifact, | ||
keyword: 'redirected to', | ||
} ); | ||
} | ||
} | ||
function rewritePath( ref, item, assoc, elem, location ) { | ||
@@ -766,5 +909,2 @@ const { path } = ref; | ||
name: assoc.name.id, art: elemref[0]._artifact, elemref: { ref: elemref }, | ||
}, { | ||
std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)', | ||
element: 'Projected association $(NAME) uses non-projected element $(ELEMREF) of $(ART)', | ||
} ); | ||
@@ -862,2 +1002,34 @@ } | ||
} | ||
/** | ||
* Get the redirection chain between the element's target and the original target. | ||
* Returns `null` if there is no valid chain. | ||
* Uses `_redirected` if valid. | ||
* | ||
* @param {XSN.Artifact} elem | ||
* @param {XSN.Artifact} origTarget | ||
* @returns {null|XSN.Artifact[]} | ||
*/ | ||
function cachedRedirectionChain( elem, origTarget ) { | ||
const target = elem.target?._artifact; | ||
if (!target || !origTarget) | ||
return null; | ||
if (target === origTarget) | ||
return []; | ||
if (elem._redirected === null) { | ||
// means: "don't touch paths after assoc" | ||
// TODO: figure out if we can assume that here as well | ||
return []; | ||
} | ||
if (elem._redirected) { | ||
// No need to recalculate if the original target is already in '_redirected'. | ||
const i = elem._redirected.findIndex(ta => ta._origin === origTarget); | ||
if (i > -1) | ||
return elem._redirected.slice(i); // TODO: check if it is always "i===0". | ||
} | ||
return redirectionChain( elem, target, origTarget, true ); | ||
} | ||
} | ||
@@ -883,3 +1055,43 @@ | ||
function findRewriteTarget( expr, index, env, user ) { | ||
if (env.kind === '$navElement' || env.kind === '$tableAlias') { | ||
const r = firstProjectionForPath( expr.path, index, env, user ); | ||
return [ r.elem, r.index ]; | ||
} | ||
const item = expr.path[index]; | ||
// If the artifact is already in the same definition, we must not check the query. | ||
// Or if it is not a query -> no $navElement -> use `elements` | ||
if (item._artifact?._main === env || !env.query && env.kind !== 'select') { | ||
if (env.elements?.[item.id]) | ||
return [ env.elements[item.id], index ]; | ||
return [ null, expr.path.length ]; | ||
} | ||
const items = (env._leadingQuery || env)._combined?.[item.id]; | ||
const allNavs = !items || Array.isArray(items) ? items : [ items ]; | ||
// If the annotation target itself has a table alias, require projections of that | ||
// table alias. Of course, that only works if we're talking about the same query. | ||
const tableAlias = (user._main?._origin === item._artifact?._main && | ||
user.value?.path[0]?._navigation?.kind === '$tableAlias') | ||
? user.value.path[0]._navigation : null; | ||
// Look at all table aliase that could project `item` and only select | ||
// those that have actual projections. | ||
const navs = allNavs?.filter(p => p._origin === item._artifact && | ||
(!tableAlias || tableAlias === p._parent)); | ||
if (!navs || navs.length === 0) | ||
return [ null, expr.path.length ]; | ||
// If there are multiple navigations for the element, just use the first that matches. | ||
// In case of table aliases, it's just one. | ||
for (const nav of navs) { | ||
const r = firstProjectionForPath( expr.path, index, nav._parent, user ); | ||
if (r.elem) | ||
return [ r.elem, r.index ]; | ||
} | ||
return [ null, expr.path.length ]; | ||
} | ||
/** | ||
@@ -984,2 +1196,3 @@ * For a path `a.b.c.d`, return a projection for the first path item that is projected, | ||
/** | ||
@@ -986,0 +1199,0 @@ * Return condensed info about reference in select item |
@@ -215,5 +215,7 @@ // Simple compiler utility functions | ||
// Return path step if the path navigates along an association whose final type | ||
// satisfies function `test`; "navigates along" = last path item not considered | ||
// without truthy optional argument `alsoTestLast`. | ||
/** | ||
* Return path step if the path navigates along an association whose final type | ||
* satisfies function `test`; "navigates along" = last path item not considered | ||
* without truthy optional argument `alsoTestLast`. | ||
*/ | ||
function withAssociation( ref, test = testFunctionPlaceholder, alsoTestLast = false ) { | ||
@@ -439,3 +441,3 @@ for (const item of ref.path || []) { | ||
function forEachExprArray( query, array, refContext, exprContext, callback ) { | ||
for (const expr of array ) { | ||
for (const expr of array) { | ||
if (expr) | ||
@@ -517,4 +519,6 @@ callback( expr, (expr.path ? refContext : exprContext), query ); | ||
// Returns what was available at view._from[0] before: | ||
// (think first whether to really use this function) | ||
/** | ||
* Returns what was available at view._from[0] before: | ||
* (think first whether to really use this function) | ||
*/ | ||
function viewFromPrimary( view ) { | ||
@@ -527,20 +531,22 @@ let query = view.$queries?.[0]; | ||
// About Helper property $expand for faster the XSN-to-CSN transformation | ||
// - null/undefined: artifact, member, items does not contain expanded members | ||
// - 'origin': all expanded (sub) elements have no new target/on and no new annotations | ||
// that value is only on elements, types, and params -> no other members | ||
// when set, only on elem/art with expanded elements | ||
// - 'target': all expanded (sub) elements might only have new target/on, but | ||
// no individual annotations on any (sub) member | ||
// when set, traverse all parents where the value has been 'origin' before | ||
// - 'annotate': at least one inferred (sub) member has an individual annotation, | ||
// not counting propagated ones; set up to the definition (main artifact) | ||
// (only set with anno on $inferred elem), annotate “beats” target | ||
// Usage according to CSN flavor: | ||
// - gensrc: do not render inferred elements (including expanded elements), | ||
// collect annotate statements with value 'annotate' | ||
// - client: do not render expanded sub elements if artifact/member is no type, has a type, | ||
// has $expand = 'origin', and all its _origin also have $expand = 'origin' | ||
// (might sometimes render the elements unnecessarily, which is not wrong) | ||
// - universal: do not render expanded sub elements if $expand = 'origin' | ||
/** | ||
* About Helper property $expand for faster the XSN-to-CSN transformation | ||
* - null/undefined: artifact, member, items does not contain expanded members | ||
* - 'origin': all expanded (sub) elements have no new target/on and no new annotations | ||
* that value is only on elements, types, and params -> no other members | ||
* when set, only on elem/art with expanded elements | ||
* - 'target': all expanded (sub) elements might only have new target/on, but | ||
* no individual annotations on any (sub) member | ||
* when set, traverse all parents where the value has been 'origin' before | ||
* - 'annotate': at least one inferred (sub) member has an individual annotation, | ||
* not counting propagated ones; set up to the definition (main artifact) | ||
* (only set with anno on $inferred elem), annotate “beats” target | ||
* Usage according to CSN flavor: | ||
* - gensrc: do not render inferred elements (including expanded elements), | ||
* collect annotate statements with value 'annotate' | ||
* - client: do not render expanded sub elements if artifact/member is no type, has a type, | ||
* has $expand = 'origin', and all its _origin also have $expand = 'origin' | ||
* (might sometimes render the elements unnecessarily, which is not wrong) | ||
* - universal: do not render expanded sub elements if $expand = 'origin' | ||
*/ | ||
function setExpandStatus( elem, status ) { | ||
@@ -625,8 +631,10 @@ // set on element | ||
// Remark: this function is based on an early check that no target element is | ||
// covered more than once by a foreign key: then… | ||
// we only need to check that all foreign key references are primary keys and | ||
// that the number of foreign and primary keys are the same. | ||
/** | ||
* Remark: this function is based on an early check that no target element is | ||
* covered more than once by a foreign key: then… | ||
* we only need to check that all foreign key references are primary keys and | ||
* that the number of foreign and primary keys are the same. | ||
*/ | ||
function isAssocToPrimaryKeys( assoc ) { | ||
let fkeys = 0; | ||
let keyCount = 0; | ||
const { foreignKeys } = assoc; | ||
@@ -642,3 +650,3 @@ if (!foreignKeys) | ||
return false; | ||
++fkeys; | ||
++keyCount; | ||
} | ||
@@ -651,5 +659,5 @@ | ||
if (elements[name].key?.val) | ||
--fkeys; | ||
--keyCount; | ||
} | ||
return fkeys === 0; | ||
return keyCount === 0; | ||
} | ||
@@ -656,0 +664,0 @@ |
@@ -177,3 +177,4 @@ // Rewrite paths in annotation expressions. | ||
resolvePathRoot, | ||
firstProjectionForPath, | ||
cachedRedirectionChain, | ||
findRewriteTarget, | ||
} = model.$functions; | ||
@@ -397,3 +398,3 @@ | ||
const isAbsolute = isAnnoPathAbsolute( expr ); | ||
const rootIndex = isAbsolute ? 1 : 0; | ||
const startIndex = isAbsolute ? 1 : 0; | ||
@@ -405,10 +406,10 @@ // We get the root environment now, even though below we resolve the root item | ||
// reset artifact link; we'll set it again | ||
// reset artifact link; we'll set it again if there are no errors | ||
setArtifactLink( expr, null ); | ||
// Adapt root path, as it isn't rewritten in rewriteItem | ||
const rootItem = expr.path[0]; | ||
if (isAbsolute) { | ||
delete rootItem._artifact; | ||
delete rootItem._navigation; | ||
// Adapt absolute root path, as it isn't rewritten in rewriteItem | ||
// The path-prefix was already adapted in rewriteAnnoExpr(). | ||
delete expr.path[0]._artifact; | ||
delete expr.path[0]._navigation; | ||
// TODO: What about `up_`? Shouldn't we set `_navigation` as well? | ||
@@ -421,8 +422,31 @@ // TODO: Can we handle `$self` of anonymous-composition-of-aspect? | ||
// Store the original artifact, so that we can use it to | ||
// calculate a redirection chain later on. | ||
expr.path.forEach((item) => { | ||
if (item._artifact) | ||
setLink( item, '_originalArtifact', item._artifact ); | ||
}); | ||
let env = rootEnv; | ||
let art = rootItem._artifact; | ||
for (let i = rootIndex; i < expr.path.length; ++i) { | ||
let art = expr.path[0]._artifact; | ||
for (let i = startIndex; i < expr.path.length; ++i) { | ||
if (i > startIndex && art.target) { | ||
// if the current artifact is an association, we need to respect the redirection | ||
// chain from original target to new one. | ||
// FIXME: Won't work with associations in projected structures. | ||
const origTarget = expr.path[i - 1]?._originalArtifact?.target?._artifact; | ||
const chain = cachedRedirectionChain( art, origTarget ); | ||
if (!chain) | ||
return reportAnnoRewriteError( expr, config ); | ||
for (const alias of chain) { | ||
art = rewriteItem( expr, config, alias, i ); | ||
if (!art) | ||
return reportAnnoRewriteError( expr, config ); | ||
} | ||
} | ||
art = rewriteItem( expr, config, env, i ); | ||
if (!art) | ||
return reportAnnoRewriteError( expr, config ); | ||
// target, items, … | ||
env = navigationEnv( art, null, null, 'nav' ); | ||
@@ -432,3 +456,3 @@ } | ||
if (rootIndex === 0 && rootItem.id.startsWith('$')) { | ||
if (startIndex === 0 && expr.path[0].id.startsWith('$')) { | ||
if (config.isInFilter) { | ||
@@ -439,3 +463,3 @@ // In filters, we must not prepend `$self`, as that would change its meaning. | ||
} | ||
// After rewriting, an element starts with `$` -> add root prefix | ||
// After rewriting, if an element starts with `$` -> add root prefix | ||
prependRootPath( config.origin, config.targetRoot, expr ); | ||
@@ -649,59 +673,21 @@ } | ||
function rewriteItem( expr, config, env, index ) { | ||
const item = expr.path[index]; | ||
const rewriteTarget = findRewriteTarget( expr, index, env, config.target ); | ||
const found = setArtifactLink( item, rewriteTarget[0] ); | ||
const found = setArtifactLink( expr.path[index], rewriteTarget[0] ); | ||
if (!found) | ||
return null; | ||
if (item.id !== found.name.id) { | ||
// Path was rewritten; original token text string is no longer accurate | ||
config.tokenExpr.$tokenTexts = true; | ||
item.id = found.name.id; | ||
} | ||
if (rewriteTarget[1] > index) | ||
if (rewriteTarget[1] > index) { | ||
// we keep the last segment, in case it has non-enumerable properties | ||
expr.path[index] = expr.path[rewriteTarget[1]]; | ||
expr.path.splice(index + 1, rewriteTarget[1] - index); | ||
return rewriteTarget[0]; | ||
} | ||
function findRewriteTarget( expr, index, env, target ) { | ||
if (env.kind === '$navElement' || env.kind === '$tableAlias') { | ||
const r = firstProjectionForPath( expr.path, index, env, target ); | ||
return [ r.elem, r.index ]; | ||
} | ||
const item = expr.path[index]; | ||
// If the artifact is already in the same definition, we must not check the query. | ||
// Or if it is not a query -> no $navElement -> use `elements` | ||
if (item._artifact._main === env || !env.query && env.kind !== 'select') { | ||
if (env.elements?.[item.id]) | ||
return [ env.elements[item.id], index ]; | ||
return [ null, expr.path.length ]; | ||
if (item.id !== found.name.id || (rewriteTarget[1] - index) !== 0) { | ||
// Path was rewritten; original token text string is no longer accurate | ||
config.tokenExpr.$tokenTexts = true; | ||
item.id = found.name.id; | ||
} | ||
const items = (env._leadingQuery || env)._combined?.[item.id]; | ||
const allNavs = !items || Array.isArray(items) ? items : [ items ]; | ||
// If the annotation target itself has a table alias, require projections of that | ||
// table alias. Of course, that only works if we're talking about the same query. | ||
const tableAlias = (target._main?._origin === item._artifact._main && | ||
target.value?.path[0]?._navigation?.kind === '$tableAlias') | ||
? target.value.path[0]._navigation : null; | ||
// Look at all table aliase that could project `item` and only select | ||
// those that have actual projections. | ||
const navs = allNavs?.filter(p => p._origin === item._artifact && | ||
(!tableAlias || tableAlias === p._parent)); | ||
if (!navs || navs.length === 0) | ||
return [ null, expr.path.length ]; | ||
// If there are multiple navigations for the element, just use the first that matches. | ||
// In case of table aliases, it's just one. | ||
for (const nav of navs) { | ||
const r = firstProjectionForPath( expr.path, index, nav._parent, target ); | ||
if (r.elem) | ||
return [ r.elem, r.index ]; | ||
} | ||
return [ null, expr.path.length ]; | ||
return setArtifactLink( expr.path[index], found ); | ||
} | ||
@@ -708,0 +694,0 @@ } |
@@ -739,4 +739,4 @@ 'use strict'; | ||
/** @type {object} */ | ||
const actionNode = (iAmAnAction) ? new Edm.Action(v, attributes) | ||
: new Edm.FunctionDefinition(v, attributes); | ||
const actionNode = (iAmAnAction) ? new Edm.Action(v, attributes, actionCsn) | ||
: new Edm.FunctionDefinition(v, attributes, actionCsn); | ||
@@ -743,0 +743,0 @@ const bpType = entityCsn ? fullQualified(entityCsn.name) : undefined; |
@@ -34,2 +34,3 @@ 'use strict'; | ||
this._jsonOnlyAttributes = Object.create(null); | ||
this._openApiHints = Object.create(null); | ||
@@ -42,2 +43,4 @@ this._children = []; | ||
this.setSapVocabularyAsAttributes(csn); | ||
this.setOpenApiHints(csn); | ||
} | ||
@@ -108,2 +111,13 @@ | ||
setOpenApiHints(csn) { | ||
if (csn && options.odataOpenapiHints) { | ||
const jsonAttr = Object.create(null); | ||
Object.entries(csn).filter(([ k, _v ] ) => k.startsWith('@OpenAPI.')).forEach(([ k, v ]) => { | ||
jsonAttr[k] = v; | ||
}); | ||
Object.assign(this._openApiHints, jsonAttr); | ||
} | ||
return this._openApiHints; | ||
} | ||
// virtual | ||
@@ -116,8 +130,7 @@ toJSON() { | ||
this.toJSONattributes(json); | ||
return this.toJSONchildren(json); | ||
return this.toJSONchildren(this.toJSONattributes(json)); | ||
} | ||
// virtual | ||
toJSONattributes(json) { | ||
toJSONattributes(json, withHints = true) { | ||
forEach(this._edmAttributes, (p, v) => { | ||
@@ -127,2 +140,11 @@ if (p !== 'Name') | ||
}); | ||
return (withHints ? this.toOpenApiHints(json) : json); | ||
} | ||
toOpenApiHints(json) { | ||
if (options.odataOpenapiHints && this._openApiHints) { | ||
Object.entries(this._openApiHints).forEach(([ p, v ]) => { | ||
json[p[0] === '@' ? p : `$${p}`] = v; | ||
}); | ||
} | ||
return json; | ||
@@ -251,2 +273,6 @@ } | ||
toJSONattributes(json) { | ||
return super.toJSONattributes(json, false); | ||
} | ||
register(entry) { | ||
@@ -267,2 +293,3 @@ if (!this._registry[entry._edmAttributes.Name]) | ||
super(version, props); | ||
this.setOpenApiHints(serviceCsn); | ||
this._annotations = annotations; | ||
@@ -331,2 +358,3 @@ this._actions = Object.create(null); | ||
} | ||
return this.toOpenApiHints(json); | ||
} | ||
@@ -561,4 +589,4 @@ | ||
class ActionFunctionBase extends Node { | ||
constructor(version, details) { | ||
super(version, details); | ||
constructor(version, details, csn) { | ||
super(version, details, csn); | ||
this._returnType = undefined; | ||
@@ -714,3 +742,3 @@ } | ||
return json; | ||
return this.toOpenApiHints(json); | ||
} | ||
@@ -766,7 +794,7 @@ } | ||
if (options.odataOpenapiHints) { | ||
if (this._openApiHints) { | ||
if (csn['@cds.autoexpose']) | ||
this.setJSON({ '@cds.autoexpose': true }); | ||
this._openApiHints['@cds.autoexpose'] = true; | ||
if (csn['@cds.autoexposed']) | ||
this.setJSON({ '@cds.autoexposed': true }); | ||
this._openApiHints['@cds.autoexposed'] = true; | ||
} | ||
@@ -820,3 +848,3 @@ } | ||
json[this._edmAttributes.Name] = this._edmAttributes.Value; | ||
return json; | ||
return super.toOpenApiHints(json); | ||
} | ||
@@ -838,7 +866,2 @@ } | ||
toJSONattributes(json) { | ||
super.toJSONattributes(json); | ||
return json; | ||
} | ||
toJSONchildren(json) { | ||
@@ -1177,2 +1200,6 @@ this._children.forEach(c => c.toJSONattributes(json)); | ||
toJSONattributes(json) { | ||
return super.toJSONattributes(json, false); | ||
} | ||
getConstantExpressionValue() { | ||
@@ -1307,2 +1334,3 @@ // short form: key: value | ||
json[`$${key}`] = this._edmAttributes[key]; | ||
return json; | ||
} | ||
@@ -1415,4 +1443,3 @@ | ||
json[`$${this.kind}`] = this._children.filter(c => c.kind !== 'Annotation').map(c => c.toJSON()); | ||
this.toJSONattributes(json); | ||
return json; | ||
return this.toJSONattributes(json); | ||
} | ||
@@ -1436,4 +1463,3 @@ } | ||
json[`$${this.kind}`] = children.length ? children[0].toJSON() : {}; | ||
this.toJSONattributes(json); | ||
return json; | ||
return this.toJSONattributes(json); | ||
} | ||
@@ -1465,4 +1491,3 @@ toJSONattributes(json) { | ||
json[`$${this.kind}`] = children.length ? children[0].toJSON() : ''; | ||
this.toJSONattributes(json); | ||
return json; | ||
return this.toJSONattributes(json); | ||
} | ||
@@ -1469,0 +1494,0 @@ |
@@ -62,3 +62,2 @@ 'use strict'; | ||
checkIfItemsOfItems(action.returns, undefined, undefined, aLoc.concat('returns')); | ||
markBindingParamPaths(action, aLoc); | ||
}); | ||
@@ -65,0 +64,0 @@ } |
@@ -389,3 +389,3 @@ 'use strict'; | ||
if (assocCsn._target.$isParamEntity) | ||
assocCsn._constraints.constraints = Object.create(null); | ||
assocCsn._constraints.constraints = {}; | ||
@@ -392,0 +392,0 @@ return assocCsn._constraints; |
@@ -1,2 +0,2 @@ | ||
// Base class for generated parser, for redepage v0.1.7 | ||
// Base class for generated parser, for redepage v0.1.12 | ||
@@ -8,3 +8,3 @@ 'use strict'; | ||
this.keywords = keywords; | ||
this.table = table; | ||
this.table = compileTable( table ); | ||
this.lexer = lexer; | ||
@@ -15,2 +15,3 @@ this.tokens = undefined; | ||
this.conditionTokenIdx = -1; | ||
this.fixKeywordTokenIdx = -1; | ||
this.conditionStackLength = -1; | ||
@@ -68,3 +69,3 @@ this.nextTokenAsId = false; | ||
this.reportUnexpectedToken_( la ); | ||
la.parsed = 0; | ||
la.parsedAs = 0; | ||
@@ -79,3 +80,4 @@ if (this.conditionTokenIdx === this.tokenIdx && | ||
} | ||
if (++this.tokenIdx > this.eofIndex) | ||
if (this.tokenIdx >= this.eofIndex) | ||
return this._stopParsing( this.stack.length ); | ||
@@ -94,3 +96,2 @@ // TODO: also sync to what comes next in current rule, at least after rule call, | ||
return this.e(); | ||
this._traceIdOrPred( '-Id' ); | ||
this.nextTokenAsId = true; | ||
@@ -112,21 +113,23 @@ return false; // do not execute action after it | ||
follow?.[0] === 'Id' && this.keywords[lk] !== false && | ||
this.conditionTokenIdx !== this.tokenIdx || | ||
follow?.includes( lk || lt )) | ||
this.fixKeywordTokenIdx !== this.tokenIdx || | ||
follow?.includes( lk || lt )) { | ||
this._tracePush( [ 'E', true ] ); | ||
return true; | ||
// Do we have possibilities to stay in rule with error recovery? | ||
const expecting = this._expecting( 0 ); // dynamic follow-set | ||
// TODO: improve performance: no check needed for a rule-end directly after | ||
// a rule end: the second is definitely successful if the first was. | ||
// TODO: do not calculate the complete dynamic follow-set, provide dedicated | ||
// function to test whether the next token is valid | ||
// we might also cache the result in the stack | ||
// ok: lk or lt -> lk=e or (lt=e && (not cond || not keyw) | ||
if (expecting[lk] || | ||
// if at failed condition, do not make Id in follow end the rule | ||
// (assuming that there is no condition for `Id` at optional rule end): | ||
expecting[lt] && !(lk && this.conditionTokenIdx === this.tokenIdx)) | ||
return true; | ||
return this.e(); | ||
} | ||
this._tracePush( [ 'E', 0 ] ); | ||
// TODO: caching | ||
const { dynamic_ } = this; | ||
let match; | ||
let depth = this.stack.length; | ||
while (match == null && --depth) { | ||
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ ); | ||
const { followState } = this.stack[depth]; | ||
match = this._pred_next( followState, lt, lk, 'E' ); | ||
this._traceSubPush( match ?? 0 ); | ||
} | ||
this.dynamic_ = dynamic_; | ||
// If the parser reaches this point with match = null, even the top-level rule | ||
// does not have a required token (typically `EOF`) at the end → the parser | ||
// must accept any token → rule exit possible (but no output '✔' in trace). | ||
return (match ?? true) || this.e(); | ||
} | ||
@@ -147,3 +150,2 @@ | ||
return this.g( state, follow ); | ||
this._traceIdOrPred( '-Id' ); | ||
this.nextTokenAsId = true; | ||
@@ -158,5 +160,4 @@ return false; // do not execute action after it | ||
// do not have to add reserved keywords from the follow-set to the `switch`. | ||
if (!lk || this.keywords[lk] === false) | ||
if (!lk || this.keywords[lk] === false) // TODO: consider fixKeywordTokenIdx ? | ||
return this.g( state, follow ); | ||
this._traceIdOrPred( '-Id' ); | ||
this.nextTokenAsId = true; | ||
@@ -171,3 +172,2 @@ return false; // do not execute action after it | ||
return this.g( state, follow ); | ||
this._traceIdOrPred( '-Id' ); | ||
this.nextTokenAsId = true; | ||
@@ -211,5 +211,5 @@ return false; // do not execute action after it | ||
c( state, parsed = 'token' ) { // consume token | ||
c( state, parsedAs = 'token' ) { // consume token | ||
const la = this.tokens[this.tokenIdx]; | ||
la.parsed = parsed; | ||
la.parsedAs = parsedAs; | ||
if (this.tokenIdx < this.eofIndex) ++this.tokenIdx; | ||
@@ -248,8 +248,12 @@ // TODO: handle identifier-including-reserved-words later (e.g. for id after a `.`) | ||
// for parser token | ||
// for parser token or token set via `/` | ||
ckA( state ) { | ||
// if it really should be considered an Id, `set this.la().parsed` yourself | ||
// if it really should be considered an Id, `set this.la().parsedAs` yourself | ||
return this.c( state, (this.l() === 'Id' ? 'keyword' : 'token') ); | ||
} | ||
skipToken_() { | ||
++this.tokenIdx; | ||
} | ||
// condition and precedence handling ------------------------------------------ | ||
@@ -264,4 +268,6 @@ | ||
if (this.conditionTokenIdx === this.tokenIdx && | ||
this.conditionStackLength === this.stack.length) | ||
this.conditionStackLength === this.stack.length) { | ||
this._tracePush( [ 'C' ] ); | ||
return true; // error recovery: ignore condition | ||
} | ||
this.conditionTokenIdx = this.tokenIdx; | ||
@@ -272,3 +278,12 @@ this.conditionStackLength = this.stack.length; | ||
if (this.constructor.tracingParser) | ||
this._tracePush( `${ fail ? '¬' : '✓' } ${ cond }` ); | ||
this._tracePush( [ 'C', cond, !fail ] ); | ||
// TODO TOOL: in this case, the default case must not have actions (tool must | ||
// add state if it does) | ||
if (fail) { // TODO: extra gcK() method instead of check below | ||
// TODO: extra method necessary for academic case | ||
// ( 'unreserved' 'foo' | <cond> Id 'bar' )` with input `unreserved bar` | ||
const { keyword } = this.la(); | ||
if (keyword && this.table[keyword]) | ||
this.fixKeywordTokenIdx = this.tokenIdx; | ||
} | ||
return !fail || this.g( state ) && false; | ||
@@ -285,3 +300,3 @@ } | ||
this.conditionStackLength === this.stack.length) { | ||
this._tracePush( `(${ this._prec })!` ); | ||
this._tracePush( [ 'C' ] ); | ||
return true; // error recovery: ignore condition | ||
@@ -299,7 +314,13 @@ } | ||
const tp = (this.prec_ == null) ? '∞' : this.prec_; | ||
const suffix = mode === 'post' && ` ≤ ${ tp }` || mode === 'none' && ` < ${ tp }`; | ||
this._tracePush( `${ fail ? '¬' : '✓' }(${ pp } < ${ prec }${ suffix || '' })` ); | ||
const suffix = mode === 'post' && `≤${ tp }` || mode === 'none' && `<${ tp }`; | ||
this._tracePush( [ 'C', `${ pp }<${ prec }${ suffix || '' }`, !fail ] ); | ||
} | ||
if (fail) | ||
if (fail) { // TODO: extra gcK() method instead of check below | ||
// TODO: extra method necessary for academic case | ||
// ( 'unreserved' 'foo' | <cond> Id 'bar' )` with input `unreserved bar` | ||
const { keyword } = this.la(); | ||
if (keyword && this.table[this.s][keyword]) | ||
this.fixKeywordTokenIdx = this.tokenIdx; | ||
return this.g( state ) && false; // TODO: reset this.prec_ ? | ||
} | ||
this.prec_ = (mode === 'right') ? prec - 1 : prec; // -1: <…,assoc=right>, <…,prefix> | ||
@@ -347,8 +368,2 @@ return true; | ||
lP( first2 ) { // only start rule if this predicate returns true | ||
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1]; | ||
// Argument first2 is just a performance hint with ckP(): | ||
if (lk2 && first2?.[0] === 'Id' && this.keywords[lk2] !== false || | ||
first2?.includes( lk2 || lt2 )) | ||
return true; | ||
// nothing to check if not a non-reserved keyword: | ||
@@ -359,22 +374,34 @@ const { keyword: lk1 } = this.tokens[this.tokenIdx]; | ||
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1]; | ||
// Argument first2 is just a performance hint with ckP(): | ||
if (lk2 && first2?.[0] === 'Id' && this.keywords[lk2] !== false || | ||
first2?.includes( lk2 || lt2 )) { | ||
this._tracePush( [ 'P', true ] ); | ||
return true; | ||
} | ||
this._tracePush( [ 'P' ] ); | ||
// now check it dynamically: | ||
let cmd = this.table[this.s][lk1]; | ||
if (typeof cmd === 'string') | ||
cmd = this.table[this.s][cmd]; | ||
if (!Array.isArray( cmd ) || cmd[2] !== 1) | ||
if (cmd[2] !== 1) | ||
throw Error( `Unexpected command '${ cmd?.[0] }' without prediction at state ${ this.s } for ‘${ lk1 }’` ); | ||
this._traceIdOrPred( '-P1' ); | ||
// if not the keyword match, the command is “goto” or “rule call” | ||
const nextState = (cmd[0] === 'ck') ? cmd[1] : this._pred_keyword( cmd[1], lk1 ); | ||
if (this._pred( nextState, lt2, lk2 )) | ||
return true; | ||
if (lt2 === 'IllegalToken') // TODO: keep? | ||
++this.tokenIdx; // for user lookahead fns and conditions | ||
const match = this._pred_next( nextState, lt2, lk2, 'P' ); | ||
--this.tokenIdx; | ||
const r = match ?? true; | ||
if (match == null) | ||
this._traceSubPush( 0 ); | ||
if (lt2 === 'IllegalToken') | ||
return true | ||
// TODO: instead of this IllegalToken test, set tokenIndex+nextState for extra | ||
// expected calculation if parser fails after Id - we would then also add the | ||
// expected tokens after keyword-interpretation | ||
this._traceIdOrPred( '-Id' ); | ||
this.nextTokenAsId = true; | ||
return false; // do not execute action after it | ||
// TODO: instead of this IllegalToken test, implement a “confirm unreserved | ||
// keyword as Id” prediction which tests whether the token after the then-Id | ||
// matches. | ||
this._traceSubPush( r ); | ||
if (!r) | ||
this.nextTokenAsId = true; | ||
return r; | ||
} | ||
@@ -385,35 +412,14 @@ | ||
// Standard weak-conflict predicate ------------------------------------------- | ||
// Weak (and fast) single-step walk and test (no rule exit, start is fine): for | ||
// pg(), pr(). The main point is that we do not (again) consider predicates. | ||
// Currently just tests against the token _type_ of the next token, not its | ||
// specific keyword; see comments below for details. | ||
_pred( nextState, lt2, lk2 ) { | ||
if (nextState) { | ||
// return this._pred_test( nextState, lt2 ); | ||
const r = this._pred_next( nextState, lt2, lk2 ); | ||
this._tracePush( this.s ); | ||
return r; | ||
} | ||
// dubious weak conflict at end of rule: | ||
this._traceIdOrPred( '-P0' ); | ||
this._tracePush( this.s ); | ||
return true; // dubious | ||
} | ||
_pred_keyword( state, keyword ) { | ||
// returns next state for first token as keyword, for lP() | ||
// returns state after matching the first token as keyword, for lP() | ||
while (state) { | ||
this._tracePush( `${ state }-P1` ); | ||
this._traceSubPush( state ); | ||
let cmd = this.table[state]; | ||
if (!Array.isArray( cmd )) { | ||
const alt = cmd[keyword] || cmd.Id; // Id to cover optimized rule call | ||
cmd = (typeof alt === 'string') | ||
? cmd[alt] | ||
: typeof alt === 'number' && [ 'g', alt ] || alt || [ 'g', cmd[''] ]; | ||
} | ||
if (!Array.isArray( cmd )) | ||
cmd = cmd[keyword] || cmd.Id || cmd['']; | ||
switch (cmd[0]) { | ||
case 'ck': case 'mk': | ||
return cmd[1]; // state after token consumption | ||
case 'g': | ||
case 'g': // TODO: another rule call? | ||
break; | ||
@@ -430,19 +436,28 @@ default: | ||
_pred_next( state, type, keyword ) { | ||
_pred_next( state, type, keyword, mode ) { | ||
let hasEnteredRule = false; | ||
while (state) { | ||
this._tracePush( `${ state }-P2` ); | ||
this._traceSubPush( state ); | ||
let cmd = this.table[state]; | ||
if (!Array.isArray( cmd )) { | ||
const alt = keyword && cmd[keyword] || cmd[type]; | ||
cmd = (typeof alt === 'string') | ||
? cmd[alt] | ||
: typeof alt === 'number' && [ 'g', alt ] || alt || [ 'default', cmd[''] ]; | ||
const lookahead = cmd[' lookahead']; | ||
cmd = lookahead | ||
? cmd[this[lookahead]( mode )] || cmd[''] | ||
: keyword && cmd[keyword] || cmd[type] || cmd['']; | ||
} | ||
switch (cmd[0]) { | ||
case 'c': case 'ck': case 'ciA': | ||
case 'c': case 'ck': case 'ciA': case 'ckA': // TODO: re-check ckA | ||
return true; | ||
case 'ci': | ||
if (!keyword || | ||
this.keywords[keyword] !== false && this.fixKeywordTokenIdx !== this.tokenIdx) | ||
return true; | ||
cmd = this.table[state]['']; // is currently always 'g' or 'e' | ||
break; | ||
case 'm': | ||
return type === cmd[2]; | ||
case 'mi': case 'ci': | ||
return type === 'Id' && (!keyword || this.keywords[keyword] !== false); | ||
case 'mi': | ||
return type === 'Id' && | ||
(!keyword || | ||
this.keywords[keyword] !== false && this.fixKeywordTokenIdx !== this.tokenIdx); | ||
case 'miA': | ||
@@ -452,2 +467,12 @@ return type === 'Id'; | ||
return keyword === cmd[2]; | ||
case 'g': case 'e': | ||
break; | ||
default: | ||
if (typeof cmd[0] !== 'number') | ||
throw Error( `Unexpected command ${ cmd[0] } at state ${ this.s }` ); | ||
// If the parser enters a rule, reaching the rule end (can happen with | ||
// option `minTokensMatched`) means "no match". | ||
hasEnteredRule = true; | ||
// If we want to support conditions before matching the first token in a | ||
// rule, we would have to handle `this.stack` and `this.dynamically_`. | ||
} | ||
@@ -457,10 +482,14 @@ // We could optimize with rule call - only 'Id' must be further investigated | ||
} | ||
this._traceIdOrPred( 'f' ); | ||
this._tracePush( this.s ); | ||
// TODO: really false, not true? | ||
// `false` means that la1 is not considered an unreserved keyword. This is | ||
// correct (consider `e: Association @Anno`), but probably not optimal for | ||
// error reporting (consider `e: Association +`). Improving that is more | ||
// costly, as we really need to consider rule exits → stack. | ||
return false; | ||
// If invalid state, the second token does not match, e.g. for `VIRTUAL +` | ||
// or `VIRTUAL §` (with IllegalToken): | ||
if (state == null) | ||
return false; | ||
// Otherwise, the parser could end the rule after having matched the keyword | ||
// with prediction. TODO: as we do not look behind the current rule for the | ||
// prediction, the tool can normally omit the prediction (and output a | ||
// message), no so with `ruleStartingWithUnreserved`. We will rather look | ||
// behind the current rule _after_ having decided that the token is to be | ||
// matched as identifier. | ||
return !hasEnteredRule && null; // let caller decide how to interpret this | ||
} | ||
@@ -470,2 +499,6 @@ | ||
// TODO: this is a slow implementation - do dedicated traversal later | ||
// It is used in giR() only and this is currently used just once. | ||
// TODO: using mode = 'R' and tracing R(…) | ||
// TODO: investigate why this was not written before adding | ||
// `<default=fallback>` in rule `fromRefWithOptAlias`. | ||
return this._expecting()[keyword]; | ||
@@ -502,12 +535,9 @@ } | ||
_exp_collect( expecting, cmd, prop ) { | ||
if (prop != null) { | ||
cmd = cmd[(typeof cmd[prop] === 'string') ? cmd[prop] : prop]; | ||
} | ||
if (typeof cmd === 'number') // ‹followState› = short form for this.g(‹followState›) | ||
cmd = [ 'g', cmd ]; | ||
if (prop != null) | ||
cmd = cmd[prop]; | ||
if (!Array.isArray( cmd )) { | ||
let reachedRuleEnd = false; | ||
for (const tok in cmd) { | ||
if (Object.hasOwn( cmd, tok ) && this._exp_collect( expecting, cmd, tok )) | ||
if (Object.hasOwn( cmd, tok ) && tok.charAt(0) !== ' ' && | ||
this._exp_collect( expecting, cmd, tok )) | ||
reachedRuleEnd = true; | ||
@@ -522,3 +552,3 @@ } | ||
case 'ckA': | ||
for (const tok of this.translateParserToken_( prop ) || [ prop ]) | ||
for (const tok of this.translateParserToken_( prop )) | ||
expecting[tok] = true; | ||
@@ -551,4 +581,4 @@ return false; | ||
translateParserToken_( _token ) { | ||
return null; | ||
translateParserToken_( token ) { | ||
return [ token ]; | ||
} | ||
@@ -559,5 +589,4 @@ | ||
_recoverInline( expecting ) { | ||
// Inline error recovery - single token deletion (TODO later: also try more !) | ||
// token position has been advanced before calling this function | ||
if (!expecting[this.lk()] && !expecting[this.l()]) | ||
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1]; | ||
if (!(lk2 && expecting[lk2] || expecting[lt2])) | ||
return false; | ||
@@ -569,4 +598,4 @@ | ||
const caller = this.stack[length]; | ||
// matched tokens (other than the one skipped one) in rule: found rule | ||
if (this.tokenIdx - 1 > caller.tokenIdx) | ||
// matched tokens in rule: found rule | ||
if (this.tokenIdx > caller.tokenIdx) | ||
break; | ||
@@ -587,2 +616,3 @@ caller.followState = null; | ||
this.skipToken_(); | ||
if (this.constructor.tracingParser) | ||
@@ -594,3 +624,2 @@ this._trace( [ this.stack[length - 1].ruleState, 'recover inside rule' ] ); | ||
_recoverPanicMode() { | ||
--this.tokenIdx | ||
const { length } = this.stack; | ||
@@ -616,3 +645,3 @@ // Panic mode: resume at token in then-expecting set: | ||
return this._error_panic( depth, length, tokenIdx ); | ||
++this.tokenIdx; | ||
this.skipToken_(); | ||
} | ||
@@ -647,3 +676,2 @@ throw Error( 'EOF was added...' ); | ||
_stopParsing( idx ) { | ||
--this.tokenIdx; | ||
if (this.constructor.tracingParser) { | ||
@@ -653,2 +681,4 @@ this.log( this.la().location.toString() + ':', 'Info:', | ||
} | ||
// TODO: run this.skipToken_() on all remaining tokens? Does ANTLR consumes | ||
// those in error recovery mode? Probably not. | ||
for (const c of this.stack) | ||
@@ -698,5 +728,5 @@ c.followState = null; | ||
} | ||
_traceIdOrPred( suffix ) { | ||
_traceSubPush( state ) { | ||
if (this.constructor.tracingParser) | ||
this.trace[this.trace.length - 1] += suffix; | ||
this.trace.at(-1).push( state ); | ||
} | ||
@@ -714,3 +744,3 @@ traceAction( location ) { // will be put into tracing parser | ||
this.log( (la || this.la()).location.toString() + ':', | ||
'Info:', msg, 'states:', this.trace.join( ' → ' ) ); | ||
'Info:', msg, 'states:', this.trace.map( traceStep ).join( ' → ' ) ); | ||
this.trace = [ this.s ?? '⚠' ]; | ||
@@ -741,5 +771,14 @@ } | ||
function traceStep( step ) { | ||
if (!Array.isArray( step )) | ||
return step; | ||
const result = { true: '✔', false: '✖' }[step.at( -1 )] ?? ''; | ||
const intro = (typeof step[1] === 'number') ? '→' : ''; | ||
const arg = step.slice( 1, result ? -1 : undefined ).join( '→' ); | ||
return `${ step[0] }(${ intro }${ arg })${ result }`; | ||
} | ||
function tokenName( type ) { | ||
if (typeof type !== 'string') | ||
type = (!type.parsed || type.parsed === 'keyword') && type.keyword || type.type; | ||
type = (!type.parsedAs || type.parsedAs === 'keyword') && type.keyword || type.type; | ||
return (/^[A-Z]+/.test( type )) ? `‹${ type }›` : `‘${ type }’`; | ||
@@ -749,3 +788,3 @@ } | ||
function tokenFullName( token, sep ) { | ||
return (token.parsed && token.parsed !== 'keyword' && token.parsed !== 'token' || | ||
return (token.parsedAs && token.parsedAs !== 'keyword' && token.parsedAs !== 'token' || | ||
token.type !== 'Id' && token.type !== token.text && token.text) | ||
@@ -756,2 +795,21 @@ ? `‘${ token.text }’${ sep }${ tokenName( token ) }` | ||
function compileTable( table ) { | ||
if (table.$compiled) | ||
return table; | ||
for (const line of table) { | ||
if (typeof line !== 'object' || Array.isArray( line )) | ||
continue; | ||
const cache = Object.create( null ); // very sparse array | ||
for (const prop of Object.keys( line )) { | ||
const alt = line[prop]; | ||
if (!Array.isArray( alt ) && prop.charAt(0) !== ' ') // string or number | ||
line[prop] = (typeof alt === 'string') ? line[alt] : (cache[alt] ??= [ 'g', alt ]); | ||
} | ||
if (!line['']) | ||
line[''] = [ 'e' ]; | ||
} | ||
table.$compiled = true; | ||
return table; | ||
} | ||
module.exports = BaseParser; |
@@ -39,3 +39,3 @@ 'use strict'; | ||
error(null, null, { name: artifactName }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Artifact $(NAME) not found, only top-level artifacts and their elements are supported for now'); | ||
@@ -42,0 +42,0 @@ return null; |
@@ -126,3 +126,3 @@ // Transform XSN (augmented CSN) into CSN | ||
elements, | ||
actions, // TODO: just normal dictionary | ||
actions: sortedDict, // TODO: just normal dictionary | ||
returns, // storing the return type of actions | ||
@@ -613,2 +613,6 @@ // special: top-level, cardinality ----------------------------------------- | ||
const val = { file: loc.file, line: loc.line, col: loc.col }; | ||
if (withLocations === 'withEndPosition' && loc.endLine) { | ||
val.endLine = loc.endLine; | ||
val.endCol = loc.endCol; | ||
} | ||
Object.defineProperty( csn, '$location', { | ||
@@ -626,16 +630,9 @@ value: val, configurable: true, writable: true, enumerable: withLocations, | ||
function sortedDict( dict ) { | ||
function sortedDict( dict, _csn, _node, prop ) { | ||
const keys = Object.keys( dict ); | ||
if (strictMode) | ||
keys.sort(); | ||
return dictionary( dict, keys ); | ||
return dictionary( dict, keys, prop ); | ||
} | ||
function actions( dict, _csn, node ) { | ||
const keys = Object.keys( dict ); | ||
if (strictMode && node.kind === 'annotate') | ||
keys.sort(); // TODO: always sort with --test-mode ? | ||
return dictionary( dict, keys, 'actions' ); | ||
} | ||
function params( dict ) { | ||
@@ -642,0 +639,0 @@ const keys = Object.keys( dict ); |
@@ -23,2 +23,3 @@ // @ts-nocheck : Issues with Tokens on `this`, e.g. `this.DOT`. | ||
const { createMessageFunctions } = require( '../base/messages' ); | ||
const { CompilerAssertion } = require( '../base/error' ); | ||
@@ -228,6 +229,30 @@ // Error listener used for ANTLR4-generated parser | ||
parser.filename = filename; // LSP compatibility | ||
parser.tokenStream = parser; // LSP compatibility: object with property `tokens` | ||
const { parseListener, attachTokens } = options; | ||
if (parseListener || attachTokens) { | ||
const combined = []; | ||
const { tokens, comments, docComments } = parser; | ||
const length = tokens.length + comments.length + docComments.length; | ||
let tokenIdx = 0; | ||
let commentIdx = 0; | ||
let docCommentIdx = 0; | ||
for (let index = 0; index < length; ++index) { | ||
if (tokens[tokenIdx].location.tokenIndex === index) // EOF has largest tokenIndex | ||
combined.push( tokens[tokenIdx++] ); | ||
else if (comments[commentIdx]?.location.tokenIndex === index) | ||
combined.push( comments[commentIdx++] ); | ||
else | ||
combined.push( docComments[docCommentIdx++] ); | ||
} | ||
if (!combined.at( -1 )) | ||
throw new CompilerAssertion( 'Invalid values for `tokenIndex`' ); | ||
for (const tok of combined) | ||
tok.start = lexer.characterPos( tok.location.line, tok.location.col ); | ||
parser._input = { tokens: combined, lexer }; // lexer for characterPos() in cdshi.js | ||
parser.getTokenStream = function getTokenStream() { | ||
return this._input; | ||
}; | ||
} | ||
// LSP feature: provide parse listener with ANTLR-like context: | ||
const { parseListener } = options; | ||
if (parseListener) { | ||
@@ -257,2 +282,10 @@ // TODO LSP: we could also call different listener methods: then LSP could | ||
}; | ||
parser.c = function c( ...args ) { // consume | ||
CdlParser.prototype.c.apply( this, args ); | ||
parseListener.visitTerminal( { symbol: this.lb() } ); | ||
}; | ||
parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery | ||
CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx` | ||
parseListener.visitErrorNode( { symbol: this.lb() } ); | ||
}; | ||
} | ||
@@ -274,4 +307,5 @@ const result = {}; | ||
const ast = result[rulespec?.returns] || (rule === 'cdl' ? new XsnSource( 'cdl' ) : {} ); | ||
if (options.attachTokens === true || options.attachTokens === filename) | ||
ast.tokenStream = parser; // with property tokens | ||
ast.options = options; | ||
if (attachTokens === true || attachTokens === filename) | ||
ast.tokenStream = parser._input; | ||
return ast; | ||
@@ -278,0 +312,0 @@ } |
@@ -340,3 +340,3 @@ // Error strategy with special handling for (non-reserved) keywords | ||
{ offending: "';'", expecting, keyword: 'with' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' ); | ||
@@ -343,0 +343,0 @@ m.expectedTokens = expecting; |
@@ -283,3 +283,3 @@ // Generic ANTLR parser class with AST-building functions | ||
this.warning( 'syntax-missing-semicolon', t, { code: ';' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' ); | ||
@@ -689,3 +689,3 @@ } | ||
{ code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' ); | ||
@@ -801,3 +801,3 @@ } | ||
this.message( 'syntax-deprecated-ident', token, { delimited: id }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' ); | ||
@@ -1055,3 +1055,3 @@ } | ||
rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN', | ||
@@ -1058,0 +1058,0 @@ } ); |
@@ -80,3 +80,3 @@ 'use strict'; | ||
if (this.str[0] !== '`' || this.str[this.str.length - 1] !== '`') | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
throw new CompilerAssertion('Invalid multi-line string sequence: Require string to be surrounded by back-ticks!'); | ||
@@ -83,0 +83,0 @@ |
@@ -158,2 +158,25 @@ // Official cds-compiler API. | ||
/** | ||
* If `true`, the CSN will have an enumerable property `$locations`. | ||
* with values for `line` and `col`, i.e. there is only a start position, | ||
* but no end position. | ||
* | ||
* If `false`, the property will be non-enumerable, i.e. it won't be | ||
* serialized when using `JSON.stringify()`. | ||
* | ||
* With value `"withEndPosition"`, the property will be enumerable and | ||
* will contain values for the end-position. Other string values | ||
* are not allowed. This value was introduced in v5.3.0. | ||
* | ||
* $location is not set on all artifacts, and it only indicates the position | ||
* of the _name_ of the artifact. | ||
*/ | ||
withLocations?: boolean|string | ||
/** | ||
* Use the new non-ANTLR based parser for compilation. | ||
* Experimental flag! | ||
* | ||
* @since v5.2.0 | ||
*/ | ||
newParser?: boolean | ||
/** | ||
* Internal option for LSP only! | ||
@@ -160,0 +183,0 @@ * If set, each AST gets a `tokenStream` property containing all lexed tokens. |
@@ -8,13 +8,20 @@ 'use strict'; | ||
const csnDictionaries = [ | ||
'args', | ||
'params', | ||
'enum', | ||
'mixin', | ||
'elements', | ||
'actions', | ||
'definitions', | ||
'vocabularies', | ||
]; | ||
const csnDictionaries = { | ||
__proto__: null, | ||
args: 1, | ||
params: 1, | ||
enum: 1, | ||
mixin: 1, | ||
elements: 1, | ||
actions: 1, | ||
definitions: 1, | ||
vocabularies: 1, | ||
}; | ||
const sortedCsnDictionaries = { | ||
__proto__: null, | ||
definitions: 1, | ||
actions: 1, | ||
}; | ||
function shallowCopy( val, _options, _sort ) { | ||
@@ -78,5 +85,5 @@ return val; | ||
} | ||
else if (csnDictionaries.includes(n) && !Array.isArray(val)) { | ||
const sortDict = n === 'definitions' && | ||
(!options || options.testMode || options.testSortCsn); | ||
else if (csnDictionaries[n] && !Array.isArray(val)) { | ||
const sortDict = (!options || options.testMode || options.testSortCsn) && | ||
sortedCsnDictionaries[n]; | ||
// Array check for property `args` which may either be a dictionary or an array. | ||
@@ -127,2 +134,4 @@ r[n] = cloneCsnDict(val, options, sort, sortDict); | ||
* used and cloneOptions are passed to sortCsn(). | ||
* @param {boolean} sortProps Whether to sort CSN properties. | ||
* @param {boolean} sortDict Whether to sort CSN dictionary entries. | ||
*/ | ||
@@ -129,0 +138,0 @@ function cloneCsnDict( csn, options, sortProps, sortDict ) { |
@@ -257,3 +257,3 @@ // Compiler options | ||
.option('-s, --service-names <list>') | ||
.option(' --fewer-localized-views') | ||
.option(' --transitive-localized-views') | ||
.help(` | ||
@@ -297,3 +297,3 @@ Usage: cdsc toOdata [options] <files...> | ||
(default) empty, all services are rendered | ||
--fewer-localized-views If set, the backends will not create localized convenience views for | ||
--transitive-localized-views If set, the backends will create localized convenience views for | ||
those views, that only have an association to a localized entity/view. | ||
@@ -343,3 +343,3 @@ `); | ||
.option(' --better-sqlite-session-variables <bool>') | ||
.option(' --fewer-localized-views') | ||
.option(' --transitive-localized-views') | ||
.option(' --with-hana-associations <bool>', { valid: [ 'true', 'false' ] }) | ||
@@ -402,3 +402,3 @@ .help(` | ||
false : Render session variables as string literals, used e.g. with sqlite3 driver | ||
--fewer-localized-views If set, the backends will not create localized convenience views for | ||
--transitive-localized-views If set, the backends will create localized convenience views for | ||
those views, that only have an association to a localized entity/view. | ||
@@ -485,3 +485,3 @@ --with-hana-associations <bool> | ||
.option(' --struct-xpr') | ||
.option(' --fewer-localized-views') | ||
.option(' --transitive-localized-views') | ||
.help(` | ||
@@ -503,4 +503,4 @@ Usage: cdsc toCsn [options] <files...> | ||
$location is an object with 'file', 'line' and 'col' properties. | ||
--fewer-localized-views If --with-locations and this option are set, the backends | ||
will not create localized convenience views for those views, | ||
--transitive-localized-views If --with-locations and this option are set, the backends | ||
will create localized convenience views for those views, | ||
that only have an association to a localized entity/view. | ||
@@ -507,0 +507,0 @@ |
@@ -40,2 +40,3 @@ 'use strict'; | ||
const PRECEDENCE_OF_IN_PREDICATE = 10; | ||
const PRECEDENCE_OF_EQUAL = 10; | ||
@@ -49,2 +50,3 @@ class AstBuildingParser extends BaseParser { | ||
this.docCommentIndex = 0; | ||
this.comments = []; | ||
@@ -170,2 +172,6 @@ this.afterBrace$ = -1; | ||
isDotForPath() { | ||
if (this.dynamic_.inSelectItem == null) | ||
return true; | ||
// false for outer select item, true for inner; TODO: it would be best to set | ||
// this.dynamic_.inSelectItem to null in filters | ||
const next = this.tokens[this.tokenIdx + 1].type; | ||
@@ -196,3 +202,3 @@ return next !== '*' && next !== '{'; | ||
*/ | ||
fileSection() { | ||
namespaceRestriction() { | ||
return ++this.topLevel$ < 1; | ||
@@ -202,2 +208,95 @@ } | ||
/** | ||
* `extend`/`annotate` is forbidden inside `extend … with definitions` and | ||
* variants | ||
*/ | ||
extensionRestriction() { | ||
// TODO: use `syntax-unexpected-extension` as message | ||
const r = this.dynamic_.inExtension; | ||
this.dynamic_.inExtension = true; | ||
return !r; | ||
} | ||
/** | ||
* `annotation` def is only allowed top-level | ||
*/ | ||
vocabularyRestriction( test ) { | ||
// TODO: use `syntax-unexpected-vocabulary` as message | ||
if (!test) | ||
this.dynamic_.inBlock = true; | ||
return !this.dynamic_.inBlock; | ||
} | ||
/** | ||
* Prepare element restrictions and check validility of final anno assignments. | ||
* TODO TOOL: `arg` for actions/conditions | ||
* | ||
* Called in rule `elementDef` at the following places: | ||
* - <prepare> after `:` (before calling `typeExpression`): | ||
* disallow `= calcExpr` and final annotation assignments | ||
* without further <prepare=calcOrDefaultRestriction> | ||
* - <prepare> in empty alternative to type expression: | ||
* allow `= calcExpr` and final annotation assignments | ||
* - <cond> before final annotation assignments: allowed? | ||
* | ||
* Called in rule `returnsSpec`: | ||
* - <prepare> after `returns`: disallow `default`. | ||
*/ | ||
elementRestriction( test ) { | ||
if (!test) { // after `:` for typeExpression, or without type | ||
const withoutType = this.lb().type !== ':'; | ||
const afterReturns = this.lb().keyword === 'returns'; | ||
this.dynamic_.elementCtx = [ withoutType, withoutType, afterReturns ]; | ||
} | ||
// or before final annotation assignments | ||
return this.dynamic_.elementCtx?.[1]; | ||
} | ||
/** | ||
* Prepare `= calcExpr` restriction and check whether it can be used. | ||
* | ||
* Called at the following places: | ||
* - <prepare> before (optionally) calling rule `nullabilityAndDefault`, | ||
* except for managed associations/compositions: | ||
* allow `= calcExpr`, allow final annotation assignments if not after `}` | ||
* (TODO: should we allow `String @anno:{ … } not null @MoreAnnos`?) | ||
* - <cond> before `default`: disallow calc expr (+ restrict default expr), | ||
* allowed? (not for `returns`) | ||
* - <cond> before `=` for calc expressions: allowed? | ||
* | ||
* To have any effect, <prepare=elementRestriction> must have been called. | ||
*/ | ||
calcOrDefaultRestriction( test, arg ) { | ||
const { elementCtx } = this.dynamic_; | ||
if (!test) { // at beginning of rule `nullabilityAndDefault` | ||
if (!elementCtx) | ||
return true; | ||
elementCtx[0] = !arg; // using `= expr` is ok (except for assoc) | ||
elementCtx[1] = this.lb().type !== '}'; // allow final annos not after block | ||
} | ||
else if (this.l() === '=') { // <cond> before `= calcExpression` | ||
return elementCtx?.[0]; | ||
} | ||
else { // <cond> before `default` | ||
if (elementCtx) | ||
elementCtx[0] = false; | ||
this.prec_ = PRECEDENCE_OF_EQUAL; // only expressions for DEFAULT expr | ||
} | ||
return !elementCtx?.[2]; // default allowed? | ||
} | ||
inExpandInline() { // not as <cond> | ||
this.dynamic_.inSelectItem = 'nested'; | ||
} | ||
/** | ||
* `virtual` and `key` cannot be used inside expand/inline | ||
* (also inside sub queries in those, which will be rejected later anyway) | ||
*/ | ||
notInExpandInline( test ) { | ||
if (!test) | ||
this.dynamic_.inSelectItem = true; | ||
return this.dynamic_.inSelectItem !== 'nested'; | ||
} | ||
/** | ||
* `;` between statements is optional only after a `}` (ex braces of structure | ||
@@ -213,25 +312,27 @@ * values for annotations). | ||
/** | ||
* Annotation assignments at the end of (element) refs are allowed. | ||
* For annotations at the beginning of columns outside parentheses | ||
*/ | ||
allowFinalAnnoAssign() { | ||
// TODO: do properly with type expression | ||
return this.afterBrace$ !== this.tokenIdx; | ||
annoInSameLine( test ) { | ||
if (!test) | ||
this.dynamic_.safeAnno = true; | ||
return this.dynamic_.safeAnno || | ||
this.lb().location.line === this.la().location.line; | ||
} | ||
// TOOL Runtime TODO: provide proto-linked dynamicContext | ||
inSameLine() { | ||
return this.lb().location.line === this.la().location.line; | ||
} | ||
/** | ||
* `...` can appear in the top-level array value only. | ||
* `...` can appear in the top-level array value only and not after `...` | ||
* without `up to`. | ||
*/ | ||
annoTopValue( test ) { | ||
if (test) | ||
return !this.stack.at( -1 ).$annoTopValue; | ||
if (this.stack.at( -2 )?.$annoTopValue) | ||
this.stack.at( -1 ).$annoTopValue = 'inner'; | ||
else if (this.lb().type === '[') | ||
this.stack.at( -1 ).$annoTopValue = 'array'; | ||
return null; | ||
ellipsisRestriction( test ) { | ||
if (!test) { | ||
this.dynamic_.arrayAnno = [ !this.dynamic_.arrayAnno ]; | ||
} | ||
else { // on '...' | ||
const { arrayAnno } = this.dynamic_; | ||
if (!arrayAnno[0]) | ||
return false; | ||
if (this.tokens[this.tokenIdx + 1]?.type === ',') | ||
arrayAnno[0] = false; | ||
} | ||
return true; | ||
} | ||
@@ -264,3 +365,3 @@ | ||
{ code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' ); | ||
@@ -274,3 +375,3 @@ } | ||
this.warning( 'syntax-missing-semicolon', next, { code: ';' }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' ); | ||
@@ -414,3 +515,3 @@ } | ||
this.message( 'syntax-deprecated-ident', location, { delimited: id }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' ); | ||
@@ -454,7 +555,7 @@ } | ||
const tokenIndex = ref?.path[ref.path.length - 1]?.location.tokenIndex; | ||
const token = this.tokens[tokenIndex ?? this.tokenIdx - 1]; | ||
const { parsed } = token; | ||
if (parsed && parsed !== 'token' && parsed !== 'keyword') { | ||
token.parsed = category; | ||
return { token, parsed }; | ||
const token = this.prevTokenWithIndex( tokenIndex ) ?? this.tokens[this.tokenIdx - 1]; | ||
const { parsedAs } = token; | ||
if (parsedAs && parsedAs !== 'token' && parsedAs !== 'keyword') { | ||
token.parsedAs = category; | ||
return { token, parsedAs }; | ||
} | ||
@@ -613,3 +714,3 @@ } | ||
rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN', | ||
@@ -653,7 +754,6 @@ } ); | ||
if (art.doc && this.options.docComment !== false) { | ||
this.docComments[art.doc.location.tokenIndex].parsed = ''; | ||
this.warning( 'syntax-duplicate-doc-comment', art.doc, {}, | ||
'Doc comment is overwritten by another one below' ); | ||
} | ||
token.parsed = 'doc'; | ||
token.parsedAs = 'doc'; | ||
const val = !this.options.docComment || parseDocComment( token.text ); | ||
@@ -773,6 +873,22 @@ art.doc = { val, location: token.location }; | ||
const { file, line, col } = start; | ||
// eslint-disable-next-line object-curly-newline | ||
return { file, line, col, endLine: end.endLine, endCol: end.endCol }; | ||
return { | ||
file, line, col, endLine: end.endLine, endCol: end.endCol, | ||
}; | ||
} | ||
// `tokenIndex` is index in “combined” token array (parsing-relevant, doc | ||
// comments, comments) → cannot be used directly | ||
prevTokenWithIndex( tokenIndex ) { | ||
if (tokenIndex != null) { | ||
let { tokenIdx } = this; | ||
while (--tokenIdx >= 0) { | ||
const token = this.tokens[tokenIdx]; | ||
if (token.location.tokenIndex === tokenIndex) | ||
return token; | ||
} | ||
} | ||
return null; | ||
} | ||
// TODO: rename to `valAst` | ||
@@ -847,4 +963,6 @@ valueWithLocation( val = undefined, token = this.lb() ) { | ||
if (location.tokenIndex != null) | ||
this.tokens[location.tokenIndex].parsed = 'func'; | ||
const funcToken = this.prevTokenWithIndex( location.tokenIndex ); | ||
// TODO: we could have an opt(?) parameter funcToken for speed-up (passing this.lr()) | ||
if (funcToken) | ||
funcToken.parsedAs = 'func'; | ||
// TODO: XSN representation of functions is a bit strange - rework | ||
@@ -893,3 +1011,3 @@ const op = { location, val: 'call' }; | ||
} | ||
this.tokens[method.location.tokenIndex].parsed = 'func'; | ||
// this.prevTokenWithIndex( method.location.tokenIndex ).parsedAs = 'func'; | ||
const func = { | ||
@@ -924,3 +1042,3 @@ op: { location: method.location, val: 'call' }, | ||
} | ||
this.error( 'syntax-unexpected-assoc', this.getCurrentToken(), {}, | ||
this.error( 'syntax-unexpected-assoc', this.la(), {}, | ||
'Unexpected association definition in select item' ); | ||
@@ -1139,3 +1257,3 @@ } | ||
if (typeof type !== 'string') | ||
type = (!type.parsed || type.parsed === 'keyword') && type.keyword || type.type; | ||
type = (!type.parsedAs || type.parsedAs === 'keyword') && type.keyword || type.type; | ||
if (/^[A-Z]+/.test( type ))// eslint-disable-next-line no-nested-ternary | ||
@@ -1142,0 +1260,0 @@ return (type === 'Id') ? 'Identifier' : (type === 'EOF') ? '<EOF>' : type; |
@@ -45,5 +45,5 @@ // Lexer for CDL grammar | ||
location; | ||
parsed; | ||
parsedAs; | ||
get isIdentifier() { // compatibility method | ||
return this.parsed !== 'keyword' && this.parsed !== 'token' && this.parsed; | ||
return this.parsedAs !== 'keyword' && this.parsedAs !== 'token' && this.parsedAs; | ||
} | ||
@@ -96,3 +96,3 @@ get tokenIndex() { | ||
// remark: end positions of multi-line tokens must be set by function | ||
tokenIndex: parser.tokens.length, | ||
tokenIndex: parser.tokens.length + parser.docComments.length + parser.comments.length, | ||
}; | ||
@@ -108,3 +108,3 @@ let keyword; | ||
location: this.location, | ||
parsed: undefined, | ||
parsedAs: undefined, | ||
} ); | ||
@@ -122,3 +122,3 @@ } | ||
endCol, | ||
tokenIndex: parser.tokens.length, | ||
tokenIndex: parser.tokens.length + parser.docComments.length + parser.comments.length, | ||
}; | ||
@@ -131,3 +131,3 @@ parser.tokens.push( { | ||
location, | ||
parsed: undefined, | ||
parsedAs: undefined, | ||
} ); | ||
@@ -149,3 +149,2 @@ } | ||
rulesRegexp.lastIndex + 2 < re.lastIndex) { // not just `/**/` | ||
lexer.location.tokenIndex = parser.docComments.length; | ||
parser.docComments.push( { | ||
@@ -157,6 +156,17 @@ __proto__: Token.prototype, | ||
location: lexer.location, | ||
parsed: undefined, | ||
parsedAs: undefined, | ||
} ); | ||
adaptEndLocation( lexer, re.lastIndex ); // also works after push ? | ||
} | ||
else { // TODO: only attach with option `attachTokens` ? | ||
parser.comments.push( { | ||
__proto__: Token.prototype, | ||
type: 'Comment', | ||
text: lexer.input.substring( beg, re.lastIndex ), | ||
keyword: false, | ||
location: lexer.location, | ||
parsedAs: undefined, | ||
} ); | ||
adaptEndLocation( lexer, re.lastIndex ); // also works after push ? | ||
} | ||
rulesRegexp.lastIndex = re.lastIndex || lexer.input.length; | ||
@@ -197,3 +207,3 @@ return []; | ||
keyword = 0; | ||
// TODO: set parsed to 0 → no further error if string is not expected? | ||
// TODO: set parsedAs to 0 → no further error if string is not expected? | ||
prefix = null; // no combination with date/time/… | ||
@@ -232,3 +242,3 @@ } | ||
keyword = 0; | ||
// TODO: set parsed to 0 → no further error if string is not expected? | ||
// TODO: set parsedAs to 0 → no further error if string is not expected? | ||
} | ||
@@ -235,0 +245,0 @@ adaptEndLocation( lexer, (rulesRegexp.lastIndex = lastIndex || lexer.input.length) ); |
@@ -81,3 +81,3 @@ // Add tenant field to entities, check validity | ||
{ anno: annoTenantIndep, value: independent }, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Can\'t add $(ANNO) with value $(VALUE) to a non-entity, which is always tenant-independent' ); | ||
@@ -123,5 +123,5 @@ } | ||
error( 'tenant-invalid-include', msgLocations( csnPath ), { names }, { | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
std: 'Can\'t include the tenant-dependent entities $(NAMES) into a tenant-independent definition', | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
one: 'Can\'t include the tenant-dependent entity $(NAMES) into a tenant-independent definition', | ||
@@ -211,3 +211,3 @@ } ); | ||
// TODO: better the final entity name of assoc navigation in FROM | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'Expecting the query source $(ART) to be tenant-dependent for a tenant-dependent query entity' ); | ||
@@ -214,0 +214,0 @@ } |
@@ -820,7 +820,8 @@ 'use strict'; | ||
// Check type parameters (length, precision, scale ...) | ||
if (!member.$ignore && member.type) | ||
_check(member, memberName, csn, path); | ||
if (!member.$ignore && member.items && member.items.type) | ||
_check(member.items, memberName, csn, path.concat([ 'items' ])); | ||
if (!member.$ignore) { | ||
if (member.type) | ||
_check(member, memberName, csn, path); | ||
if (member.items?.type) | ||
_check(member.items, memberName, csn, path.concat([ 'items' ])); | ||
} | ||
}, [ 'definitions', artifactName ]); | ||
@@ -830,3 +831,3 @@ | ||
function _check(node, nodeName, model, path) { | ||
if (node.type) { | ||
if (node.type && !node.virtual) { | ||
const absolute = node.type; | ||
@@ -833,0 +834,0 @@ switch (absolute) { |
@@ -582,3 +582,3 @@ // Custom resolve functionality for the CDS compiler | ||
warning('file-unexpected-case-mismatch', [ using.location, using ], {}, | ||
// eslint-disable-next-line max-len | ||
// eslint-disable-next-line @stylistic/js/max-len | ||
'The imported filename differs on the filesystem; ensure that capitalization matches the actual file\'s name'); | ||
@@ -585,0 +585,0 @@ } |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "5.2.0", | ||
"version": "5.3.0", | ||
"description": "CDS (Core Data Services) compiler and backends", | ||
@@ -5,0 +5,0 @@ "homepage": "https://cap.cloud.sap/", |
@@ -56,6 +56,9 @@ # redirected-to-complex | ||
Ensure that the redirected association points to an entity that is a reasonable | ||
redirection target. That means, the redirection target shouldn't accidentally | ||
make it a to-many association. | ||
First, ensure that the redirected association points to an entity that is | ||
a reasonable redirection target. That means, the redirection target shouldn't | ||
accidentally make it a to-many association. | ||
Then add an explicit ON-condition or explicit foreign keys to the redirected | ||
association. That will silence the compiler message. | ||
## Related Messages | ||
@@ -62,0 +65,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5344827
109900