Repository: rwjblue/ember-template-lint
Pull request #138 (2016-11-12 16:29:06)

Support for Plugins
@rwjblue Basic implementation for #136. 

- [X] Add a new `plugins `.
- [X] Expand `extends` to support external configurations. 
- [X] `extends` supports an array of configurations.
- [ ] Expose public API.


I don't know which APIs will become public. It may be better if `exposing the public API` is done by somebody else.


ppcano: @rwjblue Included latest changes. 

Let me know any suggested changes or improvements, thanks!
ppcano: @rwjblue Applied the changes.

1) Let me know when I should rebase into one commit

2) Node 4 and 0.12 fails because using  [resolve](https://github.com/rwjblue/ember-template-lint/pull/138/commits/1e58d95b3e4a35e94aeb20defd35dc845bcbb036). Should I rollback to use `existSync`?

rwjblue: Do we have any tests for a plugin loading a configuration that extends from another plugins configuration?
ppcano: @rwjblue 

> Do we have any tests for a plugin loading a configuration that extends from another plugins configuration?

No. Is this the only test you are missing?

ppcano: @rwjblue Complete latest changes. Waiting your final approval to rebase into one commit.
rwjblue: Looks good. Let's rebase / squash and get it out in a beta version for testing...

Patch:
diff --git a/lib/config/index.js b/lib/config/index.js
new file mode 100644
index 0000000..6b870ee
--- /dev/null
+++ b/lib/config/index.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+  'recommended': require('./recommended')
+};
diff --git a/lib/config/recommended.js b/lib/config/recommended.js
index f116c7f..0dd44de 100644
--- a/lib/config/recommended.js
+++ b/lib/config/recommended.js
@@ -14,8 +14,5 @@ module.exports = {
     'style-concatenation': true,
     'deprecated-inline-view-helper': true,
     'unused-block-params': true
-  },
-
-  pending: [],
-  ignore: []
+  }
 };
diff --git a/lib/get-config.js b/lib/get-config.js
index e230b29..d72d1d1 100644
--- a/lib/get-config.js
+++ b/lib/get-config.js
@@ -1,28 +1,41 @@
 'use strict';
 
 var path = require('path');
+var resolve = require('resolve');
 var assign = require('lodash').assign;
-var existsSync = require('exists-sync');
 var chalk = require('chalk');
-var rules = require('./rules');
+var defaultRules = require('./rules');
+var defaultConfigurations = require('./config/index');
 
-var KNOWN_ROOT_PROPERTIES = ['extends', 'rules', 'pending', 'ignore'];
+var KNOWN_ROOT_PROPERTIES = ['extends', 'rules', 'pending', 'ignore', 'plugins'];
 
