Repository: tclindner/npm-package-json-lint
Pull request #94 (2018-10-05 14:57:40)

Add new valid-values-name-scope rule
**Description of change**
Add new valid-values-name-scope rule as requested in #69.

**Checklist**

  - [x] Unit tests have been added
  - [ ] Specific notes for documentation, if applicable

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/tclindner/npm-package-json-lint/94)
<!-- Reviewable:end -->


jogold: @tclindner the build is passing, except for a timeout for Node v9, can you restart it?
tclindner: @jogold, thank you so much for contributing this rule 🎉 You did a fantastic job on it 😄 

Patch:
diff --git a/src/rules/valid-values-name-scope.js b/src/rules/valid-values-name-scope.js
new file mode 100644
index 0000000..09ffaa2
--- /dev/null
+++ b/src/rules/valid-values-name-scope.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const LintIssue = require('./../LintIssue');
+const matchValidValue = require('./../validators/valid-values').matchValidValue;
+const lintId = 'valid-values-name-scope';
+const nodeName = 'name';
+const message = 'Invalid value for name scope';
+const ruleType = 'array';
+
+/**
+ * Lints package.json file to check for valid scope values in the name field
+ *
+ * @param  {Object}   packageJsonData   Valid package.json object
+ * @param  {String}   severity          'error' or 'warning'
+ * @param  {Array}    validValues       An array of valid values
+ * @return {Object|Boolean}             LintIssue object if invalid. True if valid
+ */
+const lint = function(packageJsonData, severity, validValues) {
+  const validRegexes = validValues.map((scope) => new RegExp(`^${scope}/`));
+
+  if (!matchValidValue(packageJsonData, nodeName, packageJsonData[nodeName], validRegexes)) {
+    return new LintIssue(lintId, severity, nodeName, message);
+  }
+
+  return true;
+};
+
+module.exports.lint = lint;
+module.exports.ruleType = ruleType;
diff --git a/src/validators/valid-values.js b/src/validators/valid-values.js
index 17f4ee5..5dba38d 100755
--- a/src/validators/valid-values.js
+++ b/src/validators/valid-values.js
@@ -16,4 +16,21 @@ const isValidValue = function(packageJsonData, nodeName, value, validValues) {
   return validValues.includes(value);
 };
 
+/**
+ * Determines whether a node matches a valid value
+ * @param  {object} packageJsonData Valid JSON
+ * @param  {string} nodeName        Name of a node in the package.json file
+ * @param  {String} value           Value to validate
+ * @param  {array}  validRegexes     Array of regex to validate against
+ * @return {boolean}                True if the node matches one of the valid regexes or is missing. False if it is not.
+ */
+const matchValidValue = function(packageJsonData, nodeName, value, validRegexes) {
+  if (!packageJsonData.hasOwnProperty(nodeName)) {
+    return true;
+  }
+
+  return validRegexes.filter((r) => r.test(value)).length > 0;
+};
+
 module.exports.isValidValue = isValidValue;
+module.exports.matchValidValue = matchValidValue;


Test patch:
diff --git a/tests/unit/rules/valid-values-name-scope.test.js b/tests/unit/rules/valid-values-name-scope.test.js
new file mode 100644
index 0000000..68548c9
--- /dev/null
+++ b/tests/unit/rules/valid-values-name-scope.test.js
@@ -0,0 +1,80 @@
+'use strict';
+
+const chai = require('chai');
+const ruleModule = require('./../../../src/rules/valid-values-name-scope');
+const lint = ruleModule.lint;
+const ruleType = ruleModule.ruleType;
+
+const should = chai.should();
+
+describe('valid-values-name-scope Unit Tests', function() {
+  context('a rule type value should be exported', function() {
+    it('it should equal "array"', function() {
+      ruleType.should.equal('array');
+    });
+  });
+
+  context('when package.json has node with invalid scope', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        name: '@great/awesome-package'
+      };
+      const validValues = [
+        '@cool',
+        '@awesome'
+      ];
+      const response = lint(packageJsonData, 'error', validValues);
+
+      response.lintId.should.equal('valid-values-name-scope');
+      response.severity.should.equal('error');
+      response.node.should.equal('name');
+      response.lintMessage.should.equal('Invalid value for name scope');
+    });
+  });
+
+  context('when package.json has node without scope', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        name: 'awesome-package'
+      };
+      const validValues = [
+        '@cool',
+        '@awesome'
+      ];
+      const response = lint(packageJsonData, 'error', validValues);
+
+      response.lintId.should.equal('valid-values-name-scope');
+      response.severity.should.equal('error');
+      response.node.should.equal('name');
+      response.lintMessage.should.equal('Invalid value for name scope');
+    });
+  });
+
+  context('when package.json has node with valid scope', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        name: '@cool/awesome-package'
+      };
+      const validValues = [
+        '@cool',
+        '@awesome'
+      ];
+      const response = lint(packageJsonData, 'error', validValues);
+
+      response.should.be.true;
+    });
+  });
+
+  context('when package.json does not have node', function() {
+    it('true should be returned', function() {
+      const packageJsonData = {};
+      const validValues = [
+        '@cool',
+        '@awesome'
+      ];
+      const response = lint(packageJsonData, 'error', validValues);
+
+      response.should.be.true;
+    });
+  });
+});
diff --git a/tests/unit/validators/valid-values.test.js b/tests/unit/validators/valid-values.test.js
index 45550db..919a82f 100644
--- a/tests/unit/validators/valid-values.test.js
+++ b/tests/unit/validators/valid-values.test.js
@@ -12,7 +12,7 @@ describe('value-values Unit Tests', function() {
     };
 
     context('when the node does not exist in the package.json file', function() {
-      it('false should be returned', function() {
+      it('true should be returned', function() {
         const validValues = [
           'Zoe Washburn',
           'Hoban Washburn',
@@ -63,4 +63,50 @@ describe('value-values Unit Tests', function() {
       });
     });
   });
+
+  describe('matchValidValue method', function() {
+    const packageJson = {
+      name: '@lerna/publish'
+    };
+
+    context('when the node does not exist in the package.json file', function() {
+      it('true should be returned', function() {
+        const validRegexes = [
+          /^@babel\//,
+          /run$/,
+          /[0-9]+/
+        ];
+        const response = validValuesObj.matchValidValue(packageJson, 'names', packageJson.name, validRegexes);
+
+        response.should.be.true;
+      });
+    });
+
+    context('when the node exists in the package.json file and the value matches', function() {
+      it('true should be returned', function() {
+        const validRegexes = [
+          /^@lerna\//,
+          /^@babel\//,
+          /run$/,
+          /[0-9]+/
+        ];
+        const response = validValuesObj.matchValidValue(packageJson, 'name', packageJson.name, validRegexes);
+
+        response.should.be.true;
+      });
+    });
+
+    context('when the node exists in the package.json file, but the value does not match', function() {
+      it('false should be returned', function() {
+        const validRegexes = [
+          /^@babel\//,
+          /run$/,
+          /[0-9]+/
+        ];
+        const response = validValuesObj.matchValidValue(packageJson, 'name', packageJson.name, validRegexes);
+
+        response.should.be.false;
+      });
+    });
+  });
 });