Skip to content

Commit

Permalink
fix(jsii): correctly inherit initializer stability (#569)
Browse files Browse the repository at this point in the history
Fix the inheriting of stability tags for the initializer. It used to
inherit from the package, it now correctly inherits from the class.

When member and class stability disagree, use the one that indicates
the fewest guarantees.

Update jsii-reflect with a "load all modules" method (to save time
in a complex dependency tree) and jsii-tree with options to show
stabilities.
  • Loading branch information
rix0rrr committed Jul 1, 2019
1 parent a2116e9 commit a4de2d8
Show file tree
Hide file tree
Showing 15 changed files with 991 additions and 711 deletions.
14 changes: 11 additions & 3 deletions packages/jsii-reflect/bin/jsii-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@ import { TypeSystem, TypeSystemTree } from '../lib';

async function main() {
const options = yargs
.usage('$0 <JSII-FILE | MODULE-DIR...>', 'Prints an ASCII tree representation of a jsii type system.', args => args
.usage('$0 [JSII-FILE | MODULE-DIR...]', 'Prints an ASCII tree representation of a jsii type system.', args => args
.positional('JSII-FILE', { type: 'string', desc: 'path to a .jsii file to load, all dependency .jsii files must be explicitly supplied' })
.positional('MODULE-DIR', { type: 'string', desc: 'path to an jsii npm module directory, all jsii dependencies will be loaded transitively' }))
.option('closure', { type: 'string', alias: 'c', desc: 'Load dependencies of package without assuming its a JSII package itself' })
.option('all', { type: 'boolean', alias: 'a', desc: 'show all details', default: false })
.option('colors', { type: 'boolean', desc: 'enable/disable ANSI colors in output', default: true })
.option('dependencies', { type: 'boolean', alias: 'd', desc: 'show assembly dependencies', default: false })
.option('inheritance', { type: 'boolean', alias: 'i', desc: 'show base classes and implemented interfaces', default: false })
.option('members', { type: 'boolean', alias: 'm', desc: 'show type members', default: false })
.option('signatures', { type: 'boolean', alias: 's', desc: 'show method and property signatures', default: false })
.option('types', { type: 'boolean', alias: 't', desc: 'show types', default: false })
.option('validate', { type: 'boolean', alias: 'V', desc: 'Validate that assemblies match schema while loading', default: true })
.option('stabilities', { type: 'boolean', alias: 'S', desc: 'Show stabilities', default: false })
.argv;

const typesys = new TypeSystem();

for (const fileOrDirectory of options.jsiiFile as string[]) {
await typesys.load(fileOrDirectory);
if (options.closure) {
await typesys.loadNpmDependencies(options.closure, { validate: options.validate });
}

for (const fileOrDirectory of (options.jsiiFile as string[] || [])) {
await typesys.load(fileOrDirectory, { validate: options.validate });
}

const tst = new TypeSystemTree(typesys, {
Expand All @@ -30,6 +37,7 @@ async function main() {
members: options.members || options.all,
inheritance: options.inheritance || options.all,
signatures: options.signatures || options.all,
stabilities: options.stabilities || options.all,
colors: options.colors
});

Expand Down
24 changes: 16 additions & 8 deletions packages/jsii-reflect/lib/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,15 @@ export class ClassType extends ReferenceType {
* @param inherited include all properties inherited from base classes (default: false)
*/
public getProperties(inherited = false): {[name: string]: Property} {
const base = inherited && this.base ? this.base.getProperties(inherited) : {};
return Object.assign(base, indexBy(
(this.classSpec.properties || []).map(p => new Property(this.system, this.assembly, this, p)),
p => p.name));
return this._getProperties(inherited, this);
}

/**
* List all methods in this class.
* @param inherited include all methods inherited from base classes (default: false)
*/
public getMethods(inherited = false): {[name: string]: Method} {
const base = inherited && this.base ? this.base.getMethods(inherited) : {};
return Object.assign(base, indexBy(
(this.classSpec.methods || []).map(m => new Method(this.system, this.assembly, this, m)),
m => m.name));
return this._getMethods(inherited, this);
}

/**
Expand All @@ -102,4 +96,18 @@ export class ClassType extends ReferenceType {
public isClassType() {
return true;
}

private _getProperties(inherited: boolean, parentType: ReferenceType): {[name: string]: Property} {
const base = inherited && this.base ? this.base._getProperties(inherited, parentType) : {};
return Object.assign(base, indexBy(
(this.classSpec.properties || []).map(p => new Property(this.system, this.assembly, parentType, this, p)),
p => p.name));
}

private _getMethods(inherited: boolean, parentType: ReferenceType): {[name: string]: Method} {
const base = inherited && this.base ? this.base._getMethods(inherited, parentType) : {};
return Object.assign(base, indexBy(
(this.classSpec.methods || []).map(m => new Method(this.system, this.assembly, parentType, this, m)),
m => m.name));
}
}
17 changes: 14 additions & 3 deletions packages/jsii-reflect/lib/docs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import jsii = require('jsii-spec');
import { Stability } from 'jsii-spec';
import { TypeSystem } from './type-system';

export interface Documentable {
Expand Down Expand Up @@ -50,9 +51,7 @@ export class Docs {
* Return the stability of this type
*/
public get stability(): jsii.Stability | undefined {
if (this.docs.stability !== undefined) { return this.docs.stability; }
if (this.parentDocs) { return this.parentDocs.stability; }
return undefined;
return lowestStability(this.docs.stability, this.parentDocs && this.parentDocs.stability);
}

public customTag(tag: string): string | undefined {
Expand All @@ -66,4 +65,16 @@ export class Docs {
public get remarks(): string {
return this.docs.remarks || '';
}
}

const stabilityPrecedence = {
[Stability.Deprecated]: 0,
[Stability.Experimental]: 1,
[Stability.Stable]: 2,
};

function lowestStability(a?: Stability, b?: Stability): Stability | undefined {
if (a === undefined) { return b; }
if (b === undefined) { return a; }
return stabilityPrecedence[a] < stabilityPrecedence[b] ? a : b;
}
42 changes: 25 additions & 17 deletions packages/jsii-reflect/lib/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,38 +50,46 @@ export class InterfaceType extends ReferenceType {
* @param inherited include all properties inherited from base classes (default: false)
*/
public getProperties(inherited = false): {[name: string]: Property} {
return this._getProperties(inherited, this);
}

/**
* List all methods in this class.
* @param inherited include all methods inherited from base classes (default: false)
*/
public getMethods(inherited = false): {[name: string]: Method} {
return this._getMethods(inherited, this);
}

public isDataType() {
return !!this.interfaceSpec.datatype;
}

public isInterfaceType() {
return true;
}

private _getProperties(inherited: boolean, parentType: ReferenceType): {[name: string]: Property} {
const base: {[name: string]: Property} = {};
if (inherited) {
for (const parent of this.getInterfaces()) {
Object.assign(base, parent.getProperties(inherited));
Object.assign(base, parent._getProperties(inherited, parentType));
}
}
return Object.assign(base, indexBy(
(this.interfaceSpec.properties || []).map(p => new Property(this.system, this.assembly, this, p)),
(this.interfaceSpec.properties || []).map(p => new Property(this.system, this.assembly, parentType, this, p)),
p => p.name));
}

/**
* List all methods in this class.
* @param inherited include all methods inherited from base classes (default: false)
*/
public getMethods(inherited = false): {[name: string]: Method} {
private _getMethods(inherited: boolean, parentType: ReferenceType): {[name: string]: Method} {
const base: {[name: string]: Property} = {};
if (inherited) {
for (const parent of this.getInterfaces()) {
Object.assign(base, parent.getMethods(inherited));
Object.assign(base, parent._getMethods(inherited, parentType));
}
}
return Object.assign(base, indexBy(
(this.interfaceSpec.methods || []).map(m => new Method(this.system, this.assembly, this, m)),
(this.interfaceSpec.methods || []).map(m => new Method(this.system, this.assembly, parentType, this, m)),
m => m.name));
}

public isDataType() {
return !!this.interfaceSpec.datatype;
}

public isInterfaceType() {
return true;
}
}
9 changes: 9 additions & 0 deletions packages/jsii-reflect/lib/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class Method extends Callable implements Documentable, Overridable, TypeM
system: TypeSystem,
assembly: Assembly,
parentType: Type,
public readonly definingType: Type,
private readonly methodSpec: jsii.Method) {
super(system, assembly, parentType, methodSpec);
}
Expand All @@ -36,6 +37,14 @@ export class Method extends Callable implements Documentable, Overridable, TypeM
return this.methodSpec.name;
}

public get overrides(): Type | undefined {
if (!this.methodSpec.overrides) {
return undefined;
}

return this.system.findFqn(this.methodSpec.overrides);
}

/**
* The return type of the method (undefined if void or initializer)
*/
Expand Down
1 change: 1 addition & 0 deletions packages/jsii-reflect/lib/property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class Property extends OptionalValue implements Documentable, Overridable
system: TypeSystem,
public readonly assembly: Assembly,
public readonly parentType: Type,
public readonly definingType: Type,
private readonly propSpec: jsii.Property) {
super(system, propSpec);
}
Expand Down
43 changes: 36 additions & 7 deletions packages/jsii-reflect/lib/tree.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import colors = require('colors/safe');
import { Stability } from 'jsii-spec';
import { AsciiTree } from 'oo-ascii-tree';
import { Assembly } from './assembly';
import { ClassType } from './class';
import { Dependency } from './dependency';
import { Documentable } from './docs';
import { EnumType } from './enum';
import { Initializer } from './initializer';
import { InterfaceType } from './interface';
Expand Down Expand Up @@ -54,6 +56,13 @@ export interface TypeSystemTreeOptions {
* @default true
*/
colors?: boolean;

/**
* Show stabilities
*
* @default false
*/
stabilities?: boolean;
}

/**
Expand Down Expand Up @@ -105,7 +114,7 @@ class AssemblyNode extends AsciiTree {
class MethodNode extends AsciiTree {
constructor(method: Method, options: TypeSystemTreeOptions) {
const args = method.parameters.map(p => p.name).join(',');
super(`${method.name}(${args}) ${colors.gray('method')}`);
super(`${maybeStatic(method)}${method.name}(${args}) ${colors.gray('method')}` + describeStability(method, options));

if (options.signatures) {
if (method.abstract) {
Expand Down Expand Up @@ -138,7 +147,7 @@ class MethodNode extends AsciiTree {
class InitializerNode extends AsciiTree {
constructor(initializer: Initializer, options: TypeSystemTreeOptions) {
const args = initializer.parameters.map(p => p.name).join(',');
super(`${initializer.name}(${args}) ${colors.gray('initializer')}`);
super(`${initializer.name}(${args}) ${colors.gray('initializer')}` + describeStability(initializer, options));

if (options.signatures) {
if (initializer.protected) {
Expand Down Expand Up @@ -171,7 +180,7 @@ class ParameterNode extends AsciiTree {

class PropertyNode extends AsciiTree {
constructor(property: Property, options: TypeSystemTreeOptions) {
super(`${property.name} ${colors.gray('property')}`);
super(`${maybeStatic(property)}${property.name} ${colors.gray('property')}` + describeStability(property, options));

if (options.signatures) {
if (property.abstract) {
Expand Down Expand Up @@ -211,7 +220,7 @@ class OptionalValueNode extends AsciiTree {

class ClassNode extends AsciiTree {
constructor(type: ClassType, options: TypeSystemTreeOptions) {
super(`${colors.gray('class')} ${colors.cyan(type.name)}`);
super(`${colors.gray('class')} ${colors.cyan(type.name)}` + describeStability(type, options));

if (options.inheritance && type.base) {
this.add(new KeyValueNode('base', type.base.name));
Expand All @@ -235,7 +244,7 @@ class ClassNode extends AsciiTree {

class InterfaceNode extends AsciiTree {
constructor(type: InterfaceType, options: TypeSystemTreeOptions) {
super(`${colors.gray('interface')} ${colors.cyan(type.name)}`);
super(`${colors.gray('interface')} ${colors.cyan(type.name)}` + describeStability(type, options));

if (options.inheritance && type.interfaces.length > 0) {
const interfaces = new TitleNode('interfaces');
Expand All @@ -254,11 +263,11 @@ class InterfaceNode extends AsciiTree {

class EnumNode extends AsciiTree {
constructor(enumType: EnumType, options: TypeSystemTreeOptions) {
super(`${colors.gray('enum')} ${colors.cyan(enumType.name)}`);
super(`${colors.gray('enum')} ${colors.cyan(enumType.name)}` + describeStability(enumType, options));

if (options.members) {
enumType.members.forEach(mem => {
this.add(new AsciiTree(mem.name));
this.add(new AsciiTree(mem.name + describeStability(mem, options)));
});
}
}
Expand Down Expand Up @@ -313,3 +322,23 @@ function withColors(enabled: boolean, block: () => void) {
}
}
}

function describeStability(thing: Documentable, options: TypeSystemTreeOptions) {
if (!options.stabilities) { return ''; }

switch (thing.docs.stability) {
case Stability.Stable: return ' (' + colors.green('stable') + ')';
case Stability.Experimental: return ' (' + colors.yellow('experimental') + ')';
case Stability.Deprecated: return ' (' + colors.red('deprecated') + ')';
}

return '';
}

function maybeStatic(mem: Property | Method) {
let isStatic;
if (mem instanceof Property) { isStatic = !!mem.static; }
if (mem instanceof Method) { isStatic = !!mem.static; }

return isStatic ? (colors.grey('static') + ' ') : '';
}
46 changes: 33 additions & 13 deletions packages/jsii-reflect/lib/type-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ export class TypeSystem {

private readonly _assemblyLookup: { [name: string]: Assembly } = { };

/**
* Load all JSII dependencies of the given NPM package directory.
*
* The NPM package itself does *not* have to be a jsii package, and does
* NOT have to declare a JSII dependency on any of the packages.
*/
public async loadNpmDependencies(packageRoot: string, options: { validate?: boolean } = {}): Promise<void> {
const pkg = require(path.resolve(packageRoot, 'package.json'));

for (const dep of dependenciesOf(pkg)) {
// Filter jsii dependencies
const depPkgJsonPath = require.resolve(`${dep}/package.json`, { paths: [ packageRoot ] });
const depPkgJson = require(depPkgJsonPath);
if (!depPkgJson.jsii) { continue; }

await this.loadModule(path.dirname(depPkgJsonPath), options);
}
}

/**
* Loads a jsii module or a single .jsii file into the type system.
*
Expand Down Expand Up @@ -87,20 +106,14 @@ export class TypeSystem {
const root = await self.addAssembly(asm, { isRoot });
const bundled: string[] = pkg.bundledDependencies || pkg.bundleDependencies || [];

const loadDependencies = async (deps: { [name: string]: string }) => {
for (const name of Object.keys(deps || {})) {
if (bundled.includes(name)) {
continue;
}
const depDir = require.resolve(`${name}/package.json`, {
paths: [ moduleDirectory ]
});
await _loadModule(path.dirname(depDir));
}
};
for (const name of dependenciesOf(pkg)) {
if (bundled.includes(name)) { continue; }

await loadDependencies(pkg.dependencies);
await loadDependencies(pkg.peerDependencies);
const depDir = require.resolve(`${name}/package.json`, {
paths: [ moduleDirectory ]
});
await _loadModule(path.dirname(depDir));
}

return root;
}
Expand Down Expand Up @@ -251,3 +264,10 @@ export class TypeSystem {
}
}
}

function dependenciesOf(packageJson: any) {
const deps = new Set<string>();
Object.keys(packageJson.dependencies || {}).forEach(deps.add.bind(deps));
Object.keys(packageJson.peerDependencies || {}).forEach(deps.add.bind(deps));
return Array.from(deps);
}
4 changes: 2 additions & 2 deletions packages/jsii-reflect/test/findClass.expected.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
typeName from Base
toString from CompositeOperation
typeName from Calculator
toString from Calculator
add from Calculator
mul from Calculator
neg from Calculator
Expand Down
Loading

0 comments on commit a4de2d8

Please sign in to comment.