Skip to content

Commit cfb6580

Browse files
authored
fix(jsii-pacmak): Python interfaces sometimes violate MRO (#4973)
The Method Resolution Order (MRO) rules for Python state (in brief) that all classes and interfaces in an inheritance declaration must be listed subclass-before-superclass, and in the same order on all classes. `jsii-pacmak` was not guaranteeing order, leading to sometimes producing an order that violates MRO. Update `jsii-pacmak` to update base classes and sort them in the right order. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 4cc7275 commit cfb6580

File tree

3 files changed

+106
-4
lines changed

3 files changed

+106
-4
lines changed

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

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
import { die, toPythonIdentifier } from './python/util';
3333
import { toPythonVersionRange, toReleaseVersion } from './version-utils';
3434
import { assertSpecIsRosettaCompatible } from '../rosetta-assembly';
35+
import { topologicalSort } from '../toposort';
3536

3637
import { TargetName } from './index';
3738

@@ -367,7 +368,7 @@ interface ISortableType {
367368
dependsOn(resolver: TypeResolver): PythonType[];
368369
}
369370

370-
function isSortableType(arg: unknown): arg is ISortableType {
371+
function isSortableType<T>(arg: T): arg is T & ISortableType {
371372
return (arg as Partial<ISortableType>).dependsOn !== undefined;
372373
}
373374

@@ -450,8 +451,10 @@ abstract class BasePythonClassType implements PythonType, ISortableType {
450451
public emit(code: CodeMaker, context: EmitContext) {
451452
context = nestedContext(context, this.fqn);
452453

453-
const classParams = this.getClassParams(context);
454-
openSignature(code, 'class', this.pythonName, classParams);
454+
this.sortBasesForMro();
455+
456+
const baseClasses = this.getClassParams(context);
457+
openSignature(code, 'class', this.pythonName, baseClasses);
455458

456459
this.generator.emitDocString(code, this.apiLocation, this.docs, {
457460
documentableItem: `class-${this.pythonName}`,
@@ -479,6 +482,95 @@ abstract class BasePythonClassType implements PythonType, ISortableType {
479482
}
480483
}
481484

485+
/**
486+
* Sort the this.bases array for proper Python MRO.
487+
*
488+
* If we don't do this, there is a chance that a superclass and subclass (or interface)
489+
* have the parents in different order, which leads to an MRO violation.
490+
*
491+
* See: <https://docs.python.org/3/howto/mro.html>
492+
*
493+
* The bottom line is: child classes higher up in the hierarchy first.
494+
* As an example, in the hierarchy:
495+
*
496+
* ```
497+
* ┌─────┐
498+
* │ A │
499+
* └─────┘
500+
* ▲
501+
* ├─────┐
502+
* │ ┌─────┐
503+
* │ │ C │
504+
* │ └─────┘
505+
* │ ▲
506+
* ├─────┘
507+
* ┌─────┐
508+
* │ B │
509+
* └─────┘
510+
* ```
511+
*
512+
* The order should be `class B(C, A)`, and not the other way around.
513+
*
514+
* This is yet another instance of topological sort.
515+
*/
516+
protected sortBasesForMro() {
517+
if (this.bases.length <= 1) {
518+
return;
519+
}
520+
521+
// Record original order
522+
const originalOrder = new Map<spec.TypeReference, number>();
523+
for (let i = 0; i < this.bases.length; i++) {
524+
originalOrder.set(this.bases[i], i);
525+
}
526+
527+
// Classify into sortable and non-sortable types
528+
const sortableBases: spec.NamedTypeReference[] = [];
529+
const nonSortableBases: spec.TypeReference[] = [];
530+
for (const baseRef of this.bases) {
531+
if (spec.isNamedTypeReference(baseRef)) {
532+
sortableBases.push(baseRef);
533+
} else {
534+
nonSortableBases.push(baseRef);
535+
}
536+
}
537+
538+
// Toposort, in reverse (highest in the tree last)
539+
const typeSystem = this.generator.reflectAssembly.system;
540+
const sorted = topologicalSort(
541+
sortableBases,
542+
(t) => t.fqn,
543+
(d) => typeBaseFqns(typeSystem.findFqn(d.fqn)),
544+
);
545+
sorted.reverse();
546+
547+
// Inside a trance, we keep original sort order in the source declarations.
548+
for (const tranche of sorted) {
549+
tranche.sort((a, b) => originalOrder.get(a)! - originalOrder.get(b)!);
550+
}
551+
552+
// Replace original bases
553+
this.bases.splice(
554+
0,
555+
this.bases.length,
556+
...sorted.flatMap((xs) => xs),
557+
...nonSortableBases,
558+
);
559+
560+
function typeBaseFqns(x: reflect.Type): string[] {
561+
if (x.isClassType()) {
562+
return [
563+
...(x.base ? [x.base?.fqn] : []),
564+
...x.getInterfaces().map((i) => i.fqn),
565+
];
566+
}
567+
if (x.isInterfaceType()) {
568+
return x.getInterfaces().map((i) => i.fqn);
569+
}
570+
return [];
571+
}
572+
}
573+
482574
protected boundResolver(resolver: TypeResolver): TypeResolver {
483575
if (this.fqn == null) {
484576
return resolver;
@@ -1024,6 +1116,8 @@ abstract class BaseProperty implements PythonBase {
10241116

10251117
class Interface extends BasePythonClassType {
10261118
public emit(code: CodeMaker, context: EmitContext) {
1119+
this.sortBasesForMro();
1120+
10271121
context = nestedContext(context, this.fqn);
10281122
emitList(code, '@jsii.interface(', [`jsii_type="${this.fqn}"`], ')');
10291123

@@ -1114,6 +1208,8 @@ class Struct extends BasePythonClassType {
11141208
}
11151209

11161210
public emit(code: CodeMaker, context: EmitContext) {
1211+
this.sortBasesForMro();
1212+
11171213
context = nestedContext(context, this.fqn);
11181214
const baseInterfaces = this.getClassParams(context);
11191215

packages/jsii-pacmak/lib/toposort.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,4 @@ interface TopoElement<T> {
7979
* We do declare the type `Toposorted<A>` here so that if we ever change
8080
* the type, we can find all usage sites quickly.
8181
*/
82-
export type Toposorted<A> = readonly A[][];
82+
export type Toposorted<A> = A[][];

packages/jsii-pacmak/lib/util.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,3 +464,9 @@ export function zip<A, B>(xs: A[], ys: B[]): Array<[A, B]> {
464464
}
465465
return ret;
466466
}
467+
468+
export function setAdd<T>(setA: Set<T>, setB: Iterable<T>) {
469+
for (const b of setB) {
470+
setA.add(b);
471+
}
472+
}

0 commit comments

Comments
 (0)