Shuunen/repo-checker

View on GitHub
src/files/eslint.file.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { clone, parseJson } from 'shuutils'
import { repoCheckerPath } from '../constants'
import { FileBase } from '../file'
import { log } from '../logger'
import { objectToJson, readFileInFolder } from '../utils'
import { recommendedVueRules, specificRepoCheckerRules, type EslintConfigRules, type EslintRcJsonFile } from './eslint.model'

// eslint-disable-next-line no-restricted-syntax
export class EsLintFile extends FileBase {
  private checkExtends() {
    const hasHardcore = this.couldContains('hardcore rules extend', /hardcore/u)
    if (!hasHardcore) this.couldContains('eslint recommended rules extend', /"eslint:recommended"/u)
    this.couldContains('unicorn rules extend', /plugin:unicorn\/all/u)
    this.shouldContains('no promise plugin (require eslint 7)', /plugin:promise\/recommended|"promise"/u, 0)
  }

  private findRules(config: EslintRcJsonFile) {
    if (config.rules && Object.keys(config.rules).length > 0) {
      log.debug(`found ${Object.keys(config.rules).length} root/global rules`)
      return config.rules
    }
    const override = config.overrides?.find(anOverride => anOverride.files.find(file => file.endsWith('.ts')))
    if (override && Object.keys(override.rules).length > 0) {
      log.debug(`found ${Object.keys(override.rules).length} override rules`)
      return override.rules
    }
    log.error('failed to find rules in eslint config')
    return {} as EslintConfigRules // eslint-disable-line @typescript-eslint/consistent-type-assertions
  }

  private injectRules(input: EslintRcJsonFile, rules: EslintConfigRules) {
    const output = clone(input)
    if (output.rules && Object.keys(output.rules).length > 0) {
      Object.assign(output.rules, rules)
      return output
    }
    const override = output.overrides?.find(anOverride => anOverride.files.find(file => file.endsWith('.ts')))
    if (override && Object.keys(override.rules).length > 0) {
      Object.assign(override.rules, rules)
      return output
    }
    log.error('failed to inject rules in eslint config')
    return output
  }

  private checkTsVue() {
    this.shouldContains('hardcore vue rules extend', /hardcore\/vue/u)
  }

  private checkTs() {
    // check here ts & vue ts projects
    this.couldContains('hardcore typescript rules extend', /hardcore\/ts/u)
    this.couldContains('a disabled explicit function return type', /"@typescript-eslint\/explicit-function-return-type": "error"/u, 0)
    if (this.data.isUsingVue) this.checkTsVue()
    // check here ts only projects
  }

  private getMissingRulesFrom(expectedRules: EslintConfigRules, rules: EslintConfigRules) {
    return Object.keys(expectedRules).filter(rule => {
      /* c8 ignore next */
      if (specificRepoCheckerRules.has(rule)) return false
      if (rule.startsWith('@typescript') && !this.data.isUsingTypescript) return false
      return !(rule in rules)
    })
  }

  private updateFileContent(input: EslintRcJsonFile, expectedRules: EslintConfigRules) {
    const fixedContent = this.injectRules(input, expectedRules)
    /* c8 ignore next 2 */
    // biome-ignore lint/performance/noDelete: check if it's worth it
    if (fixedContent.overrides?.length === 0) delete fixedContent.overrides
    this.fileContent = objectToJson(fixedContent)
  }

  private reportMissingRules(expectedRules: EslintConfigRules, missingRules: readonly string[]) {
    const total = Object.keys(expectedRules).length
    const isOk = this.test(missingRules.length === 0, `current .eslintrc.json has only ${total - missingRules.length} of the ${total} custom rules in repo-checker .eslintrc.json`, true)
    if (!isOk) log.warn('missing rules :', missingRules.map(rule => `"${rule}": ${JSON.stringify(expectedRules[rule])}`).join(', '))
  }

  private async getExpectedRules() {
    const expectedJsonString = await readFileInFolder(repoCheckerPath, '.eslintrc.json')
    const data = parseJson<EslintRcJsonFile>(expectedJsonString)
    let expectedContent = clone(data.value)
    if (this.data.isUsingVue) expectedContent = this.injectRules(expectedContent, recommendedVueRules)
    const expectedRules = this.findRules(expectedContent)
    log.debug('found', Object.keys(expectedRules).length, 'expected rules')
    return expectedRules
  }

  private async getRules(input: object) {
    const content = clone(input)
    const rules = this.findRules(content)
    const expectedRules = await this.getExpectedRules()
    const missingRules = this.getMissingRulesFrom(expectedRules, rules)
    log.debug('found', missingRules.length, 'missing rules')
    return { expectedRules, missingRules }
  }

  public async start() {
    await this.checkNoFileExists('xo.config.js')
    await this.checkEslint()
  }

  private async checkEslint() {
    const filename = '.eslintrc.json'
    const hasFile = await this.fileExists(filename)
    if (!hasFile) {
      log.debug('skipping eslintrc checks')
      return
    }
    await this.inspectFile(filename)
    this.checkExtends()
    await this.checkRules()
    if (this.data.isUsingTailwind) this.shouldContains('tailwind rules extend', /plugin:tailwindcss\/recommended/u)
    if (this.data.isUsingTypescript) this.checkTs()
  }

  private async checkRules() {
    const data = parseJson<EslintRcJsonFile>(this.fileContent)
    if (data.error) {
      log.warn('cannot check empty or invalid .eslintrc.json file')
      return
    }
    const { expectedRules, missingRules } = await this.getRules(data.value)
    if (missingRules.length === 0) return
    if (this.canFix) {
      this.updateFileContent(data.value, expectedRules)
      return
    }
    this.reportMissingRules(expectedRules, missingRules)
  }
}