UNPKG

9.31 kBJavaScriptView Raw
1var esprima = require('esprima');
2var babelJscs = require('babel-jscs');
3var Errors = require('./errors');
4var JsFile = require('./js-file');
5var Configuration = require('./config/configuration');
6
7var MAX_FIX_ATTEMPTS = 5;
8
9function getInternalErrorMessage(rule, e) {
10 return 'Error running rule ' + rule + ': ' +
11 'This is an issue with JSCS and not your codebase.\n' +
12 'Please file an issue (with the stack trace below) at: ' +
13 'https://github.com/jscs-dev/node-jscs/issues/new\n' + e;
14}
15
16/**
17 * Starts Code Style checking process.
18 *
19 * @name StringChecker
20 */
21var StringChecker = function() {
22 this._configuredRules = [];
23
24 this._errorsFound = 0;
25 this._maxErrorsExceeded = false;
26
27 // Need to be defined here because Configuration module can choose
28 // custom esprima or chose parsers based on "esnext" option
29 this._esprima = esprima;
30
31 this._configuration = this._createConfiguration();
32 this._configuration.registerDefaultPresets();
33};
34
35StringChecker.prototype = {
36 /**
37 * Registers single Code Style checking rule.
38 *
39 * @param {Rule} rule
40 */
41 registerRule: function(rule) {
42 this._configuration.registerRule(rule);
43 },
44
45 /**
46 * Registers built-in Code Style checking rules.
47 */
48 registerDefaultRules: function() {
49 this._configuration.registerDefaultRules();
50 },
51
52 /**
53 * Get processed config.
54 *
55 * @return {Object}
56 */
57 getProcessedConfig: function() {
58 return this._configuration.getProcessedConfig();
59 },
60
61 /**
62 * Loads configuration from JS Object. Activates and configures required rules.
63 *
64 * @param {Object} config
65 */
66 configure: function(config) {
67 this._configuration.load(config);
68
69 if (this._configuration.hasCustomEsprima()) {
70 this._esprima = this._configuration.getCustomEsprima();
71 } else if (this._configuration.isESNextEnabled()) {
72 this._esprima = babelJscs;
73 }
74
75 this._verbose = this._configuration.getVerbose();
76
77 this._configuredRules = this._configuration.getConfiguredRules();
78 this._maxErrors = this._configuration.getMaxErrors();
79 },
80
81 /**
82 * Checks file provided with a string.
83 *
84 * @param {String} source
85 * @param {String} [filename='input']
86 * @returns {Errors}
87 */
88 checkString: function(source, filename) {
89 filename = filename || 'input';
90
91 var file = this._createJsFileInstance(filename, source);
92
93 var errors = new Errors(file, this._verbose);
94
95 file.getParseErrors().forEach(function(parseError) {
96 if (!this._maxErrorsExceeded) {
97 this._addParseError(errors, parseError);
98 }
99 }, this);
100
101 // Do not check empty strings
102 if (file.getFirstToken({includeComments: true}).type === 'EOF') {
103 return errors;
104 }
105
106 this._checkJsFile(file, errors);
107
108 return errors;
109 },
110
111 /**
112 * Fix provided error.
113 *
114 * @param {JsFile} file
115 * @param {Errors} errors
116 * @protected
117 */
118 _fixJsFile: function(file, errors) {
119 var list = errors.getErrorList();
120 var configuration = this.getConfiguration();
121
122 list.forEach(function(error) {
123 if (error.fixed) {
124 return;
125 }
126
127 var instance = configuration.getConfiguredRule(error.rule);
128
129 if (instance && instance._fix) {
130 try {
131
132 // "error.fixed = true" should go first, so rule can
133 // decide for itself (with "error.fixed = false")
134 // if it can fix this particular error
135 error.fixed = true;
136 instance._fix(file, error);
137
138 } catch (e) {
139 error.fixed = undefined;
140 errors.add(getInternalErrorMessage(error.rule, e), 1, 0);
141 }
142 }
143 });
144 },
145
146 /**
147 * Checks a file specified using JsFile instance.
148 * Fills Errors instance with validation errors.
149 *
150 * @param {JsFile} file
151 * @param {Errors} errors
152 * @protected
153 */
154 _checkJsFile: function(file, errors) {
155 if (this._maxErrorsExceeded) {
156 return;
157 }
158
159 var errorFilter = this._configuration.getErrorFilter();
160
161 this._configuredRules.forEach(function(rule) {
162 errors.setCurrentRule(rule.getOptionName());
163
164 try {
165 rule.check(file, errors);
166 } catch (e) {
167 errors.setCurrentRule('internalError');
168 errors.add(getInternalErrorMessage(rule.getOptionName(), e.stack), 1, 0);
169 }
170 }, this);
171
172 this._configuration.getUnsupportedRuleNames().forEach(function(rulename) {
173 errors.add('Unsupported rule: ' + rulename, 1, 0);
174 });
175
176 // sort errors list to show errors as they appear in source
177 errors.getErrorList().sort(function(a, b) {
178 return (a.line - b.line) || (a.column - b.column);
179 });
180
181 if (errorFilter) {
182 errors.filter(errorFilter);
183 }
184
185 if (this.maxErrorsEnabled()) {
186 if (this._maxErrors === -1 || this._maxErrors === null) {
187 this._maxErrorsExceeded = false;
188
189 } else {
190 this._maxErrorsExceeded = this._errorsFound + errors.getErrorCount() > this._maxErrors;
191 errors.stripErrorList(Math.max(0, this._maxErrors - this._errorsFound));
192 }
193 }
194
195 this._errorsFound += errors.getErrorCount();
196 },
197
198 /**
199 * Adds parse error to the error list.
200 *
201 * @param {Errors} errors
202 * @param {Error} parseError
203 * @private
204 */
205 _addParseError: function(errors, parseError) {
206 if (this._maxErrorsExceeded) {
207 return;
208 }
209
210 errors.setCurrentRule('parseError');
211 errors.add(parseError.description, parseError.lineNumber, parseError.column);
212
213 if (this.maxErrorsEnabled()) {
214 this._errorsFound += 1;
215 this._maxErrorsExceeded = this._errorsFound >= this._maxErrors;
216 }
217 },
218
219 /**
220 * Creates configured JsFile instance.
221 *
222 * @param {String} filename
223 * @param {String} source
224 * @private
225 */
226 _createJsFileInstance: function(filename, source) {
227 return new JsFile({
228 filename: filename,
229 source: source,
230 esprima: this._esprima,
231 esprimaOptions: this._configuration.getEsprimaOptions(),
232 es3: this._configuration.isES3Enabled(),
233 es6: this._configuration.isESNextEnabled()
234 });
235 },
236
237 /**
238 * Checks file provided with a string.
239 *
240 * @param {String} source
241 * @param {String} [filename='input']
242 * @returns {{output: String, errors: Errors}}
243 */
244 fixString: function(source, filename) {
245 filename = filename || 'input';
246
247 var file = this._createJsFileInstance(filename, source);
248 var errors = new Errors(file, this._verbose);
249
250 var parseErrors = file.getParseErrors();
251 if (parseErrors.length > 0) {
252 parseErrors.forEach(function(parseError) {
253 this._addParseError(errors, parseError);
254 }, this);
255
256 return {output: source, errors: errors};
257 } else {
258 var attempt = 0;
259 do {
260 // Changes to current sources are made in rules through assertions.
261 this._checkJsFile(file, errors);
262
263 // If assertions weren't used but rule has "fix" method,
264 // which we could use.
265 this._fixJsFile(file, errors);
266
267 var hasFixes = errors.getErrorList().some(function(err) {
268 return err.fixed;
269 });
270
271 if (!hasFixes) {
272 break;
273 }
274
275 file = this._createJsFileInstance(filename, file.render());
276 errors = new Errors(file, this._verbose);
277 attempt++;
278 } while (attempt < MAX_FIX_ATTEMPTS);
279
280 return {output: file.getSource(), errors: errors};
281 }
282 },
283
284 /**
285 * Returns `true` if max erros limit is enabled.
286 *
287 * @returns {Boolean}
288 */
289 maxErrorsEnabled: function() {
290 return this._maxErrors !== null && this._maxErrors !== -1;
291 },
292
293 /**
294 * Returns `true` if error count exceeded `maxErrors` option value.
295 *
296 * @returns {Boolean}
297 */
298 maxErrorsExceeded: function() {
299 return this._maxErrorsExceeded;
300 },
301
302 /**
303 * Returns new configuration instance.
304 *
305 * @protected
306 * @returns {Configuration}
307 */
308 _createConfiguration: function() {
309 return new Configuration();
310 },
311
312 /**
313 * Returns current configuration instance.
314 *
315 * @returns {Configuration}
316 */
317 getConfiguration: function() {
318 return this._configuration;
319 },
320
321 /**
322 * Returns the current esprima parser
323 *
324 * @return {Esprima}
325 */
326 getEsprima: function() {
327 return this._esprima || this._configuration.getCustomEsprima();
328 }
329};
330
331module.exports = StringChecker;