Repository: abuiles/ember-watson
Pull request #41 (2015-06-12 23:00:30)

Adds formula for explicity async: false in DS relationships
Implements #40.


Patch:
diff --git a/README.md b/README.md
index 17dba96..22c1e32 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,11 @@ want to convert code somewhere different to `app/`.
 
 For more info about this please refer to the following PR [Encourage decorator-style Ember.computed/Ember.observer](https://github.com/emberjs/guides/pull/110)
 
+### `ember watson:convert-ember-data-async-false-relationships`
+
+Adds an explicit `async: false` options to all `belongsTo` and `hasMany` that
+either have no options or its options does not contain an explicit async value.
+
 #### `ember watson:convert-ember-data-model-lookups`
 
 This changes the way model lookups happen when working with Ember
diff --git a/addon/commands/convert-ember-data-async-false-relationships.js b/addon/commands/convert-ember-data-async-false-relationships.js
new file mode 100644
index 0000000..178f72a
--- /dev/null
+++ b/addon/commands/convert-ember-data-async-false-relationships.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var Watson = require('../../index');
+var watson = new Watson();
+
+module.exports = {
+  name: 'watson:convert-ember-data-async-false-relationships',
+  description: 'include explicit async false option to relationships implicitly being synced',
+  works: 'insideProject',
+  anonymousOptions: [
+    '<path>'
+  ],
+  run: function(commandOptions, rawArgs) {
+    var path = rawArgs[0] ||  'app';
+
+    watson.transformEmberDataAsyncFalseRelationships(commandOptions.appPath);
+  }
+};
diff --git a/bin/ember-watson b/bin/ember-watson
index 57ded91..2ee6212 100755
--- a/bin/ember-watson
+++ b/bin/ember-watson
@@ -31,4 +31,12 @@ program
     watson.transformEmberDataModelLookups(appPath);
   });
 
+program
+  .command('convert-ember-data-async-false-relationships [appPath]')
+  .description('convert Ember Data relationship with implicit async: false to explicit option')
+  .action(funtion(appPath) {
+    appPath = appPath || 'app';
+    watson.transformEmberDataAsyncFalseRelationships;
+  });
+
 program.parse(process.argv);
diff --git a/lib/formulas/ember-data-async-false-relationships-transforms.js b/lib/formulas/ember-data-async-false-relationships-transforms.js
new file mode 100644
index 0000000..0c4f8d4
--- /dev/null
+++ b/lib/formulas/ember-data-async-false-relationships-transforms.js
@@ -0,0 +1,83 @@
+'use strict';
+
+var recast     = require('recast');
+var builders   = recast.types.builders;
+var types      = recast.types.namedTypes;
+var emberData  = require('./helpers/ember-data');
+
+module.exports = function transformEmberDataAsyncFalseRelationship(source) {
+  var ast = recast.parse(source);
+
+  recast.visit(ast, {
+    visitCallExpression: function(path) {
+      var node = path.node;
+
+      if(emberData.isRelationshipMacro(node)) {
+        replaceArguments(path);
+      }
+
+      this.traverse(path);
+    },
+  });
+
+  function asyncProperty() {
+    var async = builders.identifier('async');
+    var False = builders.literal(false);
+
+    return builders.property('init', async, False);
+  }
+
+  function rebuildArguments(args) {
+    var reargs        = args.slice();
+    var options = reargs[1] || builders.objectExpression([]);
+
+    options.properties.push(asyncProperty());
+
+    reargs[1] = options;
+
+    return reargs;
+  }
+
+  function replaceArguments(path) {
+    var node = path.node;
+
+    if (hasImplicitAsync(node)) {
+      var rebuilt = builders.callExpression(node.callee, rebuildArguments(node.arguments));
+      path.replace(rebuilt);
+    }
+  }
+
+  // Returns true if the call does not contain an options object
+  // or, if there is one, there is no asyn option in it.
+  function hasImplicitAsync(node) {
+    var args = node.arguments;
+
+    if (args.length >= 2) {
+      var keyNames = getKeyNamesFromObjectExpression(args[1]);
+
+      return keyNames.indexOf('async') < 0;
+    } else {
+      return true;
+    }
+  }
+
+  // Return the names of the properties only if they are identifiers
+  function getKeyNamesFromObjectExpression(objExp) {
+    if (types.ObjectExpression.check(objExp)) {
+      var names = [];
+      var props = objExp.properties;
+
+      for (var i = 0, len = props.length; i < len; i++) {
+        if (types.Identifier.check(props[i].key)) {
+          names.push(props[i].key.name);
+        }
+      }
+
+      return names;
+    } else {
+      return [];
+    }
+  }
+
+  return recast.print(ast, {tabWidth: 2, quote: 'single'}).code;
+};
diff --git a/lib/formulas/ember-data-model-lookup-transform.js b/lib/formulas/ember-data-model-lookup-transform.js
index 62ed7d8..32e3e99 100644
--- a/lib/formulas/ember-data-model-lookup-transform.js
+++ b/lib/formulas/ember-data-model-lookup-transform.js
@@ -6,6 +6,7 @@ var types      = recast.types.namedTypes;
 var Inflected  = require('inflected');
 var underscore = Inflected.underscore.bind(Inflected);
 var dasherize  = Inflected.dasherize.bind(Inflected);
