text: /**
   * @fileoverview Used for creating a suggested configuration based on project code.
   * @author Ian VanSchooten
   */
  
  'use strict'
  
  // ------------------------------------------------------------------------------
  // Requirements
  // ------------------------------------------------------------------------------
  
  const lodash = require('lodash')
  
  const Linter = require('../linter')
  
  const configRule = require('./config-rule')
  
  const ConfigOps = require('./config-ops')
  
  const recConfig = require('../../conf/eslint-recommended')
  
  const debug = require('debug')('eslint:autoconfig')
  const linter = new Linter()
  
  // ------------------------------------------------------------------------------
  // Data
  // ------------------------------------------------------------------------------
  
  const MAX_CONFIG_COMBINATIONS = 17
  // 16 combinations + 1 for severity only
  
  const RECOMMENDED_CONFIG_NAME = 'eslint:recommended'
  
  // ------------------------------------------------------------------------------
  // Private
  // ------------------------------------------------------------------------------
  
  /**
   * Information about a rule configuration, in the context of a Registry.
   *
   * @typedef {Object}     registryItem
   * @param   {ruleConfig} config        A valid configuration for the rule
   * @param   {number}     specificity   The number of elements in the ruleConfig array
   * @param   {number}     errorCount    The number of errors encountered when linting with the config
   */
  
  /**
   * This callback is used to measure execution status in a progress bar
   * @callback progressCallback
   * @param {number} The total number of times the callback will be called.
   */
  
  /**
   * Create registryItems for rules
   * @param   {rulesConfig} rulesConfig Hash of rule names and arrays of ruleConfig items
   * @returns {Object}                  registryItems for each rule in provided rulesConfig
   */
  function makeRegistryItems (rulesConfig) {
    return Object.keys(rulesConfig).reduce((accumulator, ruleId) => {
      accumulator[ruleId] = rulesConfig[ruleId].map(config => ({
        config,
        specificity: config.length || 1,
        errorCount: void 0
      }))
      return accumulator
    }, {})
  }
  
  /**
   * Creates an object in which to store rule configs and error counts
   *
   * Unless a rulesConfig is provided at construction, the registry will not contain
   * any rules, only methods.  This will be useful for building up registries manually.
   *
   * Registry class
   */
  class Registry {
    /**
       * @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations
       */
    constructor (rulesConfig) {
      this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {}
    }
  
    /**
       * Populate the registry with core rule configs.
       *
       * It will set the registry's `rule` property to an object having rule names
       * as keys and an array of registryItems as values.
       *
       * @returns {void}
       */
    populateFromCoreRules () {
      const rulesConfig = configRule.createCoreRuleConfigs()
  
      this.rules = makeRegistryItems(rulesConfig)
    }
  
    /**
       * Creates sets of rule configurations which can be used for linting
       * and initializes registry errors to zero for those configurations (side effect).
       *
       * This combines as many rules together as possible, such that the first sets
       * in the array will have the highest number of rules configured, and later sets
       * will have fewer and fewer, as not all rules have the same number of possible
       * configurations.
       *
       * The length of the returned array will be <= MAX_CONFIG_COMBINATIONS.
       *
       * @param   {Object}   registry The autoconfig registry
       * @returns {Object[]}          "rules" configurations to use for linting
       */
    buildRuleSets () {
      let idx = 0
      const ruleIds = Object.keys(this.rules)
  
      const ruleSets = []
  
      /**
           * Add a rule configuration from the registry to the ruleSets
           *
           * This is broken out into its own function so that it doesn't need to be
           * created inside of the while loop.
           *
           * @param   {string} rule The ruleId to add.
           * @returns {void}
           */
      const addRuleToRuleSet = function (rule) {
        /*
               * This check ensures that there is a rule configuration and that
               * it has fewer than the max combinations allowed.
               * If it has too many configs, we will only use the most basic of
               * the possible configurations.
               */
        const hasFewCombos = (this.rules[rule].length <= MAX_CONFIG_COMBINATIONS)
  
        if (this.rules[rule][idx] && (hasFewCombos || this.rules[rule][idx].specificity <= 2)) {
          /*
                   * If the rule has too many possible combinations, only take
                   * simple ones, avoiding objects.
                   */
          if (!hasFewCombos && typeof this.rules[rule][idx].config[1] === 'object') {
            return
          }
  
          ruleSets[idx] = ruleSets[idx] || {}
          ruleSets[idx][rule] = this.rules[rule][idx].config
  
          /*
                   * Initialize errorCount to zero, since this is a config which
                   * will be linted.
                   */
          this.rules[rule][idx].errorCount = 0
        }
      }.bind(this)
  
      while (ruleSets.length === idx) {
        ruleIds.forEach(addRuleToRuleSet)
        idx += 1
      }
  
      return ruleSets
    }
  
    /**
       * Remove all items from the registry with a non-zero number of errors
       *
       * Note: this also removes rule configurations which were not linted
       * (meaning, they have an undefined errorCount).
       *
       * @returns {void}
       */
    stripFailingConfigs () {
      const ruleIds = Object.keys(this.rules)
  
      const newRegistry = new Registry()
  
      newRegistry.rules = Object.assign({}, this.rules)
      ruleIds.forEach(ruleId => {
        const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (registryItem.errorCount === 0))
  
        if (errorFreeItems.length > 0) {
          newRegistry.rules[ruleId] = errorFreeItems
        } else {
          delete newRegistry.rules[ruleId]
        }
      })
  
      return newRegistry
    }
  
    /**
       * Removes rule configurations which were not included in a ruleSet
       *
       * @returns {void}
       */
    stripExtraConfigs () {
      const ruleIds = Object.keys(this.rules)
  
      const newRegistry = new Registry()
  
      newRegistry.rules = Object.assign({}, this.rules)
      ruleIds.forEach(ruleId => {
        newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (typeof registryItem.errorCount !== 'undefined'))
      })
  
      return newRegistry
    }
  
    /**
       * Creates a registry of rules which had no error-free configs.
       * The new registry is intended to be analyzed to determine whether its rules
       * should be disabled or set to warning.
       *
       * @returns {Registry}  A registry of failing rules.
       */
    getFailingRulesRegistry () {
      const ruleIds = Object.keys(this.rules)
  
      const failingRegistry = new Registry()
  
      ruleIds.forEach(ruleId => {
        const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0))
  
        if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) {
          failingRegistry.rules[ruleId] = failingConfigs
        }
      })
  
      return failingRegistry
    }
  
    /**
       * Create an eslint config for any rules which only have one configuration
       * in the registry.
       *
       * @returns {Object} An eslint config with rules section populated
       */
    createConfig () {
      const ruleIds = Object.keys(this.rules)
  
      const config = { rules: {} }
  
      ruleIds.forEach(ruleId => {
        if (this.rules[ruleId].length === 1) {
          config.rules[ruleId] = this.rules[ruleId][0].config
        }
      })
  
      return config
    }
  
    /**
       * Return a cloned registry containing only configs with a desired specificity
       *
       * @param   {number} specificity Only keep configs with this specificity
       * @returns {Registry}           A registry of rules
       */
    filterBySpecificity (specificity) {
      const ruleIds = Object.keys(this.rules)
  
      const newRegistry = new Registry()
  
      newRegistry.rules = Object.assign({}, this.rules)
      ruleIds.forEach(ruleId => {
        newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity))
      })
  
      return newRegistry
    }
  
    /**
       * Lint SourceCodes against all configurations in the registry, and record results
       *
       * @param   {Object[]} sourceCodes  SourceCode objects for each filename
       * @param   {Object}   config       ESLint config object
       * @param   {progressCallback} [cb] Optional callback for reporting execution status
       * @returns {Registry}              New registry with errorCount populated
       */
    lintSourceCode (sourceCodes, config, cb) {
      let lintedRegistry = new Registry()
  
      lintedRegistry.rules = Object.assign({}, this.rules)
  
      const ruleSets = lintedRegistry.buildRuleSets()
  
      lintedRegistry = lintedRegistry.stripExtraConfigs()
  
      debug('Linting with all possible rule combinations')
  
      const filenames = Object.keys(sourceCodes)
      const totalFilesLinting = filenames.length * ruleSets.length
  
      filenames.forEach(filename => {
        debug(`Linting file: ${filename}`)
  
        let ruleSetIdx = 0
  
        ruleSets.forEach(ruleSet => {
          const lintConfig = Object.assign({}, config, { rules: ruleSet })
          const lintResults = linter.verify(sourceCodes[filename], lintConfig)
  
          lintResults.forEach(result => {
            /*
                       * It is possible that the error is from a configuration comment
                       * in a linted file, in which case there may not be a config
                       * set in this ruleSetIdx.
                       * (https://github.com/eslint/eslint/issues/5992)
                       * (https://github.com/eslint/eslint/issues/7860)
                       */
            if (
              lintedRegistry.rules[result.ruleId] &&
                          lintedRegistry.rules[result.ruleId][ruleSetIdx]
            ) {
              lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1
            }
          })
  
          ruleSetIdx += 1
  
          if (cb) {
            cb(totalFilesLinting) // eslint-disable-line callback-return
          }
        })
  
        // Deallocate for GC
        sourceCodes[filename] = null
      })
  
      return lintedRegistry
    }
  }
  
  /**
   * Extract rule configuration into eslint:recommended where possible.
   *
   * This will return a new config with `"extends": "eslint:recommended"` and
   * only the rules which have configurations different from the recommended config.
   *
   * @param   {Object} config config object
   * @returns {Object}        config object using `"extends": "eslint:recommended"`
   */
  function extendFromRecommended (config) {
    const newConfig = Object.assign({}, config)
  
    ConfigOps.normalizeToStrings(newConfig)
  
    const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]))
  
    recRules.forEach(ruleId => {
      if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
        delete newConfig.rules[ruleId]
      }
    })
    newConfig.extends = RECOMMENDED_CONFIG_NAME
    return newConfig
  }
  
  // ------------------------------------------------------------------------------
  // Public Interface
  // ------------------------------------------------------------------------------
  
  module.exports = {
    Registry,
    extendFromRecommended
  }
  