From 7721942e12754cdb58c48447f1e31f5479462a39 Mon Sep 17 00:00:00 2001 From: Michael Haufe Date: Sun, 2 May 2021 01:45:44 -0500 Subject: [PATCH] Fix #207 (#208) --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- src/Contracted.ts | 4 ++-- src/index.ts | 4 ++-- src/lib/ClassRegistration.ts | 28 +++++++++++++--------------- src/tests/override.test.ts | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6e7ff..1128eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.20.5 + +* Features named with a symbol now support `@override` + ## v0.20.4 * Improved error messaging with `@override` when feature is missing diff --git a/package-lock.json b/package-lock.json index 202a27c..b5be12d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@final-hill/decorator-contracts", - "version": "0.20.4", + "version": "0.20.5", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 910ad5b..ee6f47d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@final-hill/decorator-contracts", - "version": "0.20.4", + "version": "0.20.5", "description": "Code Contracts for TypeScript and ECMAScript classes", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/Contracted.ts b/src/Contracted.ts index 238017c..141bc9f 100644 --- a/src/Contracted.ts +++ b/src/Contracted.ts @@ -10,7 +10,7 @@ import { assert, checkedMode, Contract } from './'; import { MSG_NO_PROPERTIES, MSG_SINGLE_CONTRACT } from './Messages'; const isContracted = Symbol('isContracted'), - innerContract = Symbol('Contract'); + innerContract = Symbol('innerContract'); /** * Checks the features of the provided object for properties. @@ -92,5 +92,5 @@ function Contracted< }; } -export {isContracted}; +export {isContracted, innerContract}; export default Contracted; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f044597..7e8d0d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,8 @@ import AssertionError from './AssertionError'; import {Contract, extend, invariant, Invariant, checkedMode, Rescue} from './Contract'; -import Contracted from './Contracted'; +import Contracted, {innerContract} from './Contracted'; import override from './override'; import assert from './assert'; -export {AssertionError, Contract, Contracted, Invariant, Rescue, invariant, extend, checkedMode, override, assert}; \ No newline at end of file +export {AssertionError, Contract, Contracted, Invariant, Rescue, invariant, innerContract, extend, checkedMode, override, assert}; \ No newline at end of file diff --git a/src/lib/ClassRegistration.ts b/src/lib/ClassRegistration.ts index 6d8d67a..7e7e51b 100644 --- a/src/lib/ClassRegistration.ts +++ b/src/lib/ClassRegistration.ts @@ -6,7 +6,7 @@ */ import { MSG_SINGLE_RETRY } from '../Messages'; -import { assert, checkedMode, Contract } from '../'; +import { assert, checkedMode, Contract, innerContract } from '../'; import { assertInvariants, assertDemands, CLASS_REGISTRY, Constructor, Feature, unChecked } from './'; import assertEnsures from './assertEnsures'; @@ -20,7 +20,7 @@ import assertEnsures from './assertEnsures'; * @returns {function(...args: any[]): any} - The original function augments with assertion checks */ function checkedFeature( - featureName: string, + featureName: PropertyKey, fnOrig: (...args: any[]) => any, registration: ClassRegistration ) { @@ -36,20 +36,18 @@ function checkedFeature( let result; try { - let old = Object.create(null); + const old = Object.create(null); unChecked(contract, () => { - old = registration.features.reduce((acc,{hasGetter, name}) => { + registration.features.forEach(({name, hasGetter}) => { if(hasGetter) { - Object.defineProperty(acc, name, {value: this[name]}); + Object.defineProperty(old, name, {value: this[name]}); } - - return acc; - }, old); + }); }); result = fnOrig.apply(this,args); assertEnsures(this, contract, className, featureName, old, args); } catch(error) { - const rescue = contract.assertions[featureName]?.rescue; + const rescue = Reflect.get(contract.assertions,featureName)?.rescue; if(rescue == null) { assertInvariants(this, contract); throw error; @@ -77,14 +75,15 @@ function checkedFeature( class ClassRegistration { #features: Feature[]; - contract!: Contract; // Assigned by the Contracted class via this.bindContract contractsChecked = false; constructor(readonly Class: Constructor) { - this.#features = Object.entries(Object.getOwnPropertyDescriptors(this.Class.prototype)) - .filter(([key]) => key != 'constructor') - .map(([key, descriptor]) => new Feature(this, key, descriptor)); + 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)!)); } /** @@ -134,8 +133,7 @@ class ClassRegistration { } const proto = this.Class.prototype; this.features.forEach(feature => { - const name = String(feature.name), - {hasGetter, hasSetter, isMethod} = feature; + const {name,hasGetter, hasSetter, isMethod} = feature; Object.defineProperty(proto, name, { enumerable: true, diff --git a/src/tests/override.test.ts b/src/tests/override.test.ts index 1756a68..96031ae 100644 --- a/src/tests/override.test.ts +++ b/src/tests/override.test.ts @@ -369,4 +369,38 @@ describe('The \'override\' decorator must have a Contracted class in it\'s ances return new Base().toString(); }).not.toThrow(); }); +}); + +describe('Features named with a symbol must support `@override`', () => { + test('Invalid declaration', () => { + expect(() => { + const method = Symbol('method'); + class BadBase { + [method](value: number): number { return value; } + } + + class Fail extends BadBase { + @override + [method](value: number): number { return value; } + } + + return new Fail()[method](7); + }).toThrow(MSG_NOT_CONTRACTED); + }); + test('Valid declaration', () => { + const method = Symbol('method'); + @Contracted() + class Base { + [method](value: number): number { return value; } + } + + class Okay extends Base { + @override + [method](value: number): number { return value; } + } + + const okay = new Okay(); + + expect(okay[method](15)).toBe(15); + }); }); \ No newline at end of file