Skip to content

Commit

Permalink
feat(rosetta): translate examples to Java and C# (#985)
Browse files Browse the repository at this point in the history
Update `rosetta` to also emit translations for Java and C#. Testing is
now done differently, by dropping source files into the
`test/translations` directory for each target language.

`pacmak` for C# now generates a `NamespaceDoc` class (which is hidden
from the IDE as much as possible) which contains the (`README`) module
documentation. This can be picked up by the doc generator to generate
namespace-level documentation.

Examples can be compiled in a special suite, and the translated examples
will be annotated depending on whether the example source successfully
compiled or not.

Fixes summary extraction for some variants of docstrings.
  • Loading branch information
skinny85 authored and rix0rrr committed Dec 24, 2019
1 parent 745ad10 commit d591b85
Show file tree
Hide file tree
Showing 716 changed files with 5,520 additions and 3,710 deletions.
5 changes: 3 additions & 2 deletions packages/@scope/jsii-calc-lib/test/assembly.jsii
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@
"@scope/jsii-calc-lib.EnumFromScopedModule": {
"assembly": "@scope/jsii-calc-lib",
"docs": {
"remarks": "See awslabs/jsii#138",
"stability": "deprecated",
"summary": "Check that enums from \\@scoped packages can be references. See awslabs/jsii#138."
"summary": "Check that enums from \\@scoped packages can be references."
},
"fqn": "@scope/jsii-calc-lib.EnumFromScopedModule",
"kind": "enum",
Expand Down Expand Up @@ -539,5 +540,5 @@
}
},
"version": "0.20.11",
"fingerprint": "D1VJJVqcvEKInLQ3r/WxIN332Yo1IRlVWkCPByyIIb0="
"fingerprint": "WSHD7tywHgFC9jmImvDuy7NGYCDWAAb49jk0ZPzCoFM="
}
18 changes: 11 additions & 7 deletions packages/jsii-calc/test/assembly.jsii
Original file line number Diff line number Diff line change
Expand Up @@ -7353,9 +7353,9 @@
"abstract": true,
"docs": {
"default": "256",
"remarks": "This default is set in the underlying FargateTaskDefinition construct.",
"remarks": "Valid values, which determines your range of valid values for the memory parameter:\n256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB\n512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB\n1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB\n2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments\n4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments\n\nThis default is set in the underlying FargateTaskDefinition construct.",
"stability": "experimental",
"summary": "The number of cpu units used by the task. Valid values, which determines your range of valid values for the memory parameter: 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments."
"summary": "The number of cpu units used by the task."
},
"immutable": true,
"locationInModule": {
Expand Down Expand Up @@ -8742,8 +8742,9 @@
},
{
"docs": {
"remarks": "Must be implemented by derived classes.",
"stability": "experimental",
"summary": "The expression that this operation consists of. Must be implemented by derived classes."
"summary": "The expression that this operation consists of."
},
"immutable": true,
"locationInModule": {
Expand Down Expand Up @@ -10108,8 +10109,9 @@
},
{
"docs": {
"remarks": "Jsdocs for static setter.",
"stability": "experimental",
"summary": "Jsdocs for static getter. Jsdocs for static setter."
"summary": "Jsdocs for static getter."
},
"locationInModule": {
"filename": "lib/compliance.ts",
Expand Down Expand Up @@ -10651,8 +10653,9 @@
"properties": [
{
"docs": {
"remarks": "Must be implemented by derived classes.",
"stability": "experimental",
"summary": "The expression that this operation consists of. Must be implemented by derived classes."
"summary": "The expression that this operation consists of."
},
"immutable": true,
"locationInModule": {
Expand Down Expand Up @@ -11973,8 +11976,9 @@
{
"abstract": true,
"docs": {
"remarks": "Must be implemented by derived classes.",
"stability": "experimental",
"summary": "The expression that this operation consists of. Must be implemented by derived classes."
"summary": "The expression that this operation consists of."
},
"immutable": true,
"locationInModule": {
Expand Down Expand Up @@ -12089,5 +12093,5 @@
}
},
"version": "0.20.11",
"fingerprint": "2idHJnE1Vv8YAFFrhqRzB2SwAvTXdSFkD7jKYcbabX4="
"fingerprint": "5M7u8+bWxu4amY/gs4Z/K4IpYqJCMGtnQiVoerS4uac="
}
2 changes: 1 addition & 1 deletion packages/jsii-pacmak/lib/targets/dotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export default class Dotnet extends Target {
public constructor(options: TargetOptions, assembliesCurrentlyBeingCompiled: string[]) {
super(options);

this.generator = new DotNetGenerator(assembliesCurrentlyBeingCompiled);
this.generator = new DotNetGenerator(assembliesCurrentlyBeingCompiled, options.rosetta);
}

// eslint-disable-next-line @typescript-eslint/require-await
Expand Down
123 changes: 89 additions & 34 deletions packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { CodeMaker } from 'codemaker';
import * as spec from '@jsii/spec';
import * as xmlbuilder from 'xmlbuilder';
import { DotNetNameUtils } from './nameutils';
import { prefixMarkdownTsCodeBlocks } from '../../util';
import { Rosetta, Translation, typeScriptSnippetFromSource, markDownToXmlDoc } from 'jsii-rosetta';
import { INCOMPLETE_DISCLAIMER_COMPILING, INCOMPLETE_DISCLAIMER_NONCOMPILING } from '..';


const SAMPLES_DISCLAIMER = '// This example is in TypeScript, examples in C# are coming soon.';

/**
* Generates the Jsii attributes and calls for the .NET runtime
*
Expand All @@ -16,7 +15,7 @@ export class DotNetDocGenerator {
private readonly code: CodeMaker;
private readonly nameutils: DotNetNameUtils = new DotNetNameUtils();

public constructor(code: CodeMaker) {
public constructor(code: CodeMaker, private readonly rosetta: Rosetta) {
this.code = code;
}

Expand All @@ -30,7 +29,7 @@ export class DotNetDocGenerator {
* Returns
* Remarks (includes examples, links, deprecated)
*/
public emitDocs(obj: spec.Method | spec.InterfaceType | spec.ClassType | spec.Property | spec.EnumType | spec.Initializer): void {
public emitDocs(obj: spec.Documentable): void {
const docs = obj.docs;

// The docs may be undefined at the method level but not the parameters level
Expand All @@ -46,7 +45,7 @@ export class DotNetDocGenerator {
});
}

// At this point we only need a valid instance of docs
// At this pdocfx namespacedocd a valid instance of docs
if (!docs) {
return;
}
Expand All @@ -55,51 +54,100 @@ export class DotNetDocGenerator {
this.emitXmlDoc('returns', docs.returns);
}

const remarks = xmlbuilder.create('remarks', { headless: true });
if (docs.remarks) {
remarks.text(`\n${prefixMarkdownTsCodeBlocks(docs.remarks, SAMPLES_DISCLAIMER)}\n`);
// Remarks does not use emitXmlDoc() because the remarks can contain code blocks
// which are fenced with <code> tags, which would be escaped to
// &lt;code&gt; if we used the xml builder.
const remarks = this.renderRemarks(docs);
if (remarks.length > 0) {
this.code.line('/// <remarks>');
remarks.forEach(r => this.code.line(`/// ${r}`));
this.code.line('/// </remarks>');
}

if (docs.default) {
remarks.text(`\ndefault:\n${docs.default}\n`);
if (docs.example) {
this.code.line('/// <example>');
this.emitXmlDoc('code', this.convertExample(docs.example));
this.code.line('/// </example>');
}
}

if (docs.stability) {
remarks.text(`\nstability: ${this.nameutils.capitalizeWord(docs.stability)}\n`);
}
public emitMarkdownAsRemarks(markdown: string | undefined) {
if (!markdown) { return; }

if (docs.example) {
remarks.text('\nexample:\n');
remarks.ele('code')
.text('\n// Examples in C# are coming soon.\n')
.text(`${docs.example}\n`);
remarks.text('\n');
const translated = markDownToXmlDoc(this.convertSamplesInMarkdown(markdown));
const lines = translated.split('\n');

this.code.line('/// <remarks>');
for (const line of lines) {
this.code.line(`/// ${line}`);
}
this.code.line('/// </remarks>');
}

/**
* Returns the lines that should go into the <remarks> section
*/
private renderRemarks(docs: spec.Docs): string[] {
const ret: string[] = [];

if (docs.see) {
remarks.text(`\nsee:\n${docs.see}\n`);
if (docs.remarks) {
const translated = markDownToXmlDoc(this.convertSamplesInMarkdown(docs.remarks));
ret.push(...translated.split('\n'));
ret.push('');
}

if (docs.subclassable) {
remarks.text('\nsubclassable\n');
// All the "tags" need to be rendered with empyt lines between them or they'll be word wrapped.

if (docs.default) { emitDocAttribute('default', docs.default); }
if (docs.stability) { emitDocAttribute('stability', this.nameutils.capitalizeWord(docs.stability)); }
if (docs.see) { emitDocAttribute('see', docs.see); }
if (docs.subclassable) { emitDocAttribute('subclassable', ''); }
for (const [k, v] of Object.entries(docs.custom || {})) {
const extraSpace = k === 'link' ? ' ' : ''; // Extra space for '@link' to keep unit tests happy
emitDocAttribute(k, v + extraSpace);
}

if (docs.custom) {
for (const [k, v] of Object.entries(docs.custom ?? {})) {
const custom = k === 'link' ? `${k}: ${v} ` : `${k}: ${v}`; // Extra space for '@link' to keep unit tests happy
remarks.text(`\n${custom}\n`);
}
// Remove leading and trailing empty lines
while (ret.length > 0 && ret[0] === '') { ret.shift(); }
while (ret.length > 0 && ret[ret.length - 1] === '') { ret.pop(); }

return ret;

function emitDocAttribute(name: string, contents: string) {
const ls = contents.split('\n');
ret.push(`<strong>${ucFirst(name)}</strong>: ${ls[0]}`);
ret.push(...ls.slice(1));
ret.push('');
}
}

private convertExample(example: string): string {
const snippet = typeScriptSnippetFromSource(example, 'example');
const translated = this.rosetta.translateSnippet(snippet, 'csharp');
if (!translated) { return example; }
return this.prefixDisclaimer(translated);
}

private convertSamplesInMarkdown(markdown: string): string {
return this.rosetta.translateSnippetsInMarkdown(markdown, 'csharp', trans => ({
language: trans.language,
source: this.prefixDisclaimer(trans)
}));
}

const remarksXml = remarks.end({ allowEmpty: true });
if (remarksXml !== '<remarks></remarks>') {
for (const line of remarksXml.split('\n')) {
this.code.line(`/// ${line}`);
}
private prefixDisclaimer(translated: Translation) {
if (translated.didCompile && INCOMPLETE_DISCLAIMER_COMPILING) {
return `// ${INCOMPLETE_DISCLAIMER_COMPILING}\n${translated.source}`;
}
if (!translated.didCompile && INCOMPLETE_DISCLAIMER_NONCOMPILING) {
return `// ${INCOMPLETE_DISCLAIMER_NONCOMPILING}\n${translated.source}`;
}
return translated.source;
}

private emitXmlDoc(tag: string, content: string, { attributes = {} }: { attributes?: { [name: string]: string } } = {}): void {
if (!content) { return; }

const xml = xmlbuilder.create(tag, { headless: true })
.text(content);
for (const [name, value] of Object.entries(attributes)) {
Expand All @@ -112,3 +160,10 @@ export class DotNetDocGenerator {
}
}
}

/**
* Uppercase the first letter
*/
function ucFirst(x: string) {
return x.substr(0, 1).toUpperCase() + x.substr(1);
}
45 changes: 43 additions & 2 deletions packages/jsii-pacmak/lib/targets/dotnet/dotnetgenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DotNetRuntimeGenerator } from './dotnetruntimegenerator';
import { DotNetTypeResolver } from './dotnettyperesolver';
import { FileGenerator } from './filegenerator';
import { DotNetNameUtils } from './nameutils';
import { Rosetta } from 'jsii-rosetta';

/**
* CODE GENERATOR V2
Expand All @@ -28,7 +29,7 @@ export class DotNetGenerator extends Generator {

private dotnetDocGenerator!: DotNetDocGenerator;

public constructor(private readonly assembliesCurrentlyBeingCompiled: string[]) {
public constructor(private readonly assembliesCurrentlyBeingCompiled: string[], private readonly rosetta: Rosetta) {
super();

// Override the openBlock to get a correct C# looking code block with the curly brace after the line
Expand All @@ -54,7 +55,9 @@ export class DotNetGenerator extends Generator {
);

this.dotnetRuntimeGenerator = new DotNetRuntimeGenerator(this.code, this.typeresolver);
this.dotnetDocGenerator = new DotNetDocGenerator(this.code);
this.dotnetDocGenerator = new DotNetDocGenerator(this.code, this.rosetta);

this.emitNamespaceDocs();

// We need to resolve the dependency tree
this.typeresolver.resolveNamespacesDependencies();
Expand Down Expand Up @@ -253,13 +256,15 @@ export class DotNetGenerator extends Generator {
this.code.line('/// <summary>Used by jsii to construct an instance of this class from a Javascript-owned object reference</summary>');
this.code.line('/// <param name="reference">The Javascript-owned object reference</param>');
this.dotnetRuntimeGenerator.emitDeprecatedAttributeIfNecessary(initializer);
this.emitHideAttribute();
this.code.openBlock(`protected ${className}(ByRefValue reference): base(reference)`);
this.code.closeBlock();
this.code.line();

this.code.line('/// <summary>Used by jsii to construct an instance of this class from DeputyProps</summary>');
this.code.line('/// <param name="props">The deputy props</param>');
this.dotnetRuntimeGenerator.emitDeprecatedAttributeIfNecessary(initializer);
this.emitHideAttribute();
this.code.openBlock(`protected ${className}(DeputyProps props): base(props)`);
this.code.closeBlock();

Expand Down Expand Up @@ -785,4 +790,40 @@ export class DotNetGenerator extends Generator {
this.firstMemberWritten = false;
}
}

/**
* Emit an unused, empty class called `NamespaceDoc` to attach the module README to
*
* There is no way to attach doc comments to a namespace in C#, and this trick has been
* semi-standardized by NDoc and Sandcastle Help File Builder.
*
* DocFX doesn't support it out of the box, but we should be able to get there with a
* bit of hackery.
*
* In any case, we need a place to attach the docs where they can be transported around,
* might as well be this method.
*/
private emitNamespaceDocs() {
if (!this.assembly.readme) { return; }

const namespace = this.assembly.targets!.dotnet!.namespace;
const className = 'NamespaceDoc';
this.openFileIfNeeded(className, namespace, false, false);

this.dotnetDocGenerator.emitMarkdownAsRemarks(this.assembly.readme.markdown);
this.emitHideAttribute();
// Traditionally this class is made 'internal', but that interacts poorly with DocFX's default filters
// which aren't overridable. So we make it public, but use attributes to hide it from users' IntelliSense,
// so that we can access the class in DocFX.
this.code.openBlock(`public class ${className}`);
this.code.closeBlock();
this.closeFileIfNeeded(className, namespace, false);
}

/**
* Emit an attribute that will hide the subsequent API element from users
*/
private emitHideAttribute() {
this.code.line('[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]');
}
}
3 changes: 2 additions & 1 deletion packages/jsii-pacmak/lib/targets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export const ALL_BUILDERS: {[key in TargetName]: BuilderFactory} = {
ruby: (ms, o) => new OneByOneBuilder('ruby', Ruby, ms, o),
};


export const INCOMPLETE_DISCLAIMER_COMPILING = 'Example automatically generated. See https://github.com/aws/jsii/issues/826';
export const INCOMPLETE_DISCLAIMER_NONCOMPILING = 'Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826';
Loading

0 comments on commit d591b85

Please sign in to comment.