diff --git a/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index a985f8c5429..a638f75738d 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -7,6 +7,7 @@ import { addDependentKeys, ComputedDescriptor, Decorator, + isElementDescriptor, makeComputedDecorator, removeDependentKeys, } from './decorator'; @@ -20,6 +21,11 @@ const CONSUMED = Object.freeze({}); export type AliasDecorator = Decorator & PropertyDecorator & AliasDecoratorImpl; export default function alias(altKey: string): AliasDecorator { + assert( + 'You attempted to use @alias as a decorator directly, but it requires a `altKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return makeComputedDecorator(new AliasedProperty(altKey), AliasDecoratorImpl) as AliasDecorator; } diff --git a/packages/@ember/object/lib/computed/computed_macros.js b/packages/@ember/object/lib/computed/computed_macros.js index d0de2a2ac2d..3ab7839e721 100644 --- a/packages/@ember/object/lib/computed/computed_macros.js +++ b/packages/@ember/object/lib/computed/computed_macros.js @@ -2,6 +2,7 @@ import { get, set, computed, + isElementDescriptor, isEmpty, isNone, alias, @@ -35,6 +36,11 @@ function expandPropertiesToArray(predicateName, properties) { function generateComputedWithPredicate(name, predicate) { return (...properties) => { + assert( + `You attempted to use @${name} as a decorator directly, but it requires at least one dependent key parameter`, + !isElementDescriptor(properties) + ); + let dependentKeys = expandPropertiesToArray(name, properties); let computedFunc = computed(...dependentKeys, function() { @@ -112,6 +118,11 @@ function generateComputedWithPredicate(name, predicate) { @public */ export function empty(dependentKey) { + assert( + 'You attempted to use @empty as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(`${dependentKey}.length`, function() { return isEmpty(get(this, dependentKey)); }); @@ -172,6 +183,11 @@ export function empty(dependentKey) { @public */ export function notEmpty(dependentKey) { + assert( + 'You attempted to use @notEmpty as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(`${dependentKey}.length`, function() { return !isEmpty(get(this, dependentKey)); }); @@ -231,6 +247,11 @@ export function notEmpty(dependentKey) { @public */ export function none(dependentKey) { + assert( + 'You attempted to use @none as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return isNone(get(this, dependentKey)); }); @@ -287,6 +308,11 @@ export function none(dependentKey) { @public */ export function not(dependentKey) { + assert( + 'You attempted to use @not as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return !get(this, dependentKey); }); @@ -355,6 +381,11 @@ export function not(dependentKey) { @public */ export function bool(dependentKey) { + assert( + 'You attempted to use @bool as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return Boolean(get(this, dependentKey)); }); @@ -417,6 +448,11 @@ export function bool(dependentKey) { @public */ export function match(dependentKey, regexp) { + assert( + 'You attempted to use @match as a decorator directly, but it requires `dependentKey` and `regexp` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { let value = get(this, dependentKey); return regexp.test(value); @@ -479,6 +515,11 @@ export function match(dependentKey, regexp) { @public */ export function equal(dependentKey, value) { + assert( + 'You attempted to use @equal as a decorator directly, but it requires `dependentKey` and `value` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return get(this, dependentKey) === value; }); @@ -540,6 +581,11 @@ export function equal(dependentKey, value) { @public */ export function gt(dependentKey, value) { + assert( + 'You attempted to use @gt as a decorator directly, but it requires `dependentKey` and `value` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return get(this, dependentKey) > value; }); @@ -601,6 +647,11 @@ export function gt(dependentKey, value) { @public */ export function gte(dependentKey, value) { + assert( + 'You attempted to use @gte as a decorator directly, but it requires `dependentKey` and `value` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return get(this, dependentKey) >= value; }); @@ -662,6 +713,11 @@ export function gte(dependentKey, value) { @public */ export function lt(dependentKey, value) { + assert( + 'You attempted to use @lt as a decorator directly, but it requires `dependentKey` and `value` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return get(this, dependentKey) < value; }); @@ -723,6 +779,11 @@ export function lt(dependentKey, value) { @public */ export function lte(dependentKey, value) { + assert( + 'You attempted to use @lte as a decorator directly, but it requires `dependentKey` and `value` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, function() { return get(this, dependentKey) <= value; }); @@ -991,6 +1052,11 @@ export const or = generateComputedWithPredicate('or', value => !value); @public */ export function oneWay(dependentKey) { + assert( + 'You attempted to use @oneWay as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return alias(dependentKey).oneWay(); } @@ -1075,6 +1141,11 @@ export function oneWay(dependentKey) { @public */ export function readOnly(dependentKey) { + assert( + 'You attempted to use @readOnly as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return alias(dependentKey).readOnly(); } @@ -1133,6 +1204,11 @@ export function readOnly(dependentKey) { @public */ export function deprecatingAlias(dependentKey, options) { + assert( + 'You attempted to use @deprecatingAlias as a decorator directly, but it requires `dependentKey` and `options` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return computed(dependentKey, { get(key) { deprecate( diff --git a/packages/@ember/object/lib/computed/reduce_computed_macros.js b/packages/@ember/object/lib/computed/reduce_computed_macros.js index 38332e618c7..499582e7f2a 100644 --- a/packages/@ember/object/lib/computed/reduce_computed_macros.js +++ b/packages/@ember/object/lib/computed/reduce_computed_macros.js @@ -4,11 +4,12 @@ import { DEBUG } from '@glimmer/env'; import { assert } from '@ember/debug'; import { - get, - computed, addObserver, - removeObserver, + computed, + get, + isElementDescriptor, notifyPropertyChange, + removeObserver, } from '@ember/-internals/metal'; import { compare, isArray, A as emberA, uniqBy as uniqByArray } from '@ember/-internals/runtime'; @@ -105,6 +106,11 @@ function multiArrayMacro(_dependentKeys, callback, name) { @public */ export function sum(dependentKey) { + assert( + 'You attempted to use @sum as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return reduceMacro(dependentKey, (sum, item) => sum + item, 0, 'sum'); } @@ -200,6 +206,11 @@ export function sum(dependentKey) { @public */ export function max(dependentKey) { + assert( + 'You attempted to use @max as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return reduceMacro(dependentKey, (max, item) => Math.max(max, item), -Infinity, 'max'); } @@ -294,6 +305,11 @@ export function max(dependentKey) { @public */ export function min(dependentKey) { + assert( + 'You attempted to use @min as a decorator directly, but it requires a `dependentKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return reduceMacro(dependentKey, (min, item) => Math.min(min, item), Infinity, 'min'); } @@ -392,6 +408,11 @@ export function min(dependentKey) { @public */ export function map(dependentKey, additionalDependentKeys, callback) { + assert( + 'You attempted to use @map as a decorator directly, but it requires atleast `dependentKey` and `callback` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + if (callback === undefined && typeof additionalDependentKeys === 'function') { callback = additionalDependentKeys; additionalDependentKeys = []; @@ -496,6 +517,11 @@ export function map(dependentKey, additionalDependentKeys, callback) { @public */ export function mapBy(dependentKey, propertyKey) { + assert( + 'You attempted to use @mapBy as a decorator directly, but it requires `dependentKey` and `propertyKey` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + assert( '`computed.mapBy` expects a property string for its second argument, ' + 'perhaps you meant to use "map"', @@ -638,6 +664,11 @@ export function mapBy(dependentKey, propertyKey) { @public */ export function filter(dependentKey, additionalDependentKeys, callback) { + assert( + 'You attempted to use @filter as a decorator directly, but it requires atleast `dependentKey` and `callback` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + if (callback === undefined && typeof additionalDependentKeys === 'function') { callback = additionalDependentKeys; additionalDependentKeys = []; @@ -715,6 +746,11 @@ export function filter(dependentKey, additionalDependentKeys, callback) { @public */ export function filterBy(dependentKey, propertyKey, value) { + assert( + 'You attempted to use @filterBy as a decorator directly, but it requires atleast `dependentKey` and `propertyKey` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + assert( `Dependent key passed to \`computed.filterBy\` shouldn't contain brace expanding pattern.`, !/[\[\]\{\}]/g.test(dependentKey) @@ -789,6 +825,11 @@ export function filterBy(dependentKey, propertyKey, value) { @public */ export function uniq(...args) { + assert( + 'You attempted to use @uniq/@union as a decorator directly, but it requires atleast one dependent key parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return multiArrayMacro( args, function(dependentKeys) { @@ -873,6 +914,11 @@ export function uniq(...args) { @public */ export function uniqBy(dependentKey, propertyKey) { + assert( + 'You attempted to use @uniqBy as a decorator directly, but it requires `dependentKey` and `propertyKey` parameters', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + assert( `Dependent key passed to \`computed.uniqBy\` shouldn't contain brace expanding pattern.`, !/[\[\]\{\}]/g.test(dependentKey) @@ -1013,6 +1059,11 @@ export let union = uniq; @public */ export function intersect(...args) { + assert( + 'You attempted to use @intersect as a decorator directly, but it requires atleast one dependent key parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return multiArrayMacro( args, function(dependentKeys) { @@ -1115,6 +1166,11 @@ export function intersect(...args) { @public */ export function setDiff(setAProperty, setBProperty) { + assert( + 'You attempted to use @setDiff as a decorator directly, but it requires atleast one dependent key parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + assert('`computed.setDiff` requires exactly two dependent arrays.', arguments.length === 2); assert( `Dependent keys passed to \`computed.setDiff\` shouldn't contain brace expanding pattern.`, @@ -1187,6 +1243,11 @@ export function setDiff(setAProperty, setBProperty) { @public */ export function collect(...dependentKeys) { + assert( + 'You attempted to use @collect as a decorator directly, but it requires atleast one dependent key parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + return multiArrayMacro( dependentKeys, function() { @@ -1373,6 +1434,11 @@ export function collect(...dependentKeys) { @public */ export function sort(itemsKey, additionalDependentKeys, sortDefinition) { + assert( + 'You attempted to use @sort as a decorator directly, but it requires atleast an `itemsKey` parameter', + !isElementDescriptor(Array.prototype.slice.call(arguments)) + ); + if (DEBUG) { let argumentsValid = false; diff --git a/packages/@ember/object/tests/computed/macro_decorators_test.js b/packages/@ember/object/tests/computed/macro_decorators_test.js new file mode 100644 index 00000000000..63f26838e41 --- /dev/null +++ b/packages/@ember/object/tests/computed/macro_decorators_test.js @@ -0,0 +1,351 @@ +import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { EMBER_NATIVE_DECORATOR_SUPPORT } from '@ember/canary-features'; +import { alias } from '@ember/-internals/metal'; +import { + and, + bool, + collect, + deprecatingAlias, + empty, + equal, + filter, + filterBy, + gt, + gte, + intersect, + lt, + lte, + map, + mapBy, + match, + max, + min, + not, + notEmpty, + oneWay, + or, + readOnly, + setDiff, + sort, + sum, + union, + uniq, + uniqBy, +} from '@ember/object/computed'; + +if (EMBER_NATIVE_DECORATOR_SUPPORT) { + moduleFor( + 'computed macros - decorators - assertions', + class extends AbstractTestCase { + ['@test and throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @and foo; + } + + new Foo(); + }, /You attempted to use @and/); + } + + ['@test alias throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @alias foo; + } + + new Foo(); + }, /You attempted to use @alias/); + } + + ['@test bool throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @bool foo; + } + + new Foo(); + }, /You attempted to use @bool/); + } + + ['@test collect throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @collect foo; + } + + new Foo(); + }, /You attempted to use @collect/); + } + + ['@test deprecatingAlias throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @deprecatingAlias foo; + } + + new Foo(); + }, /You attempted to use @deprecatingAlias/); + } + + ['@test empty throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @empty foo; + } + + new Foo(); + }, /You attempted to use @empty/); + } + + ['@test equal throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @equal foo; + } + + new Foo(); + }, /You attempted to use @equal/); + } + + ['@test filter throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @filter foo; + } + + new Foo(); + }, /You attempted to use @filter/); + } + + ['@test filterBy throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @filterBy foo; + } + + new Foo(); + }, /You attempted to use @filterBy/); + } + + ['@test gt throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @gt foo; + } + + new Foo(); + }, /You attempted to use @gt/); + } + + ['@test gte throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @gte foo; + } + + new Foo(); + }, /You attempted to use @gte/); + } + + ['@test intersect throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @intersect foo; + } + + new Foo(); + }, /You attempted to use @intersect/); + } + + ['@test lt throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @lt foo; + } + + new Foo(); + }, /You attempted to use @lt/); + } + + ['@test lte throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @lte foo; + } + + new Foo(); + }, /You attempted to use @lte/); + } + + ['@test map throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @map foo; + } + + new Foo(); + }, /You attempted to use @map/); + } + + ['@test mapBy throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @mapBy foo; + } + + new Foo(); + }, /You attempted to use @mapBy/); + } + + ['@test match throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @match foo; + } + + new Foo(); + }, /You attempted to use @match/); + } + + ['@test max throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @max foo; + } + + new Foo(); + }, /You attempted to use @max/); + } + + ['@test min throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @min foo; + } + + new Foo(); + }, /You attempted to use @min/); + } + + ['@test not throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @not foo; + } + + new Foo(); + }, /You attempted to use @not/); + } + + ['@test notEmpty throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @notEmpty foo; + } + + new Foo(); + }, /You attempted to use @notEmpty/); + } + + ['@test oneWay throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @oneWay foo; + } + + new Foo(); + }, /You attempted to use @oneWay/); + } + + ['@test or throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @or foo; + } + + new Foo(); + }, /You attempted to use @or/); + } + + ['@test readOnly throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @readOnly foo; + } + + new Foo(); + }, /You attempted to use @readOnly/); + } + + ['@test setDiff throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @setDiff foo; + } + + new Foo(); + }, /You attempted to use @setDiff/); + } + + ['@test sort throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @sort foo; + } + + new Foo(); + }, /You attempted to use @sort/); + } + + ['@test sum throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @sum foo; + } + + new Foo(); + }, /You attempted to use @sum/); + } + + ['@test union throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @union foo; + } + + new Foo(); + }, /You attempted to use @uniq\/@union/); + } + + ['@test uniq throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @uniq foo; + } + + new Foo(); + }, /You attempted to use @uniq\/@union/); + } + + ['@test uniqBy throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @uniqBy foo; + } + + new Foo(); + }, /You attempted to use @uniqBy/); + } + + ['@test alias throws an error if used without parameters']() { + expectAssertion(() => { + class Foo { + @alias foo; + } + + new Foo(); + }, /You attempted to use @alias/); + } + } + ); +}