Repository: tclindner/npm-package-json-lint
Pull request #39 (2017-06-02 11:34:00)

Enhance absolute version dependency to accept "1.0.0"
Instead of relying only on a "=" prefix, this enhanced version works by
making sure all "non-absolute" version rangs are not used, e.g. "~", "^".

From the semver Backus-Naur grammar
(https://www.npmjs.com/package/semver) is only missing that a "x" and
"X" can be used instead of "*".

This change is motivated by the experience over the last several days that a workflow which tries to make sure a "=" prefixed version ends up in the package.json and yarn.lock is to tedious.
Main reason is that one needs to manually look up the latest version to run e.g.:
`yarn add winston@=2.3.1`
With this pull request you just need to run:
`yarn add -E winston`
No need to manually check the version :)

tclindner: Nice work, @chr1shaefn3r! I really like this improvement. I'll get a new version packaged up this weekend. 

Patch:
diff --git a/src/rules/prefer-absolute-version-dependencies.js b/src/rules/prefer-absolute-version-dependencies.js
index 588f0b4..959b67f 100644
--- a/src/rules/prefer-absolute-version-dependencies.js
+++ b/src/rules/prefer-absolute-version-dependencies.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const areVersRangesValid = require('./../validators/dependency-audit').areVersRangesValid;
+const isVersionAbsolute = require('./../validators/dependency-audit').isVersionAbsolute;
 const LintIssue = require('./../LintIssue');
 const lintId = 'prefer-absolute-version-dependencies';
 const nodeName = 'dependencies';