+var emberData  = require('./helpers/ember-data');
 
 function normalize(string) {
   return dasherize(underscore(string));
@@ -14,42 +15,16 @@ function normalize(string) {
 module.exports = function transformEmberDataModelLookups(source){
   var ast = recast.parse(source);
 
-  var STORE_METHODS = [
-    'all',
-    'createRecord',
-    'filter',
-    'find',
-    'fetch',
-    'fetchAll',
-    'findById',
-    'findByIds',
-    'findQuery',
-    'getById',
-    'hasRecordForId',
-    'metaForType',
-    'modelFor',
-    'modelFactoryFor',
-    'normalize',
-    'push',
-    'pushMany',
-    'pushPayload',
-    'recordForId',
-    'recordIsLoaded',
-    'update',
-    'serializerFor',
-    'setMetadataFor',
-    'unloadAll'
-  ];
 
   recast.visit(ast, {
     visitCallExpression: function(path) {
       var node = path.node;
 
-      if (isRelationshipMacro(node)) {
+      if (emberData.isRelationshipMacro(node)) {
         replaceArguments(path);
       }
 
-      if (isStoreMethod(node)) {
+      if (emberData.isStoreMethod(node)) {
         replaceArguments(path);
       }
 
@@ -57,51 +32,6 @@ module.exports = function transformEmberDataModelLookups(source){
     }
   });
 
-  function isHasMany(node) {
-    var mightBeHasMany;
-
-    if (types.MemberExpression.check(node.callee)) {
-      mightBeHasMany = node.callee.object.name === 'DS' &&
-        node.callee.property.name === 'hasMany';
-    } else if (types.Identifier.check(node.callee)) {
-      mightBeHasMany = node.callee.name === 'hasMany';
-    }
-
-    return mightBeHasMany && isSupportedModelCall(node);
-  }
-
-  function isBelongsTo(node) {
-    var mightBeBelongsTo;
-
-    if (types.MemberExpression.check(node.callee)) {
-      mightBeBelongsTo = node.callee.object.name === 'DS' &&
-        node.callee.property.name === 'belongsTo';
-    } else if (types.Identifier.check(node.callee)) {
-      mightBeBelongsTo = node.callee.name === 'belongsTo';
-    }
-
-    return mightBeBelongsTo && isSupportedModelCall(node);
-  }
-
-  // First arguments is a string, or a variable that is capitalized, or a member expression.
-  function isSupportedModelCall(node) {
-    var firstArg;
-    if (node.arguments.length) {
-      firstArg = node.arguments[0];
-      if (types.Literal.check(firstArg)) {
-        return typeof firstArg.value === 'string';
-      } else if (types.Identifier.check(firstArg)) {
-        return firstArg.name[0] === firstArg.name[0].toUpperCase();
-      }
-      return types.MemberExpression.check(firstArg);
-    }
-    return false;
-  }
-
-  function isRelationshipMacro(node) {
-    return isHasMany(node) || isBelongsTo(node);
-  }
-
   function rebuildArguments(args) {
     var reargs        = args.slice();
     var modelNameNode = reargs[0];
@@ -134,15 +64,5 @@ module.exports = function transformEmberDataModelLookups(source){
     return modelName;
   }
 
-
-  function isStoreMethod(node) {
-    var maybeStoreMethod;
-    if (types.MemberExpression.check(node.callee)) {
-      maybeStoreMethod = node.callee.object.property && node.callee.object.property.name === 'store' &&
-        STORE_METHODS.indexOf(node.callee.property.name) >= 0;
-    }
-    return maybeStoreMethod && isSupportedModelCall(node);
-  }
-
   return recast.print(ast, {tabWidth: 2, quote: 'single'}).code;
 };
diff --git a/lib/formulas/helpers/ember-data.js b/lib/formulas/helpers/ember-data.js
new file mode 100644
index 0000000..7e0f499
--- /dev/null
+++ b/lib/formulas/helpers/ember-data.js
@@ -0,0 +1,92 @@
+'use strict';
+
+var recast     = require('recast');
+var types      = recast.types.namedTypes;
+
+var STORE_METHODS = [
+  'all',
+  'createRecord',
+  'filter',
+  'find',
+  'fetch',
+  'fetchAll',
+  'findById',
+  'findByIds',
+  'findQuery',
+  'getById',
+  'hasRecordForId',
+  'metaForType',
+  'modelFor',
+  'modelFactoryFor',
+  'normalize',
+  'push',
+  'pushMany',
+  'pushPayload',
+  'recordForId',
+  'recordIsLoaded',
+  'update',
+  'serializerFor',
+  'setMetadataFor',
+  'unloadAll'
+];
+
+function isHasMany(node) {
+  var mightBeHasMany;
+
+  if (types.MemberExpression.check(node.callee)) {
+    mightBeHasMany = node.callee.object.name === 'DS' &&
+    node.callee.property.name === 'hasMany';
+  } else if (types.Identifier.check(node.callee)) {
+    mightBeHasMany = node.callee.name === 'hasMany';
+  }
+
+  return mightBeHasMany && isSupportedModelCall(node);
+}
+
+function isBelongsTo(node) {
+  var mightBeBelongsTo;
+
+  if (types.MemberExpression.check(node.callee)) {
+    mightBeBelongsTo = node.callee.object.name === 'DS' &&
+    node.callee.property.name === 'belongsTo';
+  } else if (types.Identifier.check(node.callee)) {
+    mightBeBelongsTo = node.callee.name === 'belongsTo';
+  }
+
+  return mightBeBelongsTo && isSupportedModelCall(node);
+}
+
+// First arguments is a string, or a variable that is capitalized, or a member expression.
+function isSupportedModelCall(node) {
+  var firstArg;
+  if (node.arguments.length) {
+    firstArg = node.arguments[0];
+    if (types.Literal.check(firstArg)) {
+      return typeof firstArg.value === 'string';
+    } else if (types.Identifier.check(firstArg)) {
+      return firstArg.name[0] === firstArg.name[0].toUpperCase();
+    }
+    return types.MemberExpression.check(firstArg);
+  }
+  return false;
+}
+
+function isRelationshipMacro(node) {
+  return isHasMany(node) || isBelongsTo(node);
+}
+
+function isStoreMethod(node) {
+  var maybeStoreMethod;
+  if (types.MemberExpression.check(node.callee)) {
+    maybeStoreMethod = node.callee.object.property && node.callee.object.property.name === 'store' &&
+    STORE_METHODS.indexOf(node.callee.property.name) >= 0;
+  }
+  return maybeStoreMethod && isSupportedModelCall(node);
+}
+
+module.exports = {
+  isHasMany: isHasMany,
+  isBelongsTo: isBelongsTo,
+  isRelationshipMacro: isRelationshipMacro,
+  isStoreMethod: isStoreMethod,
+};
diff --git a/lib/watson.js b/lib/watson.js
index db75d8d..db5cb86 100644
--- a/lib/watson.js
+++ b/lib/watson.js
@@ -6,12 +6,21 @@ var findFiles = require('./helpers/find-files');
 var qunitTransforms = require('./formulas/qunit-transforms');
 var prototypeExtensionTransforms = require('./formulas/prototype-extension-transforms');
 var transformEmberDataModelLookups = require('./formulas/ember-data-model-lookup-transform');
+var transformEmberDataAsyncFalseRelationships = require('./formulas/ember-data-async-false-relationships-transforms');
 
 
 module.exports = EmberWatson;
 
 function EmberWatson() { }
 
+EmberWatson.prototype._transformEmberDataAsyncFalseRelationships = transformEmberDataAsyncFalseRelationships;
+
+EmberWatson.prototype.transformEmberDataAsyncFalseRelationships = function(rootPath) {
+  var files = findFiles(rootPath, '.js');
+
+  transform(files, this._transformEmberDataAsyncFalseRelationships);
+};
+
 EmberWatson.prototype._transformEmberDataModelLookups = transformEmberDataModelLookups;
 
 EmberWatson.prototype.transformEmberDataModelLookups = function(rootPath) {


Test patch:
diff --git a/tests/ember-data-async-false-relationship-test.js b/tests/ember-data-async-false-relationship-test.js
new file mode 100644
index 0000000..27425dd
--- /dev/null
+++ b/tests/ember-data-async-false-relationship-test.js
@@ -0,0 +1,36 @@
+'use strict';
+
+var Watson      = require('../index.js');
+var fs          = require('fs');
+var astEquality = require('./helpers/ast-equality');
+
+describe('ember data async false relationships', function() {
+  var baseDir = './tests/fixtures/ember-data-async-false-relationships';
+  var watson;
+
+  beforeEach(function() {
+    watson = new Watson();
+  });
+
+  describe('hasMany relationship macro', function() {
+
+    it('adds explicit async: false to options', function() {
+        var source            = fs.readFileSync(baseDir + '/has-many-old.js');
+        var newSource         = watson._transformEmberDataAsyncFalseRelationships(source);
+        var expectedNewSource = fs.readFileSync(baseDir + '/has-many-new.js');
+
+        astEquality(newSource, expectedNewSource);
+    });
+  });
+
+  describe('belongsTo relationship macro', function() {
+
+    it('adds explicit async: false to options', function() {
+        var source            = fs.readFileSync(baseDir + '/belongs-to-old.js');
+        var newSource         = watson._transformEmberDataAsyncFalseRelationships(source);
+        var expectedNewSource = fs.readFileSync(baseDir + '/belongs-to-new.js');
+
+        astEquality(newSource, expectedNewSource);
+    });
+  });
+});
diff --git a/tests/fixtures/ember-data-async-false-relationships/belongs-to-new.js b/tests/fixtures/ember-data-async-false-relationships/belongs-to-new.js
new file mode 100644
index 0000000..95dcb38
--- /dev/null
+++ b/tests/fixtures/ember-data-async-false-relationships/belongs-to-new.js
@@ -0,0 +1,24 @@
+import DS from 'ember-data';
+
+const {belongsTo} = DS;
+
+var Post = DS.Model.extend({
+  comments: DS.belongsTo('post-comments', {async: true}),
+  images: belongsTo('images', {async: true}),
+  pingbacks: DS.belongsTo('pingbacks', {
+    async: false
+  }),
+  links: belongsTo('links', {
+    async: false
+  }),
+  subposts: DS.belongsTo('posts', {
+    polymorphic: true,
+    async: false
+  }),
+  variables: belongsTo('variables', {
+    polymorphic: true,
+    async: false
+  }),
+});
+
+export default Post;
diff --git a/tests/fixtures/ember-data-async-false-relationships/belongs-to-old.js b/tests/fixtures/ember-data-async-false-relationships/belongs-to-old.js
new file mode 100644
index 0000000..797ac8d
--- /dev/null
+++ b/tests/fixtures/ember-data-async-false-relationships/belongs-to-old.js
@@ -0,0 +1,14 @@
+import DS from 'ember-data';
+
+const {belongsTo} = DS;
+
+var Post = DS.Model.extend({
+  comments: DS.belongsTo('post-comments', {async: true}),
+  images: belongsTo('images', {async: true}),
+  pingbacks: DS.belongsTo('pingbacks'),
+  links: belongsTo('links'),
+  subposts: DS.belongsTo('posts', {polymorphic: true}),
+  variables: belongsTo('variables', {polymorphic: true}),
+});
+
+export default Post;
diff --git a/tests/fixtures/ember-data-async-false-relationships/has-many-new.js b/tests/fixtures/ember-data-async-false-relationships/has-many-new.js
new file mode 100644
index 0000000..77b817a
--- /dev/null
+++ b/tests/fixtures/ember-data-async-false-relationships/has-many-new.js
@@ -0,0 +1,24 @@
+import DS from 'ember-data';
+
+const {hasMany} = DS;
+
+var Post = DS.Model.extend({
+  comments: DS.hasMany('post-comments', {async: true}),
+  images: hasMany('images', {async: true}),
+  pingbacks: DS.hasMany('pingbacks', {
+    async: false
+  }),
+  links: hasMany('links', {
+    async: false
+  }),
+  subposts:  DS.hasMany('posts', {
+    polymorphic: true,
+    async: false
+  }),
+  variables: hasMany('variables', {
+    polymorphic: true,
+    async: false
+  }),
+});
+
+export default Post;
diff --git a/tests/fixtures/ember-data-async-false-relationships/has-many-old.js b/tests/fixtures/ember-data-async-false-relationships/has-many-old.js
new file mode 100644
index 0000000..c22f1a1
--- /dev/null
+++ b/tests/fixtures/ember-data-async-false-relationships/has-many-old.js
@@ -0,0 +1,14 @@
+import DS from 'ember-data';
+
+const {hasMany} = DS;
+
+var Post = DS.Model.extend({
+  comments: DS.hasMany('post-comments', {async: true}),
+  images: hasMany('images', {async: true}),
+  pingbacks: DS.hasMany('pingbacks'),
+  links: hasMany('links'),
+  subposts:  DS.hasMany('posts', {polymorphic: true}),
+  variables: hasMany('variables', {polymorphic: true}),
+});
+
+export default Post;