UNPKG

jscs

Version:

JavaScript Code Style

927 lines (791 loc) 21.4 kB
var assert = require('assert'); var path = require('path'); var fs = require('fs'); var minimatch = require('minimatch'); var defaults = { cwd: '.', maxErrors: 50 }; var _ = require('lodash'); var BUILTIN_OPTIONS = { plugins: true, preset: true, excludeFiles: true, additionalRules: true, fileExtensions: true, extract: true, maxErrors: true, configPath: true, es3: true, errorFilter: true, fix: true }; /** * JSCS Configuration. * Browser/Rhino-compatible. * * @name Configuration */ function Configuration() { /** * List of the registered (not used) presets. * * @protected * @type {Object} */ this._presets = {}; /** * Name of the preset (if used). * * @protected * @type {String|null} */ this._presetName = null; /** * List of loaded presets. * * @protected * @type {String|null} */ this._loadedPresets = []; /** * List of rules instances. * * @protected * @type {Object} */ this._rules = {}; /** * List of configurated rule instances. * * @protected * @type {Object} */ this._ruleSettings = {}; /** * List of configured rules. * * @protected * @type {Array} */ this._configuredRules = []; /** * List of unsupported rules. * * @protected * @type {Array} */ this._unsupportedRuleNames = []; /** * File extensions that would be checked. * * @protected * @type {Array} */ this._fileExtensions = []; /** * List of defined options (not complete). * * @protected * @type {Array} */ this._definedOptions = []; /** * Default file extensions that would be checked. * * @protected * @type {Array} */ this._defaultFileExtensions = ['.js']; /** * Exclusion masks. * * @protected * @type {Array} */ this._excludedFileMasks = []; /** * Default exclusion masks, will be rewritten if user has their own masks. * * @protected * @type {Array} */ this._defaultExcludedFileMasks = ['.git/**', 'node_modules/**']; /** * List of existing files that falls under exclusion masks. * * @protected * @type {Array} */ this._excludedFileMatchers = []; /** * Extraction masks. * * @protected * @type {Array} */ this._extractFileMasks = []; /** * Default extractions masks. * * @protected * @type {Array} */ this._defaultExtractFileMasks = ['**/*.+(htm|html|xhtml)']; /** * List of file matchers from which to extract JavaScript. * * @protected * @type {Array} */ this._extractFileMatchers = []; /** * Maxixum amount of error that would be reportered. * * @protected * @type {Number} */ this._maxErrors = defaults.maxErrors; /** * JSCS CWD. * * @protected * @type {String} */ this._basePath = defaults.cwd; /** * List of overrided options (usually from CLI). * * @protected * @type {Object} */ this._overrides = {}; /** * Is "ES3" mode enabled?. * * @protected * @type {Boolean} */ this._es3Enabled = false; /** * A filter function that determines whether or not to report an error. * * @protected * @type {Function|null} */ this._errorFilter = null; } /** * Load settings from a configuration. * * @param {Object} config */ Configuration.prototype.load = function(config) { // Load all the options this._processConfig(config); // Load defaults if they weren't set this._loadDefaults(config); // Load and apply all the rules this._useRules(); }; /** * Load default values for options which were not defined * * @private */ Configuration.prototype._loadDefaults = function() { if (!this._isDefined('excludeFiles')) { this._loadExcludedFiles(this._defaultExcludedFileMasks); } if (!this._isDefined('fileExtensions')) { this._loadFileExtensions(this._defaultFileExtensions); } }; /** * Returns resulting configuration after preset is applied and options are processed. * * @return {Object} */ Configuration.prototype.getProcessedConfig = function() { var result = {}; Object.keys(this._ruleSettings).forEach(function(key) { result[key] = this._ruleSettings[key]; }, this); result.excludeFiles = this._excludedFileMasks; result.fileExtensions = this._fileExtensions; result.extract = this._extractFileMasks; result.maxErrors = this._maxErrors; result.preset = this._presetName; result.es3 = this._es3Enabled; result.errorFilter = this._errorFilter; return result; }; /** * Returns list of configured rules. * * @returns {Rule[]} */ Configuration.prototype.getConfiguredRules = function() { return this._configuredRules; }; /** * Returns configured rule. * * @returns {Rule | null} */ Configuration.prototype.getConfiguredRule = function(name) { return this._configuredRules.filter(function(rule) { return rule.getOptionName() === name; })[0] || null; }; /** * Returns the list of unsupported rule names. * * @return {String[]} */ Configuration.prototype.getUnsupportedRuleNames = function() { return this._unsupportedRuleNames; }; /** * Returns excluded file mask list. * * @returns {String[]} */ Configuration.prototype.getExcludedFileMasks = function() { return this._excludedFileMasks; }; /** * Returns `true` if specified file path is excluded. * * @param {String} filePath * @returns {Boolean} */ Configuration.prototype.isFileExcluded = function(filePath) { filePath = path.resolve(filePath); return this._excludedFileMatchers.some(function(matcher) { return matcher.match(filePath); }); }; /** * Returns true if the file extension matches a file extension to process. * * @returns {Boolean} */ Configuration.prototype.hasCorrectExtension = function(testPath) { var extension = path.extname(testPath).toLowerCase(); var basename = path.basename(testPath).toLowerCase(); var fileExtensions = this.getFileExtensions(); return !( fileExtensions.indexOf(extension) < 0 && fileExtensions.indexOf(basename) < 0 && fileExtensions.indexOf('*') < 0 ); }; /** * Returns file extension list. * * @returns {String[]} */ Configuration.prototype.getFileExtensions = function() { return this._fileExtensions; }; /** * Returns extract file masks. * * @returns {String[]} */ Configuration.prototype.getExtractFileMasks = function() { return this._extractFileMasks; }; /** * Should filePath to be extracted? * * @returns {Boolean} */ Configuration.prototype.shouldExtractFile = function(filePath) { filePath = path.resolve(filePath); return this._extractFileMatchers.some(function(matcher) { return matcher.match(filePath); }); }; /** * Returns maximal error count. * * @returns {Number|null} */ Configuration.prototype.getMaxErrors = function() { return this._maxErrors; }; /** * Getter "fix" option value. * * @return {Boolean} */ Configuration.prototype.getFix = function() { return !!this._fix; }; /** * Returns `true` if `es3` option is enabled. * * @returns {Boolean} */ Configuration.prototype.isES3Enabled = function() { return this._es3Enabled; }; /** * Returns the loaded error filter. * * @returns {Function|null} */ Configuration.prototype.getErrorFilter = function() { return this._errorFilter; }; /** * Returns base path. * * @returns {String} */ Configuration.prototype.getBasePath = function() { return this._basePath; }; /** * Overrides specified settings. * * @param {String} overrides */ Configuration.prototype.override = function(overrides) { Object.keys(overrides).forEach(function(key) { this._overrides[key] = overrides[key]; }, this); }; /** * returns options, but not rules, from the provided config * * @param {Object} config * @returns {Object} */ Configuration.prototype._getOptionsFromConfig = function(config) { return Object.keys(config).reduce(function(options, key) { if (BUILTIN_OPTIONS[key]) { options[key] = config[key]; } return options; }, {}); }; Configuration.prototype._errorOnRemovedOptions = function(config) { var errors = ['Config values to remove in 3.0:']; if (config.hasOwnProperty('esprima')) { errors.push('The `esprima` option since CST uses babylon (the babel parser) under the hood'); } if (config.hasOwnProperty('esprimaOptions')) { errors.push('The `esprimaOptions` option.'); } if (config.hasOwnProperty('esnext')) { errors.push('The `esnext` option is enabled by default.'); } if (config.hasOwnProperty('verbose')) { errors.push('The `verbose` option is enabled by default.'); } if (errors.length > 1) { throw new Error(errors.join('\n')); } }; /** * Processes configuration and returns config options. * * @param {Object} config */ Configuration.prototype._processConfig = function(config) { var overrides = this._overrides; var currentConfig = {}; // Copy configuration so original config would be intact copyConfiguration(config, currentConfig); // Override the properties copyConfiguration(overrides, currentConfig); this._errorOnRemovedOptions(currentConfig); // NOTE: options is a separate object to ensure that future options must be added // to BUILTIN_OPTIONS to work, which also assures they aren't mistaken for a rule var options = this._getOptionsFromConfig(currentConfig); // Base path if (this._basePath === defaults.cwd && options.configPath) { assert( typeof options.configPath === 'string', '`configPath` option requires string value' ); this._basePath = path.dirname(options.configPath); } if (options.hasOwnProperty('plugins')) { assert(Array.isArray(options.plugins), '`plugins` option requires array value'); options.plugins.forEach(function(plugin) { this._loadPlugin(plugin, options.configPath); }, this); if (!this._isDefined('plugins')) { this._definedOptions.push('plugins'); } } if (options.hasOwnProperty('additionalRules')) { assert(Array.isArray(options.additionalRules), '`additionalRules` option requires array value'); options.additionalRules.forEach(function(rule) { this._loadAdditionalRule(rule, options.configPath); }, this); if (!this._isDefined('additionalRules')) { this._definedOptions.push('additionalRules'); } } if (options.hasOwnProperty('extract')) { this._loadExtract(options.extract); } if (options.hasOwnProperty('fileExtensions')) { this._loadFileExtensions(options.fileExtensions); } if (options.hasOwnProperty('excludeFiles')) { this._loadExcludedFiles(options.excludeFiles); } if (options.hasOwnProperty('fix')) { this._loadFix(options.fix); } this._loadMaxError(options); if (options.hasOwnProperty('es3')) { this._loadES3(options.es3); } if (options.hasOwnProperty('errorFilter')) { this._loadErrorFilter(options.errorFilter, options.configPath); } // Apply presets if (options.hasOwnProperty('preset')) { this._loadPreset(options.preset, options.configPath); } this._loadRules(currentConfig); }; /** * Loads plugin data. * * @param {function(Configuration)} plugin * @protected */ Configuration.prototype._loadPlugin = function(plugin) { assert(typeof plugin === 'function', '`plugin` should be a function'); plugin(this); }; /** * Load rules. * * @param {Object} config * @protected */ Configuration.prototype._loadRules = function(config) { Object.keys(config).forEach(function(key) { // Only rules should be processed if (BUILTIN_OPTIONS[key]) { return; } if (this._rules[key]) { var optionValue = config[key]; // Disable rule it it equals "false" or "null" if (optionValue === null || optionValue === false) { delete this._ruleSettings[key]; } else { this._ruleSettings[key] = config[key]; } } else if (this._unsupportedRuleNames.indexOf(key) === -1) { this._unsupportedRuleNames.push(key); } }, this); }; /** * Loads an error filter. * * @param {Function|null} errorFilter * @protected */ Configuration.prototype._loadErrorFilter = function(errorFilter) { assert( typeof errorFilter === 'function' || errorFilter === null, '`errorFilter` option requires a function or null value' ); this._errorFilter = errorFilter; if (!this._isDefined('errorFilter')) { this._definedOptions.push('errorFilter'); } }; /** * Load "es3" option. * * @param {Boolean} es3 * @protected */ Configuration.prototype._loadES3 = function(es3) { assert( typeof es3 === 'boolean' || es3 === null, '`es3` option requires boolean or null value' ); this._es3Enabled = Boolean(es3); if (!this._isDefined('es3')) { this._definedOptions.push('es3'); } }; /** * Load "maxError" option. * * @param {Object} options * @protected */ Configuration.prototype._loadMaxError = function(options) { // If "fix" option is enabled, set to Inifinity, otherwise this option // doesn't make sense with "fix" conjunction if (this._fix === true) { this._maxErrors = Infinity; return; } if (!options.hasOwnProperty('maxErrors')) { return; } var maxErrors = options.maxErrors === null ? null : Number(options.maxErrors); assert( maxErrors === -1 || maxErrors > 0 || maxErrors === null, '`maxErrors` option requires -1, null value or positive number' ); this._maxErrors = maxErrors; if (!this._isDefined('fix')) { this._definedOptions.push('fix'); } }; /** * Load "fix" option. * * @param {Boolean|null} fix * @protected */ Configuration.prototype._loadFix = function(fix) { fix = fix === null ? false : fix; assert( typeof fix === 'boolean', '`fix` option requires boolean or null value' ); this._fix = fix; if (!this._isDefined('fix')) { this._definedOptions.push('fix'); } }; /** * Load preset. * * @param {Object} preset * @protected */ Configuration.prototype._loadPreset = function(preset) { if (this._loadedPresets.indexOf(preset) > -1) { return; } // Do not keep adding preset from CLI (#2087) delete this._overrides.preset; this._loadedPresets.push(preset); // If preset is loaded from another preset - preserve the original name if (!this._presetName) { this._presetName = preset; } assert(typeof preset === 'string', '`preset` option requires string value'); var presetData = this._presets[preset]; assert(Boolean(presetData), 'Preset "' + preset + '" does not exist'); if (!this._isDefined('preset')) { this._definedOptions.push('preset'); } // Process config from the preset this._processConfig(this._presets[preset]); }; /** * Load file extensions. * * @param {String|Array} extensions * @protected */ Configuration.prototype._loadFileExtensions = function(extensions) { assert( typeof extensions === 'string' || Array.isArray(extensions), '`fileExtensions` option requires string or array value' ); this._fileExtensions = this._fileExtensions.concat(extensions).map(function(ext) { return ext.toLowerCase(); }); if (!this._isDefined('fileExtensions')) { this._definedOptions.push('fileExtensions'); } }; /** * Is option defined? * * @param {String} name - name of the option * * @return {Boolean} */ Configuration.prototype._isDefined = function(name) { return this._definedOptions.indexOf(name) > -1; }; /** * Load excluded paths. * * @param {Array} masks * @protected */ Configuration.prototype._loadExcludedFiles = function(masks) { assert(Array.isArray(masks), '`excludeFiles` option requires array value'); this._excludedFileMasks = this._excludedFileMasks.concat(masks); this._excludedFileMatchers = this._excludedFileMasks.map(function(fileMask) { return new minimatch.Minimatch(path.resolve(this._basePath, fileMask), { dot: true }); }, this); if (!this._isDefined('excludeFiles')) { this._definedOptions.push('excludeFiles'); } }; /** * Load paths for extract. * * @param {Array} masks * @protected */ Configuration.prototype._loadExtract = function(masks) { if (masks === true) { masks = this._defaultExtractFileMasks; } else if (masks === false) { masks = []; } assert(Array.isArray(masks), '`extract` option should be array of strings'); this._extractFileMasks = masks.slice(); this._extractFileMatchers = this._extractFileMasks.map(function(fileMask) { return new minimatch.Minimatch(path.resolve(this._basePath, fileMask), { dot: true }); }, this); if (!this._isDefined('extract')) { this._definedOptions.push('extract'); } }; /** * Loads additional rule. * * @param {Rule} additionalRule * @protected */ Configuration.prototype._loadAdditionalRule = function(additionalRule) { assert(typeof additionalRule === 'object', '`additionalRule` should be an object'); this.registerRule(additionalRule); }; /** * Includes plugin in the configuration environment. * * @param {function(Configuration)|*} plugin */ Configuration.prototype.usePlugin = function(plugin) { this._loadPlugin(plugin); }; /** * Apply the rules. * * @protected */ Configuration.prototype._useRules = function() { this._configuredRules = []; Object.keys(this._ruleSettings).forEach(function(optionName) { var rule = this._rules[optionName]; rule.configure(this._ruleSettings[optionName]); this._configuredRules.push(rule); }, this); }; /** * Adds rule to the collection. * * @param {Rule|Function} rule Rule instance or rule class. */ Configuration.prototype.registerRule = function(rule) { if (typeof rule === 'function') { var RuleClass = rule; rule = new RuleClass(); } var optionName = rule.getOptionName(); assert(!this._rules.hasOwnProperty(optionName), 'Rule "' + optionName + '" is already registered'); this._rules[optionName] = rule; }; /** * Returns list of registered rules. * * @returns {Rule[]} */ Configuration.prototype.getRegisteredRules = function() { var rules = this._rules; return Object.keys(rules).map(function(ruleOptionName) { return rules[ruleOptionName]; }); }; /** * Adds preset to the collection. * * @param {String} presetName * @param {Object} presetConfig */ Configuration.prototype.registerPreset = function(presetName, presetConfig) { assert(_.isPlainObject(presetConfig), 'Preset should be an object'); for (var key in presetConfig) { assert(typeof presetConfig[key] !== 'function', 'Preset should be an JSON object'); } this._presets[presetName] = presetConfig; }; /** * Returns registered presets object (key - preset name, value - preset content). * * @returns {Object} */ Configuration.prototype.getRegisteredPresets = function() { return this._presets; }; /** * Returns `true` if preset with specified name exists. * * @param {String} presetName * @return {Boolean} */ Configuration.prototype.hasPreset = function(presetName) { return this._presets.hasOwnProperty(presetName); }; /** * Returns name of the active preset. * * @return {String} */ Configuration.prototype.getPresetName = function() { return this._presetName; }; /** * Registers built-in Code Style cheking rules. */ Configuration.prototype.registerDefaultRules = function() { var dir = path.join(__dirname, '../rules'); fs.readdirSync(dir).forEach(function(rule) { this.registerRule( require(path.join(dir, rule)) ); }, this); }; /** * Registers built-in Code Style cheking presets. */ Configuration.prototype.registerDefaultPresets = function() { var dir = path.join(__dirname, '../../presets/'); fs.readdirSync(dir).forEach(function(preset) { var name = preset.split('.')[0]; var p = path.join(dir, preset); this.registerPreset(name, require(p)); }, this); this.registerPreset('wikimedia', require('jscs-preset-wikimedia')); }; module.exports = Configuration; function copyConfiguration(source, dest) { Object.keys(source).forEach(function(key) { dest[key] = source[key]; }); if (source.configPath) { dest.configPath = source.configPath; } }