Source: bids/validator/sidecarValidator.js

import { BidsHedIssue } from '../types/issues'
import ParsedHedString from '../../parser/parsedHedString'
import { generateIssue, IssueError } from '../../issues/issues'
import { getCharacterCount } from '../../utils/string.js'
import { BidsValidator } from './validator'

/**
 * Validator for HED data in BIDS JSON sidecars.
 */
export class BidsHedSidecarValidator extends BidsValidator {
  /**
   * The BIDS sidecar being validated.
   * @type {BidsSidecar}
   */
  sidecar

  /**
   * Constructor for the BidsHedSidecarValidator.
   *
   * @param {BidsSidecar} sidecar - The BIDS sidecar being validated.
   * @param {Schemas} hedSchemas - The schemas used for the sidecar validation.
   */
  constructor(sidecar, hedSchemas) {
    super(hedSchemas)
    this.sidecar = sidecar
  }

  /**
   * Validate a BIDS JSON sidecar file. Errors and warnings are stored.
   *
   */
  validate() {
    // Allow schema to be set a validation time -- this is checked by the superclass of BIDS file
    const [errorIssues, warningIssues] = this.sidecar.parseHed(this.hedSchemas, false)
    this.errors.push(...BidsHedIssue.fromHedIssues(errorIssues, this.sidecar.file))
    this.warnings.push(...BidsHedIssue.fromHedIssues(warningIssues, this.sidecar.file))
    if (errorIssues.length > 0) {
      return
    }

    this.errors.push(...this._validateStrings(), ...this._validateCurlyBraces())
    if (errorIssues.length > 0) {
      return
    }

    // Columns that aren't splices should have an annotation that stands on its own.
    const [errors, warnings] = this.sidecar.parseHed(this.hedSchemas, true)
    this.errors.push(...BidsHedIssue.fromHedIssues(errors, this.sidecar.file))
    this.warnings.push(...BidsHedIssue.fromHedIssues(warnings, this.sidecar.file))
  }

  /**
   * Validate this sidecar's HED strings.
   *
   * @returns {BidsHedIssue[]} All issues found.
   */
  _validateStrings() {
    const issues = []

    for (const [sidecarKeyName, hedData] of this.sidecar.parsedHedData) {
      if (hedData instanceof ParsedHedString) {
        // Value options have HED as string.
        issues.push(...this._checkDetails(sidecarKeyName, hedData))
      } else if (hedData instanceof Map) {
        // Categorical options have HED as a Map.
        for (const valueString of hedData.values()) {
          issues.push(...this._checkDetails(sidecarKeyName, valueString))
        }
      } else {
        IssueError.generateAndThrowInternalError('Unexpected type found in sidecar parsedHedData map.')
      }
    }
    return issues
  }

  /**
   * Check definitions and placeholders for a string associated with a sidecar key.
   *
   * @param {string} sidecarKeyName - The name of the sidecar key associated with string to be checked.
   * @param {ParsedHedString} hedString - The parsed string to be checked.
   * @returns {BidsHedIssue[]} - Issues associated with the check.
   * @private
   */
  _checkDetails(sidecarKeyName, hedString) {
    const issues = this._checkDefs(sidecarKeyName, hedString, true)
    issues.push(...this._checkPlaceholders(sidecarKeyName, hedString))
    return issues
  }

  /**
   * Validate the Def and Def-expand usage against the sidecar definitions.
   *
   * @param {string} sidecarKeyName - Name of the sidecar key for this HED string
   * @param {ParsedHedString} hedString - The parsed HED string object associated with this key.
   * @param {boolean} placeholdersAllowed - If true, placeholders are allowed here.
   * @returns {BidsHedIssue[]} - Issues encountered such as missing definitions or improper Def-expand values.
   * @private
   */
  _checkDefs(sidecarKeyName, hedString, placeholdersAllowed) {
    let issues = this.sidecar.definitions.validateDefs(hedString, this.hedSchemas, placeholdersAllowed)
    if (issues.length > 0) {
      return BidsHedIssue.fromHedIssues(issues, this.sidecar.file, { sidecarKeyName: sidecarKeyName })
    }
    issues = this.sidecar.definitions.validateDefExpands(hedString, this.hedSchemas, placeholdersAllowed)
    return BidsHedIssue.fromHedIssues(issues, this.sidecar.file, { sidecarKeyName: sidecarKeyName })
  }

  _checkPlaceholders(sidecarKeyName, hedString) {
    const numberPlaceholders = getCharacterCount(hedString.hedString, '#')
    const sidecarKey = this.sidecar.sidecarKeys.get(sidecarKeyName)
    if (!sidecarKey.valueString && !sidecarKey.hasDefinitions && numberPlaceholders > 0) {
      return [
        BidsHedIssue.fromHedIssue(
          generateIssue('invalidSidecarPlaceholder', { column: sidecarKeyName, string: hedString.hedString }),
          this.sidecar.file,
        ),
      ]
    } else if (sidecarKey.valueString && numberPlaceholders === 0) {
      return [
        BidsHedIssue.fromHedIssue(
          generateIssue('missingPlaceholder', { column: sidecarKeyName, string: hedString.hedString }),
          this.sidecar.file,
        ),
      ]
    }
    if (sidecarKey.valueString && numberPlaceholders > 1) {
      return [
        BidsHedIssue.fromHedIssue(
          generateIssue('invalidSidecarPlaceholder', { column: sidecarKeyName, string: hedString.hedString }),
          this.sidecar.file,
        ),
      ]
    }
    return []
  }

  /**
   * Validate this sidecar's curly braces -- checking recursion and missing columns.
   *
   * @returns {BidsHedIssue[]} All issues found.
   */
  _validateCurlyBraces() {
    const issues = []
    const references = this.sidecar.columnSpliceMapping

    for (const [key, referredKeys] of references) {
      for (const referredKey of referredKeys) {
        if (references.has(referredKey)) {
          issues.push(
            BidsHedIssue.fromHedIssue(
              generateIssue('recursiveCurlyBracesWithKey', { column: referredKey, referrer: key }),
              this.sidecar.file,
            ),
          )
        }
        if (!this.sidecar.parsedHedData.has(referredKey) && referredKey !== 'HED') {
          issues.push(
            BidsHedIssue.fromHedIssue(
              generateIssue('undefinedCurlyBraces', { column: referredKey }),
              this.sidecar.file,
            ),
          )
        }
      }
    }

    return issues
  }
}

export default BidsHedSidecarValidator