diff --git a/packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts b/packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts index 193cad5a04..5e2ebb5491 100644 --- a/packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts +++ b/packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts @@ -2,6 +2,7 @@ import * as spec from '@jsii/spec'; import { CodeMaker } from 'codemaker'; import { Rosetta, + TargetLanguage, Translation, enforcesStrictMode, typeScriptSnippetFromSource, @@ -166,7 +167,10 @@ export class DotNetDocGenerator { 'example', enforcesStrictMode(this.assembly), ); - const translated = this.rosetta.translateSnippet(snippet, 'csharp'); + const translated = this.rosetta.translateSnippet( + snippet, + TargetLanguage.CSHARP, + ); if (!translated) { return example; } @@ -176,7 +180,7 @@ export class DotNetDocGenerator { private convertSamplesInMarkdown(markdown: string): string { return this.rosetta.translateSnippetsInMarkdown( markdown, - 'csharp', + TargetLanguage.CSHARP, enforcesStrictMode(this.assembly), (trans) => ({ language: trans.language, diff --git a/packages/jsii-pacmak/lib/targets/java.ts b/packages/jsii-pacmak/lib/targets/java.ts index b7d939165f..154ed3dd75 100644 --- a/packages/jsii-pacmak/lib/targets/java.ts +++ b/packages/jsii-pacmak/lib/targets/java.ts @@ -5,6 +5,7 @@ import * as fs from 'fs-extra'; import * as reflect from 'jsii-reflect'; import { Rosetta, + TargetLanguage, typeScriptSnippetFromSource, Translation, enforcesStrictMode, @@ -2916,7 +2917,10 @@ class JavaGenerator extends Generator { 'example', enforcesStrictMode(this.assembly), ); - const translated = this.rosetta.translateSnippet(snippet, 'java'); + const translated = this.rosetta.translateSnippet( + snippet, + TargetLanguage.JAVA, + ); if (!translated) { return example; } @@ -2926,7 +2930,7 @@ class JavaGenerator extends Generator { private convertSamplesInMarkdown(markdown: string): string { return this.rosetta.translateSnippetsInMarkdown( markdown, - 'java', + TargetLanguage.JAVA, enforcesStrictMode(this.assembly), (trans) => ({ language: trans.language, diff --git a/packages/jsii-pacmak/lib/targets/python.ts b/packages/jsii-pacmak/lib/targets/python.ts index 0a27664e29..9a0ac115eb 100644 --- a/packages/jsii-pacmak/lib/targets/python.ts +++ b/packages/jsii-pacmak/lib/targets/python.ts @@ -4,6 +4,7 @@ import * as escapeStringRegexp from 'escape-string-regexp'; import * as fs from 'fs-extra'; import * as reflect from 'jsii-reflect'; import { + TargetLanguage, Translation, Rosetta, enforcesStrictMode, @@ -2290,7 +2291,10 @@ class PythonGenerator extends Generator { 'example', enforcesStrictMode(this.assembly), ); - const translated = this.rosetta.translateSnippet(snippet, 'python'); + const translated = this.rosetta.translateSnippet( + snippet, + TargetLanguage.PYTHON, + ); if (!translated) { return example; } @@ -2300,7 +2304,7 @@ class PythonGenerator extends Generator { public convertMarkdown(markdown: string): string { return this.rosetta.translateSnippetsInMarkdown( markdown, - 'python', + TargetLanguage.PYTHON, enforcesStrictMode(this.assembly), (trans) => ({ language: trans.language, diff --git a/packages/jsii-rosetta/bin/jsii-rosetta.ts b/packages/jsii-rosetta/bin/jsii-rosetta.ts index 16c461a82c..da6eba97a5 100644 --- a/packages/jsii-rosetta/bin/jsii-rosetta.ts +++ b/packages/jsii-rosetta/bin/jsii-rosetta.ts @@ -10,6 +10,8 @@ import { import { translateMarkdown } from '../lib/commands/convert'; import { extractSnippets } from '../lib/commands/extract'; import { readTablet } from '../lib/commands/read'; +import { transliterateAssembly } from '../lib/commands/transliterate'; +import { TargetLanguage } from '../lib/languages'; import { PythonVisitor } from '../lib/languages/python'; import { VisualizeAstVisitor } from '../lib/languages/visualize'; import * as logging from '../lib/logging'; @@ -166,6 +168,60 @@ function main() { } }), ) + .command( + 'transliterate [ASSEMBLY..]', + '(EXPERIMENTAL) Transliterates the designated assemblies', + (command) => + command + .positional('ASSEMBLY', { + type: 'string', + string: true, + default: new Array(), + required: true, + describe: 'Assembly to transliterate', + }) + .option('language', { + alias: 'l', + type: 'string', + string: true, + default: new Array(), + describe: 'Language ID to transliterate to', + }) + .options('strict', { + alias: 's', + type: 'boolean', + describe: + 'Fail if an example that needs live transliteration fails to compile (which could cause incorrect transpilation results)', + }) + .option('tablet', { + alias: 't', + type: 'string', + describe: + 'Language tablet containing pre-translated code examples to use (these are generated by the `extract` command)', + }), + wrapHandler((args) => { + const assemblies = ( + args.ASSEMBLY.length > 0 ? args.ASSEMBLY : ['.'] + ).map((dir) => path.resolve(process.cwd(), dir)); + const languages = + args.language.length > 0 + ? args.language.map((lang) => { + const target = Object.entries(TargetLanguage).find( + ([k]) => k === lang, + )?.[1]; + if (target == null) { + throw new Error( + `Unknown target language: ${lang}. Expected one of ${Object.keys( + TargetLanguage, + ).join(', ')}`, + ); + } + return target; + }) + : Object.values(TargetLanguage); + return transliterateAssembly(assemblies, languages, args); + }), + ) .command( 'read [KEY] [LANGUAGE]', 'Display snippets in a language tablet file', diff --git a/packages/jsii-rosetta/jest.config.ts b/packages/jsii-rosetta/jest.config.ts index e37577d92f..e13e1670b0 100644 --- a/packages/jsii-rosetta/jest.config.ts +++ b/packages/jsii-rosetta/jest.config.ts @@ -1,3 +1,7 @@ -import config from '../../jest.config'; +import { join } from 'path'; -export default config; +import { overriddenConfig } from '../../jest.config'; + +export default overriddenConfig({ + setupFiles: [join(__dirname, 'jestsetup.js')], +}); diff --git a/packages/jsii-rosetta/jestsetup.js b/packages/jsii-rosetta/jestsetup.js new file mode 100644 index 0000000000..566fd0feb0 --- /dev/null +++ b/packages/jsii-rosetta/jestsetup.js @@ -0,0 +1,4 @@ +// Require `mock-fs` before `jest` initializes, as `mock-fs` relies on +// hijacking the `fs` module, which `jest` also hijacks (and that needs to +// happen last). +require('mock-fs'); diff --git a/packages/jsii-rosetta/lib/commands/transliterate.ts b/packages/jsii-rosetta/lib/commands/transliterate.ts new file mode 100644 index 0000000000..6b342609e1 --- /dev/null +++ b/packages/jsii-rosetta/lib/commands/transliterate.ts @@ -0,0 +1,203 @@ +import { Assembly, Docs, SPEC_FILE_NAME, Type, TypeKind } from '@jsii/spec'; +import { readJson, writeJson } from 'fs-extra'; +import { resolve } from 'path'; + +import { fixturize } from '../fixtures'; +import { TargetLanguage } from '../languages'; +import { debug } from '../logging'; +import { Rosetta } from '../rosetta'; +import { SnippetParameters, typeScriptSnippetFromSource } from '../snippet'; +import { Translation } from '../tablets/tablets'; + +export interface TransliterateAssemblyOptions { + /** + * Whather transliteration should fail upon failing to compile an example that + * required live transliteration. + * + * @default false + */ + readonly strict?: boolean; + + /** + * A pre-build translation tablet (as produced by `jsii-rosetta extract`). + * + * @default - Only the default tablet (`.jsii.tabl.json`) files will be used. + */ + readonly tablet?: string; +} + +/** + * Prepares transliterated versions of the designated assemblies into the + * selected taregt languages. + * + * @param assemblyLocations the directories which contain assemblies to + * transliterate. + * @param targetLanguages the languages into which to transliterate. + * @param tabletLocation an optional Rosetta tablet file to source + * pre-transliterated snippets from. + * + * @experimental + */ +export async function transliterateAssembly( + assemblyLocations: readonly string[], + targetLanguages: readonly TargetLanguage[], + options: TransliterateAssemblyOptions = {}, +): Promise { + const rosetta = new Rosetta({ + includeCompilerDiagnostics: true, + liveConversion: true, + targetLanguages, + }); + if (options.tablet) { + await rosetta.loadTabletFromFile(options.tablet); + } + const assemblies = await loadAssemblies(assemblyLocations, rosetta); + + for (const [location, loadAssembly] of assemblies.entries()) { + for (const language of targetLanguages) { + const now = new Date().getTime(); + // eslint-disable-next-line no-await-in-loop + const result = await loadAssembly(); + if (result.readme?.markdown) { + result.readme.markdown = rosetta.translateSnippetsInMarkdown( + result.readme.markdown, + language, + true /* strict */, + (translation) => ({ + language: translation.language, + source: prefixDisclaimer(translation), + }), + location, + ); + } + for (const type of Object.values(result.types ?? {})) { + transliterateType(type, rosetta, language, location); + } + // eslint-disable-next-line no-await-in-loop + await writeJson( + resolve(location, `${SPEC_FILE_NAME}.${language}`), + result, + { spaces: 2 }, + ); + const then = new Date().getTime(); + debug( + `Done transliterating ${result.name}@${ + result.version + } to ${language} after ${then - now} milliseconds`, + ); + } + } + + rosetta.printDiagnostics(process.stderr); + if (rosetta.hasErrors && options.strict) { + throw new Error( + 'Strict mode is enabled and some examples failed compilation!', + ); + } +} + +/** + * Given a set of directories containing `.jsii` assemblies, load all the + * assemblies into the provided `Rosetta` instance and return a map of + * directories to assembly-loading functions (the function re-loads the original + * assembly from disk on each invocation). + * + * @param directories the assembly-containing directories to traverse. + * @param rosetta the `Rosetta` instance in which to load assemblies. + * + * @returns a map of directories to a function that loads the `.jsii` assembly + * contained therein from disk. + */ +async function loadAssemblies( + directories: readonly string[], + rosetta: Rosetta, +): Promise> { + const result = new Map(); + + for (const directory of directories) { + const loader = () => readJson(resolve(directory, SPEC_FILE_NAME)); + // eslint-disable-next-line no-await-in-loop + await rosetta.addAssembly(await loader(), directory); + result.set(directory, loader); + } + + return result; +} + +type Mutable = { -readonly [K in keyof T]: Mutable }; +type AssemblyLoader = () => Promise>; + +function prefixDisclaimer(translation: Translation): string { + const message = translation.didCompile + ? 'Example automatically generated. See https://github.com/aws/jsii/issues/826' + : 'Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826'; + return `${commentToken()} ${message}\n${translation.source}`; + + function commentToken() { + // This is future-proofed a bit, but don't read too much in this... + switch (translation.language) { + case 'python': + case 'ruby': + return '#'; + case 'csharp': + case 'java': + case 'go': + default: + return '//'; + } + } +} + +function transliterateType( + type: Type, + rosetta: Rosetta, + language: TargetLanguage, + workingDirectory: string, +): void { + transliterateDocs(type.docs); + switch (type.kind) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore 7029 + case TypeKind.Class: + transliterateDocs(type?.initializer?.docs); + + // fallthrough + case TypeKind.Interface: + for (const method of type.methods ?? []) { + transliterateDocs(method.docs); + for (const parameter of method.parameters ?? []) { + transliterateDocs(parameter.docs); + } + } + for (const property of type.properties ?? []) { + transliterateDocs(property.docs); + } + break; + + case TypeKind.Enum: + for (const member of type.members) { + transliterateDocs(member.docs); + } + break; + + default: + throw new Error(`Unsupported type kind: ${(type as any).kind}`); + } + + function transliterateDocs(docs: Docs | undefined) { + if (docs?.example) { + const snippet = fixturize( + typeScriptSnippetFromSource( + docs.example, + 'example', + true /* strict */, + { [SnippetParameters.$PROJECT_DIRECTORY]: workingDirectory }, + ), + ); + const translation = rosetta.translateSnippet(snippet, language); + if (translation != null) { + docs.example = prefixDisclaimer(translation); + } + } + } +} diff --git a/packages/jsii-rosetta/lib/index.ts b/packages/jsii-rosetta/lib/index.ts index 428e2f272f..f26bb05f93 100644 --- a/packages/jsii-rosetta/lib/index.ts +++ b/packages/jsii-rosetta/lib/index.ts @@ -1,5 +1,6 @@ export * from './translate'; export { renderTree } from './o-tree'; +export { TargetLanguage } from './languages/target-language'; export { CSharpVisitor } from './languages/csharp'; export { JavaVisitor } from './languages/java'; export { PythonVisitor } from './languages/python'; diff --git a/packages/jsii-rosetta/lib/languages/csharp.ts b/packages/jsii-rosetta/lib/languages/csharp.ts index 88e1bf8baf..4b00a61ee2 100644 --- a/packages/jsii-rosetta/lib/languages/csharp.ts +++ b/packages/jsii-rosetta/lib/languages/csharp.ts @@ -23,6 +23,7 @@ import { } from '../typescript/types'; import { flat, partition, setExtend } from '../util'; import { DefaultVisitor } from './default'; +import { TargetLanguage } from './target-language'; interface CSharpLanguageContext { /** @@ -74,7 +75,7 @@ interface CSharpLanguageContext { type CSharpRenderer = AstRenderer; export class CSharpVisitor extends DefaultVisitor { - public readonly language = 'csharp'; + public readonly language = TargetLanguage.CSHARP; public readonly defaultContext = { propertyOrMethod: false, diff --git a/packages/jsii-rosetta/lib/languages/index.ts b/packages/jsii-rosetta/lib/languages/index.ts index 5e6759529f..8bfa9c34cd 100644 --- a/packages/jsii-rosetta/lib/languages/index.ts +++ b/packages/jsii-rosetta/lib/languages/index.ts @@ -2,8 +2,10 @@ import { AstHandler } from '../renderer'; import { CSharpVisitor } from './csharp'; import { JavaVisitor } from './java'; import { PythonVisitor } from './python'; +import { TargetLanguage } from './target-language'; + +export { TargetLanguage }; -export type TargetLanguage = 'python' | 'csharp' | 'java'; export type VisitorFactory = () => AstHandler; export const TARGET_LANGUAGES: { [key in TargetLanguage]: VisitorFactory } = { diff --git a/packages/jsii-rosetta/lib/languages/java.ts b/packages/jsii-rosetta/lib/languages/java.ts index ea0bef5328..2ada081a38 100644 --- a/packages/jsii-rosetta/lib/languages/java.ts +++ b/packages/jsii-rosetta/lib/languages/java.ts @@ -2,6 +2,7 @@ import * as ts from 'typescript'; import { isStructType } from '../jsii/jsii-utils'; import { jsiiTargetParam } from '../jsii/packages'; +import { TargetLanguage } from '../languages/target-language'; import { OTree, NO_SYNTAX } from '../o-tree'; import { AstRenderer } from '../renderer'; import { @@ -102,7 +103,7 @@ interface InsideTypeDeclaration { type JavaRenderer = AstRenderer; export class JavaVisitor extends DefaultVisitor { - public readonly language = 'java'; + public readonly language = TargetLanguage.JAVA; public readonly defaultContext = {}; public mergeContext( diff --git a/packages/jsii-rosetta/lib/languages/python.ts b/packages/jsii-rosetta/lib/languages/python.ts index f058af97e5..9e6749f406 100644 --- a/packages/jsii-rosetta/lib/languages/python.ts +++ b/packages/jsii-rosetta/lib/languages/python.ts @@ -7,6 +7,7 @@ import { structPropertyAcceptsUndefined, } from '../jsii/jsii-utils'; import { jsiiTargetParam } from '../jsii/packages'; +import { TargetLanguage } from '../languages/target-language'; import { NO_SYNTAX, OTree, renderTree } from '../o-tree'; import { AstRenderer, nimpl, CommentSyntax } from '../renderer'; import { @@ -86,7 +87,7 @@ export interface PythonVisitorOptions { } export class PythonVisitor extends DefaultVisitor { - public readonly language = 'python'; + public readonly language = TargetLanguage.PYTHON; public readonly defaultContext = {}; public constructor(private readonly options: PythonVisitorOptions = {}) { diff --git a/packages/jsii-rosetta/lib/languages/target-language.ts b/packages/jsii-rosetta/lib/languages/target-language.ts new file mode 100644 index 0000000000..29c30e963d --- /dev/null +++ b/packages/jsii-rosetta/lib/languages/target-language.ts @@ -0,0 +1,5 @@ +export enum TargetLanguage { + PYTHON = 'python', + CSHARP = 'csharp', + JAVA = 'java', +} diff --git a/packages/jsii-rosetta/lib/rosetta.ts b/packages/jsii-rosetta/lib/rosetta.ts index ffb9385505..838a331ec7 100644 --- a/packages/jsii-rosetta/lib/rosetta.ts +++ b/packages/jsii-rosetta/lib/rosetta.ts @@ -9,7 +9,11 @@ import { transformMarkdown } from './markdown/markdown'; import { MarkdownRenderer } from './markdown/markdown-renderer'; import { ReplaceTypeScriptTransform } from './markdown/replace-typescript-transform'; import { CodeBlock } from './markdown/types'; -import { TypeScriptSnippet } from './snippet'; +import { + SnippetParameters, + TypeScriptSnippet, + updateParameters, +} from './snippet'; import { DEFAULT_TABLET_NAME, LanguageTablet, @@ -24,14 +28,19 @@ export interface RosettaOptions { * * @default false */ - liveConversion?: boolean; + readonly liveConversion?: boolean; /** * Target languages to use for live conversion * * @default All languages */ - targetLanguages?: TargetLanguage[]; + readonly targetLanguages?: readonly TargetLanguage[]; + + /** + * Whether to include compiler diagnostics in the compilation results. + */ + readonly includeCompilerDiagnostics?: boolean; } /** @@ -49,10 +58,14 @@ export interface RosettaOptions { export class Rosetta { private readonly loadedTablets: LanguageTablet[] = []; private readonly liveTablet = new LanguageTablet(); - private readonly extractedSnippets: Record = {}; - private readonly translator = new Translator(false); + private readonly extractedSnippets = new Map(); + private readonly translator: Translator; - public constructor(private readonly options: RosettaOptions = {}) {} + public constructor(private readonly options: RosettaOptions = {}) { + this.translator = new Translator( + options.includeCompilerDiagnostics ?? false, + ); + } /** * Diagnostics encountered while doing live translation @@ -104,7 +117,7 @@ export class Rosetta { for (const tsnip of allTypeScriptSnippets([ { assembly, directory: assemblyDir }, ])) { - this.extractedSnippets[tsnip.visibleSource] = tsnip; + this.extractedSnippets.set(tsnip.visibleSource, tsnip); } } } @@ -136,7 +149,7 @@ export class Rosetta { } // See if we're going to live-convert it with full source information - const extracted = this.extractedSnippets[source.visibleSource]; + const extracted = this.extractedSnippets.get(source.visibleSource); if (extracted !== undefined) { const snippet = this.translator.translate( extracted, @@ -159,12 +172,18 @@ export class Rosetta { targetLang: TargetLanguage, strict: boolean, translationToCodeBlock: (x: Translation) => CodeBlock = id, + compileDirectory = process.cwd(), ): string { return transformMarkdown( markdown, new MarkdownRenderer(), new ReplaceTypeScriptTransform('markdown', strict, (tsSnip) => { - const translated = this.translateSnippet(tsSnip, targetLang); + const translated = this.translateSnippet( + updateParameters(tsSnip, { + [SnippetParameters.$COMPILATION_DIRECTORY]: compileDirectory, + }), + targetLang, + ); if (!translated) { return undefined; } diff --git a/packages/jsii-rosetta/lib/snippet.ts b/packages/jsii-rosetta/lib/snippet.ts index 7c447d2f4b..ad23682d72 100644 --- a/packages/jsii-rosetta/lib/snippet.ts +++ b/packages/jsii-rosetta/lib/snippet.ts @@ -57,7 +57,11 @@ export function updateParameters( ): TypeScriptSnippet { return { ...snippet, - parameters: Object.assign({}, snippet.parameters ?? {}, params), + parameters: Object.assign( + Object.create(null), + snippet.parameters ?? {}, + params, + ), }; } diff --git a/packages/jsii-rosetta/lib/tablets/tablets.ts b/packages/jsii-rosetta/lib/tablets/tablets.ts index 248c367b29..6321b4a189 100644 --- a/packages/jsii-rosetta/lib/tablets/tablets.ts +++ b/packages/jsii-rosetta/lib/tablets/tablets.ts @@ -51,7 +51,7 @@ export class LanguageTablet { if (!obj.toolVersion || !obj.snippets) { throw new Error(`File '${filename}' does not seem to be a Tablet file`); } - if (obj.toolVersion !== TOOL_VERSION) { + if (obj.toolVersion !== TOOL_VERSION && TOOL_VERSION !== '0.0.0') { throw new Error( `Tablet file '${filename}' has been created with version '${obj.toolVersion}', cannot read with current version '${TOOL_VERSION}'`, ); diff --git a/packages/jsii-rosetta/lib/translate.ts b/packages/jsii-rosetta/lib/translate.ts index 20142ba337..9d917f6823 100644 --- a/packages/jsii-rosetta/lib/translate.ts +++ b/packages/jsii-rosetta/lib/translate.ts @@ -51,7 +51,7 @@ export class Translator { public translate( snip: TypeScriptSnippet, - languages = Object.keys(TARGET_LANGUAGES) as TargetLanguage[], + languages: readonly TargetLanguage[] = Object.values(TargetLanguage), ) { logging.debug( `Translating ${snippetKey(snip)} ${inspect(snip.parameters ?? {})}`, @@ -134,7 +134,8 @@ export class SnippetTranslator { const source = completeSource(snippet); const fakeCurrentDirectory = - snippet.parameters?.[SnippetParameters.$COMPILATION_DIRECTORY]; + snippet.parameters?.[SnippetParameters.$COMPILATION_DIRECTORY] ?? + snippet.parameters?.[SnippetParameters.$PROJECT_DIRECTORY]; this.compilation = compiler.compileInMemory( snippet.where, source, diff --git a/packages/jsii-rosetta/test/commands/transliterate.test.ts b/packages/jsii-rosetta/test/commands/transliterate.test.ts new file mode 100644 index 0000000000..ecb85ef88e --- /dev/null +++ b/packages/jsii-rosetta/test/commands/transliterate.test.ts @@ -0,0 +1,917 @@ +import { SPEC_FILE_NAME } from '@jsii/spec'; +import * as fs from 'fs-extra'; +import * as jsii from 'jsii'; +import * as os from 'os'; +import * as path from 'path'; + +import { transliterateAssembly } from '../../lib/commands/transliterate'; +import { TargetLanguage } from '../../lib/languages/target-language'; + +jest.setTimeout(60_000); + +test('single assembly, all languages', () => + withTemporaryDirectory(async (tmpDir) => { + // GIVEN + const compilationResult = await jsii.compileJsiiForTest({ + 'README.md': ` +# README +\`\`\`ts +const object: IInterface = new ClassName('this', 1337, { foo: 'bar' }); +object.property = EnumType.OPTION_A; +object.methodCall(); + +ClassName.staticMethod(EnumType.OPTION_B); +\`\`\` +`, + 'index.ts': ` +/** + * @example new ClassName('this', 1337, { property: EnumType.OPTION_B }); + */ +export enum EnumType { + /** + * @example new ClassName('this', 1337, { property: EnumType.OPTION_A }); + */ + OPTION_A = 1, + + /** + * @example new ClassName('this', 1337, { property: EnumType.OPTION_B }); + */ + OPTION_B = 2, +} + +export interface IInterface { + /** + * A property value. + * + * @example + * iface.property = EnumType.OPTION_B; + */ + property: EnumType; + + /** + * An instance method call. + * + * @example + * iface.methodCall(); + */ + methodCall(): void; +} + +export interface ClassNameProps { + readonly property?: EnumType; + readonly foo?: string; +} + +export class ClassName implements IInterface { + /** + * A static method. It can be invoked easily. + * + * @example ClassName.staticMethod(); + */ + public static staticMethod(_enm?: EnumType): void { + // ... + } + + public property: EnumType; + + /** + * Create a new instance of ClassName. + * + * @example new ClassName('this', 1337, { property: EnumType.OPTION_B }); + */ + public constructor(_this: string, _elite: number, props: ClassNameProps) { + this.property = props.property ?? EnumType.OPTION_A; + } + + public methodCall(): void { + // ... + } +}`, + }); + fs.writeJsonSync( + path.join(tmpDir, SPEC_FILE_NAME), + compilationResult.assembly, + { + spaces: 2, + }, + ); + for (const [file, content] of Object.entries(compilationResult.files)) { + fs.writeFileSync(path.resolve(tmpDir, file), content, 'utf-8'); + } + fs.mkdirSync(path.resolve(tmpDir, 'rosetta')); + fs.writeFileSync( + path.resolve(tmpDir, 'rosetta', 'default.ts-fixture'), + `import { EnumType, IInterface, ClassName } from '.';\ndeclare const iface: IInterface\n/// here`, + 'utf-8', + ); + + // WHEN + await expect( + transliterateAssembly([tmpDir], Object.values(TargetLanguage), { + strict: true, + }), + ).resolves.not.toThrow(); + + // THEN + expect( + fs.readJsonSync(path.join(tmpDir, `${SPEC_FILE_NAME}.csharp`)), + ).toMatchInlineSnapshot( + { + fingerprint: expect.any(String), + jsiiVersion: expect.any(String), + }, + ` + Object { + "author": Object { + "name": "John Doe", + "roles": Array [ + "author", + ], + }, + "description": "testpkg", + "fingerprint": Any, + "homepage": "https://github.com/aws/jsii.git", + "jsiiVersion": Any, + "license": "Apache-2.0", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "testpkg", + "readme": Object { + "markdown": "# README + + \`\`\`csharp + // Example automatically generated. See https://github.com/aws/jsii/issues/826 + IInterface object = new ClassName(\\"this\\", 1337, new ClassNameProps { Foo = \\"bar\\" }); + object.Property = EnumType.OPTION_A; + object.MethodCall(); + + ClassName.StaticMethod(EnumType.OPTION_B); + \`\`\`", + }, + "repository": Object { + "type": "git", + "url": "https://github.com/aws/jsii.git", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "testpkg", + }, + }, + "types": Object { + "testpkg.ClassName": Object { + "assembly": "testpkg", + "fqn": "testpkg.ClassName", + "initializer": Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps { Property = EnumType.OPTION_B });", + "summary": "Create a new instance of ClassName.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 57, + }, + "parameters": Array [ + Object { + "name": "_this", + "type": Object { + "primitive": "string", + }, + }, + Object { + "name": "_elite", + "type": Object { + "primitive": "number", + }, + }, + Object { + "name": "props", + "type": Object { + "fqn": "testpkg.ClassNameProps", + }, + }, + ], + }, + "interfaces": Array [ + "testpkg.IInterface", + ], + "kind": "class", + "locationInModule": Object { + "filename": "index.ts", + "line": 40, + }, + "methods": Array [ + Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName.StaticMethod();", + "remarks": "It can be invoked easily.", + "summary": "A static method.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 46, + }, + "name": "staticMethod", + "parameters": Array [ + Object { + "name": "_enm", + "optional": true, + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + "static": true, + }, + Object { + "docs": Object { + "summary": "An instance method call.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 61, + }, + "name": "methodCall", + "overrides": "testpkg.IInterface", + }, + ], + "name": "ClassName", + "properties": Array [ + Object { + "docs": Object { + "summary": "A property value.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 50, + }, + "name": "property", + "overrides": "testpkg.IInterface", + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + "testpkg.ClassNameProps": Object { + "assembly": "testpkg", + "datatype": true, + "fqn": "testpkg.ClassNameProps", + "kind": "interface", + "locationInModule": Object { + "filename": "index.ts", + "line": 35, + }, + "name": "ClassNameProps", + "properties": Array [ + Object { + "abstract": true, + "immutable": true, + "locationInModule": Object { + "filename": "index.ts", + "line": 37, + }, + "name": "foo", + "optional": true, + "type": Object { + "primitive": "string", + }, + }, + Object { + "abstract": true, + "immutable": true, + "locationInModule": Object { + "filename": "index.ts", + "line": 36, + }, + "name": "property", + "optional": true, + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + "testpkg.EnumType": Object { + "assembly": "testpkg", + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps { Property = EnumType.OPTION_B });", + }, + "fqn": "testpkg.EnumType", + "kind": "enum", + "locationInModule": Object { + "filename": "index.ts", + "line": 5, + }, + "members": Array [ + Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps { Property = EnumType.OPTION_A });", + }, + "name": "OPTION_A", + }, + Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps { Property = EnumType.OPTION_B });", + }, + "name": "OPTION_B", + }, + ], + "name": "EnumType", + }, + "testpkg.IInterface": Object { + "assembly": "testpkg", + "fqn": "testpkg.IInterface", + "kind": "interface", + "locationInModule": Object { + "filename": "index.ts", + "line": 17, + }, + "methods": Array [ + Object { + "abstract": true, + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + iface.MethodCall();", + "summary": "An instance method call.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 32, + }, + "name": "methodCall", + }, + ], + "name": "IInterface", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + iface.Property = EnumType.OPTION_B;", + "summary": "A property value.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 24, + }, + "name": "property", + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + }, + "version": "0.0.1", + } + `, + ); + expect( + fs.readJsonSync(path.join(tmpDir, `${SPEC_FILE_NAME}.java`)), + ).toMatchInlineSnapshot( + { + fingerprint: expect.any(String), + jsiiVersion: expect.any(String), + }, + ` + Object { + "author": Object { + "name": "John Doe", + "roles": Array [ + "author", + ], + }, + "description": "testpkg", + "fingerprint": Any, + "homepage": "https://github.com/aws/jsii.git", + "jsiiVersion": Any, + "license": "Apache-2.0", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "testpkg", + "readme": Object { + "markdown": "# README + + \`\`\`java + // Example automatically generated. See https://github.com/aws/jsii/issues/826 + IInterface object = new ClassName(\\"this\\", 1337, new ClassNameProps().foo(\\"bar\\")); + object.getProperty() = EnumType.getOPTION_A(); + object.methodCall(); + + ClassName.staticMethod(EnumType.getOPTION_B()); + \`\`\`", + }, + "repository": Object { + "type": "git", + "url": "https://github.com/aws/jsii.git", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "testpkg", + }, + }, + "types": Object { + "testpkg.ClassName": Object { + "assembly": "testpkg", + "fqn": "testpkg.ClassName", + "initializer": Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps().property(EnumType.getOPTION_B()));", + "summary": "Create a new instance of ClassName.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 57, + }, + "parameters": Array [ + Object { + "name": "_this", + "type": Object { + "primitive": "string", + }, + }, + Object { + "name": "_elite", + "type": Object { + "primitive": "number", + }, + }, + Object { + "name": "props", + "type": Object { + "fqn": "testpkg.ClassNameProps", + }, + }, + ], + }, + "interfaces": Array [ + "testpkg.IInterface", + ], + "kind": "class", + "locationInModule": Object { + "filename": "index.ts", + "line": 40, + }, + "methods": Array [ + Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName.staticMethod();", + "remarks": "It can be invoked easily.", + "summary": "A static method.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 46, + }, + "name": "staticMethod", + "parameters": Array [ + Object { + "name": "_enm", + "optional": true, + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + "static": true, + }, + Object { + "docs": Object { + "summary": "An instance method call.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 61, + }, + "name": "methodCall", + "overrides": "testpkg.IInterface", + }, + ], + "name": "ClassName", + "properties": Array [ + Object { + "docs": Object { + "summary": "A property value.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 50, + }, + "name": "property", + "overrides": "testpkg.IInterface", + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + "testpkg.ClassNameProps": Object { + "assembly": "testpkg", + "datatype": true, + "fqn": "testpkg.ClassNameProps", + "kind": "interface", + "locationInModule": Object { + "filename": "index.ts", + "line": 35, + }, + "name": "ClassNameProps", + "properties": Array [ + Object { + "abstract": true, + "immutable": true, + "locationInModule": Object { + "filename": "index.ts", + "line": 37, + }, + "name": "foo", + "optional": true, + "type": Object { + "primitive": "string", + }, + }, + Object { + "abstract": true, + "immutable": true, + "locationInModule": Object { + "filename": "index.ts", + "line": 36, + }, + "name": "property", + "optional": true, + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + "testpkg.EnumType": Object { + "assembly": "testpkg", + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps().property(EnumType.getOPTION_B()));", + }, + "fqn": "testpkg.EnumType", + "kind": "enum", + "locationInModule": Object { + "filename": "index.ts", + "line": 5, + }, + "members": Array [ + Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps().property(EnumType.getOPTION_A()));", + }, + "name": "OPTION_A", + }, + Object { + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + new ClassName(\\"this\\", 1337, new ClassNameProps().property(EnumType.getOPTION_B()));", + }, + "name": "OPTION_B", + }, + ], + "name": "EnumType", + }, + "testpkg.IInterface": Object { + "assembly": "testpkg", + "fqn": "testpkg.IInterface", + "kind": "interface", + "locationInModule": Object { + "filename": "index.ts", + "line": 17, + }, + "methods": Array [ + Object { + "abstract": true, + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + iface.methodCall();", + "summary": "An instance method call.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 32, + }, + "name": "methodCall", + }, + ], + "name": "IInterface", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "example": "// Example automatically generated. See https://github.com/aws/jsii/issues/826 + iface.getProperty() = EnumType.getOPTION_B();", + "summary": "A property value.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 24, + }, + "name": "property", + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + }, + "version": "0.0.1", + } + `, + ); + expect( + fs.readJsonSync(path.join(tmpDir, `${SPEC_FILE_NAME}.python`)), + ).toMatchInlineSnapshot( + { + fingerprint: expect.any(String), + jsiiVersion: expect.any(String), + }, + ` + Object { + "author": Object { + "name": "John Doe", + "roles": Array [ + "author", + ], + }, + "description": "testpkg", + "fingerprint": Any, + "homepage": "https://github.com/aws/jsii.git", + "jsiiVersion": Any, + "license": "Apache-2.0", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "testpkg", + "readme": Object { + "markdown": "# README + + \`\`\`python + # Example automatically generated. See https://github.com/aws/jsii/issues/826 + object = ClassName(\\"this\\", 1337, foo=\\"bar\\") + object.property = EnumType.OPTION_A + object.method_call() + + ClassName.static_method(EnumType.OPTION_B) + \`\`\`", + }, + "repository": Object { + "type": "git", + "url": "https://github.com/aws/jsii.git", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "testpkg", + }, + }, + "types": Object { + "testpkg.ClassName": Object { + "assembly": "testpkg", + "fqn": "testpkg.ClassName", + "initializer": Object { + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName(\\"this\\", 1337, property=EnumType.OPTION_B)", + "summary": "Create a new instance of ClassName.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 57, + }, + "parameters": Array [ + Object { + "name": "_this", + "type": Object { + "primitive": "string", + }, + }, + Object { + "name": "_elite", + "type": Object { + "primitive": "number", + }, + }, + Object { + "name": "props", + "type": Object { + "fqn": "testpkg.ClassNameProps", + }, + }, + ], + }, + "interfaces": Array [ + "testpkg.IInterface", + ], + "kind": "class", + "locationInModule": Object { + "filename": "index.ts", + "line": 40, + }, + "methods": Array [ + Object { + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName.static_method()", + "remarks": "It can be invoked easily.", + "summary": "A static method.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 46, + }, + "name": "staticMethod", + "parameters": Array [ + Object { + "name": "_enm", + "optional": true, + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + "static": true, + }, + Object { + "docs": Object { + "summary": "An instance method call.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 61, + }, + "name": "methodCall", + "overrides": "testpkg.IInterface", + }, + ], + "name": "ClassName", + "properties": Array [ + Object { + "docs": Object { + "summary": "A property value.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 50, + }, + "name": "property", + "overrides": "testpkg.IInterface", + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + "testpkg.ClassNameProps": Object { + "assembly": "testpkg", + "datatype": true, + "fqn": "testpkg.ClassNameProps", + "kind": "interface", + "locationInModule": Object { + "filename": "index.ts", + "line": 35, + }, + "name": "ClassNameProps", + "properties": Array [ + Object { + "abstract": true, + "immutable": true, + "locationInModule": Object { + "filename": "index.ts", + "line": 37, + }, + "name": "foo", + "optional": true, + "type": Object { + "primitive": "string", + }, + }, + Object { + "abstract": true, + "immutable": true, + "locationInModule": Object { + "filename": "index.ts", + "line": 36, + }, + "name": "property", + "optional": true, + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + "testpkg.EnumType": Object { + "assembly": "testpkg", + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName(\\"this\\", 1337, property=EnumType.OPTION_B)", + }, + "fqn": "testpkg.EnumType", + "kind": "enum", + "locationInModule": Object { + "filename": "index.ts", + "line": 5, + }, + "members": Array [ + Object { + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName(\\"this\\", 1337, property=EnumType.OPTION_A)", + }, + "name": "OPTION_A", + }, + Object { + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + ClassName(\\"this\\", 1337, property=EnumType.OPTION_B)", + }, + "name": "OPTION_B", + }, + ], + "name": "EnumType", + }, + "testpkg.IInterface": Object { + "assembly": "testpkg", + "fqn": "testpkg.IInterface", + "kind": "interface", + "locationInModule": Object { + "filename": "index.ts", + "line": 17, + }, + "methods": Array [ + Object { + "abstract": true, + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + iface.method_call()", + "summary": "An instance method call.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 32, + }, + "name": "methodCall", + }, + ], + "name": "IInterface", + "properties": Array [ + Object { + "abstract": true, + "docs": Object { + "example": "# Example automatically generated. See https://github.com/aws/jsii/issues/826 + iface.property = EnumType.OPTION_B", + "summary": "A property value.", + }, + "locationInModule": Object { + "filename": "index.ts", + "line": 24, + }, + "name": "property", + "type": Object { + "fqn": "testpkg.EnumType", + }, + }, + ], + }, + }, + "version": "0.0.1", + } + `, + ); + })); + +async function withTemporaryDirectory( + callback: (dir: string) => Promise, +): Promise { + const tmpdir = fs.mkdtempSync( + path.join(os.tmpdir(), path.basename(__filename)), + ); + return callback(tmpdir).finally(() => fs.removeSync(tmpdir)); +} diff --git a/packages/jsii-rosetta/test/rosetta.test.ts b/packages/jsii-rosetta/test/rosetta.test.ts index c9bcb7e920..dadcc54130 100644 --- a/packages/jsii-rosetta/test/rosetta.test.ts +++ b/packages/jsii-rosetta/test/rosetta.test.ts @@ -19,11 +19,14 @@ test('Rosetta object can do live translation', () => { // GIVEN const rosetta = new Rosetta({ liveConversion: true, - targetLanguages: ['python'], + targetLanguages: [TargetLanguage.PYTHON], }); // WHEN - const translated = rosetta.translateSnippet(SAMPLE_CODE, 'python'); + const translated = rosetta.translateSnippet( + SAMPLE_CODE, + TargetLanguage.PYTHON, + ); // THEN expect(translated).toMatchObject({ @@ -47,7 +50,10 @@ test('Can use preloaded tablet', () => { rosetta.addTablet(tablet); // WHEN - const translated = rosetta.translateSnippet(SAMPLE_CODE, 'python'); + const translated = rosetta.translateSnippet( + SAMPLE_CODE, + TargetLanguage.PYTHON, + ); // THEN expect(translated).toMatchObject({ @@ -60,11 +66,14 @@ test('Rosetta object can do live translation', () => { // GIVEN const rosetta = new Rosetta({ liveConversion: true, - targetLanguages: ['python'], + targetLanguages: [TargetLanguage.PYTHON], }); // WHEN - const translated = rosetta.translateSnippet(SAMPLE_CODE, 'python'); + const translated = rosetta.translateSnippet( + SAMPLE_CODE, + TargetLanguage.PYTHON, + ); // THEN expect(translated).toMatchObject({ @@ -77,7 +86,7 @@ test('Rosetta object can do translation and annotation of snippets in MarkDown', // GIVEN const rosetta = new Rosetta({ liveConversion: true, - targetLanguages: ['python'], + targetLanguages: [TargetLanguage.PYTHON], }); // WHEN @@ -91,7 +100,7 @@ test('Rosetta object can do translation and annotation of snippets in MarkDown', '```', 'That was it, thank you for your attention.', ].join('\n'), - 'python', + TargetLanguage.PYTHON, false, (trans) => { return { @@ -142,7 +151,10 @@ describe('with mocked filesystem', () => { // WHEN const rosetta = new Rosetta(); await rosetta.loadTabletFromFile('/test.tablet'); - const translated = rosetta.translateSnippet(SAMPLE_CODE, 'python'); + const translated = rosetta.translateSnippet( + SAMPLE_CODE, + TargetLanguage.PYTHON, + ); // THEN expect(translated).toMatchObject({ @@ -158,7 +170,10 @@ describe('with mocked filesystem', () => { // WHEN const rosetta = new Rosetta(); await rosetta.addAssembly(fakeAssembly({}), '/'); - const translated = rosetta.translateSnippet(SAMPLE_CODE, 'python'); + const translated = rosetta.translateSnippet( + SAMPLE_CODE, + TargetLanguage.PYTHON, + ); // THEN expect(translated).toMatchObject({