Skip to content

Commit a0741cc

Browse files
authored
fix(jsii): check that static and nonstatic members don't share a name (#430)
This is not allowed in Java, and leads to compiler warnings in C#. Fixes #427.
1 parent af10554 commit a0741cc

File tree

4 files changed

+152
-9
lines changed

4 files changed

+152
-9
lines changed

packages/jsii/doc/STATICS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Statics
2+
3+
## Constraints
4+
5+
### TypeScript
6+
7+
Static and non-static members live in completely different namespaces. Statics only exist on the class,
8+
and cannot be accessed through the class.
9+
10+
Hence, it is perfectly fine to have a static and a non-static with the same name.
11+
12+
Superclass statics can be accessed through subclasses, and they can be overridden by subclasses.
13+
14+
### Java
15+
16+
Statics and non-statics share a namespace. There cannot be a static and a
17+
nonstatic with the same name on the same class. The same holds for inherited
18+
members; a non-static in a superclass prevents a static of the same name in a
19+
subclass, same for a static in a superclass and a nonstatic in a subclass.
20+
21+
Superclass statics can be accessed through subclasses, and even through
22+
instances and subclass instances.
23+
24+
Statics can be overridden, though they do not participate in polymorphism.
25+
26+
### C#
27+
28+
Does not allow static and nonstatic members with the same name on the same class.
29+
30+
**Will** allow static and nonstatic members of the same name on subclasses,
31+
but will issue a compiler warning that the user probably intended to use the
32+
`new` keyword to unambiguously introduce a new symbol.
33+
34+
**Will** allow overriding of statics on a subclass, but will issue a warning
35+
about the `new` keyword again.
36+
37+
## Rules
38+
39+
In order to accomodate Java, we disallow any inherited members to conflict on
40+
staticness.

