Skip to content

Commit

Permalink
bugfix #236
Browse files Browse the repository at this point in the history
  • Loading branch information
mlhaufe committed Aug 21, 2022
1 parent ddb5f89 commit 92fb76b
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 95 deletions.
41 changes: 12 additions & 29 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,27 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
// See: https://github.com/Microsoft/vscode-recipes/tree/master/debugging-jest-tests
"configurations": [
{
"name": "Jest All",
"type": "pwa-node",
"type": "node",
"request": "launch",
"runtimeArgs": [
"run-script",
"test",
"--runInBand"
],
"runtimeExecutable": "npm",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
"name": "Jest All",
"program": "${workspaceRoot}\\node_modules\\jest\\bin\\jest.js",
"args": [
"-i"
],
"sourceMaps": true
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Jest Current File",
"type": "pwa-node",
"type": "node",
"request": "launch",
"runtimeArgs": [
"run-script",
"test",
"--runInBand",
"name": "Jest Current File",
"program": "${workspaceRoot}\\node_modules\\jest\\bin\\jest.js",
"args": [
"-i",
"${fileBasenameNoExtension}"
],
"runtimeExecutable": "npm",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**"
//"!**/node_modules/**"
],
"sourceMaps": true
"internalConsoleOptions": "openOnSessionStart",
}
]
}
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Changelog

## vNext
## v0.24.0

* Updated debugger settings to leverage modern VSCode
* Fixed a subtle issue with subcontracting resolution (breaking change) ([#236](https://github.com/final-hill/decorator-contracts/issues/236))
* Updated VSCode debug settings to support the modern IDE
* Updated npm dependencies

## v0.23.1

Expand Down
30 changes: 18 additions & 12 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@final-hill/decorator-contracts",
"version": "0.23.1",
"version": "0.24.0",
"description": "Code Contracts for TypeScript and ECMAScript classes",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -86,4 +86,4 @@
"files": [
"dist"
]
}
}
22 changes: 11 additions & 11 deletions src/Contracted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const isContracted = Symbol('isContracted'),
*/
function hasProperties<U>(registration: ClassRegistration, obj: U): boolean {
return Object.entries(Object.getOwnPropertyDescriptors(obj))
.some(([key, desc]) => new Feature(registration,key,desc).isProperty);
.some(([key, desc]) => new Feature(registration, key, desc).isProperty);
}

