Skip to content

Commit 4d2f268

Browse files
authored
fix: require distinct argument and property names (#272)
Restrict the usage of datatype parameters in functions, so that languages with keyword argument support can inline datatype parameters into the function declaration. This means it cannot share any field names with remaining function argument names. Fixes #268.
1 parent 03103f3 commit 4d2f268

File tree

2 files changed

+95
-3
lines changed

2 files changed

+95
-3
lines changed

Diff for: packages/jsii/lib/assembler.ts

+80-3
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class Assembler implements Emitter {
190190
*
191191
* @returns the de-referenced type, if it was found, otherwise ``undefined``.
192192
*/
193-
private _dereference(ref: spec.NamedTypeReference, referencingNode: ts.Node): spec.Type | undefined {
193+
private _dereference(ref: spec.NamedTypeReference, referencingNode: ts.Node | null): spec.Type | undefined {
194194
const [assm, ] = ref.fqn.split('.');
195195
let type;
196196
if (assm === this.projectInfo.name) {
@@ -615,11 +615,13 @@ export class Assembler implements Emitter {
615615
this._diagnostic(declaration, ts.DiagnosticCategory.Error, `Unable to compute signature for ${type.fqn}#${symbol.name}`);
616616
return;
617617
}
618+
const parameters = await Promise.all(signature.getParameters().map(p => this._toParameter(p)));
619+
618620
const returnType = signature.getReturnType();
619621
const method: spec.Method = {
620622
abstract: _isAbstract(symbol, type),
621623
name: symbol.name,
622-
parameters: await Promise.all(signature.getParameters().map(p => this._toParameter(p))),
624+
parameters,
623625
protected: _isProtected(symbol),
624626
returns: _isVoid(returnType) ? undefined : await this._typeReference(returnType, declaration),
625627
static: _isStatic(symbol),
@@ -628,6 +630,29 @@ export class Assembler implements Emitter {
628630

629631
this._visitDocumentation(symbol, method);
630632

633+
// If the last parameter is a datatype, verify that it does not share any field names with
634+
// other function arguments, so that it can be turned into keyword arguments by jsii frontends
635+
// that support such.
636+
const lastParamTypeRef = apply(last(parameters), x => x.type);
637+
const lastParamSymbol = last(signature.getParameters());
638+
if (lastParamTypeRef && spec.isNamedTypeReference(lastParamTypeRef)) {
639+
this._deferUntilTypesAvailable(symbol.name, [lastParamTypeRef], lastParamSymbol!.declarations[0], (lastParamType) => {
640+
if (!spec.isInterfaceType(lastParamType) || !lastParamType.datatype) { return; }
641+
642+
// Liftable datatype, make sure no parameter names match any of the properties in the datatype
643+
const propNames = this.allProperties(lastParamType);
644+
const paramNames = new Set(parameters.slice(0, parameters.length - 1).map(x => x.name));
645+
const sharedNames = intersection(propNames, paramNames);
646+
647+
if (sharedNames.size > 0) {
648+
this._diagnostic(
649+
declaration,
650+
ts.DiagnosticCategory.Error,
651+
`Name occurs in both function arguments and in datatype properties, rename one: ${Array.from(sharedNames).join(', ')}`);
652+
}
653+
});
654+
}
655+
631656
type.methods = type.methods || [];
632657
type.methods.push(method);
633658
}
@@ -867,6 +892,31 @@ export class Assembler implements Emitter {
867892
def.dependedFqns = def.dependedFqns.filter(fqns.has.bind(fqns));
868893
}
869894
}
895+
896+
/**
897+
* Return the set of all (inherited) properties of an interface
898+
*/
899+
private allProperties(root: spec.InterfaceType): Set<string> {
900+
const self = this;
901+
902+
const ret = new Set<string>();
903+
recurse(root);
904+
return ret;
905+
906+
function recurse(int: spec.InterfaceType) {
907+
for (const property of int.properties || []) {
908+
ret.add(property.name);
909+
}
910+
911+
for (const baseRef of int.interfaces || []) {
912+
const base = self._dereference(baseRef, null);
913+
if (!base) { throw new Error('Impossible to have unresolvable base in allProperties()'); }
914+
if (!spec.isInterfaceType(base)) { throw new Error('Impossible to have non-interface base in allProperties()'); }
915+
916+
recurse(base);
917+
}
918+
}
919+
}
870920
}
871921

872922
/**
@@ -1024,6 +1074,33 @@ interface DeferredRecord {
10241074
cb: () => void;
10251075
}
10261076

1077+
/**
1078+
* Return the last element from a list
1079+
*/
1080+
function last<T>(xs: T[]): T | undefined {
1081+
return xs.length > 0 ? xs[xs.length - 1] : undefined;
1082+
}
1083+
1084+
/**
1085+
* Apply a function to a value if it's not equal to undefined
1086+
*/
1087+
function apply<T, U>(x: T | undefined, fn: (x: T) => U | undefined): U | undefined {
1088+
return x !== undefined ? fn(x) : undefined;
1089+
}
1090+
1091+
/**
1092+
* Return the intersection of two sets
1093+
*/
1094+
function intersection<T>(xs: Set<T>, ys: Set<T>): Set<T> {
1095+
const ret = new Set<T>();
1096+
for (const x of xs) {
1097+
if (ys.has(x)) {
1098+
ret.add(x);
1099+
}
1100+
}
1101+
return ret;
1102+
}
1103+
10271104
/**
10281105
* Whether or not the given name is conventionally an interface name
10291106
*
@@ -1032,4 +1109,4 @@ interface DeferredRecord {
10321109
*/
10331110
function isInterfaceName(name: string) {
10341111
return name.length >= 2 && name.charAt(0) === 'I' && name.charAt(1).toUpperCase() === name.charAt(1);
1035-
}
1112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
///!MATCH_ERROR: Name occurs in both function arguments and in datatype properties, rename one: dontWorry
2+
3+
export interface Lyrics {
4+
dontWorry: string;
5+
beHappy: string;
6+
}
7+
8+
export class MyClass {
9+
// Can't have an argument name 'dontWorry' if the last parameter is a datatype
10+
// which also has a field 'dontWorry'--that will give errors if the datatype
11+
// argument fields are lifted to keyword arguments.
12+
public dance(dontWorry: string, lyrics: Lyrics) {
13+
return `${dontWorry}: ${lyrics.beHappy}`;
14+
}
15+
}

0 commit comments

Comments
 (0)