Shuunen/repo-checker

View on GitHub
src/file.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { bgYellow, black, fillTemplate } from 'shuutils'
import { ProjectData, templatePath } from './constants'
import { log } from './logger'
import { fileExists, getFileSizeInKo, join, messageToCode, readFileInFolder, readableRegex, writeFile } from './utils'

const defaultAmount = 99

// eslint-disable-next-line no-restricted-syntax
export class FileBase {

  public passed: string[] = []

  public warnings: string[] = []

  public failed: string[] = []

  public folderPath = ''

  public data = new ProjectData()

  public canFix = false

  public canForce = false

  public fileContent = ''

  public fileName = ''

  private originalFileContent = ''

  // eslint-disable-next-line @typescript-eslint/max-params
  public constructor (folderPath = '', data: Readonly<ProjectData> = new ProjectData(), canFix = false, canForce = false) {
    this.folderPath = folderPath
    this.data = data
    this.canFix = canFix
    this.canForce = canForce
  }

  // eslint-disable-next-line complexity, sonarjs/cognitive-complexity, @typescript-eslint/max-params
  public shouldContains (name: string, regex?: Readonly<RegExp>, nbMatchExpected = defaultAmount, willJustWarn = false, helpMessage = '', canFix = false): boolean {
    // eslint-disable-next-line security/detect-non-literal-regexp
    const regexp = regex ?? new RegExp(name, 'u')
    const isOk = this.checkContains(regexp, nbMatchExpected)
    const willFix = this.canFix && canFix && !isOk
    let finalName = name
    finalName += isOk || willFix ? '' : ` -- ${helpMessage.length > 0 ? helpMessage : readableRegex(regexp)} ${canFix ? bgYellow(black('[ fixable ]')) : ''}`
    const have = willJustWarn ? 'could have' : 'does not have'
    const message = `${this.fileName} ${isOk ? 'has' : have} ${finalName} `
    if (willFix) log.fix(message)
    else this.test(isOk, message, willJustWarn)
    return isOk
  }

  // eslint-disable-next-line @typescript-eslint/max-params
  public couldContains (name: string, regex?: Readonly<RegExp>, nbMatchExpected = defaultAmount, helpMessage = '', canFix = false): boolean {
    return this.shouldContains(name, regex, nbMatchExpected, true, helpMessage, canFix)
  }

  public checkContains (regex: Readonly<RegExp>, nbMatchExpected = defaultAmount): boolean {
    // eslint-disable-next-line regexp/prefer-regexp-exec
    const matches = this.fileContent.match(regex) ?? []
    const hasExpectedMatches = nbMatchExpected === defaultAmount ? matches.length > 0 : nbMatchExpected === matches.length
    // eslint-disable-next-line @typescript-eslint/no-base-to-string
    if (!hasExpectedMatches) log.debug(regex.toString().replace('\n', ''), `matched ${matches.length} instead of ${nbMatchExpected === defaultAmount ? 'one or more' : nbMatchExpected}`)
    return hasExpectedMatches
  }

  public test (isValid: boolean, message: string, willJustWarn = false): boolean {
    const finalMessage = message.startsWith(this.fileName) ? message : `${this.fileName} ${message}`
    const code = messageToCode(finalMessage)
    if (isValid) { this.passed.push(code); log.test(isValid, finalMessage) }
    else if (willJustWarn) { this.warnings.push(code); log.warn(finalMessage) }
    else { this.failed.push(code); log.error(finalMessage) }
    return isValid
  }

  public couldContainsSchema (url: string): boolean {
    const line = `"$schema": "${url}",`
    const hasSchema = this.couldContains('a $schema declaration', /"\$schema": "/u, 1, `like ${line}`, true)
    if (hasSchema) return true
    if (!this.canFix) return hasSchema
    // eslint-disable-next-line prefer-named-capture-group, regexp/prefer-named-capture-group
    this.fileContent = this.fileContent.replace(/(^\{\n)(\s+)/u, `$1$2${line}\n$2`)
    return true
  }

  public async end (): Promise<void> {
    await this.updateFile()
    await this.checkIssues()
  }

  public async inspectFile (fileName: string): Promise<void> {
    this.fileName = fileName
    this.originalFileContent = await readFileInFolder(this.folderPath, fileName).catch(() => '')
    this.fileContent = this.originalFileContent
  }

  // eslint-disable-next-line max-statements
  public async updateFile (): Promise<void> {
    if (this.originalFileContent.trim() === this.fileContent.trim()) { log.debug('avoid file update when updated content is the same'); return }
    if (!this.canFix) { log.debug('cant update file if fix not active'); return }
    if (this.failed.length > 0 && !this.canForce) { log.debug('cant update file without force if some checks failed'); return }
    if (this.fileName.length === 0) { log.debug('cant update file without a file name, probably running tests'); return }
    await writeFile(join(this.folderPath, this.fileName), this.fileContent) // if you don't await, the file is updated after the end of the process and tests are failing
    log.debug('updated', this.fileName, 'with the new content')
  }

  public async fileExists (fileName: string): Promise<boolean> {
    return await fileExists(join(this.folderPath, fileName))
  }

  /**
   * Check if a file exists, create a file if fix enabled, trigger a success or fail
   * @param fileName the file name to check
   * @param willJustWarn just warn if the file is not found
   * @returns true if the file is found
   */
  public async checkFileExists (fileName: string, willJustWarn = false): Promise<boolean> {
    let hasFile = await this.fileExists(fileName)
    if (!hasFile && this.canFix) {
      const fileContent = await this.initFile(fileName)
      hasFile = fileContent.length > 0
    }
    this.test(hasFile, `has a ${fileName} file`, willJustWarn)
    return hasFile
  }

  public async checkNoFileExists (fileName: string): Promise<void> {
    const hasFile = await this.fileExists(fileName)
    this.test(!hasFile, `has no ${fileName} file`)
  }

  public async getFileSizeInKo (filePath: string): Promise<number> {
    return await getFileSizeInKo(join(this.folderPath, filePath))
  }

  public async initFile (fileName: string): Promise<string> {
    const template = await readFileInFolder(templatePath, fileName).catch(() => '')
    if (template === '') {
      log.debug(`found no template ${fileName}, using a empty string instead`)
      return ''
    }
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const fileContent = fillTemplate(template, this.data as unknown as Record<string, unknown>)
    if (fileContent.includes('{{')) log.warn(`please provide a data file to be able to fix a "${fileName}" file`)
    else void this.createFile(fileName, fileContent)
    return fileContent
  }

  // eslint-disable-next-line @typescript-eslint/require-await
  private async createFile (fileName: string, fileContent: string): Promise<void> {
    void writeFile(join(this.folderPath, fileName), fileContent)
    log.fix('created', fileName)
  }

  private async checkIssues (): Promise<void> {
    if (this.failed.length > 0 && this.canFix) {
      if (this.canForce) {
        await this.initFile(this.fileName)
        return
      }
      log.info('this file has at least one issue, if you want repo-checker to overwrite this file use --force')
    }
  }
}