Skip to content

Commit 7a6278a

Browse files
authored
feat: Document overriden/inherited members (#238)
Add a documentation line outlining the parent declaration of overridden members as well as inherited members that are not overridden locally. This should make browsing the documentation a lot nicer. Inherited and overridden statements in the documentation always refer to the fully qualified name of the super statement. Fixes #196
1 parent 30d1491 commit 7a6278a

File tree

5 files changed

+668
-37
lines changed

5 files changed

+668
-37
lines changed

packages/jsii-pacmak/bin/jsii-pacmak.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { VERSION_DESC } from '../lib/version';
1414
const targetConstructors = await Target.findAll();
1515
const argv = yargs
1616
.usage('Usage: jsii-pacmak [-t target,...] [-o outdir] [package-dir]')
17+
.env('JSII_PACMAK')
1718
.option('targets', {
1819
alias: ['target', 't'],
1920
type: 'array',
@@ -47,6 +48,11 @@ import { VERSION_DESC } from '../lib/version';
4748
desc: 'force generation of new artifacts, even if the fingerprints match',
4849
default: false
4950
})
51+
.option('force-subdirectory', {
52+
type: 'boolean',
53+
desc: 'force generation into a target-named subdirectory, even in single-target mode',
54+
default: false
55+
})
5056
.option('recurse', {
5157
alias: 'R',
5258
type: 'boolean',
@@ -80,9 +86,9 @@ import { VERSION_DESC } from '../lib/version';
8086
const rootDir = path.resolve(process.cwd(), argv._[0] || '.');
8187

8288
const visited = new Set<string>();
83-
await buildPackage(rootDir);
89+
await buildPackage(rootDir, true /* isRoot */, argv.forceSubdirectory);
8490

85-
async function buildPackage(packageDir: string, isRoot = true) {
91+
async function buildPackage(packageDir: string, isRoot: boolean, forceSubdirectory: boolean) {
8692
if (visited.has(packageDir)) {
8793
return; // already built
8894
}
@@ -103,7 +109,7 @@ import { VERSION_DESC } from '../lib/version';
103109
if (argv.recurse) {
104110
for (const dep of Object.keys(pkg.dependencies || { })) {
105111
const depDir = resolveDependencyDirectory(packageDir, dep);
106-
await buildPackage(depDir, /* isRoot */ false);
112+
await buildPackage(depDir, /* isRoot */ false, forceSubdirectory);
107113
}
108114
}
109115

@@ -127,7 +133,7 @@ import { VERSION_DESC } from '../lib/version';
127133
const tarball = await npmPack(packageDir, tmpdir);
128134
for (const targetName of targets) {
129135
// if we are targeting a single language, output to outdir, otherwise outdir/<target>
130-
const targetOutputDir = targets.length > 1 ? path.join(outDir, targetName) : outDir;
136+
const targetOutputDir = (targets.length > 1 || forceSubdirectory) ? path.join(outDir, targetName) : outDir;
131137
logging.debug(`Building ${pkg.name}/${targetName}: ${targetOutputDir}`);
132138
await generateTarget(packageDir, targetName, targetOutputDir, tarball);
133139
}

packages/jsii-pacmak/lib/targets/sphinx.ts

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ class SphinxDocsGenerator extends Generator {
233233
this.namespaceStack.push({ name: className, underClass: true });
234234
}
235235

236-
protected onEndClass(_cls: spec.ClassType) {
236+
protected onEndClass(cls: spec.ClassType) {
237+
this.renderInheritedMembers(cls);
237238
this.code.closeBlock();
238239
this.namespaceStack.pop();
239240
if (!this.topNamespace.underClass) { this.closeSection(); }
@@ -342,7 +343,8 @@ class SphinxDocsGenerator extends Generator {
342343
this.code.line();
343344
}
344345

345-
protected onEndInterface(_ifc: spec.InterfaceType) {
346+
protected onEndInterface(ifc: spec.InterfaceType) {
347+
this.renderInheritedMembers(ifc);
346348
this.code.closeBlock();
347349
if (!this.topNamespace.underClass) { this.closeSection(); }
348350
}
@@ -359,6 +361,62 @@ class SphinxDocsGenerator extends Generator {
359361
this.renderProperty(property);
360362
}
361363

364+
private renderInheritedMembers(entity: spec.ClassType | spec.InterfaceType) {
365+
const inherited = this.getInheritedMembers(entity);
366+
if (Object.keys(inherited).length === 0) { return; }
367+
for (const source of Object.keys(inherited).sort()) {
368+
const entities = inherited[source];
369+
for (const method of entities.methods) {
370+
this.renderMethod(method, source);
371+
for (const overload of this.createOverloadsForOptionals(method)) {
372+
this.renderMethod(overload, source);
373+
}
374+
}
375+
for (const property of entities.properties) {
376+
this.renderProperty(property, source);
377+
}
378+
}
379+
}
380+
381+
private getInheritedMembers(entity: spec.ClassType | spec.InterfaceType): InheritedMembers {
382+
const parents = parentTypes(entity);
383+
const knownMembers = new Set<string>([
384+
...(entity.methods || []).map(m => m.name!),
385+
...(entity.properties || []).map(p => p.name)
386+
]);
387+
const result: InheritedMembers = {};
388+
for (const parent of parents) {
389+
const parentType = this.findType(parent.fqn) as spec.ClassType | spec.InterfaceType;
390+
for (const method of parentType.methods || []) {
391+
if (method.static || knownMembers.has(method.name!)) { continue; }
392+
result[parentType.fqn] = result[parentType.fqn] || { methods: [], properties: [] };
393+
result[parentType.fqn].methods.push(method);
394+
knownMembers.add(method.name!);
395+
}
396+
for (const property of parentType.properties || []) {
397+
if (property.static || knownMembers.has(property.name!)) { continue; }
398+
result[parentType.fqn] = result[parentType.fqn] || { methods: [], properties: [] };
399+
result[parentType.fqn].properties.push(property);
400+
knownMembers.add(property.name);
401+
}
402+
for (const superType of parentTypes(parentType)) {
403+
parents.push(superType);
404+
}
405+
}
406+
return result;
407+
408+
function parentTypes(type: spec.ClassType | spec.InterfaceType) {
409+
const types = new Array<spec.NamedTypeReference>();
410+
if (spec.isClassType(type) && type.base) {
411+
types.push(type.base);
412+
}
413+
if (type.interfaces) {
414+
types.push(...type.interfaces);
415+
}
416+
return types;
417+
}
418+
}
419+
362420
/**
363421
* Adds a title to the current code file, using the appropriate header
364422
* adornment given the current TOC depth. It will start using simple
@@ -402,7 +460,7 @@ class SphinxDocsGenerator extends Generator {
402460
signature += ', ';
403461
}
404462

405-
if (p.type.optional) {
463+
if (p.type.optional && !params.slice(idx + 1).find(e => !e.type.optional)) {
406464
signature += '[';
407465
signaturePosfix += ']';
408466
}
@@ -437,22 +495,40 @@ class SphinxDocsGenerator extends Generator {
437495
}
438496
}
439497

440-
private renderMethod(method: spec.Method) {
498+
private renderMethod(method: spec.Method, inheritedFrom?: string) {
441499
const signature = this.renderMethodSignature(method);
442500

443501
const type = method.static ? `py:staticmethod` : `py:method`;
444502

445503
this.code.line();
446504
this.code.openBlock(`.. ${type}:: ${method.name}${signature}`);
447505

506+
if (inheritedFrom) {
507+
this.code.line();
508+
this.code.line(`*Inherited from* :py:meth:\`${inheritedFrom} <${inheritedFrom}.${method.name}>\``);
509+
} else if (method.overrides) {
510+
this.code.line();
511+
const superType = this.findType(method.overrides.fqn) as spec.ClassType | spec.InterfaceType;
512+
if (spec.isInterfaceType(superType) || superType.methods!.find(m => m.name === method.name && !!m.abstract)) {
513+
this.code.line(`*Implements* :py:meth:\`${method.overrides.fqn}.${method.name}\``);
514+
} else {
515+
this.code.line(`*Overrides* :py:meth:\`${method.overrides.fqn}.${method.name}\``);
516+
}
517+
}
448518
this.renderDocsLine(method);
449519
this.code.line();
520+
if (method.protected) {
521+
this.code.line('*Protected method*');
522+
this.code.line();
523+
}
450524

451525
this.renderMethodParameters(method);
452526

453527
// @return doc
454528
if (method.docs && method.docs.return) {
455-
this.code.line(`:return: ${method.docs.return}`);
529+
const [firstLine, ...rest] = method.docs.return.split('\n');
530+
this.code.line(`:return: ${firstLine}`);
531+
rest.forEach(line => this.code.line(` ${line}`));
456532
}
457533

458534
if (method.returns) {
@@ -542,7 +618,7 @@ class SphinxDocsGenerator extends Generator {
542618
} else {
543619
throw new Error('Unexpected type ref');
544620
}
545-
if (type.optional) { result.ref = `${result.ref} or undefined`; }
621+
if (type.optional) { result.ref = `${result.ref} or \`\`undefined\`\``; }
546622
return result;
547623

548624
// Wrap a string between parenthesis if it contains " or "
@@ -552,12 +628,28 @@ class SphinxDocsGenerator extends Generator {
552628
}
553629
}
554630

555-
private renderProperty(prop: spec.Property) {
631+
private renderProperty(prop: spec.Property, inheritedFrom?: string) {
556632
this.code.line();
557633
const type = this.renderTypeRef(prop.type);
558634
this.code.openBlock(`.. py:attribute:: ${prop.name}`);
635+
if (inheritedFrom) {
636+
this.code.line();
637+
this.code.line(`*Inherited from* :py:attr:\`${inheritedFrom} <${inheritedFrom}.${prop.name}>\``);
638+
} else if (prop.overrides) {
639+
this.code.line();
640+
const superType = this.findType(prop.overrides.fqn) as spec.ClassType | spec.InterfaceType;
641+
if (spec.isInterfaceType(superType) || superType.properties!.find(p => p.name === prop.name && !!p.abstract)) {
642+
this.code.line(`*Implements* :py:meth:\`${prop.overrides.fqn}.${prop.name}\``);
643+
} else {
644+
this.code.line(`*Overrides* :py:attr:\`${prop.overrides.fqn}.${prop.name}\``);
645+
}
646+
}
559647
this.renderDocsLine(prop);
560648
this.code.line();
649+
if (prop.protected) {
650+
this.code.line('*Protected property*');
651+
this.code.line();
652+
}
561653
const readonly = prop.immutable ? ' *(readonly)*' : '';
562654
const abs = prop.abstract ? ' *(abstract)*' : '';
563655
const stat = prop.static ? ' *(static)*' : '';
@@ -665,3 +757,5 @@ function formatLanguage(language: string): string {
665757
return language;
666758
}
667759
}
760+
761+
type InheritedMembers = { [typeFqn: string]: { methods: spec.Method[], properties: spec.Property[] } };

packages/jsii-pacmak/test/expected.jsii-calc-base/sphinx/_scope_jsii-calc-base.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,10 @@ BaseProps (interface)
196196
:type: string *(abstract)*
197197

198198

199+
.. py:attribute:: foo
200+
201+
*Inherited from* :py:attr:`@scope/jsii-calc-base-of-base.VeryBaseProps <@scope/jsii-calc-base-of-base.VeryBaseProps.foo>`
202+
203+
:type: :py:class:`@scope/jsii-calc-base-of-base.Very`\ *(abstract)*
204+
205+

packages/jsii-pacmak/test/expected.jsii-calc-lib/sphinx/_scope_jsii-calc-lib.rst

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ MyFirstStruct (interface)
249249

250250
.. py:attribute:: firstOptional
251251
252-
:type: string[] or undefined *(abstract)*
252+
:type: string[] or ``undefined`` *(abstract)*
253253

254254

255255
Number
@@ -296,12 +296,32 @@ Number
296296

297297
.. py:attribute:: value
298298
299+
*Implements* :py:meth:`@scope/jsii-calc-lib.Value.value`
300+
299301
The number.
300302

301303

302304
:type: number *(readonly)*
303305

304306

307+
.. py:method:: typeName() -> any
308+
309+
*Inherited from* :py:meth:`@scope/jsii-calc-base.Base <@scope/jsii-calc-base.Base.typeName>`
310+
311+
:return: the name of the class (to verify native type names are created for derived classes).
312+
:rtype: any
313+
314+
315+
.. py:method:: toString() -> string
316+
317+
*Inherited from* :py:meth:`@scope/jsii-calc-lib.Value <@scope/jsii-calc-lib.Value.toString>`
318+
319+
String representation of the value.
320+
321+
322+
:rtype: string
323+
324+
305325
Operation
306326
^^^^^^^^^
307327

@@ -337,13 +357,33 @@ Operation
337357

338358
.. py:method:: toString() -> string
339359
360+
*Overrides* :py:meth:`@scope/jsii-calc-lib.Value.toString`
361+
340362
String representation of the value.
341363

342364

343365
:rtype: string
344366
:abstract: Yes
345367

346368

369+
.. py:method:: typeName() -> any
370+
371+
*Inherited from* :py:meth:`@scope/jsii-calc-base.Base <@scope/jsii-calc-base.Base.typeName>`
372+
373+
:return: the name of the class (to verify native type names are created for derived classes).
374+
:rtype: any
375+
376+
377+
.. py:attribute:: value
378+
379+
*Inherited from* :py:attr:`@scope/jsii-calc-lib.Value <@scope/jsii-calc-lib.Value.value>`
380+
381+
The value.
382+
383+
384+
:type: number *(readonly)* *(abstract)*
385+
386+
347387
StructWithOnlyOptionals (interface)
348388
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
349389

@@ -381,17 +421,17 @@ StructWithOnlyOptionals (interface)
381421
The first optional!
382422

383423

384-
:type: string or undefined *(abstract)*
424+
:type: string or ``undefined`` *(abstract)*
385425

386426

387427
.. py:attribute:: optional2
388428
389-
:type: number or undefined *(abstract)*
429+
:type: number or ``undefined`` *(abstract)*
390430

391431

392432
.. py:attribute:: optional3
393433
394-
:type: boolean or undefined *(abstract)*
434+
:type: boolean or ``undefined`` *(abstract)*
395435

396436

397437
Value
@@ -443,3 +483,11 @@ Value
443483
:type: number *(readonly)* *(abstract)*
444484

445485

486+
.. py:method:: typeName() -> any
487+
488+
*Inherited from* :py:meth:`@scope/jsii-calc-base.Base <@scope/jsii-calc-base.Base.typeName>`
489+
490+
:return: the name of the class (to verify native type names are created for derived classes).
491+
:rtype: any
492+
493+

0 commit comments

Comments
 (0)