Skip to content

Commit

Permalink
Merge pull request #14 from ember-codemods/suchita/readOnlyComputed
Browse files Browse the repository at this point in the history
Add support for readOnly computed property conversion
  • Loading branch information
suchitadoshi1987 committed Jan 14, 2020
2 parents 625343b + e08d5da commit a3b12b3
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 32 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The codemod accepts the following options:
## Transforms

<!--TRANSFORMS_START-->
* [ember-tracked-properties-codemod](transforms/tracked-properties/README.md)
* [tracked-properties](transforms/tracked-properties/README.md)
<!--TRANSFORMS_END-->

## Contributing
Expand Down
98 changes: 85 additions & 13 deletions transforms/tracked-properties/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ npx ember-tracked-properties-codemod path/of/files/ or/some**/*glob.js
## Input / Output

<!--FIXTURES_TOC_START-->
* [basic-with-prefix-false](#basic-with-prefix-false)
* [basic](#basic)
* [chained-complex-computed](#chained-complex-computed)
* [chained-computed](#chained-computed)
* [complex](#complex)
* [non-computed-decorators](#non-computed-decorators)
* [with-tracked](#with-tracked)
<!--FIXTURES_TOC_END-->

- [basic-with-prefix-false](#basic-with-prefix-false)
- [basic](#basic)
- [chained-complex-computed](#chained-complex-computed)
- [chained-computed](#chained-computed)
- [complex](#complex)
- [non-computed-decorators](#non-computed-decorators)
- [read-only-computed-decorators](#read-only-computed-decorators)
- [with-tracked](#with-tracked)
<!--FIXTURES_TOC_END-->

## <!--FIXTURES_CONTENT_START-->

Expand Down Expand Up @@ -292,9 +294,9 @@ export default class Foo extends Component {
return get(this, 'isFoo') ? `Name: ${get(this, 'baz')}` : 'Baz';
}

@computed('bar', 'isFoo').readOnly()
@(computed('bar', 'isFoo').readOnly())
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bab')}` : 'Bar';
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}
}
```
Expand All @@ -308,7 +310,7 @@ import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

export default class Foo extends Component {
bar;
@tracked bar;
// baz class property
@tracked baz = 'barBaz';

Expand All @@ -320,9 +322,79 @@ export default class Foo extends Component {
return get(this, 'isFoo') ? `Name: ${get(this, 'baz')}` : 'Baz';
}

@computed('bar', 'isFoo').readOnly()
@(computed('isFoo').readOnly())
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}
}
```

---

<a id="read-only-computed-decorators">**read-only-computed-decorators**</a>

**Input** (<small>[read-only-computed-decorators.input.js](__testfixtures__/read-only-computed-decorators.input.js)</small>):

```js
import Component from '@ember/component';
import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

export default class Foo extends Component {
bar;
// baz class property
baz = 'barBaz';

@alias('model.isFoo')
isFoo;

@computed('baz', 'bar')
get barBazInfo() {
return `Bar: ${get(this, 'bar')}, Baz: ${get(this, 'baz')}`;
}

@(computed('bar', 'isFoo').readOnly())
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bab')}` : 'Bar';
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}

// This should not remove the 'blah' decorator since its not a computed property.
@blah('bar')
get barData() {
return get(this, 'bar');
}
}
```

**Output** (<small>[read-only-computed-decorators.output.js](__testfixtures__/read-only-computed-decorators.output.js)</small>):

```js
import { tracked } from '@glimmer/tracking';
import Component from '@ember/component';
import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

export default class Foo extends Component {
@tracked bar;
// baz class property
@tracked baz = 'barBaz';

@alias('model.isFoo')
isFoo;

get barBazInfo() {
return `Bar: ${get(this, 'bar')}, Baz: ${get(this, 'baz')}`;
}

@(computed('isFoo').readOnly())
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}

// This should not remove the 'blah' decorator since its not a computed property.
@blah('bar')
get barData() {
return get(this, 'bar');
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export default class Foo extends Component {

@computed('bar', 'isFoo').readOnly()
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bab')}` : 'Bar';
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

export default class Foo extends Component {
bar;
@tracked bar;
// baz class property
@tracked baz = 'barBaz';

Expand All @@ -16,8 +16,8 @@ export default class Foo extends Component {
return get(this, 'isFoo') ? `Name: ${get(this, 'baz')}` : 'Baz';
}

@computed('bar', 'isFoo').readOnly()
@computed('isFoo').readOnly()
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bab')}` : 'Bar';
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Component from '@ember/component';
import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

export default class Foo extends Component {
bar;
// baz class property
baz = 'barBaz';

@alias('model.isFoo')
isFoo;

@computed('baz', 'bar')
get barBazInfo() {
return `Bar: ${get(this, 'bar')}, Baz: ${get(this, 'baz')}`;
}

@(computed('bar', 'isFoo').readOnly())
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}

// This should not remove the 'blah' decorator since its not a computed property.
@blah('bar')
get barData() {
return get(this, 'bar');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { tracked } from '@glimmer/tracking';
import Component from '@ember/component';
import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

export default class Foo extends Component {
@tracked bar;
// baz class property
@tracked baz = 'barBaz';

@alias('model.isFoo')
isFoo;

get barBazInfo() {
return `Bar: ${get(this, 'bar')}, Baz: ${get(this, 'baz')}`;
}

@(computed('isFoo').readOnly())
get barInfo() {
return get(this, 'isFoo') ? `Name: ${get(this, 'bar')}` : 'Bar';
}

// This should not remove the 'blah' decorator since its not a computed property.
@blah('bar')
get barData() {
return get(this, 'bar');
}
}
78 changes: 64 additions & 14 deletions transforms/tracked-properties/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,46 @@ const DEFAULT_OPTIONS = {
alwaysPrefix: 'true',
};

/**
* Return true if the computed property is readOnly.
* @param {*} nodeItem
*/
function _isReadOnlyComputedProperty(nodeItem) {
return (
_isComputedProperty(nodeItem) &&
nodeItem.expression.callee.property &&
nodeItem.expression.callee.property.name === 'readOnly'
);
}

