@cap-js/cds-typer 0.2.5-beta.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/compile.js CHANGED
@@ -4,865 +4,9 @@ const fs = require('fs')
4
4
  const { normalize } = require('path')
5
5
  const cds = require('@sap/cds')
6
6
  const util = require('./util')
7
-
8
- const { SourceFile, Path, Library, writeout, baseDefinitions, Buffer } = require('./file')
9
- const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
7
+ const { writeout } = require('./file')
10
8
  const { Logger } = require('./logging')
11
-
12
-
13
-
14
- /** @typedef {import('./file').Buffer} Buffer */
15
- /** @typedef {import('./file').File} File */
16
-
17
- /**
18
- * @typedef {{
19
- * cardinality?: { max?: '*' | number }
20
- * }} EntityCSN
21
- */
22
-
23
- /**
24
- * @typedef {{
25
- * definitions?: Object<string, EntityCSN>
26
- * }} CSN
27
- */
28
-
29
- /**
30
- * @typedef { {
31
- * rootDirectory: string,
32
- * logLevel: number,
33
- * jsConfigPath?: string
34
- * }} CompileParameters
35
- */
36
-
37
- /**
38
- * `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable)
39
- * `inlineDeclarations = 'structured'` -> @see inline.StructuredInlineDeclarationResolver
40
- * `inlineDeclarations = 'flat'` -> @see inline.FlatInlineDeclarationResolver
41
- * @typedef { {
42
- * propertiesOptional: boolean,
43
- * inlineDeclarations: 'flat' | 'structured',
44
- * }} VisitorOptions
45
- */
46
-
47
- /**
48
- * @typedef {{
49
- * typeName: string,
50
- * singular: string,
51
- * plural: string
52
- * }} Inflection
53
- */
54
-
55
- /**
56
- * When nested inline types require additional imports. E.g.:
57
- * ```cds
58
- * // mymodel.cds
59
- * Foo {
60
- * bar: {
61
- * baz: a.b.c.Baz // need to require a.b.c in mymodel.cds!
62
- * }
63
- * }
64
- * ```
65
- * @typedef {{
66
- * isBuiltin: boolean,
67
- * isInlineDeclaration: boolean,
68
- * isForeignKeyReference: boolean,
69
- * type: string,
70
- * path?: Path,
71
- * csn?: CSN,
72
- * imports: Path[]
73
- * inner: TypeResolveInfo
74
- * }} TypeResolveInfo
75
- */
76
-
77
- /**
78
- * @typedef {{
79
- * entity: String
80
- * }} Context
81
- */
82
-
83
- /**
84
- * Builtin types defined by CDS.
85
- */
86
- const Builtins = {
87
- UUID: 'string',
88
- String: 'string',
89
- Binary: 'string',
90
- LargeString: 'string',
91
- LargeBinary: 'string',
92
- Integer: 'number',
93
- UInt8: 'number',
94
- Int16: 'number',
95
- Int32: 'number',
96
- Int64: 'number',
97
- Integer64: 'number',
98
- Decimal: 'number',
99
- DecimalFloat: 'number',
100
- Float: 'number',
101
- Double: 'number',
102
- Boolean: 'boolean',
103
- Date: 'Date',
104
- DateTime: 'Date',
105
- Time: 'Date',
106
- Timestamp: 'Date',
107
- //
108
- Composition: 'Array',
109
- Association: 'Array'
110
- }
111
-
112
- const defaults = {
113
- // FIXME: add defaults for remaining parameters
114
- propertiesOptional: true,
115
- inlineDeclarations: 'flat'
116
- }
117
-
118
-
119
- class Visitor {
120
- /**
121
- * Generates singular and plural inflection for the passed type.
122
- * Several cases are covered here:
123
- * - explicit annotation by the user in the CSN
124
- * - implicitly derived inflection based on simple grammar rules
125
- * - collisions between singular and plural name (resolved by appending a '_' suffix)
126
- * - inline type definitions, which don't really have a linguistic plural,
127
- * but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
128
- * @param {TypeResolveInfo} typeInfo information about the type gathered so far.
129
- * @param {string} [namespace] namespace the type occurs in. If passed, will be shaved off from the name
130
- * @returns {Inflection}
131
- */
132
- inflect (typeInfo, namespace) {
133
- let typeName
134
- let singular
135
- let plural
136
-
137
- if (typeInfo.isInlineDeclaration) {
138
- // if we detected an inline declaration, we take a quick detour via an InlineDeclarationResolver
139
- // to rectify the typeName (which would be just '{' elsewise).
140
- // The correct typename in string form is required in stringifyLambda(...)
141
- // Note that whenever the typeName is relevant, it is assumed to be in structured form
142
- // (i.e. a struct), so we always use a StructuredInlineDeclarationResolver here, regardless of
143
- // what is configured for nested declarations in the visitor.
144
- // FIXME: in most other places where we have an inline declaration, we actually don't need the typeName.
145
- // If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
146
- // piece of code instead to reduce overhead.
147
- const into = new Buffer()
148
- new StructuredInlineDeclarationResolver(this).printInlineType(undefined, {typeInfo}, into, '')
149
- typeName = into.join(' ')
150
- singular = typeName
151
- plural = `Array<${typeName}>`
152
- } else {
153
- // TODO: make sure the resolution still works. Currently, we only cut off the namespace!
154
- singular = util.singular4(typeInfo.csn)
155
- plural = util.getPluralAnnotation(typeInfo.csn)
156
- ? util.plural4(typeInfo.csn)
157
- : typeInfo.plainName
158
-
159
- // don't slice off namespace if it isn't part of the inflected name.
160
- // This happens when the user adds an annotation and singular4 therefore
161
- // already returns an identifier without namespace
162
- if (namespace && singular.startsWith(namespace)) {
163
- // TODO: not totally sure why plural doesn't have to be sliced
164
- singular = singular.slice(namespace.length + 1)
165
- }
166
-
167
- if (singular === plural) {
168
- // same as when creating the entity
169
- plural += '_'
170
- }
171
- }
172
- if (!singular || !plural) {
173
- this.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
174
- }
175
-
176
- return { typeName, singular, plural }
177
- }
178
-
179
- /**
180
- * Gathers all files that are supposed to be written to
181
- * the type directory. Including generated source files,
182
- * as well as library files.
183
- * @returns {File[]} a full list of files to be written
184
- */
185
- getWriteoutFiles() {
186
- return Object.values(this.files).concat(this.libraries.filter(lib => lib.referenced))
187
- }
188
-
189
- /**
190
- * @param csn root CSN
191
- * @param {VisitorOptions} options
192
- */
193
- constructor(csn, options = {}, logger = new Logger()) {
194
- this.options = { ...defaults, ...options }
195
- this.logger = logger
196
- this.csn = csn
197
-
198
- /** @type {Context[]} **/
199
- this.contexts = []
200
-
201
- /** @type {Library[]} */
202
- this.libraries = [new Library(require.resolve('../library/cds.hana.ts'))]
203
-
204
- /** @type {Object<string, File>} */
205
- this.files = {}
206
- this.files[baseDefinitions.path.asIdentifier()] = baseDefinitions
207
- this.inlineDeclarationResolver = this.options.inlineDeclarations === 'structured'
208
- ? new StructuredInlineDeclarationResolver(this)
209
- : new FlatInlineDeclarationResolver(this)
210
-
211
- // Entities inherit their ancestors annotations:
212
- // https://cap.cloud.sap/docs/cds/cdl#annotation-propagation
213
- // This is a problem if we annotate @singular/ @plural to an entity A,
214
- // as we don't want all descendents B, C, ... to share the ancestor's
215
- // annotated inflexion
216
- // -> remove all such annotations that appear in a parent as well.
217
- // BUT: we can't just delete the attributes. Imagine three classes
218
- // A <- B <- C
219
- // where A contains a @singular annotation.
220
- // If we erase the annotation from B, C will still contain it and
221
- // can not detect that its own annotation was inherited without
222
- // travelling up the entire inheritance chain up to A.
223
- // So instead, we monkey patch and maintain a dictionary "erased"
224
- // when removing an annotation which we also check.
225
- const erase = (entity, parent, attr) => {
226
- if (attr in entity) {
227
- const ea = entity[attr]
228
- if (parent[attr] === ea || (parent.erased && parent.erased[attr] === ea)) {
229
- if (!('erased' in entity)) {
230
- entity.erased = {}
231
- }
232
- entity.erased[attr] = ea
233
- delete entity[attr]
234
- this.logger.info(`Removing inherited attribute ${attr} from ${entity.name}.`)
235
- }
236
- }
237
- }
238
-
239
- for (const entity of Object.values(csn.definitions)) {
240
- let i = 0
241
- if (entity.includes) {
242
- while (
243
- (util.getSingularAnnotation(entity) || util.getPluralAnnotation(entity)) &&
244
- i < entity.includes.length
245
- ) {
246
- const parent = this.csn.definitions[entity.includes[i]]
247
- Object.values(util.annotations).flat().forEach(an => erase(entity, parent, an))
248
- i++
249
- }
250
- }
251
- }
252
-
253
- this.visitDefinitions()
254
- }
255
-
256
- /**
257
- * Determines the file corresponding to the namespace.
258
- * If no such file exists yet, it is created first.
259
- * @param {string} path the name of the namespace (foo.bar.baz)
260
- * @returns {SourceFile} the file corresponding to that namespace name
261
- */
262
- getNamespaceFile(path) {
263
- if (!(path in this.files)) {
264
- this.files[path] = new SourceFile(path)
265
- }
266
- return this.files[path]
267
- }
268
-
269
- /**
270
- * Visits all definitions within the CSN definitions.
271
- */
272
- visitDefinitions() {
273
- for (const [name, entity] of Object.entries(this.csn.definitions)) {
274
- this.visitEntity(name, entity)
275
- }
276
- }
277
-
278
- /**
279
- * Wraps type into association to scalar.
280
- * @param {string} t the singular type name.
281
- * @returns {string}
282
- */
283
- _createToOneAssociation(t) {
284
- return `${baseDefinitions.path.asIdentifier()}.Association.to<${t}>`
285
- }
286
-
287
- /**
288
- * Wraps type into association to vector.
289
- * @param {string} t the singular type name.
290
- * @returns {string}
291
- */
292
- _createToManyAssociation(t) {
293
- return `${baseDefinitions.path.asIdentifier()}.Association.to.many<${t}>`
294
- }
295
-
296
- /**
297
- * Wraps type into composition of scalar.
298
- * @param {string} t the singular type name.
299
- * @returns {string}
300
- */
301
- _createCompositionOfOne(t) {
302
- return `${baseDefinitions.path.asIdentifier()}.Composition.of<${t}>`
303
- }
304
-
305
- /**
306
- * Wraps type into composition of vector.
307
- * @param {string} t the singular type name.
308
- * @returns {string}
309
- */
310
- _createCompositionOfMany(t) {
311
- return `${baseDefinitions.path.asIdentifier()}.Composition.of.many<${t}>`
312
- }
313
-
314
- /**
315
- * Puts a passed string in docstring format.
316
- * @param {string} doc raw string to docify. May contain linebreaks.
317
- * @returns {string[]} an array of lines wrapped in doc format. The result is not
318
- * concatenated to be properly indented by `buffer.add(...)`.
319
- */
320
- _docify(doc) {
321
- return doc ? ['/**'].concat(doc.split('\n').map((line) => `* ${line}`)).concat(['*/']) : []
322
- }
323
-
324
- /**
325
- * Transforms an entity or CDS aspect into a JS aspect (aka mixin).
326
- * That is, for an element A we get:
327
- * - the function A(B) to mix the aspect into another class B
328
- * - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
329
- * - the type A to use for external typing and is derived from AXtended.
330
- * @param {string} name the name of the entity
331
- * @param {CSN} element the pointer into the CSN to extract the elements from
332
- * @param {Buffer} buffer the buffer to write the resulting definitions into
333
- * @param {string?} cleanName the clean name to use. If not passed, it is derived from the passed name instead.
334
- */
335
- _aspectify(name, entity, buffer, cleanName = undefined) {
336
- const clean = cleanName ?? this._trimNamespace(name)
337
- const ns = this._resolveNamespace(name.split('.'))
338
- const file = this.getNamespaceFile(ns)
339
-
340
- const identSingular = name => name
341
- const identAspect = name => `_${name}Aspect`
342
-
343
- this.contexts.push({
344
- entity: name
345
- })
346
-
347
- // CLASS ASPECT
348
- // while I would personally prefer arrow-function style, using function syntax spares us troubles
349
- // with hoisting:
350
- //
351
- // # does not work
352
- // const A_fn = () => class ...
353
- // const A = B_fn(A_fn(Entity)) // error: B called before declaration
354
- // const B_fn = () => class ...
355
- //
356
- // # works
357
- // function A_fn () { return class ... }
358
- // const A = B_fn(A_fn(Entity))
359
- // function B_fn () { return class ... }
360
-
361
- buffer.add(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => any>(Base: TBase) {`)
362
- buffer.indent()
363
- buffer.add(`return class ${clean} extends Base {`)
364
- buffer.indent()
365
- for (const [ename, element] of Object.entries(entity.elements ?? {})) {
366
- this.visitElement(ename, element, file, buffer)
367
- }
368
- for (const [aname, action] of Object.entries(entity.actions ?? {})) {
369
- buffer.add(SourceFile.stringifyLambda(
370
- aname,
371
- Object.entries(action.params ?? {}).map(([n,t]) => [n, this.resolveAndRequire(t, file).typeName]),
372
- action.returns ? this.resolveAndRequire(action.returns, file).typeName : 'any'
373
- ))
374
- //this.visitEntity(aname, action, file, buffer)
375
- }
376
- buffer.outdent()
377
- buffer.add('};')
378
- buffer.outdent()
379
- buffer.add('}')
380
-
381
- // CLASS WITH ADDED ASPECTS
382
- file.addImport(baseDefinitions.path)
383
- const rhs = (entity.includes ?? [])
384
- .map((parent) => {
385
- const [ns, n] = this.untangle(parent)
386
- file.addImport(ns)
387
- return [ns, n]
388
- })
389
- .concat([[undefined, clean]]) // add own aspect without namespace AFTER imports were created
390
- .reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
391
- .reduce(
392
- (wrapped, [ns, n]) =>
393
- !ns || ns.isCwd(file.path.asDirectory()) ? `${identAspect(n)}(${wrapped})` : `${ns.asIdentifier()}.${identAspect(n)}(${wrapped})`,
394
- `${baseDefinitions.path.asIdentifier()}.Entity`
395
- )
396
-
397
- buffer.add(`export class ${identSingular(clean)} extends ${rhs} {}`)
398
- //buffer.add(`export type ${clean} = InstanceType<typeof ${identSingular(clean)}>`)
399
- this.contexts.pop()
400
- }
401
-
402
- _printEntity(name, entity) {
403
- const clean = this._trimNamespace(name)
404
- const ns = this._resolveNamespace(name.split('.'))
405
- const file = this.getNamespaceFile(ns)
406
- // entities are expected to be in plural anyway, so we would favour the regular name.
407
- // If the user decides to pass a @plural annotation, that gets precedence over the regular name.
408
- let plural = util.unlocalize(this._trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name))
409
- const singular = util.unlocalize(util.singular4(entity, true))
410
- if (singular === plural) {
411
- plural += '_'
412
- this.logger.warning(
413
- `Derived singular and plural forms for '${singular}' are the same. This usually happens when your CDS entities are named following singular flexion. Consider naming your entities in plural or providing '@singular:'/ '@plural:' annotations to have a clear distinction between the two. Plural form will be renamed to '${plural}' to avoid compilation errors within the output.`
414
- )
415
- }
416
- if (singular in this.csn.definitions) {
417
- this.logger.error(
418
- `Derived singular '${singular}' for your entity '${name}', already exists. The resulting types will be erronous. Please consider using '@singular:'/ '@plural:' annotations in your model to resolve this collision.`
419
- )
420
- }
421
- file.addClass(singular, name)
422
- file.addClass(plural, name)
423
-
424
- const parent = this._resolveParent(entity.name)
425
- const buffer =
426
- parent && parent.kind === 'entity' ? file.getSubNamespace(this._trimNamespace(parent.name)) : file.classes
427
-
428
- // we can't just use "singular" here, as it may have the subnamespace removed:
429
- // "Books.text" is just "text" in "singular". Within the inflected exports we need
430
- // to have Books.texts = Books.text, so we derive the singular once more without cutting off the ns.
431
- // Directly deriving it from the plural makes sure we retain any parent namespaces of kind "entity",
432
- // which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
433
- file.addInflection(util.singular4(plural), plural, clean)
434
- if ('doc' in entity) {
435
- this._docify(entity.doc).forEach((d) => buffer.add(d))
436
- }
437
-
438
- this._aspectify(name, entity, file.classes, singular)
439
-
440
- // PLURAL
441
- if (plural.includes('.')) {
442
- // Foo.text -> namespace Foo { class text { ... }}
443
- plural = plural.split('.').pop()
444
- }
445
- // plural can not be a type alias to $singular[] but needs to be a proper class instead,
446
- // so it can get passed as value to CQL functions.
447
- buffer.add(`export class ${plural} extends Array<${singular}> {}`)
448
- buffer.add('')
449
- }
450
-
451
- _printAction(name, action) {
452
- this.logger.debug(`Printing action ${name}:\n${JSON.stringify(action, null, 2)}`)
453
- const ns = this._resolveNamespace(name.split('.'))
454
- const file = this.getNamespaceFile(ns)
455
- const params = action.params
456
- ? Object.entries(action.params).map(([pname, ptype]) => [
457
- pname,
458
- this.resolveAndRequire(ptype, file).typeName,
459
- ])
460
- : []
461
- const returns = this.resolveAndRequire(action.returns, file).typeName
462
- file.addAction(name.split('.').at(-1), params, returns)
463
- }
464
-
465
- _printType(name, type) {
466
- this.logger.debug(`Printing type ${name}:\n${JSON.stringify(type, null, 2)}`)
467
- const clean = this._trimNamespace(name)
468
- const ns = this._resolveNamespace(name.split('.'))
469
- const file = this.getNamespaceFile(ns)
470
- if ('enum' in type) {
471
- file.addEnum(
472
- name,
473
- clean,
474
- Object.entries(type.enum).map(([k, v]) => [k, v.val])
475
- )
476
- } else {
477
- // alias
478
- file.addType(name, clean, this.resolveAndRequire(type, file).typeName)
479
- }
480
- // TODO: annotations not handled yet
481
- }
482
-
483
- _printAspect(name, aspect) {
484
- this.logger.debug(`Printing aspect ${name}`)
485
- const clean = this._trimNamespace(name)
486
- const ns = this._resolveNamespace(name.split('.'))
487
- const file = this.getNamespaceFile(ns)
488
- // aspects are technically classes and can therefore be added to the list of defined classes.
489
- // Still, when using them as mixins for a class, they need to already be defined.
490
- // So we separate them into another buffer which is printed before the classes.
491
- file.addClass(clean, name)
492
- file.aspects.add(`// the following represents the CDS aspect '${clean}'`)
493
- this._aspectify(name, aspect, file.aspects, clean)
494
- }
495
-
496
- /**
497
- * Visits a single entity from the CSN's definition field.
498
- * Will call _printEntity or _printAction based on the entity's kind.
499
- * @param {string} name name of the entity, fully qualified as is used in the definition field.
500
- * @param {CSN} entity CSN data belonging to the entity to perform lookups in.
501
- */
502
- visitEntity(name, entity) {
503
- switch (entity.kind) {
504
- case 'entity':
505
- this._printEntity(name, entity)
506
- break
507
- case 'action':
508
- this._printAction(name, entity)
509
- break
510
- case 'type':
511
- this._printType(name, entity)
512
- break
513
- case 'aspect':
514
- this._printAspect(name, entity)
515
- break
516
- default:
517
- this.logger.error(`Unhandled entity kind '${entity.kind}'.`)
518
- }
519
- }
520
-
521
- /**
522
- * Attempts to retrieve the max cardinality of a CSN for an entity.
523
- * @param {EntityCSN} element csn of entity to retrieve cardinality for
524
- * @returns {number} max cardinality of the element.
525
- * If no cardinality is attached to the element, cardinality is 1.
526
- * If it is set to '*', result is Infinity.
527
- */
528
- getMaxCardinality(element) {
529
- const cardinality = element && element.cardinality && element.cardinality.max ? element.cardinality.max : 1
530
- return cardinality === '*' ? Infinity : parseInt(cardinality)
531
- }
532
-
533
- /**
534
- * Conveniently combines _resolveNamespace and _trimNamespace
535
- * to end up with both the resolved Path of the namespace,
536
- * and the clean name of the class.
537
- * @param {string} fq the fully qualified name of an entity.
538
- * @returns {[Path, string]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity.
539
- */
540
- untangle(fq) {
541
- const ns = this._resolveNamespace(fq.split('.'))
542
- const name = this._trimNamespace(fq)
543
- return [new Path(ns.split('.')), name]
544
- }
545
-
546
- /**
547
- * A self reference is a property that references the class it appears in.
548
- * They need to be detected on CDS level, as the emitted TS types will try to
549
- * refer to refer to types via their alias that hides the aspectification.
550
- * If we attempt to directly refer to this alias while it has not been fully created,
551
- * that will result in a TS error.
552
- * @param {String} entityName
553
- * @returns {boolean} true, if `entityName` refers to the surrounding class
554
- * @example
555
- * ```ts
556
- * class TreeNode {
557
- * value: number
558
- * parent: TreeNode // <- self reference
559
- * }
560
- * ```
561
- */
562
- isSelfReference(entityName) {
563
- return entityName === this.contexts.at(-1)?.entity
564
- }
565
-
566
- /**
567
- * Convenient API to consume resolveType.
568
- * Internally calls resolveType, determines how it has to be imported,
569
- * used, etc. relative to file and just returns the name under
570
- * which it will finally be known within file.
571
- *
572
- * For example:
573
- * model1.cds contains entity Foo
574
- * model2.cds references Foo
575
- *
576
- * calling resolveAndRequire({... Foo}, model2.d.ts) would then:
577
- * 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
578
- * 2. resolve any singular/ plural issues and association/ composition around it
579
- * 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
580
- *
581
- * @param {CSN} element the CSN element to resolve the type for.
582
- * @param {SourceFile} file source file for context.
583
- * @returns {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} info about the resolved type
584
- */
585
- resolveAndRequire(element, file) {
586
- const typeInfo = this.resolveType(element, file)
587
- const cardinality = this.getMaxCardinality(element)
588
-
589
- let typeName = typeInfo.plainName ?? typeInfo.type
590
-
591
- if (typeInfo.isBuiltin === true) {
592
- const [toOne, toMany] = {
593
- 'Association': [this._createToOneAssociation, this._createToManyAssociation],
594
- 'Composition': [this._createCompositionOfOne, this._createCompositionOfMany]
595
- }[element.constructor.name] ?? []
596
-
597
- if (toOne && toMany) {
598
- const target = typeof element.target === 'string' ? { type: element.target } : element.target
599
- const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
600
- typeName = cardinality > 1
601
- ? toMany.bind(this)(plural)
602
- : toOne.bind(this)(this.isSelfReference(element.target) ? 'this' : singular)
603
- file.addImport(baseDefinitions.path)
604
- }
605
- } else {
606
- // TODO: this could go into resolve type
607
- // resolve and maybe generate an import.
608
- // Inline declarations don't have a corresponding path, etc., so skip those.
609
- if (typeInfo.isInlineDeclaration === false) {
610
- const namespace = this._resolveNamespace(typeInfo.path.parts)
611
- const parent = new Path(namespace.split('.')) //t.path.getParent()
612
- typeInfo.inflection = this.inflect(typeInfo, namespace)
613
-
614
- if (!parent.isCwd(file.path.asDirectory())) {
615
- file.addImport(parent)
616
- // prepend namespace
617
- typeName = `${parent.asIdentifier()}.${typeName}`
618
- typeInfo.inflection.singular = `${parent.asIdentifier()}.${typeInfo.inflection.singular}`
619
- typeInfo.inflection.plural = `${parent.asIdentifier()}.${typeInfo.inflection.plural}`
620
- }
621
-
622
- if (element.type.ref?.length > 1) {
623
- const [entity, ...members] = element.type.ref
624
- const lookup = this.inlineDeclarationResolver.getTypeLookup(members)
625
- typeName = `__.DeepRequired<${typeInfo.inflection.singular}>${lookup}`
626
- file.addImport(baseDefinitions.path)
627
- }
628
- }
629
- // FIXME NOW: inline declarations, aka structs go here!
630
-
631
- for (const imp of typeInfo.imports ?? []) {
632
- if (!imp.isCwd(file.path.asDirectory())) {
633
- file.addImport(imp)
634
- }
635
- }
636
- }
637
-
638
- if (typeInfo.isInlineDeclaration === true) {
639
- typeInfo.inflection = this.inflect(typeInfo)
640
- }
641
-
642
- return {typeName, typeInfo}
643
- }
644
-
645
- /**
646
- * Visits a single element in an entity.
647
- * @param {string} name name of the element
648
- * @param {CSN} element CSN data belonging to the the element.
649
- * @param {SourceFile} file the namespace file the surrounding entity is being printed into.
650
- * @param {Buffer} buffer buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
651
- * @returns @see InlineDeclarationResolver.visitElement
652
- */
653
- visitElement(name, element, file, buffer) {
654
- return this.inlineDeclarationResolver.visitElement(name, element, file, buffer)
655
- }
656
-
657
- /**
658
- * Resolves the fully qualified name of an entity to its parent entity.
659
- * _resolveParent(a.b.c.D) -> CSN {a.b.c}
660
- * @param {string} name fully qualified name of the entity to resolve the parent of.
661
- * @returns {CSN} the resolved parent CSN.
662
- */
663
- _resolveParent(name) {
664
- return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
665
- }
666
-
667
- /**
668
- * Resolves a fully qualified identifier to a namespace.
669
- * In an identifier 'a.b.c.D.E', the namespace is the part of the identifier
670
- * read from left to right which does not contain a kind 'context' or 'service'.
671
- * That is, if in the above example 'D' is a context and 'E' is a service,
672
- * the resulting namespace is 'a.b.c'.
673
- * @param {string[]} pathParts the distinct parts of the namespace, i.e. ['a','b','c','D','E']
674
- * @returns {string} the namespace's name, i.e. 'a.b.c'.
675
- */
676
- _resolveNamespace(pathParts) {
677
- let result
678
- while (result === undefined) {
679
- const path = pathParts.join('.')
680
- const def = this.csn.definitions[path]
681
- if (!def) {
682
- result = path
683
- } else if (['context', 'service'].includes(def.kind)) {
684
- result = path
685
- } else {
686
- pathParts = pathParts.slice(0, -1)
687
- }
688
- }
689
- return result
690
- }
691
-
692
- /**
693
- * Resolves an element's type to either a builtin or a user defined type.
694
- * Enriched with additional information for improved printout (see return type).
695
- * @param {CSN} element the CSN element to resolve the type for.
696
- * @param {SourceFile} file source file for context.
697
- * @returns {TypeResolveInfo} description of the resolved type
698
- */
699
- resolveType(element, file) {
700
- // while resolving inline declarations, it can happen that we land here
701
- // with an already resolved type. In that case, just return the type we have.
702
- if (element && Object.hasOwn(element, 'isBuiltin')) { return element }
703
-
704
- const result = {
705
- isBuiltin: false, // will be rectified in the corresponding handlers, if needed
706
- isInlineDeclaration: false,
707
- isForeignKeyReference: false,
708
- }
709
-
710
- // FIXME: switch case
711
- if (element?.type === undefined) {
712
- // "fallback" type "empty object". May be overriden via _resolveInlineDeclarationType
713
- // later on with an inline declaration
714
- result.type = '{}'
715
- result.isInlineDeclaration = true
716
- } else {
717
- this._resolvePotentialReferenceType(element.type, result, file)
718
- }
719
-
720
- // objects and arrays
721
- if (element?.items) {
722
- this._resolveInlineDeclarationType(element.items, result, file)
723
- } else if (element?.elements) {
724
- this._resolveInlineDeclarationType(element.elements, result, file)
725
- }
726
-
727
- if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
728
- this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
729
- }
730
- return result
731
- }
732
-
733
-
734
- /**
735
- * Resolves an inline declaration of a type.
736
- * We can encounter declarations like:
737
- *
738
- * record : array of {
739
- * column : String;
740
- * data : String;
741
- * }
742
- *
743
- * These have to be resolved to a new type.
744
- *
745
- * @param {any[]} items the properties of the inline declaration.
746
- * @param {TypeResolveInfo} into @see resolveType()
747
- * @param {SourceFile} relativeToindent the sourcefile in which we have found the reference to the type.
748
- * This is important to correctly detect when a field in the inline declaration is referencing
749
- * types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
750
- */
751
- _resolveInlineDeclarationType(items, into, relativeTo) {
752
- return this.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
753
- }
754
-
755
- /**
756
- * Attempts to resolve a type that could reference another type.
757
- * @param {?} val
758
- * @param {TypeResolveInfo} into see resolveType()
759
- * @param {SourceFile} file only needed as we may call _resolveInlineDeclarationType from here. Will be expelled at some point.
760
- */
761
- _resolvePotentialReferenceType(val, into, file) {
762
- // FIXME: get rid of file parameter! it is only used to pass to _resolveInlineDeclarationType
763
- if (val.elements) {
764
- this._resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
765
- } else if (val.constructor === Object && 'ref' in val) {
766
- this._resolveTypeName(val.ref[0], into)
767
- into.isForeignKeyReference = true
768
- } else {
769
- // val is string
770
- this._resolveTypeName(val, into)
771
- }
772
- }
773
-
774
- /**
775
- * Convenience method to shave off the namespace of a fully qualified path.
776
- * More specifically, only the parts (reading from right to left) that are of
777
- * kind "entity" are retained.
778
- * a.b.c.Foo -> Foo
779
- * Bar -> Bar
780
- * sap.cap.Book.text -> Book.text (assuming Book and text are both of kind "entity")
781
- * @param {string} p path
782
- * @returns {string} the entity name without leading namespace.
783
- */
784
- _trimNamespace(p) {
785
- // start on right side, go up while we have an entity at hand
786
- // we cant start on left side, as that clashes with undefined entities like "sap"
787
- const parts = p.split('.')
788
- if (parts.length <= 1) {
789
- return p
790
- }
791
-
792
- let qualifier = parts.join('.')
793
- while (
794
- this.csn.definitions[qualifier] &&
795
- ['entity', 'type', 'aspect'].includes(this.csn.definitions[qualifier].kind)
796
- ) {
797
- parts.pop()
798
- qualifier = parts.join('.')
799
- }
800
-
801
- return qualifier ? p.substring(qualifier.length + 1) : p
802
- }
803
-
804
- /**
805
- * Attempts to resolve a string to a type.
806
- * String is supposed to refer to either a builtin type
807
- * or any type defined in CSN.
808
- * @param {string} t fully qualified type, like cds.String, or a.b.c.d.Foo
809
- * @param {TypeResolveInfo} into optional dictionary to fill by reference, see resolveType()
810
- * @returns @see resolveType
811
- */
812
- _resolveTypeName(t, into) {
813
- const result = into || {}
814
- const path = t.split('.')
815
- if (path.length === 2 && path[0] === 'cds') {
816
- // builtin type
817
- const resolvedBuiltin = Builtins[path[1]]
818
- if (!resolvedBuiltin) {
819
- throw new Error(`Can not resolve apparent builtin type '${t}' to any CDS type.`)
820
- }
821
- result.type = resolvedBuiltin
822
- result.isBuiltin = true
823
- } else if (t in this.csn.definitions) {
824
- // user-defined type
825
- result.type = this._trimNamespace(util.singular4(this.csn.definitions[t])) //(path[path.length - 1])
826
- result.isBuiltin = false
827
- result.path = new Path(path) // FIXME: relative to current file
828
- result.csn = this.csn.definitions[t]
829
- result.plainName = this._trimNamespace(t)
830
- } else {
831
- // type offered by some library
832
- const lib = this.libraries.find(lib => lib.offers(t))
833
- if (lib) {
834
- // only use the last name of the (fully qualified) type name in this case.
835
- // We can not use _trimNamespace, as that actually does a semantic lookup within the CSN.
836
- // But entities that are found in libraries are not part of that CSN and have therefore be
837
- // separated from their namespace in a more barbarian way.
838
- // Luckily, this is not an issue, as libraries are supposed to be flat. So we can assume the
839
- // last portion of the type to refer to the entity.
840
- // We use this plain name as type name because consider:
841
- //
842
- // ```cds
843
- // entity Book { title: hana.VARCHAR }
844
- // ```
845
- //
846
- // ```ts
847
- // import * as _cds_hana from '../../cds/hana'
848
- // class Book { title: _cds_hana.cds.hana.VARCHAR } // <- how it would be without discarding the namespace
849
- // class Book { title: _cds_hana.VARCHAR } // <- how we want it to look
850
- // ```
851
- const plain = t.split('.').at(-1)
852
- lib.referenced = true
853
- result.type = plain
854
- result.isBuiltin = false
855
- result.path = lib.path
856
- result.csn = { name: t }
857
- result.plainName = plain
858
- } else {
859
- throw new Error(`Can not resolve '${t}' to any builtin, library-, or user defined type.`)
860
- }
861
- }
862
-
863
- return result
864
- }
865
- }
9
+ const { Visitor } = require('./visitor')
866
10
 
867
11
  /**
868
12
  * Writes the accompanying jsconfig.json file to the specified paths.
@@ -920,7 +64,6 @@ const compileFromCSN = async (csn, parameters) => {
920
64
  }
921
65
 
922
66
  module.exports = {
923
- Visitor,
924
67
  compileFromFile,
925
68
  compileFromCSN,
926
69
  }