/**
Expand All @@ -40,10 +40,10 @@ function Contracted<
T extends Contract<any> = Contract<any>,
U extends ClassType<any> = ClassType<any>
>(contract: T = new Contract() as T): ClassDecorator {
return function(Base: U & {[innerContract]?: Contract<any>}) {
return function (Base: U & { [innerContract]?: Contract<any> }) {
assert(!Object.getOwnPropertySymbols(Base).includes(isContracted), MSG_SINGLE_CONTRACT);

if(contract[checkedMode] === false) {
if (contract[checkedMode] === false) {
return Base;
}

Expand All @@ -62,15 +62,15 @@ function Contracted<
super(...args);

const Class = this.constructor as ClassType<any>,
classRegistration = CLASS_REGISTRY.getOrCreate(Class);
classRegistration = CLASS_REGISTRY.getOrCreate(Class);

assert(!hasProperties(classRegistration,this), MSG_NO_PROPERTIES);
assert(!hasProperties(classRegistration, this), MSG_NO_PROPERTIES);

if(!classRegistration.contractsChecked) {
if (!classRegistration.contractsChecked) {
let ancRegistrations = classRegistration.ancestry().reverse();
// top-down check overrides and pre-existing properties
[...ancRegistrations, classRegistration].forEach(ancRegistration => {
if(!ancRegistration.contractsChecked) {
if (!ancRegistration.contractsChecked) {
ancRegistration.checkOverrides();
ancRegistration.contractsChecked = true;
}
Expand All @@ -79,7 +79,7 @@ function Contracted<
// bottom-up to closest Contracted class bind contracts
ancRegistrations = takeWhile(ancRegistrations.reverse(), (cr => cr.Class !== Base));
[classRegistration, ...ancRegistrations, CLASS_REGISTRY.get(Base)!].forEach(registration => {
registration.bindContract(this[innerContract]);
registration.bindContract(InnerContracted[innerContract]);
});
}

Expand All @@ -91,8 +91,8 @@ function Contracted<
// FIXME: dirty hack. Possibly resolved by moving to contracts as classes
// The static getter is used by the construction invariant check
// The instance getter is used by the feature declarations
static get [innerContract](){ return contract; }
get [innerContract](){ return contract; }
static get [innerContract]() { return contract; }
get [innerContract]() { return contract; }
}

const classRegistration = CLASS_REGISTRY.getOrCreate(InnerContracted);
Expand All @@ -104,5 +104,5 @@ function Contracted<
} as ClassDecorator;
}

export {isContracted, innerContract};
export { isContracted, innerContract };
export default Contracted;
59 changes: 31 additions & 28 deletions src/lib/ClassRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,43 @@ function checkedFeature(
registration: ClassRegistration
) {
return function innerCheckedFeature(this: any, ...args: any[]) {
const {contract, Class} = registration,
const { Class } = registration;

assert(this instanceof Class, MSG_INVALID_CONTEXT);

const contract = Reflect.get(this, innerContract),
className = Class.name;
if(!contract[checkedMode]) {
return fnOrig.apply(this,args);
if (!contract[checkedMode]) {
return fnOrig.apply(this, args);
}

assert(this instanceof Class, MSG_INVALID_CONTEXT);
assertInvariants(this, contract);
assertDemands(this, contract, className, featureName, args);

let result;
try {
const old = Object.create(null);
unChecked(contract, () => {
registration.features.forEach(({name, hasGetter}) => {
if(hasGetter) {
Object.defineProperty(old, name, {value: this[name]});
registration.features.forEach(({ name, hasGetter }) => {
if (hasGetter) {
Object.defineProperty(old, name, { value: this[name] });
}
});
});
const within: number = Reflect.get(contract.assertions, featureName)?.within,
// TODO:Not all environments support performance.now() as a global and conditional import is inconvenient
// TODO:Not all environments currently support performance.now() as a global and conditional import is inconvenient
t0 = Date.now();
result = fnOrig.apply(this,args);
result = fnOrig.apply(this, args);
const t1 = Date.now();
if(within) {
if (within) {
assert(t1 - t0 < within,
`Timing constraint violated. Constraint: ${within}ms, actual: ${t1 - t0}ms`
);
}
assertEnsures(this, contract, className, featureName, old, args);
} catch(error) {
const rescue = Reflect.get(contract.assertions,featureName)?.rescue;
if(rescue == null) {
} catch (error) {
const rescue = Reflect.get(contract.assertions, featureName)?.rescue;
if (rescue == null) {
assertInvariants(this, contract);
throw error;
}
Expand All @@ -71,7 +74,7 @@ function checkedFeature(
result = innerCheckedFeature.call(this, ...args);
});
});
if(!hasRetried) {
if (!hasRetried) {
assertInvariants(this, contract);
throw error;
}
Expand All @@ -92,8 +95,8 @@ class ClassRegistration {
const proto = this.Class.prototype;
this.#features =
Reflect.ownKeys(proto)
.filter(key => key != 'constructor' && key !== innerContract)
.map(key => new Feature(this, key, Object.getOwnPropertyDescriptor(proto,key)!));
.filter(key => key != 'constructor' && key !== innerContract)
.map(key => new Feature(this, key, Object.getOwnPropertyDescriptor(proto, key)!));
}

/**
Expand Down Expand Up @@ -126,12 +129,12 @@ class ClassRegistration {
* @returns {ClassRegistration[]} - The ancestor class registrations
*/
ancestry(): ClassRegistration[] {
if(this.ParentClass == null) {
if (this.ParentClass == null) {
return [];
} else {
const parentRegistry = CLASS_REGISTRY.getOrCreate(this.ParentClass);

return [parentRegistry,...parentRegistry.ancestry()];
return [parentRegistry, ...parentRegistry.ancestry()];
}
}

Expand All @@ -142,23 +145,23 @@ class ClassRegistration {
* @returns {Feature[]} - The feature names
*/
ancestryFeatures(): Feature[] {
return this.ancestry().flatMap(({features}) => features);
return this.ancestry().flatMap(({ features }) => features);
}

bindContract<T extends Contract<any>>(contract: T) {
this.contract = contract;
if(!contract[checkedMode]) {
if (!contract[checkedMode]) {
return;
}
const proto = this.Class.prototype;
this.features.forEach(feature => {
const {name,hasGetter, hasSetter, isMethod} = feature;
const { name, hasGetter, hasSetter, isMethod } = feature;

Object.defineProperty(proto, name, {
enumerable: true,
...(hasGetter ? {get: checkedFeature(name, feature.getter!, this) } : {}),
...(hasSetter ? {set: checkedFeature(name, feature.setter!, this) } : {}),
...(isMethod ? {value: checkedFeature(name, feature.value, this) } : {})
...(hasGetter ? { get: checkedFeature(name, feature.getter!, this) } : {}),
...(hasSetter ? { set: checkedFeature(name, feature.setter!, this) } : {}),
...(isMethod ? { value: checkedFeature(name, feature.value, this) } : {})
});

feature.descriptor = Object.getOwnPropertyDescriptor(proto, name)!;
Expand All @@ -170,10 +173,10 @@ class ClassRegistration {
* @throws {AssertionError} - Throws if the verification fails
*/
checkOverrides(): void {
const ancestryFeatureNames = new Set(this.ancestryFeatures().map(({name}) => name));
this.features.forEach(({name, hasOverrides}) => {
const ancestryFeatureNames = new Set(this.ancestryFeatures().map(({ name }) => name));
this.features.forEach(({ name, hasOverrides }) => {
const str = `${this.Class.name}.prototype.${String(name)}`;
assert(!hasOverrides || ancestryFeatureNames.has(name),`Unnecessary @override declaration on ${str}`);
assert(!hasOverrides || ancestryFeatureNames.has(name), `Unnecessary @override declaration on ${str}`);
assert(hasOverrides || !ancestryFeatureNames.has(name), `@override decorator missing on ${str}`);
});
}
Expand All @@ -186,7 +189,7 @@ class ClassRegistration {
* @returns {Feature | undefined} - The feature if it exists else otherwise
*/
findFeature(propertyKey: PropertyKey): Feature | undefined {
return this.features.find(({name}) => name === propertyKey) ??
return this.features.find(({ name }) => name === propertyKey) ??
this.parentRegistration?.findFeature(propertyKey);
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/lib/assertDemands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ import unChecked from './unChecked';
* @param {any[]} args - The arguments of the feature to apply to the assertion
* @throws {AssertionError}
*/
function assertDemands<U>(ctx: U, contract: Contract<any>, className: string, featureName: PropertyKey, args: any[]){
function assertDemands<U extends object>(ctx: U, contract: Contract<U>, className: string, featureName: PropertyKey, args: any[]) {
let result = true;
const d: Demands<any, any> | undefined = Reflect.get(contract.assertions, featureName)?.demands,
demandsError = `demands not met on ${className}.prototype.${String(featureName)}\r\n${d}`;
if(contract[checkedMode]) {
if(d) {
if (contract[checkedMode]) {
if (d) {
unChecked(contract, () =>
result = d.call(ctx,ctx, ...args)
result = d.call(ctx, ctx, ...args)
);
}
}

if(contract[extend] && !result) {
assertDemands(ctx,contract[extend]!, className, featureName, args);
if (contract[extend] && !result) {
assertDemands(ctx, contract[extend], className, featureName, args);
} else {
assert(result,demandsError);
assert(result, demandsError);
}
}

Expand Down
Loading

0 comments on commit 92fb76b

Please sign in to comment.