@@ -8,9 +8,8 @@ const message = 'You are using an invalid version range. Please use absolute ver
 const ruleType = 'dependencies-version-range';
 
 const lint = function(packageJsonData, lintType) {
-  const rangeSpecifier = '=';
 
-  if (!areVersRangesValid(packageJsonData, nodeName, rangeSpecifier)) {
+  if (!isVersionAbsolute(packageJsonData, nodeName)) {
     return new LintIssue(lintId, lintType, nodeName, message);
   }
 
diff --git a/src/rules/prefer-absolute-version-devDependencies.js b/src/rules/prefer-absolute-version-devDependencies.js
index f3ec66e..0033328 100644
--- a/src/rules/prefer-absolute-version-devDependencies.js
+++ b/src/rules/prefer-absolute-version-devDependencies.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const areVersRangesValid = require('./../validators/dependency-audit').areVersRangesValid;
+const isVersionAbsolute = require('./../validators/dependency-audit').isVersionAbsolute;
 const LintIssue = require('./../LintIssue');
 const lintId = 'prefer-absolute-version-devDependencies';
 const nodeName = 'devDependencies';
@@ -8,9 +8,8 @@ const message = 'You are using an invalid version range. Please use absolute ver
 const ruleType = 'devDependencies-version-range';
 
 const lint = function(packageJsonData, lintType) {
-  const rangeSpecifier = '=';
 
-  if (!areVersRangesValid(packageJsonData, nodeName, rangeSpecifier)) {
+  if (!isVersionAbsolute(packageJsonData, nodeName)) {
     return new LintIssue(lintId, lintType, nodeName, message);
   }
 
diff --git a/src/validators/dependency-audit.js b/src/validators/dependency-audit.js
index 4286bca..b5313cd 100755
--- a/src/validators/dependency-audit.js
+++ b/src/validators/dependency-audit.js
@@ -105,7 +105,39 @@ const areVersRangesValid = function(packageJsonData, nodeName, rangeSpecifier) {
   return rangesValid;
 };
 
+/**
+ * Determines whether or not all dependency versions are absolut
+ * @param  {object} packageJsonData         Valid JSON
+ * @param  {string} nodeName                Name of a node in the package.json file
+ * @return {boolean}                        False if the package has an non-absolute version. True if it is not or the node is missing.
+ */
+const isVersionAbsolute = function(packageJsonData, nodeName) {
+  if (!packageJsonData.hasOwnProperty(nodeName)) {
+    return true;
+  }
+
+  const NOT_FOUND = -1;
+  const firstCharOfStr = 0;
+  let rangesValid = true;
+
+  for (const dependencyName in packageJsonData[nodeName]) {
+    const dependencyVersion = packageJsonData[nodeName][dependencyName];
+
+    if (dependencyVersion.startsWith('^', firstCharOfStr) ||
+      dependencyVersion.startsWith('~', firstCharOfStr) ||
+      dependencyVersion.startsWith('>', firstCharOfStr) ||
+      dependencyVersion.startsWith('<', firstCharOfStr) ||
+      dependencyVersion.indexOf('*') !== NOT_FOUND
+    ) {
+      rangesValid = false;
+    }
+  }
+
+  return rangesValid;
+};
+
 module.exports.hasDependency = hasDependency;
 module.exports.hasDepPrereleaseVers = hasDepPrereleaseVers;
 module.exports.hasDepVersZero = hasDepVersZero;
 module.exports.areVersRangesValid = areVersRangesValid;
+module.exports.isVersionAbsolute = isVersionAbsolute;


Test patch:
diff --git a/tests/unit/rules/prefer-absolute-version-dependenciesTest.js b/tests/unit/rules/prefer-absolute-version-dependenciesTest.js
index cea9834..6ea0312 100644
--- a/tests/unit/rules/prefer-absolute-version-dependenciesTest.js
+++ b/tests/unit/rules/prefer-absolute-version-dependenciesTest.js
@@ -34,6 +34,19 @@ describe('prefer-absolute-version-dependencies Unit Tests', function() {
     });
   });
 
+  context('when package.json has node with a valid value', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        dependencies: {
+          'gulp-npm-package-json-lint': '1.0.0'
+        }
+      };
+      const response = lint(packageJsonData, 'error');
+
+      response.should.be.true();
+    });
+  });
+
   context('when package.json does not have node', function() {
     it('true should be returned', function() {
       const packageJsonData = {};
diff --git a/tests/unit/rules/prefer-absolute-version-devDependenciesTest.js b/tests/unit/rules/prefer-absolute-version-devDependenciesTest.js
index 64bd9a3..15a41c4 100644
--- a/tests/unit/rules/prefer-absolute-version-devDependenciesTest.js
+++ b/tests/unit/rules/prefer-absolute-version-devDependenciesTest.js
@@ -34,6 +34,19 @@ describe('prefer-absolute-version-devDependencies Unit Tests', function() {
     });
   });
 
+  context('when package.json has node with a valid value', function() {
+    it('LintIssue object should be returned', function() {
+      const packageJsonData = {
+        devDependencies: {
+          'gulp-npm-package-json-lint': '1.0.0'
+        }
+      };
+      const response = lint(packageJsonData, 'error');
+
+      response.should.be.true();
+    });
+  });
+
   context('when package.json does not have node', function() {
     it('true should be returned', function() {
       const packageJsonData = {};
diff --git a/tests/unit/validators/dependency-auditTest.js b/tests/unit/validators/dependency-auditTest.js
index 1af0a00..f9f354f 100755
--- a/tests/unit/validators/dependency-auditTest.js
+++ b/tests/unit/validators/dependency-auditTest.js
@@ -477,4 +477,107 @@ describe('dependency-audit Unit Tests', function() {
       });
     });
   });
+
+  describe('isVersionAbsolute method', function() {
+    context('when the node does not exist in the package.json file', function() {
+      it('true should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '^1.0.0',
+            'grunt-npm-package-json-lint': '~2.0.0-beta1',
+            'gulp-npm-package-json-lint': '^2.0.0-rc1'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'devDependencies');
+
+        response.should.be.true();
+      });
+    });
+
+    context('when the node exists in the package.json file, not all versions are absolute', function() {
+      it('with caret versioning false should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '^1.0.0',
+            'gulp-npm-package-json-lint': '^2.0.0-rc1'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.false();
+      });
+      it('with tilde versioning false should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '~1.0.0',
+            'gulp-npm-package-json-lint': '~2.0.0-rc1'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.false();
+      });
+      it('with star versioning false should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '1.0.*',
+            'gulp-npm-package-json-lint': '2.*'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.false();
+      });
+      it('with greater versioning false should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '>1.0.0',
+            'gulp-npm-package-json-lint': '>=2.0.0'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.false();
+      });
+      it('with greater versioning false should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '>1.0.0',
+            'gulp-npm-package-json-lint': '>=2.0.0'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.false();
+      });
+      it('with less versioning false should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '<1.0.0',
+            'gulp-npm-package-json-lint': '<=2.0.0'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.false();
+      });
+    });
+
+    context('when the node exists in the package.json file, all versions are absolute', function() {
+      it('true should be returned', function() {
+        const packageJson = {
+          dependencies: {
+            'npm-package-json-lint': '1.0.0',
+            'grunt-npm-package-json-lint': '2.1.0',
+            'gulp-npm-package-json-lint': '=2.4.0'
+          }
+        };
+        const response = dependencyAudit.isVersionAbsolute(packageJson, 'dependencies');
+
+        response.should.be.true();
+      });
+    });
+
+  });
+
 });