@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
507 lines (447 loc) • 13.9 kB
JavaScript
;
// API for `@sap/cds-lsp`.
//
// THIS FILE IS CONSIDERED INTERNAL!
// We do not guarantee stability for any project besides the CAP LSP server.
//
// This files includes an iterator over "semantic tokens" in an XSN model.
// "Semantic tokens" are identifiers, but also the "return" parameter.
// See <../../internalDoc/lsp/IdentifierCrawling.md> for details.
const { CompilerAssertion } = require('../base/error');
const $inferred = Symbol.for( 'cds.$inferred' );
// TODO: Remove hints; they should not be necessary in the best case
const HINTS = {
USING_ALIAS: 'using-alias',
DEFINITION_NAME: 'definition',
NAMESPACE_STATEMENT: 'namespace-statement',
};
// eslint-disable-next-line no-unused-vars
class LspSemanticTokenEvent {
event; // 'reference' | 'definition',
semanticToken;
node;
hint; // TODO: Remove
}
/**
* All actions to report semantic tokens in a model.
*/
const artifactActions = {
__proto__: null,
// e.g. sources or services
artifacts: dictOf( artifactTokens ),
extensions: arrayOf( extensionTokens ),
namespace: namespaceTokens,
// e.g. via CSN input
vocabularies: dictOf( artifactTokens ),
definitions: dictOf( artifactTokens ),
extern: artifactTokens,
name: definitionNameTokens,
path: pathReferenceTokens,
type: artifactTokens,
target: artifactTokens,
targetAspect: artifactTokens,
targetElement: artifactTokens,
returns: returnsTokens,
items: artifactTokens,
elements: elementsTokens,
enum: dictOf( artifactTokens ),
foreignKeys: dictOf( artifactTokens ),
actions: dictOf( artifactTokens ),
params: dictOf( artifactTokens ),
mixin: dictOf( artifactTokens ),
excludingDict: dictOf( nameAsReference ),
// Don't crawl `$tableAliases`, as they are set multiple times in queries
// via different `$tableAliases`.
// $tableAliases: null,
// NOT $queries, as that doesn't cover UNIONs (e.g. `orderBy` vs `$orderBy`)
query: artifactTokens,
from: artifactTokens,
includes: arrayOf( artifactTokens ),
columns: arrayOf( artifactTokens ),
expand: arrayOf( artifactTokens ),
inline: arrayOf( artifactTokens ),
args: argsTokens,
on: artifactTokens,
default: artifactTokens,
value: artifactTokens,
sym: enumSymToken,
where: artifactTokens,
groupBy: artifactTokens,
orderBy: artifactTokens,
having: artifactTokens,
suffix: artifactTokens,
limit: artifactTokens,
rows: artifactTokens,
offset: artifactTokens,
'@': annotationTokens,
};
/** Returns a generator that applies the given function on all entries and yields the result. */
function dictOf( func ) {
return function* dictionary( dict ) {
for (const [ item ] of iterateGeneric({ dict }, 'dict'))
yield* func( item );
};
}
/** Returns a generator that applies the given function on all entries and yields the result. */
function arrayOf( func ) {
return function* array( arr ) {
if (!Array.isArray(arr))
return;
for (const item of arr)
yield* func( item );
};
}
/** Generator equivalent of iterateGeneric of forEachGeneric() */
function* iterateGeneric( obj, prop ) {
const dict = obj[prop];
if (!dict)
return;
for (const name in dict) {
obj = dict[name];
if (Array.isArray( obj )) {
for (const item of obj)
yield [ item, name, prop ]; // parser or source duplicates (e.g. USING vs definition)
}
else {
yield [ obj, name, prop ];
if (Array.isArray( obj.$duplicates )) { // redefinitions
for (const dup of obj.$duplicates)
yield [ dup, name, prop ];
}
}
}
}
/**
* A generator that yields all semantic tokens in an XSN model.
* Semantic tokens include identifiers (references/definitions) and the "returns" parameter.
*
* @param {XSN.Model} xsn
* @param {CSN.Options} options
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* traverseSemanticTokens( xsn, options ) {
if (!xsn)
throw new CompilerAssertion('Expected valid XSN model for traverseSemanticTokens(…)');
if (!options)
throw new CompilerAssertion('Expected valid options for traverseSemanticTokens(…)');
if (xsn.sources)
yield* dictOf( artifactTokens )( xsn.sources );
}
/**
* Report semantic tokens in artifacts, including definitions, elements, params, etc.
*
* @param {XSN.Artifact} art
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* artifactTokens( art ) {
if (!art || art.builtin || art.$inferred)
return null;
if (Array.isArray( art )) {
for (const entry of art)
yield* artifactTokens( entry );
return null;
}
for (const prop in art) {
if (artifactActions[prop])
yield* artifactActions[prop](art[prop], art);
else if (prop.charAt(0) === '@')
yield* artifactActions['@'](art[prop]);
}
return null;
}
/**
* For an extension, yield all semantic tokens.
* We don't use `artifactTokens` for it, because extensions are a special case:
* - they have a name, but actually refer to some other artifact.
* - their artifacts such as elements may overlap with existing definitions, because
* extensions are applied; if they were applied, `_parent` does not point to the
* extension, which means we can't use it to skip them in `artifactTokens`.
* - we only need to handle `annotate` and `extend` kinds specifically:
* if an extension was not applied, pass it to `artifactTokens`;
* if an extension was applied, we only need to report its name (i.e. reference)
* and traverse over all artifacts
*
* @param {XSN.Extension} ext
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* extensionTokens( ext ) {
if (ext.kind !== 'extend' && ext.kind !== 'annotate')
return null;
const wasApplied = ext.name._artifact && !ext.name._artifact.$inferred;
if (!wasApplied) {
yield* artifactTokens( ext );
return null;
}
yield* nameAsReference( ext );
// We need to traverse all dictionaries that could themselves contain
// extensions. Enum extensions or columns don't need to be traversed,
// for example, because there can't be inner extensions.
yield* dictOf( extensionTokens )( ext.params );
yield* dictOf( extensionTokens )( ext.actions );
yield* dictOf( extensionTokens )( ext.elements );
if (ext.returns)
yield* extensionTokens( ext.returns );
// Artifact extensions are always definitions, and can't have nested `extend`s,
// hence no need to traverse them with `extensionTokens`.
yield* dictOf( artifactTokens )( ext.artifacts );
return null;
}
/**
* Report all semantic tokens in an annotation assignment.
*
* @param {XSN.Artifact} anno
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* annotationTokens( anno ) {
// TODO: Also report annotation names
if (anno.kind === '$annotation')
yield* annotationValueTokens( anno );
}
function* argsTokens( args, art ) {
if (Array.isArray(args)) {
// e.g. unnamed function arguments
yield* arrayOf( artifactTokens )( args );
}
else {
// e.g. named arguments
for (const [ param ] of iterateGeneric( art, 'args' )) {
yield* nameAsReference( param );
yield* artifactTokens( param );
}
}
}
function* enumSymToken( sym, expr ) {
yield {
event: 'reference',
semanticToken: expr.sym,
node: expr,
hint: undefined,
};
}
/**
* A namespace is always considered a reference and not a definition.
*
* @param {XSN.Artifact} def
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* namespaceTokens( def ) {
if (!def.name)
return null;
for (let i = 0; i < def.name.path.length; ++i) {
yield {
event: 'reference',
semanticToken: def.name.path[i],
node: def,
hint: (i === def.name.path.length - 1) ? HINTS.NAMESPACE_STATEMENT : null,
};
}
return null;
}
/**
* An annotation value may contain expressions which we need to report.
*
* @param {object} anno
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* annotationValueTokens( anno ) {
if (Array.isArray(anno)) {
for (const entry of anno)
yield* annotationValueTokens( entry );
}
else if (anno.$tokenTexts) {
yield* artifactTokens( anno );
}
else if (Array.isArray(anno.val)) {
yield* annotationValueTokens( anno.val );
}
else if (anno.struct) {
for (const [ struct ] of iterateGeneric( anno, 'struct' ))
yield* annotationValueTokens( struct );
}
}
/**
* A `returns` structure may contain sub-elements. But we report the `returns`
* token as well, as it is considered a token with semantic value.
*
* @param {XSN.Artifact} art
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* returnsTokens( art ) {
if (art.kind === 'param') {
// report the `returns` parameter
yield {
event: 'definition',
semanticToken: art.name,
node: art,
hint: undefined,
};
yield* artifactTokens( art );
}
}
/**
* Report elements if they should be traversed. They are not always traversed
* to avoid duplication due to `expand` and `columns` also being traversed.
*
* @param {Record<string, XSN.Artifact>} elements
* @param {XSN.Artifact} art
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* elementsTokens( elements, art ) {
if (shouldTraverseElements( art ))
yield* dictOf( artifactTokens )( elements );
}
/**
* Report all references in `ref`.
*
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* pathReferenceTokens( path, ref, user = ref, hint = null ) {
if (!path)
return null;
// don't report cds.Association/cds.Composition
// TODO: Or report the `Association` keyword, similar to `returns`?
if (path.length === 1 && ref._artifact?.category === 'relation')
return null;
yield* artifactTokens( path );
// parser prepends a fake `type of` segment, which we need to skip
const root = ref.scope === 'typeOf' ? 1 : 0;
for (let i = root; i < path.length; ++i) {
if (!path[i].$inferred) { // e.g. `id` when expanded from `$user`
yield {
event: 'reference',
semanticToken: path[i],
node: user,
hint,
};
}
}
return null;
}
/**
* Some XSN nodes such as entries in `excludingDict` or named arguments are references
* but don't have a `path` property, only a `name` property. Report such names
* as references.
*
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* nameAsReference( ref, hint = null ) {
if (!ref.name || ref.name.$inferred)
return null;
if (ref.name.path) {
yield* pathReferenceTokens( ref.name.path, ref.name, ref, hint );
}
else {
yield {
event: 'reference',
semanticToken: ref.name,
node: ref,
hint,
};
}
return null;
}
/**
* Traverse the name of a definition, and report N-1 path steps as references
* and of course the definition itself.
*
* @returns {Generator<LspSemanticTokenEvent>}
*/
function* definitionNameTokens( name, art ) {
if (!art.kind)
return null; // e.g. parameter references
if (!name)
return null; // e.g. column that couldn't be populated
if (art.kind === '$annotation')
return null; // annotation name, e.g. in `@anno: (elem)`
if ((name.$inferred && name.$inferred !== 'as') ||
art.kind === 'select' || art.kind === '$join') {
// Internal names such as numbers for SELECTs or the `$internal` names must
// not be reported.
return null;
}
if (art.kind === 'extend' || art.kind === 'annotate') {
yield* nameAsReference( art );
return null;
}
// Report references in a name (N-1 path steps).
for (let i = 0; i < name.path?.length - 1; ++i) {
yield {
event: 'reference',
semanticToken: name.path[i],
node: art,
hint: HINTS.DEFINITION_NAME,
};
}
const hint = art.kind === 'using' ? HINTS.USING_ALIAS : null;
if (name.path) {
// Only take the last path step; all others are considered references.
const implicitName = name.path[name.path.length - 1];
yield {
event: 'definition',
semanticToken: implicitName,
node: art,
hint,
};
}
else if (name.id) {
// Not all names have a path; some (e.g. parameters) only have an ID.
yield {
event: 'definition',
semanticToken: name,
node: art,
hint,
};
}
return null;
}
/**
* Returns true if `elements` of the given `art` should be traversed.
* Elements are _not_ traversed, e.g. for `expand`, to avoid duplicates.
*
* @returns {boolean}
*/
function shouldTraverseElements( art ) {
return (
// $expand: 'origin' -> normal expansion
// $expand: 'annotate' -> additional annotation (needs to traverse annotation expressions)
art.$expand !== 'origin' && !art.elements[$inferred] && (
// sub-elements are always traversed except for `expand`, which is handled on its own.
art.kind === 'element' && !art.expand ||
// all non-query elements are traversed; because `_main` on bound actions may point
// to a query, we handle parameters explicitly.
art.kind === 'param' || !(art._main || art).$queries
)
);
}
/**
* Given a LspSemanticTokenEvent, returns a generator that yields the referenced
* object and its origin's until the deepest entry is found.
*
* @param obj
* @returns {Generator<*, void, *>}
*/
function* getSemanticTokenOrigin( obj ) {
let ref = obj.semanticToken;
if (obj.event === 'definition') {
ref = obj.node;
}
else {
if (!ref?._artifact)
return; // unknown -> abort
// take first artifact for duplicates (best effort)
ref = Array.isArray(ref._artifact) ? ref._artifact[0] : ref._artifact;
yield ref;
}
if (!ref._effectiveType)
return; // abort for unresolved references and cyclic ones
while (ref._origin) {
yield ref._origin;
ref = ref._origin;
if (!ref || typeof ref === 'string')
break;
}
}
module.exports = {
traverseSemanticTokens,
getSemanticTokenOrigin,
};