Skip to content
This repository was archived by the owner on Mar 23, 2024. It is now read-only.

Commit 37a65de

Browse files
mrjoelkempJoel Kemp
authored andcommitted
Configuration: Auto-generation
Fixes #797 Closes gh-908
1 parent 0f78baf commit 37a65de

File tree

10 files changed

+572
-20
lines changed

10 files changed

+572
-20
lines changed

OVERVIEW.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ cat myfile.js | jscs
4949

5050
## CLI
5151

52+
### `--auto-configure` (Experimental)
53+
Presents a walkthrough that allows you to generate a JSCS configuration by
54+
choosing a preset and handling violated rules.
55+
56+
```
57+
jscs --auto-configure path
58+
```
59+
60+
`path` can be a file or directory to check the presets against
61+
5262
### `--config`
5363
Allows to define path to the config file.
5464
```

bin/jscs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ program
1515
.usage('[options] <file ...>')
1616
.description('A code style linter for programmatically enforcing your style guide.')
1717
.option('-c, --config [path]', 'configuration file path')
18+
.option('--auto-configure [path]', 'auto-generate a JSCS configuration file')
1819
.option('-e, --esnext', 'attempts to parse esnext code (currently es6)')
1920
.option('--es3', 'validates code as es3')
2021
.option('-s, --esprima <path>', 'attempts to use a custom version of Esprima')

lib/cli.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
var Checker = require('./checker');
99
var configFile = require('./cli-config');
10+
var ConfigGenerator = require('./config/generator');
1011

1112
var Vow = require('vow');
1213
var supportsColor = require('supports-color');
@@ -50,7 +51,7 @@ module.exports = function(program) {
5051
* Trying to load config.
5152
* Custom config path can be specified using '-c' option.
5253
*/
53-
if (!config && !program.preset) {
54+
if (!config && !program.preset && !program.autoConfigure) {
5455
if (program.config) {
5556
console.error('Configuration source', program.config, 'was not found.');
5657
} else {
@@ -62,7 +63,7 @@ module.exports = function(program) {
6263
return returnArgs;
6364
}
6465

65-
if (!args.length && process.stdin.isTTY) {
66+
if (!args.length && process.stdin.isTTY && typeof program.autoConfigure !== 'string') {
6667
console.error('No input files specified. Try option --help for usage information.');
6768
defer.reject(1);
6869

@@ -96,6 +97,20 @@ module.exports = function(program) {
9697

9798
return returnArgs;
9899
}
100+
if (program.autoConfigure) {
101+
var generator = new ConfigGenerator();
102+
103+
generator
104+
.generate(program.autoConfigure)
105+
.then(function() {
106+
defer.resolve(0);
107+
}, function(error) {
108+
console.error('Configuration generation failed due to ', error);
109+
defer.reject(1);
110+
});
111+
112+
return returnArgs;
113+
}
99114

100115
// Handle usage like 'cat myfile.js | jscs' or 'jscs -''
101116
var usedDash = args[args.length - 1] === '-';

lib/config/generator.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
var path = require('path');
2+
var fs = require('fs');
3+
4+
var Vow = require('vow');
5+
var Table = require('cli-table');
6+
var prompt = require('prompt');
7+
var colors = require('colors');
8+
var assign = require('lodash.assign');
9+
10+
var Checker = require('../checker');
11+
var utils = require('../utils');
12+
13+
prompt.message = '';
14+
prompt.delimiter = '';
15+
prompt.start();
16+
17+
/**
18+
* Script that walks the user through the autoconfig flow
19+
*
20+
* @type {String[]}
21+
* @private
22+
*/
23+
var prompts = [
24+
{
25+
name: colors.green('Please choose a preset number:'),
26+
require: true,
27+
pattern: /\d+/
28+
},
29+
{
30+
name: 'Create an (e)xception for this rule, or (f)ix the errors yourself?',
31+
require: true,
32+
pattern: /(e|f)/
33+
}
34+
];
35+
36+
/**
37+
* JSCS Configuration Generator
38+
*
39+
* @name Generator
40+
*/
41+
function Generator() {
42+
this._config = {};
43+
}
44+
45+
/**
46+
* Generates a configuration object based for the given path
47+
* based on the best fitting preset
48+
*
49+
* @param {String} path - The path containing file(s) used to guide the configuration
50+
*
51+
* @return {Promise} Resolved with the generated, JSCS configuration
52+
*/
53+
Generator.prototype.generate = function(path) {
54+
var checker = getChecker();
55+
var _path = utils.normalizePath(path, checker.getConfiguration().getBasePath());
56+
var presetNames = Object.keys(checker.getConfiguration().getRegisteredPresets());
57+
var statsForPresets;
58+
59+
console.log('Checking', _path, 'against the presets');
60+
61+
return Vow
62+
.all(presetNames.map(this._checkAgainstPreset.bind(this, _path)))
63+
.then(function(resultsPerPreset) {
64+
statsForPresets = this._generateStatsForPresets(resultsPerPreset, presetNames);
65+
return statsForPresets;
66+
}.bind(this))
67+
.then(this._showErrorCounts.bind(this))
68+
.then(this._getUserPresetChoice.bind(this, prompts[0]))
69+
.then(function showViolatedRules(choiceObj) {
70+
var presetIndex = choiceObj[prompts[0].name] - 1;
71+
var presetName = statsForPresets[presetIndex].name;
72+
73+
console.log('You chose the ' + presetName + ' preset');
74+
75+
this._config.preset = presetName;
76+
77+
var errorList = statsForPresets[presetIndex].errors;
78+
errorList = getUniqueErrorNames(errorList);
79+
80+
var violatedRuleCount = errorList.length;
81+
82+
if (!violatedRuleCount) { return this._config; }
83+
84+
console.log(_path + ' violates ' + violatedRuleCount + ' rule' + (violatedRuleCount > 1 ? 's' : ''));
85+
86+
var errorPrompts = generateRuleHandlingPrompts(errorList);
87+
88+
return this._getUserViolationChoices(errorPrompts)
89+
.then(this._handleViolatedRules.bind(this, errorPrompts, errorList))
90+
.then(function() {
91+
return this._config;
92+
}.bind(this));
93+
}.bind(this))
94+
.then(function flushConfig(config) {
95+
fs.writeFileSync(process.cwd() + '/.jscsrc', JSON.stringify(this._config, null, '\t'));
96+
console.log('Generated a .jscsrc configuration file in ' + process.cwd());
97+
}.bind(this));
98+
};
99+
100+
/**
101+
* @private
102+
* @param {Object[][]} resultsPerPreset - List of error objects for each preset's run of checkPath
103+
* @param {String[]} presetNames
104+
* @return {Object[]} Aggregated datapoints for each preset
105+
*/
106+
Generator.prototype._generateStatsForPresets = function(resultsPerPreset, presetNames) {
107+
return resultsPerPreset.map(function(presetResults, idx) {
108+
var errorCollection = [].concat.apply([], presetResults);
109+
110+
var presetStats = {
111+
name: presetNames[idx],
112+
sum: 0,
113+
errors: []
114+
};
115+
116+
errorCollection.forEach(function(error) {
117+
presetStats.sum += error.getErrorCount();
118+
presetStats.errors = presetStats.errors.concat(error.getErrorList());
119+
});
120+
121+
return presetStats;
122+
});
123+
};
124+
125+
/**
126+
* @private
127+
* @param {Object[]} statsForPresets
128+
*/
129+
Generator.prototype._showErrorCounts = function(statsForPresets) {
130+
var table = getTable();
131+
132+
statsForPresets.forEach(function(presetStats, idx) {
133+
table.push([idx + 1, presetStats.name, presetStats.sum]);
134+
});
135+
136+
console.log(table.toString());
137+
};
138+
139+
/**
140+
* Prompts the user to choose a preset
141+
*
142+
* @private
143+
* @param {Object} prompt
144+
* @return {Promise}
145+
*/
146+
Generator.prototype._getUserPresetChoice = function(prompt) {
147+
return this._showPrompt(prompt);
148+
};
149+
150+
/**
151+
* Prompts the user to nullify rules or fix violations themselves
152+
*
153+
* @private
154+
* @param {Object[]} errorPrompts
155+
* @return {Promise}
156+
*/
157+
Generator.prototype._getUserViolationChoices = function(errorPrompts) {
158+
return this._showPrompt(errorPrompts);
159+
};
160+
161+
function promisify(fn) {
162+
return function() {
163+
var deferred = Vow.defer();
164+
var args = [].slice.call(arguments);
165+
166+
args.push(function(err, result) {
167+
if (err) {
168+
deferred.reject(err);
169+
} else {
170+
deferred.resolve(result);
171+
}
172+
});
173+
174+
fn.apply(null, args);
175+
176+
return deferred.promise();
177+
};
178+
}
179+
180+
/** @private */
181+
Generator.prototype._showPrompt = promisify(prompt.get.bind(prompt));
182+
183+
/**
184+
* @private
185+
* @param {Object[]} errorPrompts
186+
* @param {String[]} errorList
187+
* @param {Object[]} choices
188+
*/
189+
Generator.prototype._handleViolatedRules = function(errorPrompts, errorList, choices) {
190+
errorPrompts.forEach(function(errorPrompt, idx) {
191+
var associatedRuleName = errorList[idx];
192+
var userChoice = choices[errorPrompt.name];
193+
194+
if (userChoice.toLowerCase() === 'e') {
195+
this._config[associatedRuleName] = null;
196+
}
197+
}, this);
198+
};
199+
200+
/**
201+
* @private
202+
* @param {String} path
203+
* @param {String} presetName
204+
* @return {Promise}
205+
*/
206+
Generator.prototype._checkAgainstPreset = function(path, presetName) {
207+
var checker = getChecker();
208+
209+
checker.configure({preset: presetName});
210+
211+
return checker.checkPath(path);
212+
};
213+
214+
/**
215+
* @private
216+
* @return {lib/Checker}
217+
*/
218+
function getChecker() {
219+
var checker = new Checker();
220+
checker.registerDefaultRules();
221+
return checker;
222+
}
223+
224+
/**
225+
* @private
226+
* @param {String[]} violatedRuleNames
227+
* @return {Object[]}
228+
*/
229+
function generateRuleHandlingPrompts(violatedRuleNames) {
230+
return violatedRuleNames.map(function(ruleName) {
231+
var prompt = assign({}, prompts[1]);
232+
prompt.name = colors.green(ruleName) + ': ' + prompt.name;
233+
return prompt;
234+
});
235+
}
236+
237+
/**
238+
* @private
239+
* @param {Object[]} errorsList
240+
* @return {String[]}
241+
*/
242+
function getUniqueErrorNames(errorsList) {
243+
var errorNameLUT = {};
244+
245+
errorsList.forEach(function(error) {
246+
errorNameLUT[error.rule] = true;
247+
});
248+
249+
return Object.keys(errorNameLUT);
250+
}
251+
252+
/**
253+
* @private
254+
* @return {Object}
255+
*/
256+
function getTable() {
257+
return new Table({
258+
chars: {
259+
top: '', 'top-mid': '', 'top-left': '', 'top-right': '',
260+
bottom: '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
261+
left: '', 'left-mid': '',
262+
mid: '', 'mid-mid': '',
263+
right: '', 'right-mid': '' ,
264+
middle: ' '
265+
},
266+
style: {
267+
'padding-left': 0,
268+
'padding-right': 0
269+
},
270+
head: ['', 'Preset', '#Errors']
271+
});
272+
}
273+
274+
module.exports = Generator;

lib/config/node-configuration.js

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var path = require('path');
22
var util = require('util');
3+
var utils = require('../utils');
34
var glob = require('glob');
45
var Configuration = require('./configuration');
56
var assert = require('assert');
@@ -51,10 +52,7 @@ NodeConfiguration.prototype.overrideFromCLI = function(program) {
5152
*/
5253
NodeConfiguration.prototype._loadPlugin = function(plugin) {
5354
if (typeof plugin === 'string') {
54-
var pluginPath = plugin;
55-
if (isRelativeRequirePath(pluginPath)) {
56-
pluginPath = path.resolve(this._basePath, pluginPath);
57-
}
55+
var pluginPath = utils.normalizePath(plugin, this._basePath);
5856
plugin = require(pluginPath);
5957
}
6058
Configuration.prototype._loadPlugin.call(this, plugin);
@@ -73,9 +71,7 @@ NodeConfiguration.prototype._loadErrorFilter = function(errorFilter) {
7371
);
7472

7573
if (errorFilter) {
76-
if (isRelativeRequirePath(errorFilter)) {
77-
errorFilter = path.resolve(this._basePath, errorFilter);
78-
}
74+
errorFilter = utils.normalizePath(errorFilter, this._basePath);
7975
errorFilter = require(errorFilter);
8076
}
8177

@@ -95,9 +91,7 @@ NodeConfiguration.prototype._loadEsprima = function(esprima) {
9591
);
9692

9793
if (esprima) {
98-
if (isRelativeRequirePath(esprima)) {
99-
esprima = path.resolve(this._basePath, esprima);
100-
}
94+
esprima = utils.normalizePath(esprima, this._basePath);
10195
esprima = require(esprima);
10296
}
10397

@@ -121,10 +115,4 @@ NodeConfiguration.prototype._loadAdditionalRule = function(additionalRule) {
121115
}
122116
};
123117

124-
function isRelativeRequirePath(requirePath) {
125-
// Logic from: https://github.com/joyent/node/blob/4f1ae11a62b97052bc83756f8cb8700cc1f61661/lib/module.js#L237
126-
var start = requirePath.substring(0, 2);
127-
return start === './' || start === '..';
128-
}
129-
130118
module.exports = NodeConfiguration;

0 commit comments

Comments
 (0)