diff --git a/bids-validator/src/schema/applyRules.test.ts b/bids-validator/src/schema/applyRules.test.ts index 8f9d2ff6b..f5d6e4d3d 100644 --- a/bids-validator/src/schema/applyRules.test.ts +++ b/bids-validator/src/schema/applyRules.test.ts @@ -101,7 +101,7 @@ Deno.test('evalCheck ensure expression language functions work', () => { const context = { x: [1, 2, 3, 4], y: [1, 1, 1, 1], - issues: new DatasetIssues(), + dataset: { issues: new DatasetIssues() }, } const rule = [ { @@ -117,12 +117,12 @@ Deno.test('evalCheck ensure expression language functions work', () => { }, ] applyRules(rule, context) - assert(!context.issues.hasIssue({ key: 'CHECK_ERROR' })) + assert(!context.dataset.issues.hasIssue({ key: 'CHECK_ERROR' })) }) Deno.test( 'evalCheck ensure expression language will fail appropriately', () => { - const context = { issues: new DatasetIssues() } + const context = { dataset: { issues: new DatasetIssues() } } const rule = [ { selectors: ['true'], @@ -130,7 +130,7 @@ Deno.test( }, ] applyRules(rule, context) - assert(context.issues.hasIssue({ key: 'CHECK_ERROR' })) + assert(context.dataset.issues.hasIssue({ key: 'CHECK_ERROR' })) }, ) @@ -146,11 +146,11 @@ Deno.test('evalColumns tests', async (t) => { filename: ['func/sub-01_task-rest_bold.nii.gz'], acq_time: ['1900-01-01T00:00:78'], }, - issues: new DatasetIssues(), + dataset: { issues: new DatasetIssues() }, } const rule = schemaDefs.rules.tabular_data.modality_agnostic.Scans evalColumns(rule, context, schema, 'rules.tabular_data.modality_agnostic.Scans') - assert(context.issues.hasIssue({ key: 'TSV_VALUE_INCORRECT_TYPE_NONREQUIRED' })) + assert(context.dataset.issues.hasIssue({ key: 'TSV_VALUE_INCORRECT_TYPE_NONREQUIRED' })) }) await t.step('check formatless column', () => { @@ -161,11 +161,11 @@ Deno.test('evalColumns tests', async (t) => { columns: { onset: ['1', '2', 'not a number'], }, - issues: new DatasetIssues(), + dataset: { issues: new DatasetIssues() }, } const rule = schemaDefs.rules.tabular_data.made_up.MadeUp evalColumns(rule, context, schema, 'rules.tabular_data.made_up.MadeUp') - assert(context.issues.hasIssue({ key: 'TSV_VALUE_INCORRECT_TYPE' })) + assert(context.dataset.issues.hasIssue({ key: 'TSV_VALUE_INCORRECT_TYPE' })) }) await t.step('verify n/a is allowed', () => { @@ -177,11 +177,11 @@ Deno.test('evalColumns tests', async (t) => { onset: ['1', '2', 'n/a'], strain_rrid: ['RRID:SCR_012345', 'RRID:SCR_012345', 'n/a'], }, - issues: new DatasetIssues(), + dataset: { issues: new DatasetIssues() }, } const rule = schemaDefs.rules.tabular_data.made_up.MadeUp evalColumns(rule, context, schema, 'rules.tabular_data.made_up.MadeUp') - assert(context.issues.size === 0) + assert(context.dataset.issues.size === 0) }) }) diff --git a/bids-validator/src/schema/applyRules.ts b/bids-validator/src/schema/applyRules.ts index b5394ce39..39a97f65a 100644 --- a/bids-validator/src/schema/applyRules.ts +++ b/bids-validator/src/schema/applyRules.ts @@ -135,14 +135,14 @@ function evalRuleChecks( ): boolean { if (rule.checks && !mapEvalCheck(rule.checks, context)) { if (rule.issue?.code && rule.issue?.message) { - context.issues.add({ + context.dataset.issues.add({ key: rule.issue.code, reason: rule.issue.message, files: [{ ...context.file, evidence: schemaPath }], severity: rule.issue.level as Severity, }) } else { - context.issues.addNonSchemaIssue('CHECK_ERROR', [ + context.dataset.issues.addNonSchemaIssue('CHECK_ERROR', [ { ...context.file, evidence: schemaPath }, ]) } @@ -241,7 +241,7 @@ export function evalColumns( let errorObject = columnObject if (!headers.includes(name) && requirement === 'required') { - context.issues.addNonSchemaIssue('TSV_COLUMN_MISSING', [ + context.dataset.issues.addNonSchemaIssue('TSV_COLUMN_MISSING', [ { ...context.file, evidence: `Column with header ${name} listed as required. ${schemaPath}`, @@ -267,7 +267,7 @@ export function evalColumns( typeCheck = (value) => sidecarDefinedTypeCheck(context.sidecar[name], value, schema) errorObject = context.sidecar[name] } else { - context.issues.addNonSchemaIssue('TSV_COLUMN_TYPE_REDEFINED', [{ + context.dataset.issues.addNonSchemaIssue('TSV_COLUMN_TYPE_REDEFINED', [{ ...context.file, evidence: `'${name}' redefined with sidecar ${inspect(context.sidecar[name])}`, }]) @@ -282,7 +282,7 @@ export function evalColumns( if ( !typeCheck(value) ) { - context.issues.addNonSchemaIssue(error_code, [ + context.dataset.issues.addNonSchemaIssue(error_code, [ { ...context.file, evidence: `'${value}' ${inspect(columnObject)}`, @@ -317,13 +317,13 @@ function evalInitialColumns( if (contextIndex === -1) { const evidence = `Column with header ${ruleHeaderName} not found, indexed from 0 it should appear in column ${ruleIndex}. ${schemaPath}` - context.issues.addNonSchemaIssue('TSV_COLUMN_MISSING', [ + context.dataset.issues.addNonSchemaIssue('TSV_COLUMN_MISSING', [ { ...context.file, evidence: evidence }, ]) } else if (ruleIndex !== contextIndex) { const evidence = `Column with header ${ruleHeaderName} found at index ${contextIndex} while rule specifies, indexed from 0, it should be in column ${ruleIndex}. ${schemaPath}` - context.issues.addNonSchemaIssue('TSV_COLUMN_ORDER_INCORRECT', [ + context.dataset.issues.addNonSchemaIssue('TSV_COLUMN_ORDER_INCORRECT', [ { ...context.file, evidence: evidence }, ]) } @@ -351,7 +351,7 @@ function evalAdditionalColumns( extraCols = extraCols.filter((header) => !(header in context.sidecar)) } if (extraCols.length) { - context.issues.addNonSchemaIssue('TSV_ADDITIONAL_COLUMNS_NOT_ALLOWED', [ + context.dataset.issues.addNonSchemaIssue('TSV_ADDITIONAL_COLUMNS_NOT_ALLOWED', [ { ...context.file, evidence: `Disallowed columns found ${extraCols}` }, ]) } @@ -380,7 +380,7 @@ function evalIndexColumns( }) const missing = index_columns.filter((col: string) => !headers.includes(col)) if (missing.length) { - context.issues.addNonSchemaIssue('TSV_COLUMN_MISSING', [ + context.dataset.issues.addNonSchemaIssue('TSV_COLUMN_MISSING', [ { ...context.file, evidence: `Columns cited as index columns not in file: ${missing}. ${schemaPath}`, @@ -397,7 +397,7 @@ function evalIndexColumns( ) }) if (uniqueIndexValues.has(indexValue)) { - context.issues.addNonSchemaIssue('TSV_INDEX_VALUE_NOT_UNIQUE', [ + context.dataset.issues.addNonSchemaIssue('TSV_INDEX_VALUE_NOT_UNIQUE', [ { ...context.file, evidence: `Row: ${i + 2}, Value: ${indexValue}` }, ]) } else { @@ -431,14 +431,14 @@ function evalJsonCheck( const keyName: string = metadataDef.name if (severity && severity !== 'ignore' && !(keyName in json)) { if (requirement.issue?.code && requirement.issue?.message) { - context.issues.add({ + context.dataset.issues.add({ key: requirement.issue.code, reason: requirement.issue.message, severity, files: [{ ...context.file }], }) } else if (severity === 'error') { - context.issues.addNonSchemaIssue( + context.dataset.issues.addNonSchemaIssue( sidecarRule ? 'SIDECAR_KEY_REQUIRED' : 'JSON_KEY_REQUIRED', [ { @@ -448,7 +448,7 @@ function evalJsonCheck( ], ) } else if (severity === 'warning') { - context.issues.addNonSchemaIssue( + context.dataset.issues.addNonSchemaIssue( sidecarRule ? 'SIDECAR_KEY_RECOMMENDED' : 'JSON_KEY_RECOMMENDED', [ { @@ -485,7 +485,7 @@ function evalJsonCheck( if (result === false) { const evidenceBase = `Failed for this file.key: ${originFileKey} Schema path: ${schemaPath}` if (!validate.errors) { - context.issues.addNonSchemaIssue('JSON_SCHEMA_VALIDATION_ERROR', [ + context.dataset.issues.addNonSchemaIssue('JSON_SCHEMA_VALIDATION_ERROR', [ { ...context.file, evidence: evidenceBase, @@ -494,7 +494,7 @@ function evalJsonCheck( } else { for (let error of validate.errors) { const message = 'message' in error ? `message: ${error['message']}` : '' - context.issues.addNonSchemaIssue('JSON_SCHEMA_VALIDATION_ERROR', [ + context.dataset.issues.addNonSchemaIssue('JSON_SCHEMA_VALIDATION_ERROR', [ { ...context.file, evidence: `${evidenceBase} ${message}`, diff --git a/bids-validator/src/schema/context.test.ts b/bids-validator/src/schema/context.test.ts index 280f678d1..40e102c58 100644 --- a/bids-validator/src/schema/context.test.ts +++ b/bids-validator/src/schema/context.test.ts @@ -4,7 +4,7 @@ import { BIDSContext } from './context.ts' import { dataFile, rootFileTree } from './fixtures.test.ts' Deno.test('test context LoadSidecar', async (t) => { - const context = new BIDSContext(rootFileTree, dataFile, new DatasetIssues()) + const context = new BIDSContext(dataFile) await context.loadSidecar() await t.step('sidecar overwrites correct fields', () => { const { rootOverwrite, subOverwrite } = context.sidecar @@ -24,7 +24,7 @@ Deno.test('test context LoadSidecar', async (t) => { }) Deno.test('test context loadSubjects', async (t) => { - const context = new BIDSContext(rootFileTree, dataFile, new DatasetIssues()) + const context = new BIDSContext(dataFile, undefined, rootFileTree) await context.loadSubjects() await t.step('context produces correct subjects object', () => { assert(context.dataset.subjects, 'subjects object exists') diff --git a/bids-validator/src/schema/context.ts b/bids-validator/src/schema/context.ts index 4893686e1..34366f6de 100644 --- a/bids-validator/src/schema/context.ts +++ b/bids-validator/src/schema/context.ts @@ -6,6 +6,7 @@ import { ContextNiftiHeader, ContextSubject, } from '../types/context.ts' +import { Schema } from '../types/schema.ts' import { BIDSFile, FileTree } from '../types/filetree.ts' import { ColumnsMap } from '../types/columns.ts' import { readEntities } from './entities.ts' @@ -19,32 +20,43 @@ import { ValidatorOptions } from '../setup/options.ts' import { logger } from '../utils/logger.ts' export class BIDSContextDataset implements ContextDataset { - dataset_description: Record - options?: ValidatorOptions - files: any[] - tree: object - ignored: any[] - modalities: any[] + #dataset_description: Record = {} + tree: FileTree + ignored: BIDSFile[] + datatypes: string[] + modalities: string[] subjects?: ContextDatasetSubjects + + issues: DatasetIssues sidecarKeyValidated: Set + options?: ValidatorOptions + schema: Schema - constructor(options?: ValidatorOptions, description = {}) { - this.dataset_description = description - this.files = [] - this.tree = {} - this.ignored = [] - this.modalities = [] + constructor( + args: Partial + ) { + this.schema = args.schema || {} as unknown as Schema + this.dataset_description = args.dataset_description || {} + this.tree = args.tree || new FileTree('/unknown', 'unknown') + this.ignored = args.ignored || [] + this.datatypes = args.datatypes || [] + this.modalities = args.modalities || [] this.sidecarKeyValidated = new Set() - if (options) { - this.options = options + if (args.options) { + this.options = args.options } - if ( - !this.dataset_description.DatasetType && - this.dataset_description.GeneratedBy - ) { - this.dataset_description.DatasetType = 'derivative' - } else if (!this.dataset_description.DatasetType) { - this.dataset_description.DatasetType = 'raw' + this.issues = args.issues || new DatasetIssues() + } + + get dataset_description(): Record { + return this.#dataset_description + } + set dataset_description(value: Record) { + this.#dataset_description = value + if (!this.dataset_description.DatasetType) { + this.dataset_description.DatasetType = this.dataset_description.GeneratedBy + ? 'derivative' + : 'raw' } } } @@ -65,43 +77,41 @@ export class BIDSContextDatasetSubjects implements ContextDatasetSubjects { } } -const defaultDsContext = new BIDSContextDataset() - export class BIDSContext implements Context { - // Internal representation of the file tree - fileTree: FileTree - filenameRules: string[] - issues: DatasetIssues - file: BIDSFile - suffix: string - extension: string - entities: Record - dataset: ContextDataset + dataset: BIDSContextDataset subject: ContextSubject + // path: string <- getter + // size: number <- getter + entities: Record datatype: string + suffix: string + extension: string modality: string sidecar: Record - sidecarKeyOrigin: Record - json: object - columns: ColumnsMap associations: ContextAssociations + columns: ColumnsMap + json: object + gzip?: object nifti_header?: ContextNiftiHeader + ome?: object + tiff?: object + + file: BIDSFile + filenameRules: string[] + sidecarKeyOrigin: Record constructor( - fileTree: FileTree, file: BIDSFile, - issues: DatasetIssues, dsContext?: BIDSContextDataset, + fileTree?: FileTree, ) { - this.fileTree = fileTree this.filenameRules = [] - this.issues = issues this.file = file const bidsEntities = readEntities(file.name) this.suffix = bidsEntities.suffix this.extension = bidsEntities.extension this.entities = bidsEntities.entities - this.dataset = dsContext ? dsContext : defaultDsContext + this.dataset = dsContext ? dsContext : new BIDSContextDataset({tree: fileTree}) this.subject = {} as ContextSubject this.datatype = '' this.modality = '' @@ -112,6 +122,10 @@ export class BIDSContext implements Context { this.associations = {} as ContextAssociations } + get schema(): Schema { + return this.dataset.schema + } + get size(): number { return this.file.size } @@ -126,7 +140,7 @@ export class BIDSContext implements Context { * In the browser, this is always at the root */ get datasetPath(): string { - return this.fileTree.path + return this.dataset.tree.path } /** @@ -141,7 +155,7 @@ export class BIDSContext implements Context { const sidecars = walkBack(this.file) for (const file of sidecars) { const json = await loadJSON(file).catch((error) => { - this.issues.addNonSchemaIssue(error.key, [file]) + this.dataset.issues.addNonSchemaIssue(error.key, [file]) return {} }) this.sidecar = { ...json, ...this.sidecar } @@ -155,7 +169,7 @@ export class BIDSContext implements Context { ) return this.nifti_header = await loadHeader(this.file).catch((error) => { - this.issues.addNonSchemaIssue(error.key, [this.file]) + this.dataset.issues.addNonSchemaIssue(error.key, [this.file]) return undefined }) } @@ -168,7 +182,7 @@ export class BIDSContext implements Context { this.columns = await loadTSV(this.file) .catch((error) => { if (error.key) { - this.issues.addNonSchemaIssue(error.key, [this.file]) + this.dataset.issues.addNonSchemaIssue(error.key, [this.file]) } logger.warning( `tsv file could not be opened by loadColumns '${this.file.path}'`, @@ -180,7 +194,7 @@ export class BIDSContext implements Context { } async loadAssociations(): Promise { - this.associations = await buildAssociations(this.file, this.issues) + this.associations = await buildAssociations(this.file, this.dataset.issues) return } @@ -189,7 +203,7 @@ export class BIDSContext implements Context { return } this.json = await loadJSON(this.file).catch((error) => { - this.issues.addNonSchemaIssue(error.key, [this.file]) + this.dataset.issues.addNonSchemaIssue(error.key, [this.file]) return {} }) } @@ -201,12 +215,12 @@ export class BIDSContext implements Context { } this.dataset.subjects = new BIDSContextDatasetSubjects() // Load subject dirs from the file tree - this.dataset.subjects.sub_dirs = this.fileTree.directories + this.dataset.subjects.sub_dirs = this.dataset.tree.directories .filter((dir) => dir.name.startsWith('sub-')) .map((dir) => dir.name) // Load participants from participants.tsv - const participants_tsv = this.fileTree.files.find( + const participants_tsv = this.dataset.tree.files.find( (file) => file.name === 'participants.tsv', ) if (participants_tsv) { @@ -217,7 +231,7 @@ export class BIDSContext implements Context { } // Load phenotype from phenotype/*.tsv - const phenotype_dir = this.fileTree.directories.find( + const phenotype_dir = this.dataset.tree.directories.find( (dir) => dir.name === 'phenotype', ) if (phenotype_dir) { diff --git a/bids-validator/src/schema/expressionLanguage.test.ts b/bids-validator/src/schema/expressionLanguage.test.ts index 91ed78215..35bfb7da4 100644 --- a/bids-validator/src/schema/expressionLanguage.test.ts +++ b/bids-validator/src/schema/expressionLanguage.test.ts @@ -5,7 +5,7 @@ import { BIDSContext } from './context.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' Deno.test('test expression functions', async (t) => { - const context = new BIDSContext(rootFileTree, dataFile, new DatasetIssues()) + const context = new BIDSContext(dataFile, undefined, rootFileTree) await t.step('index function', () => { const index = expressionFunctions.index diff --git a/bids-validator/src/schema/expressionLanguage.ts b/bids-validator/src/schema/expressionLanguage.ts index a501ef4d5..58c07c9c8 100644 --- a/bids-validator/src/schema/expressionLanguage.ts +++ b/bids-validator/src/schema/expressionLanguage.ts @@ -6,7 +6,7 @@ function exists(this: BIDSContext, list: string[], rule: string = 'dataset'): nu } const prefix: string[] = [] - const fileTree = rule == 'file' ? this.file.parent : this.fileTree + const fileTree = rule == 'file' ? this.file.parent : this.dataset.tree // Stimuli and subject-relative paths get prefixes if (rule == 'stimuli') { diff --git a/bids-validator/src/schema/walk.test.ts b/bids-validator/src/schema/walk.test.ts index eaabc303d..eb4afc9cd 100644 --- a/bids-validator/src/schema/walk.test.ts +++ b/bids-validator/src/schema/walk.test.ts @@ -1,13 +1,13 @@ import { assert, assertEquals } from '../deps/asserts.ts' -import { BIDSContext } from './context.ts' +import { BIDSContext, BIDSContextDataset } from './context.ts' import { walkFileTree } from './walk.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { simpleDataset, simpleDatasetFileCount } from '../tests/simple-dataset.ts' Deno.test('file tree walking', async (t) => { await t.step('visits each file and creates a BIDSContext', async () => { - const issues = new DatasetIssues() - for await (const context of walkFileTree(simpleDataset, issues)) { + const dsContext = new BIDSContextDataset({tree: simpleDataset}) + for await (const context of walkFileTree(dsContext)) { assert( context instanceof BIDSContext, 'walk file tree did not return a BIDSContext', @@ -15,9 +15,9 @@ Deno.test('file tree walking', async (t) => { } }) await t.step('visits every file expected', async () => { - const issues = new DatasetIssues() + const dsContext = new BIDSContextDataset({tree: simpleDataset}) let accumulator = 0 - for await (const context of walkFileTree(simpleDataset, issues)) { + for await (const context of walkFileTree(dsContext)) { assert( context instanceof BIDSContext, 'walk file tree did not return a BIDSContext', diff --git a/bids-validator/src/schema/walk.ts b/bids-validator/src/schema/walk.ts index 7213a4fe1..caebf99d1 100644 --- a/bids-validator/src/schema/walk.ts +++ b/bids-validator/src/schema/walk.ts @@ -6,24 +6,20 @@ import { loadTSV } from '../files/tsv.ts' /** Recursive algorithm for visiting each file in the dataset, creating a context */ export async function* _walkFileTree( fileTree: FileTree, - root: FileTree, - issues: DatasetIssues, - dsContext?: BIDSContextDataset, + dsContext: BIDSContextDataset, ): AsyncIterable { for (const file of fileTree.files) { - yield new BIDSContext(root, file, issues, dsContext) + yield new BIDSContext(file, dsContext) } for (const dir of fileTree.directories) { - yield* _walkFileTree(dir, root, issues, dsContext) + yield* _walkFileTree(dir, dsContext) } loadTSV.cache.delete(fileTree.path) } /** Walk all files in the dataset and construct a context for each one */ export async function* walkFileTree( - fileTree: FileTree, - issues: DatasetIssues, - dsContext?: BIDSContextDataset, + dsContext: BIDSContextDataset, ): AsyncIterable { - yield* _walkFileTree(fileTree, fileTree, issues, dsContext) + yield* _walkFileTree(dsContext.tree, dsContext) } diff --git a/bids-validator/src/tests/local/hed-integration.test.ts b/bids-validator/src/tests/local/hed-integration.test.ts index 1bdb1c36d..a07f76c93 100644 --- a/bids-validator/src/tests/local/hed-integration.test.ts +++ b/bids-validator/src/tests/local/hed-integration.test.ts @@ -30,16 +30,17 @@ Deno.test('hed-validator not triggered', async (t) => { const PATH = 'tests/data/bids-examples/ds003' const tree = await readFileTree(PATH) const schema = await loadSchema() - const issues = new DatasetIssues() - const dsContext = new BIDSContextDataset(undefined, { 'HEDVersion': ['bad_version'] }) + const dsContext = new BIDSContextDataset({dataset_description: { + 'HEDVersion': ['bad_version'], + }}) await t.step('detect hed returns false', async () => { const eventFile = getFile(tree, 'sub-01/func/sub-01_task-rhymejudgment_events.tsv') assert(eventFile !== undefined) assert(eventFile instanceof BIDSFileDeno) - const context = new BIDSContext(tree, eventFile, issues, dsContext) + const context = new BIDSContext(eventFile, dsContext) await context.asyncLoads() await hedValidate(schema as unknown as GenericSchema, context) - assert(issues.size === 0) + assert(context.dataset.issues.size === 0) }) }) @@ -47,16 +48,17 @@ Deno.test('hed-validator fails with bad schema version', async (t) => { const PATH = 'tests/data/bids-examples/eeg_ds003645s_hed_library' const tree = await readFileTree(PATH) const schema = await loadSchema() - const issues = new DatasetIssues() - const dsContext = new BIDSContextDataset(undefined, { 'HEDVersion': ['bad_version'] }) + const dsContext = new BIDSContextDataset({dataset_description: { + 'HEDVersion': ['bad_version'], + }}) await t.step('detect hed returns false', async () => { const eventFile = getFile(tree, 'sub-002/eeg/sub-002_task-FacePerception_run-3_events.tsv') assert(eventFile !== undefined) assert(eventFile instanceof BIDSFileDeno) - const context = new BIDSContext(tree, eventFile, issues, dsContext) + const context = new BIDSContext(eventFile, dsContext) await context.asyncLoads() await hedValidate(schema as unknown as GenericSchema, context) - assert(issues.size === 1) - assert(issues.has('HED_ERROR')) + assert(context.dataset.issues.size === 1) + assert(context.dataset.issues.has('HED_ERROR')) }) }) diff --git a/bids-validator/src/tests/schema-expression-language.test.ts b/bids-validator/src/tests/schema-expression-language.test.ts index d820c2c53..e2a933331 100644 --- a/bids-validator/src/tests/schema-expression-language.test.ts +++ b/bids-validator/src/tests/schema-expression-language.test.ts @@ -21,8 +21,10 @@ Deno.test('validate schema expression tests', async (t) => { const header = ['expression', 'desired', 'actual', 'result'].map((x) => colors.magenta(x)) for (const test of schema.meta.expression_tests) { await t.step(`${test.expression} evals to ${test.result}`, () => { + const context = { file: { parent: null }, dataset: { tree: null } } as unknown as BIDSContext + Object.assign(context, expressionFunctions) // @ts-expect-error - const context = expressionFunctions as BIDSContext + context.exists.bind(context) const actual_result = evalCheck(test.expression, context) if (equal(actual_result, test.result)) { results.push([ diff --git a/bids-validator/src/types/check.ts b/bids-validator/src/types/check.ts index 081d7e966..7e9a96fbb 100644 --- a/bids-validator/src/types/check.ts +++ b/bids-validator/src/types/check.ts @@ -18,5 +18,4 @@ export type RuleCheckFunction = ( export type DSCheckFunction = ( schema: GenericSchema, dsContext: BIDSContextDataset, - issues: DatasetIssues, ) => Promise diff --git a/bids-validator/src/types/context.ts b/bids-validator/src/types/context.ts index 16f6f89b4..6c4d79910 100644 --- a/bids-validator/src/types/context.ts +++ b/bids-validator/src/types/context.ts @@ -1,4 +1,6 @@ +import { Schema } from './schema.ts' import { ValidatorOptions } from '../setup/options.ts' +import { FileTree } from '../types/filetree.ts' export interface ContextDatasetSubjects { sub_dirs: string[] @@ -8,10 +10,10 @@ export interface ContextDatasetSubjects { export interface ContextDataset { dataset_description: Record - files: any[] - tree: object + tree: FileTree ignored: any[] - modalities: any[] + datatypes: string[] + modalities: string[] subjects?: ContextDatasetSubjects options?: ValidatorOptions sidecarKeyValidated: Set @@ -93,6 +95,7 @@ export interface ContextNiftiHeader { sform_code: number } export interface Context { + schema: Schema dataset: ContextDataset subject: ContextSubject path: string diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index b62de3e4f..74a4e3f2b 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -37,7 +37,6 @@ export async function validate( fileTree: FileTree, options: ValidatorOptions, ): Promise { - const issues = new DatasetIssues() const summary = new Summary() const schema = await loadSchema(options.schema) summary.schemaVersion = schema.schema_version @@ -49,17 +48,15 @@ export async function validate( (file: BIDSFile) => file.name === 'dataset_description.json', ) - let dsContext + const dsContext = new BIDSContextDataset({options, schema, tree: fileTree}) if (ddFile) { - const description = await loadJSON(ddFile).catch((error) => { - issues.addNonSchemaIssue(error.key, [ddFile]) + dsContext.dataset_description = await loadJSON(ddFile).catch((error) => { + dsContext.issues.addNonSchemaIssue(error.key, [ddFile]) return {} as Record }) - summary.dataProcessed = description.DatasetType === 'derivative' - dsContext = new BIDSContextDataset(options, description) + summary.dataProcessed = dsContext.dataset_description.DatasetType === 'derivative' } else { - dsContext = new BIDSContextDataset(options) - issues.addNonSchemaIssue('MISSING_DATASET_DESCRIPTION', [] as IssueFile[]) + dsContext.issues.addNonSchemaIssue('MISSING_DATASET_DESCRIPTION', [] as IssueFile[]) } const bidsDerivatives: FileTree[] = [] @@ -85,7 +82,7 @@ export async function validate( return false }) - for await (const context of walkFileTree(fileTree, issues, dsContext)) { + for await (const context of walkFileTree(dsContext)) { // TODO - Skip ignored files for now (some tests may reference ignored files) if (context.file.ignored) { continue @@ -104,7 +101,7 @@ export async function validate( await summary.update(context) } for (const check of perDSChecks) { - await check(schema as unknown as GenericSchema, dsContext, issues) + await check(schema as unknown as GenericSchema, dsContext) } let derivativesSummary: Record = {} @@ -116,7 +113,7 @@ export async function validate( ) let output: ValidationResult = { - issues, + issues: dsContext.issues, summary: summary.formatOutput(), } diff --git a/bids-validator/src/validators/filenameIdentify.test.ts b/bids-validator/src/validators/filenameIdentify.test.ts index 4958c71d5..5f00a5255 100644 --- a/bids-validator/src/validators/filenameIdentify.test.ts +++ b/bids-validator/src/validators/filenameIdentify.test.ts @@ -3,14 +3,12 @@ import { BIDSContext } from '../schema/context.ts' import { _findRuleMatches, datatypeFromDirectory, hasMatch } from './filenameIdentify.ts' import { BIDSFileDeno } from '../files/deno.ts' import { FileTree } from '../types/filetree.ts' -import { DatasetIssues } from '../issues/datasetIssues.ts' import { FileIgnoreRules } from '../files/ignore.ts' import { loadSchema } from '../setup/loadSchema.ts' const PATH = 'tests/data/valid_dataset' const schema = await loadSchema() const fileTree = new FileTree(PATH, '/') -const issues = new DatasetIssues() const ignore = new FileIgnoreRules([]) const node = { @@ -30,7 +28,7 @@ Deno.test('test _findRuleMatches', async (t) => { await t.step('Rule stem matches', async () => { const fileName = 'participants.json' const file = new BIDSFileDeno(PATH, fileName, ignore) - const context = new BIDSContext(fileTree, file, issues) + const context = new BIDSContext(file, undefined, fileTree) _findRuleMatches(node, schemaPath, context) assertEquals(context.filenameRules[0], schemaPath) }) @@ -41,7 +39,7 @@ Deno.test('test _findRuleMatches', async (t) => { async () => { const fileName = 'task-rest_bold.json' const file = new BIDSFileDeno(PATH, fileName, ignore) - const context = new BIDSContext(fileTree, file, issues) + const context = new BIDSContext(file, undefined, fileTree) _findRuleMatches(recurseNode, schemaPath, context) assertEquals(context.filenameRules[0], `${schemaPath}.recurse`) }, @@ -55,7 +53,7 @@ Deno.test('test datatypeFromDirectory', (t) => { ] filesToTest.map((test) => { const file = new BIDSFileDeno(PATH, test[0], ignore) - const context = new BIDSContext(fileTree, file, issues) + const context = new BIDSContext(file, undefined, fileTree) datatypeFromDirectory(schema, context) assertEquals(context.datatype, test[1]) }) @@ -65,7 +63,7 @@ Deno.test('test hasMatch', async (t) => { await t.step('hasMatch', async () => { const fileName = '/sub-01/ses-01/func/sub-01_ses-01_task-nback_run-01_bold.nii' const file = new BIDSFileDeno(PATH, fileName, ignore) - const context = new BIDSContext(fileTree, file, issues) + const context = new BIDSContext(file, undefined, fileTree) hasMatch(schema, context) }) @@ -78,10 +76,10 @@ Deno.test('test hasMatch', async (t) => { ignore, ) - const context = new BIDSContext(fileTree, file, issues) + const context = new BIDSContext(file, undefined, fileTree) await hasMatch(schema, context) assertEquals( - context.issues + context.dataset.issues .getFileIssueKeys(context.file.path) .includes('NOT_INCLUDED'), true, @@ -92,7 +90,7 @@ Deno.test('test hasMatch', async (t) => { const path = `${PATH}/../bids-examples/fnirs_automaticity` const fileName = 'events.json' const file = new BIDSFileDeno(path, fileName, ignore) - const context = new BIDSContext(fileTree, file, issues) + const context = new BIDSContext(file, undefined, fileTree) context.filenameRules = [ 'rules.files.raw.task.events__mri', 'rules.files.raw.task.events__pet', diff --git a/bids-validator/src/validators/filenameIdentify.ts b/bids-validator/src/validators/filenameIdentify.ts index 2dbac234f..a4cdf32c2 100644 --- a/bids-validator/src/validators/filenameIdentify.ts +++ b/bids-validator/src/validators/filenameIdentify.ts @@ -100,7 +100,7 @@ export function hasMatch(schema, context) { context.filenameRules.length === 0 && context.file.path !== '/.bidsignore' ) { - context.issues.addNonSchemaIssue('NOT_INCLUDED', [context.file]) + context.dataset.issues.addNonSchemaIssue('NOT_INCLUDED', [context.file]) } /* we have matched multiple rules and a datatype, lets see if we have one diff --git a/bids-validator/src/validators/filenameValidate.test.ts b/bids-validator/src/validators/filenameValidate.test.ts index f2d76dcf0..be344ab7f 100644 --- a/bids-validator/src/validators/filenameValidate.test.ts +++ b/bids-validator/src/validators/filenameValidate.test.ts @@ -4,7 +4,6 @@ import { assertEquals } from '../deps/asserts.ts' import { BIDSContext } from '../schema/context.ts' import { atRoot, entityLabelCheck, missingLabel } from './filenameValidate.ts' import { BIDSFileDeno } from '../files/deno.ts' -import { DatasetIssues } from '../issues/datasetIssues.ts' import { FileIgnoreRules } from '../files/ignore.ts' import { loadSchema } from '../setup/loadSchema.ts' @@ -19,14 +18,14 @@ Deno.test('test missingLabel', async (t) => { Deno.writeTextFileSync(`${tmpDir}/${basename}`, '') const context = new BIDSContext( - fileTree, new BIDSFileDeno(tmpDir, `/${basename}`, ignore), - new DatasetIssues(), + undefined, + fileTree, ) await missingLabel(schema, context) assertEquals( - context.issues + context.dataset.issues .getFileIssueKeys(context.file.path) .includes('ENTITY_WITH_NO_LABEL'), true, @@ -40,14 +39,14 @@ Deno.test('test missingLabel', async (t) => { Deno.writeTextFileSync(`${tmpDir}/${basename}`, '') const context = new BIDSContext( - fileTree, new BIDSFileDeno(tmpDir, `/${basename}`, ignore), - new DatasetIssues(), + undefined, + fileTree, ) await missingLabel(schema, context) assertEquals( - context.issues + context.dataset.issues .getFileIssueKeys(context.file.path) .includes('ENTITY_WITH_NO_LABEL'), false, diff --git a/bids-validator/src/validators/filenameValidate.ts b/bids-validator/src/validators/filenameValidate.ts index 23e1a4076..66c17d322 100644 --- a/bids-validator/src/validators/filenameValidate.ts +++ b/bids-validator/src/validators/filenameValidate.ts @@ -44,7 +44,7 @@ export async function missingLabel( ) if (fileNoLabelEntities.length) { - context.issues.addNonSchemaIssue('ENTITY_WITH_NO_LABEL', [ + context.dataset.issues.addNonSchemaIssue('ENTITY_WITH_NO_LABEL', [ { ...context.file, evidence: fileNoLabelEntities.join(', ') }, ]) } @@ -120,7 +120,7 @@ export async function entityLabelCheck( const rePattern = new RegExp(`^${pattern}$`) const label = context.entities[fileEntity] if (!rePattern.test(label)) { - context.issues.addNonSchemaIssue('INVALID_ENTITY_LABEL', [ + context.dataset.issues.addNonSchemaIssue('INVALID_ENTITY_LABEL', [ { ...context.file, evidence: `entity: ${fileEntity} label: ${label} pattern: ${pattern}`, @@ -150,24 +150,24 @@ async function checkRules(schema: GenericSchema, context: BIDSContext) { ) } } else { - const ogIssues = context.issues + const ogIssues = context.dataset.issues const noIssues: [string, DatasetIssues][] = [] const someIssues: [string, DatasetIssues][] = [] for (const path of context.filenameRules) { const tempIssues = new DatasetIssues() - context.issues = tempIssues + context.dataset.issues = tempIssues for (const check of ruleChecks) { check(path, schema as unknown as GenericSchema, context) } tempIssues.size ? someIssues.push([path, tempIssues]) : noIssues.push([path, tempIssues]) } if (noIssues.length) { - context.issues = ogIssues + context.dataset.issues = ogIssues context.filenameRules = [noIssues[0][0]] } else if (someIssues.length) { // What would we want to do with each rules issues? Add all? - context.issues = ogIssues - context.issues.addNonSchemaIssue('ALL_FILENAME_RULES_HAVE_ISSUES', [ + context.dataset.issues = ogIssues + context.dataset.issues.addNonSchemaIssue('ALL_FILENAME_RULES_HAVE_ISSUES', [ { ...context.file, evidence: `Rules that matched with issues: ${ @@ -210,7 +210,7 @@ function entityRuleIssue( ) if (missingRequired.length) { - context.issues.addNonSchemaIssue('MISSING_REQUIRED_ENTITY', [ + context.dataset.issues.addNonSchemaIssue('MISSING_REQUIRED_ENTITY', [ { ...context.file, evidence: `${missingRequired.join(', ')} missing from rule ${path}`, @@ -224,7 +224,7 @@ function entityRuleIssue( ) if (entityNotInRule.length) { - context.issues.addNonSchemaIssue('ENTITY_NOT_IN_RULE', [ + context.dataset.issues.addNonSchemaIssue('ENTITY_NOT_IN_RULE', [ { ...context.file, evidence: `${entityNotInRule.join(', ')} not in rule ${path}`, @@ -244,7 +244,7 @@ function datatypeMismatch( Array.isArray(rule.datatypes) && !rule.datatypes.includes(context.datatype) ) { - context.issues.addNonSchemaIssue('DATATYPE_MISMATCH', [ + context.dataset.issues.addNonSchemaIssue('DATATYPE_MISMATCH', [ { ...context.file, evidence: `Datatype rule being applied: ${path}` }, ]) } @@ -260,7 +260,7 @@ async function extensionMismatch( Array.isArray(rule.extensions) && !rule.extensions.includes(context.extension) ) { - context.issues.addNonSchemaIssue('EXTENSION_MISMATCH', [ + context.dataset.issues.addNonSchemaIssue('EXTENSION_MISMATCH', [ { ...context.file, evidence: `Rule: ${path}` }, ]) } diff --git a/bids-validator/src/validators/hed.ts b/bids-validator/src/validators/hed.ts index c537d1674..c41447ab6 100644 --- a/bids-validator/src/validators/hed.ts +++ b/bids-validator/src/validators/hed.ts @@ -77,13 +77,13 @@ export async function hedValidate( hedValidationIssues.push(...file.validate(hedSchemas)) } } catch (error) { - context.issues.addNonSchemaIssue('HED_ERROR', [{ ...context.file, evidence: error }]) + context.dataset.issues.addNonSchemaIssue('HED_ERROR', [{ ...context.file, evidence: error }]) } hedValidationIssues.map((hedIssue) => { const code = hedIssue.code if (code in hedOldToNewLookup) { - context.issues.addNonSchemaIssue( + context.dataset.issues.addNonSchemaIssue( hedOldToNewLookup[code], [{ ...hedIssue.file, evidence: hedIssue.evidence }], ) diff --git a/bids-validator/src/validators/internal/emptyFile.ts b/bids-validator/src/validators/internal/emptyFile.ts index 25ef3bd32..b7b43bf40 100644 --- a/bids-validator/src/validators/internal/emptyFile.ts +++ b/bids-validator/src/validators/internal/emptyFile.ts @@ -3,7 +3,7 @@ import { ContextCheckFunction } from '../../types/check.ts' // Non-schema EMPTY_FILE implementation export const emptyFile: ContextCheckFunction = (schema, context) => { if (context.file.size === 0) { - context.issues.addNonSchemaIssue('EMPTY_FILE', [context.file]) + context.dataset.issues.addNonSchemaIssue('EMPTY_FILE', [context.file]) } return Promise.resolve() }