Repository: serverless/serverless (JavaScript)
Date: 2017-09-18 18:51:06+00:00
Task type: Bug Fix

Issue:
Deduplication during plugins loading may cause plugin to be excluded unintentionally (without any warning)
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

Based on this code:
https://github.com/serverless/serverless/blob/master/lib/classes/PluginManager.js#L70

Serverless framework does plugin deduplication using class names that those plugins expose. This requires class names to be unique, which:
1. Is not documented (guess how many plugins have their classes named `Plugin`, `ServerlessPlugin`, `MyPlugin`)
1. Error prone since any class name that is not UUID can potentially be duplicated in another plugin

What makes debugging even harder is that there is no warning message printed / error thrown if duplicated plugin is detected.

Solution depends on the intent of this check. If it's in place to prevent accidental listing of the same plugin twice, why not to remove duplicates from the plugin list itself?

## Additional Data

* ***Serverless Framework Version you're using***: 1.22.0
* ***Operating System***:
* ***Stack Trace***:
* ***Provider Error messages***:

Deduplication during plugins loading may cause plugin to be excluded unintentionally (without any warning)
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

Based on this code:
https://github.com/serverless/serverless/blob/master/lib/classes/PluginManager.js#L70

Serverless framework does plugin deduplication using class names that those plugins expose. This requires class names to be unique, which:
1. Is not documented (guess how many plugins have their classes named `Plugin`, `ServerlessPlugin`, `MyPlugin`)
1. Error prone since any class name that is not UUID can potentially be duplicated in another plugin

What makes debugging even harder is that there is no warning message printed / error thrown if duplicated plugin is detected.

Solution depends on the intent of this check. If it's in place to prevent accidental listing of the same plugin twice, why not to remove duplicates from the plugin list itself?

## Additional Data

* ***Serverless Framework Version you're using***: 1.22.0
* ***Operating System***:
* ***Stack Trace***:
* ***Provider Error messages***:



Issue discussion:

Solution patch:
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index 5e0b3f7d41f..75fd3d89e15 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -67,8 +67,10 @@ class PluginManager {
     }
 
     // don't load plugins twice
-    const loadedPlugins = this.plugins.map(plugin => plugin.constructor.name);
-    if (_.includes(loadedPlugins, Plugin.name)) return;
+    if (this.plugins.some(plugin => plugin instanceof Plugin)) {
+      this.serverless.cli.log(`WARNING: duplicate plugin ${Plugin.name} was not loaded\n`);
+      return;
+    }
 
     this.loadCommands(pluginInstance);
     this.loadHooks(pluginInstance);


Test patch:
diff --git a/lib/classes/PluginManager.test.js b/lib/classes/PluginManager.test.js
index 9fec8833b45..95864844cef 100644
--- a/lib/classes/PluginManager.test.js
+++ b/lib/classes/PluginManager.test.js
@@ -546,6 +546,28 @@ describe('PluginManager', () => {
       expect(pluginManager.plugins.length).to.equal(1);
     });
 
+    it('should load two plugins that happen to have the same class name', () => {
+      function getFirst() {
+        return class PluginMock {
+        };
+      }
+
+      function getSecond() {
+        return class PluginMock {
+        };
+      }
+
+      const first = getFirst();
+      const second = getSecond();
+
+      pluginManager.addPlugin(first);
+      pluginManager.addPlugin(second);
+
+      expect(pluginManager.plugins[0]).to.be.instanceof(first);
+      expect(pluginManager.plugins[1]).to.be.instanceof(second);
+      expect(pluginManager.plugins.length).to.equal(2);
+    });
+
     it('should load the plugin commands', () => {
       pluginManager.addPlugin(SynchronousPluginMock);