/**
* Return true if the nodeItem is a computed property. It could either
* be a regular or readOnly computed property.
* @param {*} nodeItem
*/
function _isComputedProperty(nodeItem) {
return (
nodeItem.expression.callee &&
(nodeItem.expression.callee.name === 'computed' ||
(nodeItem.expression.callee.object &&
nodeItem.expression.callee.object.callee.name === 'computed'))
);
}

/**
* If the nodeItem is a computed property, then return an array of argument values.
* @param {*} nodeItem
*/
function _getArgValues(nodeItem) {
if (_isComputedProperty(nodeItem)) {
const nodeArguments = _isReadOnlyComputedProperty(nodeItem)
? nodeItem.expression.callee.object.arguments
: nodeItem.expression.arguments;

return nodeArguments.map(item => item.value);
}
}

module.exports = function transformer(file, api) {
const configOptions = Object.assign({}, DEFAULT_OPTIONS, getOptions());
const classProps = [];
Expand All @@ -24,8 +64,15 @@ module.exports = function transformer(file, api) {
.forEach(path => {
path.node.body.forEach(classItem => {
// Collect all the class properties in the file and add it to the
// classProps array.
if (classItem.type === 'ClassProperty' && !classItem.decorators) {
// classProps array. If there is a decorator associated with a class
// property, then only add it to the array if it is a @tracked property.
if (
classItem.type === 'ClassProperty' &&
(!classItem.decorators ||
classItem.decorators.every(
item => item.expression.name === 'tracked'
))
) {
classProps.push(classItem.key.name);
}
// Collect all the dependent keys of the computed properties present in the file
Expand All @@ -36,13 +83,8 @@ module.exports = function transformer(file, api) {
classItem.decorators
) {
classItem.decorators.forEach(decoratorItem => {
if (
decoratorItem.expression.callee &&
decoratorItem.expression.callee.name === 'computed'
) {
const argValues = decoratorItem.expression.arguments.map(
item => item.value
);
const argValues = _getArgValues(decoratorItem);
if (argValues) {
computedPropsMap[classItem.key.name] = argValues;
computedProps = computedProps.concat(argValues);
}
Expand All @@ -63,7 +105,7 @@ module.exports = function transformer(file, api) {
if (!path.node.decorators && computedProps.includes(path.node.key.name)) {
shouldImportBeAdded = true;
const trackedDecorator = buildTrackedDecorator(path.node.key.name, j);

// @TODO: Determine if @tracked can be prefixed alongside other decorators in a property,
// if yes, then change this code to push the trackedDecorator along with the
// others.
Expand All @@ -86,20 +128,28 @@ module.exports = function transformer(file, api) {
.filter(path => {
return (
path.node.expression.type === 'CallExpression' &&
path.node.expression.callee &&
path.node.expression.callee.name === 'computed'
_isComputedProperty(path.node)
);
})
.forEach(path => {
const isReadOnlyProperty = _isReadOnlyComputedProperty(path.node);
const computedPropArguments = isReadOnlyProperty
? path.node.expression.callee.object.arguments
: path.node.expression.arguments;

const dependentKeys = getDependentKeys(
path.node.expression.arguments,
computedPropArguments,
computedPropsMap,
classProps
);
if (!dependentKeys.length) {
path.replace();
} else {
path.node.expression.arguments = dependentKeys;
if (isReadOnlyProperty) {
path.node.expression.callee.object.arguments = dependentKeys;
} else {
path.node.expression.arguments = dependentKeys;
}
}
});

Expand Down

0 comments on commit a3b12b3

Please sign in to comment.