Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class Assembler implements Emitter {
private readonly mainFile: string;
private readonly tscRootDir?: string;
private readonly compressAssembly?: boolean;
private readonly usedFeatures = new Set<spec.JsiiFeature>();

private readonly _typeChecker: ts.TypeChecker;

Expand Down Expand Up @@ -119,6 +120,9 @@ export class Assembler implements Emitter {

this.mainFile = path.resolve(projectInfo.projectRoot, mainFile);
this.runtimeTypeInfoInjector = new RuntimeTypeInfoInjector(projectInfo.version);

// Always enabled features
this.usedFeatures.add('class-covariant-overrides' as any);
}

public get customTransformers(): ts.CustomTransformers {
Expand Down Expand Up @@ -220,6 +224,7 @@ export class Assembler implements Emitter {
jsiiVersion,
bin: this.projectInfo.bin,
fingerprint: '<TBD>',
usedFeatures: this.usedFeatures.size > 0 ? Array.from(this.usedFeatures) : undefined,
};

if (this.deprecatedRemover) {
Expand Down
7 changes: 6 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export type MultipleSourceFiles = {
[name: string]: string;
};

/**
* Assembly features supported by this compiler
*/
export const ASSEMBLY_FEATURES_SUPPORTED: spec.JsiiFeature[] = ['class-covariant-overrides' as any];

/**
* Compile a piece of source and return the JSII assembly for it
*
Expand Down Expand Up @@ -115,7 +120,7 @@ export function compileJsiiForTest(
if (errors.length > 0 || emitResult.emitSkipped) {
throw new JsiiError('There were compiler errors');
}
const assembly = loadAssemblyFromPath(process.cwd(), false);
const assembly = loadAssemblyFromPath(process.cwd(), false, ASSEMBLY_FEATURES_SUPPORTED);
const files: Record<string, string> = {};

for (const filename of Object.keys(source)) {
Expand Down
3 changes: 2 additions & 1 deletion src/project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as semver from 'semver';
import * as ts from 'typescript';

import { findDependencyDirectory } from './common/find-utils';
import { ASSEMBLY_FEATURES_SUPPORTED } from './helpers';
import { JsiiDiagnostic } from './jsii-diagnostic';
import { TypeScriptConfigValidationRuleSet } from './tsconfig';
import { JsiiError, parsePerson, parseRepository } from './utils';
Expand Down Expand Up @@ -392,7 +393,7 @@ class DependencyResolver {
return this.cache.get(jsiiFile)!;
}

const assembly = loadAssemblyFromFile(jsiiFile);
const assembly = loadAssemblyFromFile(jsiiFile, true, ASSEMBLY_FEATURES_SUPPORTED);

// Continue loading any dependencies declared in the asm
const resolvedDependencies = assembly.dependencies
Expand Down
16 changes: 15 additions & 1 deletion src/transforms/deprecated-remover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import {
EnumMember,
Initializer,
InterfaceType,
IntersectionTypeReference,
isClassOrInterfaceType,
isClassType,
isCollectionTypeReference,
isEnumType,
isMethod,
isNamedTypeReference,
isPrimitiveTypeReference,
isUnionTypeReference,
Method,
Parameter,
Property,
Stability,
TypeReference,
UnionTypeReference,
} from '@jsii/spec';
import * as ts from 'typescript';

Expand Down Expand Up @@ -305,7 +308,10 @@ export class DeprecatedRemover {
if (isCollectionTypeReference(ref)) {
return this.tryFindReference(ref.collection.elementtype, fqns);
}
return ref.union.types.map((type) => this.tryFindReference(type, fqns)).find((typeRef) => typeRef != null);

return setTypeMembers(ref)
.map((type) => this.tryFindReference(type, fqns))
.find((typeRef) => typeRef != null);
}

private shouldFqnBeStripped(fqn: string) {
Expand Down Expand Up @@ -770,3 +776,11 @@ class DeprecationRemovalTransformer {
);
}
}

function setTypeMembers(x: UnionTypeReference | IntersectionTypeReference) {
if (isUnionTypeReference(x)) {
return x.union.types;
} else {
return x.intersection.types;
}
}
188 changes: 165 additions & 23 deletions src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ function _defaultValidations(): ValidationFunction[] {
if (!overridden) {
return _validateMethodOverride(method, baseType);
}
_assertSignaturesMatch(overridden, method, `${type.fqn}#${method.name}`, `overriding ${baseType.fqn}`);
_assertSignaturesMatch(overridden, method, `${type.fqn}#${method.name}`, `overriding ${baseType.fqn}`, {
allowCovariance: true,
});
method.overrides = baseType.fqn;
return true;
}
Expand All @@ -237,7 +239,9 @@ function _defaultValidations(): ValidationFunction[] {
if (!overridden) {
return _validatePropertyOverride(property, baseType);
}
_assertPropertiesMatch(overridden, property, `${type.fqn}#${property.name}`, `overriding ${baseType.fqn}`);
_assertPropertiesMatch(overridden, property, `${type.fqn}#${property.name}`, `overriding ${baseType.fqn}`, {
allowCovariance: true,
});
property.overrides = baseType.fqn;
return true;
}
Expand All @@ -254,7 +258,9 @@ function _defaultValidations(): ValidationFunction[] {
const ifaceType = _dereference(iface, assembly, validator) as spec.InterfaceType;
const implemented = (ifaceType.methods ?? []).find((m) => m.name === method.name);
if (implemented) {
_assertSignaturesMatch(implemented, method, `${type.fqn}#${method.name}`, `implementing ${ifaceType.fqn}`);
_assertSignaturesMatch(implemented, method, `${type.fqn}#${method.name}`, `implementing ${ifaceType.fqn}`, {
allowCovariance: false,
});
// We won't replace a previous overrides declaration from a method override, as those have
// higher precedence than an initial implementation.
method.overrides = method.overrides ?? iface;
Expand Down Expand Up @@ -290,6 +296,7 @@ function _defaultValidations(): ValidationFunction[] {
property,
`${type.fqn}#${property.name}`,
`implementing ${ifaceType.fqn}`,
{ allowCovariance: false },
);
// We won't replace a previous overrides declaration from a property override, as those
// have higher precedence than an initial implementation.
Expand All @@ -303,7 +310,15 @@ function _defaultValidations(): ValidationFunction[] {
return false;
}

function _assertSignaturesMatch(expected: spec.Method, actual: spec.Method, label: string, action: string) {
function _assertSignaturesMatch(
expected: spec.Method,
actual: spec.Method,
label: string,
action: string,
opts: {
allowCovariance: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type covariance.

By the way, does that mean we also have argument type contravariance? Or no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only return types and readonly properties. Argument type covariance is also still not allowed.

},
) {
if (!!expected.protected !== !!actual.protected) {
const expVisibility = expected.protected ? 'protected' : 'public';
const actVisibility = actual.protected ? 'protected' : 'public';
Expand All @@ -316,12 +331,24 @@ function _defaultValidations(): ValidationFunction[] {
),
);
}

// Types must generally be the same, but can be covariant sometimes
if (!deepEqual(actual.returns, expected.returns)) {
const expType = spec.describeTypeReference(expected.returns?.type);
const actType = spec.describeTypeReference(actual.returns?.type);
diagnostic(
JsiiDiagnostic.JSII_5003_OVERRIDE_CHANGES_RETURN_TYPE.createDetached(label, action, actType, expType),
);
const actualReturnType = actual.returns?.type;
const expectedReturnType = expected.returns?.type;

if (
// static members can never change
actual.static ||
// Check if this is a valid covariant return type (actual is more specific than expected)
!(opts.allowCovariance && _isCovariantOf(actualReturnType, expectedReturnType))
) {
const expType = spec.describeTypeReference(expectedReturnType);
const actType = spec.describeTypeReference(actualReturnType);
diagnostic(
JsiiDiagnostic.JSII_5003_OVERRIDE_CHANGES_RETURN_TYPE.createDetached(label, action, actType, expType),
);
}
}
const expectedParams = expected.parameters ?? [];
const actualParams = actual.parameters ?? [];
Expand Down Expand Up @@ -363,7 +390,113 @@ function _defaultValidations(): ValidationFunction[] {
}
}

function _assertPropertiesMatch(expected: spec.Property, actual: spec.Property, label: string, action: string) {
function _isCovariantOf(candidateType?: spec.TypeReference, expectedType?: spec.TypeReference): boolean {
// one void, while other isn't => not covariant
if ((candidateType === undefined) !== (expectedType === undefined)) {
return false;
}

// Same type is always covariant
if (deepEqual(candidateType, expectedType)) {
return true;
}

if (!spec.isNamedTypeReference(candidateType) || !spec.isNamedTypeReference(expectedType)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For collections't, won't A ⊑ B ⇒ List<A> ⊑ List<B> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've currently excluded lists. Will see if that's supported in C#.

return false;
}

const candidateTypeSpec = _dereference(candidateType.fqn, assembly, validator);
const expectedTypeSpec = _dereference(expectedType.fqn, assembly, validator);

if (!candidateTypeSpec || !expectedTypeSpec) {
return false;
}

// Handle class-to-class inheritance
if (spec.isClassType(candidateTypeSpec) && spec.isClassType(expectedTypeSpec)) {
// Check if candidateType extends expectedType (directly or indirectly)
Comment on lines +415 to +417
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also call _interfaceExtendsInterface here?

return _classExtendsClass(candidateTypeSpec, expectedType.fqn);
}

// Handle class implementing interface
if (spec.isClassType(candidateTypeSpec) && spec.isInterfaceType(expectedTypeSpec)) {
return _classImplementsInterface(candidateTypeSpec, expectedType.fqn);
}

return false;
}

function _classExtendsClass(classType: spec.ClassType, targetFqn: string): boolean {
let current = classType;
while (current.base) {
if (current.base === targetFqn) {
return true;
}
const baseType = _dereference(current.base, assembly, validator);
if (!spec.isClassType(baseType)) {
break;
}
current = baseType;
}
return false;
}

function _classImplementsInterface(classType: spec.ClassType, interfaceFqn: string): boolean {
// Check direct interfaces
if (classType.interfaces?.includes(interfaceFqn)) {
return true;
}

// Check inherited interfaces
if (classType.interfaces) {
for (const iface of classType.interfaces) {
const ifaceType = _dereference(iface, assembly, validator);
if (spec.isInterfaceType(ifaceType) && _interfaceExtendsInterface(ifaceType, interfaceFqn)) {
return true;
}
}
}

// Check base class interfaces
if (classType.base) {
const baseType = _dereference(classType.base, assembly, validator);
if (spec.isClassType(baseType)) {
return _classImplementsInterface(baseType, interfaceFqn);
}
}

return false;
}

function _interfaceExtendsInterface(interfaceType: spec.InterfaceType, targetFqn: string): boolean {
if (interfaceType.fqn === targetFqn) {
return true;
}

if (interfaceType.interfaces) {
for (const iface of interfaceType.interfaces) {
if (iface === targetFqn) {
return true;
}
const ifaceType = _dereference(iface, assembly, validator);
if (spec.isInterfaceType(ifaceType) && _interfaceExtendsInterface(ifaceType, targetFqn)) {
return true;
}
}
}

return false;
}

function _assertPropertiesMatch(
expected: spec.Property,
actual: spec.Property,
label: string,
action: string,
opts: {
allowCovariance: boolean;
},
) {
const actualNode = bindings.getPropertyRelatedNode(actual);
const expectedNode = bindings.getPropertyRelatedNode(expected);
if (!!expected.protected !== !!actual.protected) {
Expand All @@ -386,19 +519,28 @@ function _defaultValidations(): ValidationFunction[] {
),
);
}
if (!deepEqual(expected.type, actual.type)) {
diagnostic(
JsiiDiagnostic.JSII_5004_OVERRIDE_CHANGES_PROP_TYPE.create(
actualNode?.type ?? declarationName(actualNode),
label,
action,
actual.type,
expected.type,
).maybeAddRelatedInformation(
expectedNode?.type ?? declarationName(expectedNode),
'The implemented declaration is here.',
),
);

// Types must generally be the same, but can be covariant sometimes
if (!deepEqual(actual.type, expected.type)) {
if (
// static members can never change
actual.static ||
// immutable properties may change in some case, as long as they are covariant
!(opts.allowCovariance && actual.immutable && _isCovariantOf(actual.type, expected.type))
) {
diagnostic(
JsiiDiagnostic.JSII_5004_OVERRIDE_CHANGES_PROP_TYPE.create(
actualNode?.type ?? declarationName(actualNode),
label,
action,
actual.type,
expected.type,
).maybeAddRelatedInformation(
expectedNode?.type ?? declarationName(expectedNode),
'The implemented declaration is here.',
),
);
}
}
if (expected.immutable !== actual.immutable) {
diagnostic(
Expand Down
Loading
Loading