Skip to content

Commit 121404e

Browse files
authored
Add new description-format rule (#90)
Closes #89
1 parent ad1f9a0 commit 121404e

File tree

7 files changed

+243
-7
lines changed

7 files changed

+243
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1111

1212
### Removed
1313

14+
## [3.3.0] - 2018-08-03
15+
### Added
16+
- New rule: [description-format](https://github.com/tclindner/npm-package-json-lint/wiki/description-format). Addresses [#89](https://github.com/tclindner/npm-package-json-lint/issues/89) from @ntwb.
17+
1418
## [3.2.0] - 2018-07-14
1519
### Added
1620
- New rule: [no-absolute-version-dependencies](https://github.com/tclindner/npm-package-json-lint/wiki/no-absolute-version-dependencies).

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "npm-package-json-lint",
3-
"version": "3.2.0",
3+
"version": "3.3.0",
44
"description": "Configurable linter for package.json files.",
55
"keywords": [
66
"lint",
@@ -45,12 +45,12 @@
4545
"plur": "^3.0.1",
4646
"semver": "^5.5.0",
4747
"strip-json-comments": "^2.0.1",
48-
"validator": "^10.4.0"
48+
"validator": "^10.5.0"
4949
},
5050
"devDependencies": {
5151
"chai": "^4.1.2",
52-
"eslint": "^5.1.0",
53-
"eslint-config-tc": "^4.0.0",
52+
"eslint": "^5.2.0",
53+
"eslint-config-tc": "^4.1.0",
5454
"eslint-formatter-pretty": "^1.3.0",
5555
"figures": "^2.0.0",
5656
"mocha": "^5.2.0",

src/NpmPackageJsonLint.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ class NpmPackageJsonLint {
2929
for (const rule in configObj) {
3030
const ruleModule = this.rules.get(rule);
3131

32-
if (ruleModule.ruleType === 'array') {
32+
if (ruleModule.ruleType === 'array' || ruleModule.ruleType === 'object') {
3333
const severity = typeof configObj[rule] === 'string' && configObj[rule] === 'off' ? configObj[rule] : configObj[rule][0];
34-
const ruleConfigArray = configObj[rule][1];
34+
const ruleConfig = configObj[rule][1];
3535

3636
if (severity !== 'off') {
37-
const lintResult = ruleModule.lint(packageJsonData, severity, ruleConfigArray);
37+
const lintResult = ruleModule.lint(packageJsonData, severity, ruleConfig);
3838

3939
if (typeof lintResult === 'object') {
4040
lintIssues.push(lintResult);

src/config/ConfigValidator.js

100644100755
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const Ajv = require('ajv');
44
const ConfigSchema = require('./ConfigSchema');
5+
const isPlainObj = require('is-plain-obj');
56

67
/**
78
* Formats an array of schema validation errors.
@@ -43,6 +44,32 @@ const isSeverityInvalid = function(severity) {
4344
return typeof severity !== 'string' || (typeof severity === 'string' && severity !== 'error' && severity !== 'warning' && severity !== 'off');
4445
};
4546

47+
/**
48+
* Validates object rule config
49+
*
50+
* @param {Object} ruleConfig Object rule
51+
* @return {Boolean} True if config is valid, false if not
52+
* @static
53+
*/
54+
const isObjectRuleConfigValid = function(ruleConfig) {
55+
const severityIndex = 0;
56+
const object = 1;
57+
58+
if (typeof ruleConfig === 'string' && ruleConfig === 'off') {
59+
return true;
60+
} else if (typeof ruleConfig === 'string' && ruleConfig !== 'off') {
61+
throw new Error('is an object type rule. It must be set to "off" if an object is not supplied.');
62+
} else if (typeof ruleConfig[severityIndex] !== 'string' || isSeverityInvalid(ruleConfig[severityIndex])) {
63+
throw new Error(`first key must be set to "error", "warning", or "off". Currently set to "${ruleConfig[severityIndex]}".`);
64+
}
65+
66+
if (!isPlainObj(ruleConfig[object])) {
67+
throw new Error(`second key must be set an object. Currently set to "${ruleConfig[object]}".`);
68+
}
69+
70+
return true;
71+
};
72+
4673
/**
4774
* Validates array rule config
4875
*
@@ -98,6 +125,8 @@ const validateRule = function(ruleModule, ruleName, userConfig, source) {
98125
try {
99126
if (ruleModule.ruleType === 'array') {
100127
isArrayRuleConfigValid(userConfig);
128+
} else if (ruleModule.ruleType === 'object') {
129+
isObjectRuleConfigValid(userConfig);
101130
} else {
102131
isStandardRuleConfigValid(userConfig);
103132
}

src/rules/description-format.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
3+
const LintIssue = require('./../LintIssue');
4+
const isString = require('./../validators/type').isString;
5+
const lintId = 'description-format';
6+
const nodeName = 'description';
7+
const ruleType = 'object';
8+
9+
const lint = function(packageJsonData, severity, config) {
10+
if (!packageJsonData.hasOwnProperty(nodeName)) {
11+
return true;
12+
}
13+
14+
const description = packageJsonData.description;
15+
16+
if (!isString(packageJsonData, nodeName)) {
17+
return new LintIssue(lintId, severity, nodeName, 'Type should be a string');
18+
}
19+
20+
if (config.hasOwnProperty('requireCapitalFirstLetter') && config.requireCapitalFirstLetter && description[0] !== description[0].toUpperCase()) {
21+
return new LintIssue(lintId, severity, nodeName, `The description should start with a capital letter. It currently starts with ${description[0]}.`);
22+
}
23+
24+
if (config.hasOwnProperty('requireEndingPeriod') && config.requireEndingPeriod && !description.endsWith('.')) {
25+
return new LintIssue(lintId, severity, nodeName, 'The description should end with a period.');
26+
}
27+
28+
return true;
29+
};
30+
31+
module.exports.lint = lint;
32+
module.exports.ruleType = ruleType;

tests/unit/config/ConfigValidator.test.js

100644100755
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,69 @@ describe('ConfigValidator Unit Tests', function() {
207207
});
208208
});
209209

210+
context('isObjectRuleConfigValid tests', function() {
211+
context('when a rule is an object rule and the first key is not equal to error, warning, or off', function() {
212+
it('an error should be thrown', function() {
213+
const ruleConfig = {
214+
'description-format': [true, {requireCapitalFirstLetter: true, requireEndingPeriod: true}]
215+
};
216+
const source = 'cli';
217+
218+
(function() {
219+
ConfigValidator.validateRules(ruleConfig, source, linterContext);
220+
}).should.throw('cli:\n\tConfiguration for rule "description-format" is invalid:\n\tfirst key must be set to "error", "warning", or "off". Currently set to "true".');
221+
});
222+
});
223+
224+
context('when a rule is an object rule and the second key is not an Object', function() {
225+
it('an error should be thrown', function() {
226+
const ruleConfig = {
227+
'description-format': ['error', 'Thomas']
228+
};
229+
const source = 'cli';
230+
231+
(function() {
232+
ConfigValidator.validateRules(ruleConfig, source, linterContext);
233+
}).should.throw('cli:\n\tConfiguration for rule "description-format" is invalid:\n\tsecond key must be set an object. Currently set to "Thomas".');
234+
});
235+
});
236+
237+
context('when a valid object rule config is passed', function() {
238+
it('true should be returned', function() {
239+
const ruleConfig = {
240+
'description-format': ['error', {requireCapitalFirstLetter: true, requireEndingPeriod: true}]
241+
};
242+
const source = 'cli';
243+
244+
ConfigValidator.validateRules(ruleConfig, source, linterContext);
245+
});
246+
});
247+
248+
context('when a valid object rule config is passed with a value of off', function() {
249+
it('true should be returned', function() {
250+
const ruleConfig = {
251+
'description-format': 'off'
252+
};
253+
const source = 'cli';
254+
255+
ConfigValidator.validateRules(ruleConfig, source, linterContext);
256+
});
257+
});
258+
259+
context('when a invalid object rule config is passed with a value of error', function() {
260+
it('true should be returned', function() {
261+
const ruleConfig = {
262+
'description-format': 'error'
263+
};
264+
const source = 'cli';
265+
266+
(function() {
267+
ConfigValidator.validateRules(ruleConfig, source, linterContext);
268+
}).should.throw('cli:\n\tConfiguration for rule "description-format" is invalid:\n\tis an object type rule. It must be set to "off" if an object is not supplied.');
269+
});
270+
});
271+
});
272+
210273
context('isStandardRuleConfigValid tests', function() {
211274
context('when a standard rule is passed with a value of error', function() {
212275
it('true should be returned', function() {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
3+
const chai = require('chai');
4+
const ruleModule = require('./../../../src/rules/description-format');
5+
const lint = ruleModule.lint;
6+
const ruleType = ruleModule.ruleType;
7+
8+
const should = chai.should();
9+
10+
describe('description-format Unit Tests', function() {
11+
context('a rule type value should be exported', function() {
12+
it('it should equal "object"', function() {
13+
ruleType.should.equal('object');
14+
});
15+
});
16+
17+
context('when package.json has node with incorrect format', function() {
18+
it('LintIssue object should be returned', function() {
19+
const packageJsonData = {
20+
description: true
21+
};
22+
const config = {
23+
requireCapitalFirstLetter: true,
24+
requireEndingPeriod: true
25+
};
26+
const response = lint(packageJsonData, 'error', config);
27+
28+
response.lintId.should.equal('description-format');
29+
response.severity.should.equal('error');
30+
response.node.should.equal('description');
31+
response.lintMessage.should.equal('Type should be a string');
32+
});
33+
});
34+
35+
context('when package.json has node with lowercase first letter', function() {
36+
it('LintIssue object should be returned', function() {
37+
const packageJsonData = {
38+
description: 'lowercase'
39+
};
40+
const config = {
41+
requireCapitalFirstLetter: true
42+
};
43+
const response = lint(packageJsonData, 'error', config);
44+
45+
response.lintId.should.equal('description-format');
46+
response.severity.should.equal('error');
47+
response.node.should.equal('description');
48+
response.lintMessage.should.equal('The description should start with a capital letter. It currently starts with l.');
49+
});
50+
});
51+
52+
context('when package.json has node without period at end', function() {
53+
it('LintIssue object should be returned', function() {
54+
const packageJsonData = {
55+
description: 'My description'
56+
};
57+
const config = {
58+
requireEndingPeriod: true
59+
};
60+
const response = lint(packageJsonData, 'error', config);
61+
62+
response.lintId.should.equal('description-format');
63+
response.severity.should.equal('error');
64+
response.node.should.equal('description');
65+
response.lintMessage.should.equal('The description should end with a period.');
66+
});
67+
});
68+
69+
context('when package.json has node with correct format', function() {
70+
it('LintIssue object should be returned', function() {
71+
const packageJsonData = {
72+
description: 'My description.'
73+
};
74+
const config = {
75+
requireCapitalFirstLetter: true,
76+
requireEndingPeriod: true
77+
};
78+
const response = lint(packageJsonData, 'error', config);
79+
80+
response.should.be.true;
81+
});
82+
});
83+
84+
context('when no rule config passed', function() {
85+
it('true should be returned', function() {
86+
const packageJsonData = {
87+
description: 'lowercase'
88+
};
89+
const config = {};
90+
const response = lint(packageJsonData, 'error', config);
91+
92+
response.should.be.true;
93+
});
94+
});
95+
96+
context('when package.json does not have node', function() {
97+
it('true should be returned', function() {
98+
const packageJsonData = {};
99+
const config = {
100+
requireCapitalFirstLetter: true,
101+
requireEndingPeriod: true
102+
};
103+
const response = lint(packageJsonData, 'error', config);
104+
105+
response.should.be.true;
106+
});
107+
});
108+
});

0 commit comments

Comments
 (0)