diff --git a/CHANGELOG.md b/CHANGELOG.md index 14681221..f6bca0e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). -## Version 0.15.0 - TBD +## Version 0.16.0 - TBD + +## Version 0.15.0 - 2023-12-21 ### Added - Support for [scoped entities](https://cap.cloud.sap/docs/cds/cdl#scoped-names) +- Support for [delimited identifiers](https://cap.cloud.sap/docs/cds/cdl#delimited-identifiers) ### Fixed - Inline enums are now available during runtime as well diff --git a/lib/components/enum.js b/lib/components/enum.js index afd27d46..c6fc4492 100644 --- a/lib/components/enum.js +++ b/lib/components/enum.js @@ -1,3 +1,5 @@ +const { normalise } = require('./identifier') + /** * Prints an enum to a buffer. To be precise, it prints * a constant object and a type which together form an artificial enum. @@ -30,7 +32,7 @@ function printEnum(buffer, name, kvs, options = {}) { buffer.add(`${opts.export ? 'export ' : ''}const ${name} = {`) buffer.indent() for (const [k, v] of kvs) { - buffer.add(`${k}: ${v},`) + buffer.add(`${normalise(k)}: ${v},`) } buffer.outdent() buffer.add('} as const;') @@ -121,7 +123,7 @@ const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn * @param {[string, string][]} kvs a list of key-value pairs. Values that are falsey are replaced by */ // ??= for inline enums. If there is some static property of that name, we don't want to override it (for example: ".actions" -const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} ??= { ${kvs.map(([k,v]) => `${k}: ${v}`).join(', ')} }` +const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} ??= { ${kvs.map(([k,v]) => `${normalise(k)}: ${v}`).join(', ')} }` module.exports = { diff --git a/lib/components/identifier.js b/lib/components/identifier.js new file mode 100644 index 00000000..944d2627 --- /dev/null +++ b/lib/components/identifier.js @@ -0,0 +1,15 @@ +const isValidIdent = /^[_$a-zA-Z][$\w]*$/ + +/** + * Normalises an identifier to a valid JavaScript identifier. + * I.e. either the identifier itself or a quoted string. + * @param {string} ident the identifier to normalise + * @returns {string} the normalised identifier + */ +const normalise = ident => ident && !isValidIdent.test(ident) + ? `"${ident}"` + : ident + +module.exports = { + normalise +} \ No newline at end of file diff --git a/lib/components/inline.js b/lib/components/inline.js index 304ca591..5acb8332 100644 --- a/lib/components/inline.js +++ b/lib/components/inline.js @@ -1,4 +1,5 @@ const { SourceFile, Buffer } = require('../file') +const { normalise } = require('./identifier') const { docify } = require('./wrappers') /** @@ -154,7 +155,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver { flatten(prefix, type) { return type.typeInfo.structuredType ? Object.entries(type.typeInfo.structuredType).map(([k,v]) => this.flatten(`${this.prefix(prefix)}${k}`, v)) - : [`${prefix}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`] + : [`${normalise(prefix)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`] } printInlineType(name, type, buffer) { @@ -197,7 +198,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver { this.printDepth++ const lineEnding = this.printDepth > 1 ? ',' : statementEnd if (type.typeInfo.structuredType) { - const prefix = name ? `${name}${this.getPropertyTypeSeparator()}`: '' + const prefix = name ? `${normalise(name)}${this.getPropertyTypeSeparator()}`: '' buffer.add(`${prefix} {`) buffer.indent() for (const [n, t] of Object.entries(type.typeInfo.structuredType)) { @@ -206,7 +207,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver { buffer.outdent() buffer.add(`}${this.getPropertyDatatype(type, '')}${lineEnding}`) } else { - buffer.add(`${name}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`) + buffer.add(`${normalise(name)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`) } this.printDepth-- return buffer diff --git a/lib/file.js b/lib/file.js index 2b77ea59..dc785da8 100644 --- a/lib/file.js +++ b/lib/file.js @@ -3,6 +3,7 @@ const fs = require('fs').promises const { readFileSync } = require('fs') const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum') +const { normalise } = require('./components/identifier') const path = require('path') const AUTO_GEN_NOTE = "// This is an automatically generated file. Please do not change its contents manually!" @@ -151,9 +152,9 @@ class SourceFile extends File { * ``` */ static stringifyLambda({name, parameters=[], returns='any', initialiser, isStatic=false}) { - const parameterTypes = parameters.map(([n, t]) => `${n}: ${t}`).join(', ') + const parameterTypes = parameters.map(([n, t]) => `${normalise(n)}: ${t}`).join(', ') const callableSignature = `(${parameterTypes}): ${returns}` - let prefix = name ? `${name}: `: '' + let prefix = name ? `${normalise(name)}: `: '' if (prefix && isStatic) { prefix = `static ${prefix}` } diff --git a/package.json b/package.json index d095fcc2..a6aee84d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/cds-typer", - "version": "0.14.0", + "version": "0.15.0", "description": "Generates .ts files for a CDS model to receive code completion in VS Code", "main": "index.js", "repository": "github:cap-js/cds-typer", diff --git a/test/ast.js b/test/ast.js index 69904e18..818f3ab6 100644 --- a/test/ast.js +++ b/test/ast.js @@ -281,7 +281,7 @@ class ASTWrapper { sourceFile.forEachChild(c => { const slim = visit(c) // ignore top-level keywords, like 'export', etc. - if (slim?.nodeType !== kinds.Keyword) { + if (slim && slim?.nodeType !== kinds.Keyword) { this.tree.push(slim) } }) diff --git a/test/unit/delimident.test.js b/test/unit/delimident.test.js new file mode 100644 index 00000000..80a387f0 --- /dev/null +++ b/test/unit/delimident.test.js @@ -0,0 +1,29 @@ +'use strict' + +const fs = require('fs').promises +const path = require('path') +const cds2ts = require('../../lib/compile') +const { ASTWrapper } = require('../ast') +const { locations } = require('../util') + +const dir = locations.testOutput('enums_test') + +describe('Delimited Identifiers', () => { + let astw + + beforeEach(async () => await fs.unlink(dir).catch(() => {})) + beforeAll(async () => { + const paths = await cds2ts + .compileFromFile(locations.unit.files('delimident/model.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' }) + astw = new ASTWrapper(path.join(paths[1], 'index.ts')) + }) + + test('Properties in Aspect Present', () => { + expect(astw.getAspectProperty('_FooAspect', 'sap-icon://a')).toBeTruthy() + const nested = astw.getAspectProperty('_FooAspect', 'sap-icon://b') + expect(nested).toBeTruthy() + expect(nested.type.subtypes[0].members[0].name).toBe('sap-icon://c') + const actions = astw.getAspectProperty('_FooAspect', 'actions') + expect(actions.type.members.find(fn => fn.name === 'sap-icon://f')).toBeTruthy() + }) +}) diff --git a/test/unit/files/delimident/model.cds b/test/unit/files/delimident/model.cds new file mode 100644 index 00000000..d8762c2e --- /dev/null +++ b/test/unit/files/delimident/model.cds @@ -0,0 +1,13 @@ +namespace delimited_identifiers_test; + +entity Foo { + ![sap-icon://a]: String; + ![sap-icon://b]: { + ![sap-icon://c]: String; + }; + c: String enum { + ![sap-icon://d] + }; +} actions { + action ![sap-icon://f]() +} \ No newline at end of file diff --git a/test/util.js b/test/util.js index 10c8ccbd..6394d073 100644 --- a/test/util.js +++ b/test/util.js @@ -314,7 +314,7 @@ const locations = { } -const cds2ts = async (cdsFile, options = {}) => await typer.compileFromFile( +const cds2ts = async (cdsFile, options = {}) => typer.compileFromFile( locations.unit.files(cdsFile), options )