packages/jsii/lib/assembler.ts

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ export class Assembler implements Emitter {
159159
*
160160
* Will not invoke the function with any 'undefined's; an error will already have been emitted in
161161
* that case anyway.
162+
*
163+
* @param fqn FQN of the current type (the type that has a dependency on baseTypes)
164+
* @param baseTypes Array of type references to be looked up
165+
* @param referencingNode Node to report a diagnostic on if we fail to look up a t ype
166+
* @param cb Callback to be invoked with the Types corresponding to the TypeReferences in baseTypes
162167
*/
163168
// tslint:disable-next-line:max-line-length
164169
private _deferUntilTypesAvailable(fqn: string, baseTypes: spec.NamedTypeReference[], referencingNode: ts.Node, cb: (...xs: spec.Type[]) => void) {
@@ -585,9 +590,64 @@ export class Assembler implements Emitter {
585590
jsiiType.initializer = { initializer: true };
586591
}
587592

593+
this._verifyNoStaticMixing(jsiiType, type.symbol.valueDeclaration);
594+
588595
return _sortMembers(jsiiType);
589596
}
590597

598+
/**
599+
* Check that this class doesn't declare any members that are of different staticness in itself or any of its bases
600+
*/
601+
private _verifyNoStaticMixing(klass: spec.ClassType, decl: ts.Declaration) {
602+
function stat(s?: boolean) {
603+
return s ? 'static' : 'non-static';
604+
}
605+
606+
// Check class itself--may have two methods/props with the same name, so check the arrays
607+
const statics = new Set((klass.methods || []).concat(klass.properties || []).filter(x => x.static).map(x => x.name));
608+
const nonStatics = new Set((klass.methods || []).concat(klass.properties || []).filter(x => !x.static).map(x => x.name));
609+
// Intersect
610+
for (const member of intersect(statics, nonStatics)) {
611+
this._diagnostic(decl, ts.DiagnosticCategory.Error,
612+
`member '${member}' of class '${klass.name}' cannot be declared both statically and non-statically`);
613+
}
614+
615+
// Check against base classes. They will not contain duplicate member names so we can load
616+
// the members into a map.
617+
const classMembers = typeMembers(klass);
618+
this._withBaseClass(klass, decl, (base, recurse) => {
619+
for (const [name, baseMember] of Object.entries(typeMembers(base))) {
620+
const member = classMembers[name];
621+
if (!member) { continue; }
622+
623+
if (!!baseMember.static !== !!member.static) {
624+
this._diagnostic(decl, ts.DiagnosticCategory.Error,
625+
// tslint:disable-next-line:max-line-length
626+
`${stat(member.static)} member '${name}' of class '${klass.name}' conflicts with ${stat(baseMember.static)} member in ancestor '${base.name}'`);
627+
}
628+
}
629+
630+
recurse();
631+
});
632+
}
633+
634+
/**
635+
* Wrapper around _deferUntilTypesAvailable, invoke the callback with the given classes' base type
636+
*
637+
* Does nothing if the given class doesn't have a base class.
638+
*
639+
* The second argument will be a `recurse` function for easy recursion up the inheritance tree
640+
* (no messing around with binding 'self' and 'this' and doing multiple calls to _withBaseClass.)
641+
*/
642+
private _withBaseClass(klass: spec.ClassType, decl: ts.Declaration, cb: (base: spec.ClassType, recurse: () => void) => void) {
643+
if (klass.base) {
644+
this._deferUntilTypesAvailable(klass.fqn, [klass.base], decl, (base) => {
645+
if (!spec.isClassType(base)) { throw new Error('Oh no'); }
646+
cb(base, () => this._withBaseClass(base, decl, cb));
647+
});
648+
}
649+
}
650+
591651
/**
592652
* @returns true if this member is internal and should be omitted from the type manifest
593653
*/
@@ -791,13 +851,13 @@ export class Assembler implements Emitter {
791851

792852
// Check that no interface declares a member that's already declared
793853
// in a base type (not allowed in C#).
794-
const memberNames = interfaceMemberNames(jsiiType);
854+
const names = memberNames(jsiiType);
795855
const checkNoIntersection = (...bases: spec.Type[]) => {
796856
for (const base of bases) {
797857
if (!spec.isInterfaceType(base)) { continue; }
798858

799-
const baseMembers = interfaceMemberNames(base);
800-
for (const memberName of memberNames) {
859+
const baseMembers = memberNames(base);
860+
for (const memberName of names) {
801861
if (baseMembers.includes(memberName)) {
802862
this._diagnostic(type.symbol.declarations[0],
803863
ts.DiagnosticCategory.Error,
@@ -1390,14 +1450,21 @@ function intersection<T>(xs: Set<T>, ys: Set<T>): Set<T> {
13901450
*
13911451
* Returns empty string for a non-interface type.
13921452
*/
1393-
function interfaceMemberNames(jsiiType: spec.InterfaceType): string[] {
1394-
const ret = new Array<string>();
1395-
if (jsiiType.methods) {
1396-
ret.push(...jsiiType.methods.map(m => m.name).filter(x => x !== undefined) as string[]);
1453+
function memberNames(jsiiType: spec.InterfaceType | spec.ClassType): string[] {
1454+
return Object.keys(typeMembers(jsiiType)).filter(n => n !== '');
1455+
}
1456+
1457+
function typeMembers(jsiiType: spec.InterfaceType | spec.ClassType): {[key: string]: spec.Property | spec.Method} {
1458+
const ret: {[key: string]: spec.Property | spec.Method} = {};
1459+
1460+
for (const prop of jsiiType.properties || []) {
1461+
ret[prop.name] = prop;
13971462
}
1398-
if (jsiiType.properties) {
1399-
ret.push(...jsiiType.properties.map(m => m.name));
1463+
1464+
for (const method of jsiiType.methods || []) {
1465+
ret[method.name || ''] = method;
14001466
}
1467+
14011468
return ret;
14021469
}
14031470

@@ -1415,3 +1482,11 @@ function getConstructor(type: ts.Type): ts.Symbol | undefined {
14151482
return type.symbol.members
14161483
&& type.symbol.members.get(ts.InternalSymbolName.Constructor);
14171484
}
1485+
1486+
function* intersect<T>(xs: Set<T>, ys: Set<T>) {
1487+
for (const x of xs) {
1488+
if (ys.has(x)) {
1489+
yield x;
1490+
}
1491+
}
1492+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
///!MATCH_ERROR: non-static member 'funFunction' of class 'Sub' conflicts with static member in ancestor 'SuperDuper'
2+
3+
export class SuperDuper {
4+
public static funFunction() {
5+
// Empty
6+
}
7+
}
8+
9+
export class Super extends SuperDuper {
10+
// Empty
11+
}
12+
13+
export class Sub extends Super {
14+
public funFunction() {
15+
// Oops
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
///!MATCH_ERROR: member 'funFunction' of class 'TheClass' cannot be declared both statically and non-statically
2+
3+
export class TheClass {
4+
public static funFunction() {
5+
// Empty
6+
}
7+
8+
public funFunction() {
9+
// Empty
10+
}
11+
}

0 commit comments

Comments
 (0)