-function readConfigFromDisk(options) {
-  var providedConfigPath = options.configPath;
-  var configPath = providedConfigPath || path.join(process.cwd(), '.template-lintrc');
 
-  if(existsSync(configPath) || existsSync(configPath + '.js') || existsSync(configPath + '.json')) {
-    return require(configPath);
-  } else {
+function resolveConfigPath(configPath, pluginPath) {
+
+  configPath = configPath || path.join(process.cwd(), '.template-lintrc');
+
+
+  if (pluginPath) {
+
+    //throws exception if not found
+    pluginPath = resolve.sync(pluginPath, { basedir: path.dirname(configPath) });
+
+  }
+
+  try {
+    var filePath = pluginPath || configPath;
+    return require(filePath);
+  } catch(e) {
     return {};
   }
+
 }
 
 function ensureRootProperties(config) {
   config.rules = config.rules || {};
   config.pending = config.pending || [];
   config.ignore = config.ignore || [];
+  config.plugins = config.plugins || [];
 }
 
 function migrateRulesFromRoot(config, options) {
@@ -50,17 +63,139 @@ function migrateRulesFromRoot(config, options) {
   }
 }
 
-function processExtends(config) {
+function processPlugins(config, options) {
+
+  var logger = options.console || console;
+
+  var configPlugins = config.plugins;
+  var pluginsHash = {};
+
+  for (var i = 0; i < configPlugins.length; i++) {
+
+    var plugin = configPlugins[i];
+    var pluginName;
+
+    if (typeof plugin === 'string') {
+      pluginName = plugin;
+      plugin = resolveConfigPath(options.configPath, pluginName);
+    }
+
+
+    var errorMessage;
+    if (typeof plugin === 'object') {
+
+      if (plugin.name) {
+        pluginsHash[plugin.name] = plugin;
+      } else if (pluginName) {
+        errorMessage = 'Plugin (' + pluginName + ') has not defined the plugin `name` property';
+      } else {
+        errorMessage = 'Inline plugin object has not defined the plugin `name` property';
+      }
+
+    } else if (pluginName) {
+      errorMessage = 'Plugin (' + pluginName + ') did not return a plain object';
+    } else {
+      errorMessage = 'Inline plugin is not a plain object';
+    }
+
+    if (errorMessage) {
+      logger.log(chalk.yellow(errorMessage));
+    }
+
+  }
+
+  config.plugins = pluginsHash;
+}
+
+function processLoadedRules(config) {
+
+  //load all the default rules in `ember-template-lint`
+  var loadedRules = assign({}, defaultRules);
+
+  //load plugin rules
+  for (var name in config.plugins) {
+
+    var pluginRules = config.plugins[name].rules;
+    if (pluginRules) {
+      loadedRules = assign(loadedRules, pluginRules);
+    }
+
+  }
+
+  config.loadedRules = loadedRules;
+
+}
+
+function processLoadedConfigurations(config) {
+
+  //load all the default configurations in `ember-template-lint`
+  var loadedConfigurations = assign({}, defaultConfigurations);
+
+  //load plugin configurations
+  for (var pluginName in config.plugins) {
+
+    var pluginConfigurations = config.plugins[pluginName].configurations;
+    if (pluginConfigurations) {
+
+      for (var configurationName in pluginConfigurations) {
+
+
+        var name = pluginName + ':' + configurationName;
+        loadedConfigurations[name] = pluginConfigurations[configurationName];
+
+      }
+    }
+
+  }
+
+  config.loadedConfigurations = loadedConfigurations;
+
+}
+
+function processExtends(config, options) {
+
+  var logger = options.console || console;
+
   if (config.extends) {
-    var extendingConfig = require('./config/' + config.extends);
 
-    config = assign({}, extendingConfig, config);
-    config.rules = assign({}, extendingConfig.rules, config.rules);
+    var extendedList = [];
+    if (typeof config.extends === 'string') {
+      extendedList = [config.extends];
+    } else if (config.extends instanceof Array) {
+      extendedList = config.extends;
+    } else {
+      logger.log(chalk.yellow('config.extends should be string or array '));
+    }
+
+    for (var i = 0; i < extendedList.length; i++) {
+
+      var extendName = extendedList[i];
+
+      var configuration = config.loadedConfigurations[extendName];
+      if (configuration) {
+
+        if (configuration.rules) {
+
+          config.rules = assign(config.rules, configuration.rules);
+
+        } else {
+
+          logger.log(chalk.yellow('Missing rules for extends: ' + extendName));
+
+        }
+
+      } else {
+
+        logger.log(chalk.yellow('Cannot find configuration for extends: ' + extendName));
+
+      }
+
+
+    }
 
     delete config.extends;
   }
 
-  return config;
 }
 
 function validateRules(config, options) {
@@ -68,7 +203,7 @@ function validateRules(config, options) {
   var invalidKeysFound = [];
 
   for (var key in config.rules) {
-    if (!rules[key]) {
+    if (!defaultRules[key]) {
       invalidKeysFound.push(key);
     }
   }
@@ -79,12 +214,15 @@ function validateRules(config, options) {
 }
 
 module.exports = function(options) {
-  var config = options.config || readConfigFromDisk(options);
+  var config = options.config || resolveConfigPath(options.configPath);
 
   ensureRootProperties(config);
   migrateRulesFromRoot(config, options);
 
-  config = processExtends(config);
+  processPlugins(config, options);
+  processLoadedRules(config);
+  processLoadedConfigurations(config);
+  processExtends(config, options);
 
   validateRules(config, options);
 
diff --git a/lib/index.js b/lib/index.js
index dbc3337..7f685cf 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,5 +1,4 @@
 var precompile = require('glimmer-engine').precompile;
-var plugins = require('./rules');
 var getConfig = require('./get-config');
 
 var WARNING_SEVERITY = 1;
@@ -42,10 +41,12 @@ Linter.prototype = {
       results.push(result);
     }
 
+    var rules = this.config.loadedRules;
+
     var astPlugins = [];
-    for (var pluginName in plugins) {
+    for (var pluginName in rules) {
 
-      var plugin = plugins[pluginName]({
+      var plugin = rules[pluginName]({
         name: pluginName,
         config: this.config.rules[pluginName],
         console: this.console,
@@ -130,4 +131,6 @@ Linter.prototype = {
   }
 };
 
+
+
 module.exports = Linter;
diff --git a/package.json b/package.json
index 91242dc..4a61eaa 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
   "keywords": [],
   "dependencies": {
     "chalk": "^1.1.3",
-    "exists-sync": "0.0.4",
+    "resolve": "^1.1.3",
     "glimmer-engine": "^0.18.0",
     "lodash": "^4.11.1"
   },


Test patch:
diff --git a/test/acceptance-test.js b/test/acceptance-test.js
index 00abbf2..a1dd27e 100644
--- a/test/acceptance-test.js
+++ b/test/acceptance-test.js
@@ -301,6 +301,184 @@ describe('public api', function() {
 
   });
 
+  describe('Linter using plugins', function() {
+    var basePath = path.join(fixturePath, 'with-plugins');
+    var linter;
+
+    beforeEach(function() {
+      linter = new Linter({
+        console: mockConsole,
+        configPath: path.join(basePath, '.template-lintrc.js')
+      });
+    });
+
+    it('returns plugin rule issues', function() {
+      var templatePath = path.join(basePath, 'app', 'templates', 'application.hbs');
+      var templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' });
+      var expected = [
+        {
+          message: 'The inline form of component is not allowed',
+          moduleId: templatePath,
+          line: 1,
+          column: 4,
+          source: '{{component value="Hej"}}',
+          rule: 'inline-component',
+          severity: 2
+        }
+      ];
+
+      var result = linter.verify({
+        source: templateContents,
+        moduleId: templatePath
+      });
+
+      assert.deepEqual(result, expected);
+    });
+
+  });
+
+  describe('Linter using plugin with extends', function() {
+    var basePath = path.join(fixturePath, 'with-plugin-with-configurations');
+    var linter;
+
+    beforeEach(function() {
+      linter = new Linter({
+        console: mockConsole,
+        configPath: path.join(basePath, '.template-lintrc.js')
+      });
+    });
+
+    it('returns plugin rule issues', function() {
+      var templatePath = path.join(basePath, 'app', 'templates', 'application.hbs');
+      var templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' });
+      var expected = [
+        {
+          message: 'The inline form of component is not allowed',
+          moduleId: templatePath,
+          line: 1,
+          column: 4,
+          source: '{{component value="Hej"}}',
+          rule: 'inline-component',
+          severity: 2
+        }
+      ];
+
+      var result = linter.verify({
+        source: templateContents,
+        moduleId: templatePath
+      });
+
+      assert.deepEqual(result, expected);
+    });
+
+  });
+  describe('Linter using plugin with multiple extends', function() {
+    var basePath = path.join(fixturePath, 'with-multiple-extends');
+    var linter;
+
+    beforeEach(function() {
+      linter = new Linter({
+        console: mockConsole,
+        configPath: path.join(basePath, '.template-lintrc.js')
+      });
+    });
+
+    it('returns plugin rule issues', function() {
+      var templatePath = path.join(basePath, 'app', 'templates', 'application.hbs');
+      var templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' });
+      var expected = [
+        {
+          message: 'Usage of triple curly brackets is unsafe',
+          moduleId: templatePath,
+          line: 2,
+          column: 2,
+          source: '{{{myVar}}}',
+          rule: 'triple-curlies',
+          severity: 2
+        },
+        {
+          message: 'The inline form of component is not allowed',
+          moduleId: templatePath,
+          line: 1,
+          column: 4,
+          source: '{{component value="Hej"}}',
+          rule: 'inline-component',
+          severity: 2
+        }
+      ];
+
+      var result = linter.verify({
+        source: templateContents,
+        moduleId: templatePath
+      });
+
+      assert.deepEqual(result, expected);
+    });
+
+  });
+  describe('Linter using plugins (inline plugins)', function() {
+    var basePath = path.join(fixturePath, 'with-inline-plugins');
+    var linter;
+
+    beforeEach(function() {
+      linter = new Linter({
+        console: mockConsole,
+        configPath: path.join(basePath, '.template-lintrc.js')
+      });
+    });
+
+    it('returns plugin rule issues', function() {
+      var templatePath = path.join(basePath, 'app', 'templates', 'application.hbs');
+      var templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' });
+      var expected = [
+        {
+          message: 'The inline form of component is not allowed',
+          moduleId: templatePath,
+          line: 1,
+          column: 4,
+          source: '{{component value="Hej"}}',
+          rule: 'inline-component',
+          severity: 2
+        }
+      ];
+
+      var result = linter.verify({
+        source: templateContents,
+        moduleId: templatePath
+      });
+
+      assert.deepEqual(result, expected);
+    });
+
+  });
+
+  describe('Linter using plugins loading a configuration that extends from another plugins configuration', function() {
+    var basePath = path.join(fixturePath, 'with-plugins-overwriting');
+    var linter;
+
+    beforeEach(function() {
+      linter = new Linter({
+        console: mockConsole,
+        configPath: path.join(basePath, '.template-lintrc.js')
+      });
+    });
+
+    it('returns plugin rule issues', function() {
+      var templatePath = path.join(basePath, 'app', 'templates', 'application.hbs');
+      var templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' });
+      var expected = [];
+
+      var result = linter.verify({
+        source: templateContents,
+        moduleId: templatePath
+      });
+
+      assert.deepEqual(result, expected);
+    });
+
+  });
+
+
   describe('Linter.prototype.statusForModule', function() {
     it('returns true when the provided moduleId is listed in `pending`', function() {
       var linter = new Linter({
diff --git a/test/fixtures/with-inline-plugins/.template-lintrc.js b/test/fixtures/with-inline-plugins/.template-lintrc.js
new file mode 100644
index 0000000..0573f88
--- /dev/null
+++ b/test/fixtures/with-inline-plugins/.template-lintrc.js
@@ -0,0 +1,14 @@
+module.exports = {
+  plugins: [
+    {
+      name: 'plugin1',
+      rules: {
+        'inline-component': require('./rules/lint-inline-component')
+      }
+    }
+  ],
+  rules: {
+    'inline-component': true,
+    'bare-strings': true
+  }
+};
diff --git a/test/fixtures/with-inline-plugins/app/templates/application.hbs b/test/fixtures/with-inline-plugins/app/templates/application.hbs
new file mode 100644
index 0000000..5194b7b
--- /dev/null
+++ b/test/fixtures/with-inline-plugins/app/templates/application.hbs
@@ -0,0 +1,1 @@
+<h2>{{component value="Hej"}}</h2>
diff --git a/test/fixtures/with-inline-plugins/rules/lint-inline-component.js b/test/fixtures/with-inline-plugins/rules/lint-inline-component.js
new file mode 100644
index 0000000..b04f00a
--- /dev/null
+++ b/test/fixtures/with-inline-plugins/rules/lint-inline-component.js
@@ -0,0 +1,28 @@
+'use strict';
+
+
+var buildPlugin = require('../../../../lib/rules/base');
+var message = 'The inline form of component is not allowed';
+
+module.exports = function(addonContext) {
+  var InlineComponent = buildPlugin(addonContext, 'inline-component');
+
+  InlineComponent.prototype.visitors = function() {
+    return {
+      MustacheStatement: function(node) {
+        if (node.path.original === 'component') {
+          this.log({
+            message: message,
+            line: node.loc && node.loc.start.line,
+            column: node.loc && node.loc.start.column,
+            source: this.sourceForNode(node)
+          });
+        }
+      }
+    };
+  };
+
+  return InlineComponent;
+};
+
+module.exports.message = message;
diff --git a/test/fixtures/with-multiple-extends/.template-lintrc.js b/test/fixtures/with-multiple-extends/.template-lintrc.js
new file mode 100644
index 0000000..d18ce22
--- /dev/null
+++ b/test/fixtures/with-multiple-extends/.template-lintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+  plugins: [
+    './plugins/plugin1'
+  ],
+  extends: [
+    'recommended',
+    'plugin1:recommended'
+  ]
+};
diff --git a/test/fixtures/with-multiple-extends/app/templates/application.hbs b/test/fixtures/with-multiple-extends/app/templates/application.hbs
new file mode 100644
index 0000000..4f12f73
--- /dev/null
+++ b/test/fixtures/with-multiple-extends/app/templates/application.hbs
@@ -0,0 +1,2 @@
+<h2>{{component value="Hej"}}</h2>
+  {{{myVar}}}
diff --git a/test/fixtures/with-multiple-extends/plugins/lint-inline-component.js b/test/fixtures/with-multiple-extends/plugins/lint-inline-component.js
new file mode 100644
index 0000000..b04f00a
--- /dev/null
+++ b/test/fixtures/with-multiple-extends/plugins/lint-inline-component.js
@@ -0,0 +1,28 @@
+'use strict';
+
+
+var buildPlugin = require('../../../../lib/rules/base');
+var message = 'The inline form of component is not allowed';
+
+module.exports = function(addonContext) {
+  var InlineComponent = buildPlugin(addonContext, 'inline-component');
+
+  InlineComponent.prototype.visitors = function() {
+    return {
+      MustacheStatement: function(node) {
+        if (node.path.original === 'component') {
+          this.log({
+            message: message,
+            line: node.loc && node.loc.start.line,
+            column: node.loc && node.loc.start.column,
+            source: this.sourceForNode(node)
+          });
+        }
+      }
+    };
+  };
+
+  return InlineComponent;
+};
+
+module.exports.message = message;
diff --git a/test/fixtures/with-multiple-extends/plugins/plugin1.js b/test/fixtures/with-multiple-extends/plugins/plugin1.js
new file mode 100644
index 0000000..da0c7f5
--- /dev/null
+++ b/test/fixtures/with-multiple-extends/plugins/plugin1.js
@@ -0,0 +1,25 @@
+
+'use strict';
+
+module.exports = {
+
+  name: 'plugin1',
+
+  rules: {
+    'inline-component': require('./lint-inline-component')
+  },
+
+  configurations: {
+
+    recommended: {
+
+      rules: {
+        'inline-component': true,
+        'bare-strings': true
+      }
+
+    }
+
+  }
+
+};
diff --git a/test/fixtures/with-plugin-with-configurations/.template-lintrc.js b/test/fixtures/with-plugin-with-configurations/.template-lintrc.js
new file mode 100644
index 0000000..22fab30
--- /dev/null
+++ b/test/fixtures/with-plugin-with-configurations/.template-lintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: [
+    './plugins/plugin1'
+  ],
+  extends: 'plugin1:recommended'
+};
diff --git a/test/fixtures/with-plugin-with-configurations/app/templates/application.hbs b/test/fixtures/with-plugin-with-configurations/app/templates/application.hbs
new file mode 100644
index 0000000..5194b7b
--- /dev/null
+++ b/test/fixtures/with-plugin-with-configurations/app/templates/application.hbs
@@ -0,0 +1,1 @@
+<h2>{{component value="Hej"}}</h2>
diff --git a/test/fixtures/with-plugin-with-configurations/plugins/lint-inline-component.js b/test/fixtures/with-plugin-with-configurations/plugins/lint-inline-component.js
new file mode 100644
index 0000000..b04f00a
--- /dev/null
+++ b/test/fixtures/with-plugin-with-configurations/plugins/lint-inline-component.js
@@ -0,0 +1,28 @@
+'use strict';
+
+
+var buildPlugin = require('../../../../lib/rules/base');
+var message = 'The inline form of component is not allowed';
+
+module.exports = function(addonContext) {
+  var InlineComponent = buildPlugin(addonContext, 'inline-component');
+
+  InlineComponent.prototype.visitors = function() {
+    return {
+      MustacheStatement: function(node) {
+        if (node.path.original === 'component') {
+          this.log({
+            message: message,
+            line: node.loc && node.loc.start.line,
+            column: node.loc && node.loc.start.column,
+            source: this.sourceForNode(node)
+          });
+        }
+      }
+    };
+  };
+
+  return InlineComponent;
+};
+
+module.exports.message = message;
diff --git a/test/fixtures/with-plugin-with-configurations/plugins/plugin1.js b/test/fixtures/with-plugin-with-configurations/plugins/plugin1.js
new file mode 100644
index 0000000..da0c7f5
--- /dev/null
+++ b/test/fixtures/with-plugin-with-configurations/plugins/plugin1.js
@@ -0,0 +1,25 @@
+
+'use strict';
+
+module.exports = {
+
+  name: 'plugin1',
+
+  rules: {
+    'inline-component': require('./lint-inline-component')
+  },
+
+  configurations: {
+
+    recommended: {
+
+      rules: {
+        'inline-component': true,
+        'bare-strings': true
+      }
+
+    }
+
+  }
+
+};
diff --git a/test/fixtures/with-plugins-overwriting/.template-lintrc.js b/test/fixtures/with-plugins-overwriting/.template-lintrc.js
new file mode 100644
index 0000000..f7accc0
--- /dev/null
+++ b/test/fixtures/with-plugins-overwriting/.template-lintrc.js
@@ -0,0 +1,13 @@
+module.exports = {
+  plugins: [
+    './plugins/plugin1',
+    './plugins/plugin2'
+  ],
+  extends: [
+    'plugin1:enable-inline-component',
+    'plugin2:disable-inline-component',
+  ],
+  rules: {
+    'bare-strings': true
+  }
+};
diff --git a/test/fixtures/with-plugins-overwriting/app/templates/application.hbs b/test/fixtures/with-plugins-overwriting/app/templates/application.hbs
new file mode 100644
index 0000000..5194b7b
--- /dev/null
+++ b/test/fixtures/with-plugins-overwriting/app/templates/application.hbs
@@ -0,0 +1,1 @@
+<h2>{{component value="Hej"}}</h2>
diff --git a/test/fixtures/with-plugins-overwriting/plugins/lint-inline-component.js b/test/fixtures/with-plugins-overwriting/plugins/lint-inline-component.js
new file mode 100644
index 0000000..b04f00a
--- /dev/null
+++ b/test/fixtures/with-plugins-overwriting/plugins/lint-inline-component.js
@@ -0,0 +1,28 @@
+'use strict';
+
+
+var buildPlugin = require('../../../../lib/rules/base');
+var message = 'The inline form of component is not allowed';
+
+module.exports = function(addonContext) {
+  var InlineComponent = buildPlugin(addonContext, 'inline-component');
+
+  InlineComponent.prototype.visitors = function() {
+    return {
+      MustacheStatement: function(node) {
+        if (node.path.original === 'component') {
+          this.log({
+            message: message,
+            line: node.loc && node.loc.start.line,
+            column: node.loc && node.loc.start.column,
+            source: this.sourceForNode(node)
+          });
+        }
+      }
+    };
+  };
+
+  return InlineComponent;
+};
+
+module.exports.message = message;
diff --git a/test/fixtures/with-plugins-overwriting/plugins/plugin1.js b/test/fixtures/with-plugins-overwriting/plugins/plugin1.js
new file mode 100644
index 0000000..3d1d896
--- /dev/null
+++ b/test/fixtures/with-plugins-overwriting/plugins/plugin1.js
@@ -0,0 +1,20 @@
+'use strict';
+
+module.exports = {
+
+  name: 'plugin1',
+
+  rules: {
+    'inline-component': require('./lint-inline-component')
+  },
+
+  configurations: {
+    'enable-inline-component': {
+      rules: {
+        'inline-component': true
+      }
+    }
+
+  }
+
+};
diff --git a/test/fixtures/with-plugins-overwriting/plugins/plugin2.js b/test/fixtures/with-plugins-overwriting/plugins/plugin2.js
new file mode 100644
index 0000000..a8ac4bb
--- /dev/null
+++ b/test/fixtures/with-plugins-overwriting/plugins/plugin2.js
@@ -0,0 +1,17 @@
+
+'use strict';
+
+module.exports = {
+
+  name: 'plugin2',
+
+  configurations: {
+    'disable-inline-component': {
+      rules: {
+        'inline-component': false
+      }
+    }
+
+  }
+
+};
diff --git a/test/fixtures/with-plugins/.template-lintrc.js b/test/fixtures/with-plugins/.template-lintrc.js
new file mode 100644
index 0000000..b584dea
--- /dev/null
+++ b/test/fixtures/with-plugins/.template-lintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+  plugins: [
+    './plugins/plugin1'
+  ],
+  rules: {
+    'inline-component': true,
+    'bare-strings': true
+  }
+};
diff --git a/test/fixtures/with-plugins/app/templates/application.hbs b/test/fixtures/with-plugins/app/templates/application.hbs
new file mode 100644
index 0000000..5194b7b
--- /dev/null
+++ b/test/fixtures/with-plugins/app/templates/application.hbs
@@ -0,0 +1,1 @@
+<h2>{{component value="Hej"}}</h2>
diff --git a/test/fixtures/with-plugins/plugins/lint-inline-component.js b/test/fixtures/with-plugins/plugins/lint-inline-component.js
new file mode 100644
index 0000000..b04f00a
--- /dev/null
+++ b/test/fixtures/with-plugins/plugins/lint-inline-component.js
@@ -0,0 +1,28 @@
+'use strict';
+
+
+var buildPlugin = require('../../../../lib/rules/base');
+var message = 'The inline form of component is not allowed';
+
+module.exports = function(addonContext) {
+  var InlineComponent = buildPlugin(addonContext, 'inline-component');
+
+  InlineComponent.prototype.visitors = function() {
+    return {
+      MustacheStatement: function(node) {
+        if (node.path.original === 'component') {
+          this.log({
+            message: message,
+            line: node.loc && node.loc.start.line,
+            column: node.loc && node.loc.start.column,
+            source: this.sourceForNode(node)
+          });
+        }
+      }
+    };
+  };
+
+  return InlineComponent;
+};
+
+module.exports.message = message;
diff --git a/test/fixtures/with-plugins/plugins/plugin1.js b/test/fixtures/with-plugins/plugins/plugin1.js
new file mode 100644
index 0000000..6a4b262
--- /dev/null
+++ b/test/fixtures/with-plugins/plugins/plugin1.js
@@ -0,0 +1,12 @@
+
+'use strict';
+
+module.exports = {
+
+  name: 'plugin1',
+
+  rules: {
+    'inline-component': require('./lint-inline-component')
+  }
+
+};
diff --git a/test/unit/get-config-test.js b/test/unit/get-config-test.js
index b20aa96..baaf5ac 100644
--- a/test/unit/get-config-test.js
+++ b/test/unit/get-config-test.js
@@ -43,7 +43,6 @@ describe('get-config', function() {
 
     assert.deepEqual(actual, expected);
   });
-
   it('can specify that it extends a default configuration', function() {
     var actual = getConfig({
       config: {
@@ -51,7 +50,8 @@ describe('get-config', function() {
       }
     });
 
-    assert.deepEqual(actual, recommendedConfig);
+    assert(actual.rules['block-indentation'] === 2);
+
   });
 
   it('can extend and override a default configuration', function() {
@@ -67,20 +67,18 @@ describe('get-config', function() {
       }
     });
 
-    assert.deepEqual(actual, expected);
     assert(actual.rules['bare-strings'] === false);
   });
 
   it('migrates rules in the config root into `rules` property', function() {
-    var expected = { ignore: [], pending: [], rules: { 'bare-strings': true }};
     var actual = getConfig({
       console: { log: function() { }},
       config: {
-        'bare-strings': true
+        'bare-strings': false
       }
     });
 
-    assert.deepEqual(actual, expected);
+    assert(actual.rules['bare-strings'] === false);
   });
 
   it('rules in the config root trigger a deprecation', function() {
@@ -114,4 +112,68 @@ describe('get-config', function() {
 
     assert(/Invalid rule configuration found/.test(message));
   });
+
+  it('warns for unknown extends', function() {
+    var message;
+    getConfig({
+      console: { log: function(_message) {
+        message = _message;
+      }},
+
+      config: {
+        extends: [
+          'recommended',
+          'plugin1:wrong-extend'
+        ]
+      }
+    });
+
+    assert(/Cannot find configuration for extends/.test(message));
+  });
+
+  it('can specify plugin without rules', function() {
+    var message;
+    var actual = getConfig({
+      console: { log: function(_message) {
+        message = _message;
+      }},
+
+      config: {
+
+        extends: 'myplugin:basic-configuration',
+
+        plugins: [{
+          name: 'myplugin',
+          configurations: {
+            'basic-configuration': {
+              rules: {
+                'bare-strings': false
+              }
+            }
+          }
+        }]
+      }
+
+    });
+
+    assert(!message);
+    assert(actual.rules['bare-strings'] === false);
+  });
+
+  it('throw exception when plugin path is incorrect', function() {
+
+    var wrongPluginPath = './bad-plugin-path/incorrect-file-name';
+
+    assert.throws(function() {
+      getConfig({
+        config: {
+          plugins: [wrongPluginPath]
+        }
+      });
+    });
+
+
+  });
+
+
 });