Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Track files referenced by other files to identify orphans #2057

Merged
merged 4 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bids-validator/src/files/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class BIDSFileBrowser implements BIDSFile {
name: string
path: string
parent: FileTree
viewed: boolean = false

constructor(file: File, ignore: FileIgnoreRules, parent?: FileTree) {
this.#file = file
Expand Down
1 change: 1 addition & 0 deletions bids-validator/src/files/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class BIDSFileDeno implements BIDSFile {
parent: FileTree
#fileInfo?: Deno.FileInfo
#datasetAbsPath: string
viewed: boolean = false

constructor(datasetPath: string, path: string, ignore: FileIgnoreRules, parent?: FileTree) {
this.#datasetAbsPath = datasetPath
Expand Down
2 changes: 2 additions & 0 deletions bids-validator/src/files/inheritance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function* walkBack(
)
})
if (exactMatch) {
exactMatch.viewed = true
yield exactMatch
} else {
console.warn(`
Expand All @@ -39,6 +40,7 @@ ${candidates.map((file) => `* ${file.path}`).join('\n')}
`)
}
} else if (candidates.length === 1) {
candidates[0].viewed = true
yield candidates[0]
}
if (!inherit) break
Expand Down
2 changes: 2 additions & 0 deletions bids-validator/src/issues/datasetIssues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Deno.test('DatasetIssues management class', async (t) => {
ignored: false,
stream: testStream,
parent: root,
viewed: false,
} as BIDSFile,
{
text,
Expand All @@ -40,6 +41,7 @@ Deno.test('DatasetIssues management class', async (t) => {
severity: 'warning',
reason: 'Readme borked',
parent: root,
viewed: false,
} as IssueFile,
]
issues.add({ key: 'TEST_FILES_ERROR', reason: 'Test issue', files })
Expand Down
9 changes: 9 additions & 0 deletions bids-validator/src/issues/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ export const bidsIssues: IssueDefinitionRecord = {
severity: 'error',
reason: 'Empty files not allowed.',
},
UNUSED_STIMULUS: {
severity: 'warning',
reason:
'There are files in the /stimuli directory that are not utilized in any _events.tsv file.',
},
SIDECAR_WITHOUT_DATAFILE: {
severity: 'error',
reason: 'A json sidecar file was found without a corresponding data file',
},
}

const hedIssues: IssueDefinitionRecord = {
Expand Down
5 changes: 5 additions & 0 deletions bids-validator/src/schema/fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const dataFile = {
stream: new ReadableStream<Uint8Array>(),
readBytes: nullReadBytes,
parent: anatFileTree,
viewed: false,
}

anatFileTree.files = [
Expand All @@ -54,6 +55,7 @@ anatFileTree.files = [
stream: new ReadableStream<Uint8Array>(),
readBytes: async (size: number) => new TextEncoder().encode(await anatJson()),
parent: anatFileTree,
viewed: false,
},
]

Expand All @@ -70,6 +72,7 @@ subjectFileTree.files = [
stream: new ReadableStream<Uint8Array>(),
readBytes: async (size: number) => new TextEncoder().encode(await subjectJson()),
parent: subjectFileTree,
viewed: false,
},
]
subjectFileTree.directories = [sessionFileTree]
Expand All @@ -84,6 +87,7 @@ stimuliFileTree.files = [...Array(10).keys()].map((i) => (
stream: new ReadableStream<Uint8Array>(),
readBytes: nullReadBytes,
parent: stimuliFileTree,
viewed: false,
}
))

Expand All @@ -97,6 +101,7 @@ rootFileTree.files = [
stream: new ReadableStream<Uint8Array>(),
readBytes: async (size: number) => new TextEncoder().encode(await rootJson()),
parent: rootFileTree,
viewed: false,
},
]
rootFileTree.directories = [stimuliFileTree, subjectFileTree]
1 change: 1 addition & 0 deletions bids-validator/src/schema/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function pseudoFile(dir: FileTree): BIDSFile {
size: [...quickWalk(dir)].reduce((acc, file) => acc + file.size, 0),
ignored: dir.ignored,
parent: dir.parent as FileTree,
viewed: false,
...nullFile,
}
}
Expand Down
5 changes: 5 additions & 0 deletions bids-validator/src/tests/simple-dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ anatFileTree.files = [
stream: new ReadableStream<Uint8Array>(),
readBytes: nullReadBytes,
parent: anatFileTree,
viewed: false,
},
]
subjectFileTree.files = []
Expand All @@ -31,6 +32,7 @@ rootFileTree.files = [
stream: new ReadableStream(),
readBytes: nullReadBytes,
parent: rootFileTree,
viewed: false,
},
{
text,
Expand All @@ -41,6 +43,7 @@ rootFileTree.files = [
stream: new ReadableStream(),
readBytes: nullReadBytes,
parent: rootFileTree,
viewed: false,
},
{
text,
Expand All @@ -51,6 +54,7 @@ rootFileTree.files = [
stream: new ReadableStream(),
readBytes: nullReadBytes,
parent: rootFileTree,
viewed: false,
},
{
text,
Expand All @@ -61,6 +65,7 @@ rootFileTree.files = [
stream: new ReadableStream(),
readBytes: nullReadBytes,
parent: rootFileTree,
viewed: false,
},
]
rootFileTree.directories = [subjectFileTree]
Expand Down
5 changes: 4 additions & 1 deletion bids-validator/src/types/filetree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export interface BIDSFile {
text: () => Promise<string>
// Read a range of bytes
readBytes: (size: number, offset?: number) => Promise<Uint8Array>
// Access the parent directory
parent: FileTree
// File has been viewed
viewed: boolean
}

export class FileTree {
Expand All @@ -43,7 +46,7 @@ export class FileTree {
return false
} else if (parts.length === 1) {
return (
this.files.some((x) => x.name === parts[0]) ||
this.files.some((x) => (x.name === parts[0] && (x.viewed = true))) ||
this.directories.some((x) => x.name === parts[0])
)
} else if (parts.length > 1) {
Expand Down
6 changes: 5 additions & 1 deletion bids-validator/src/validators/bids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { filenameIdentify } from './filenameIdentify.ts'
import { filenameValidate } from './filenameValidate.ts'
import { DatasetIssues } from '../issues/datasetIssues.ts'
import { emptyFile } from './internal/emptyFile.ts'
import { sidecarWithoutDatafile, unusedStimulus } from './internal/unusedFile.ts'
import { BIDSContext, BIDSContextDataset } from '../schema/context.ts'
import { parseOptions } from '../setup/options.ts'
import { hedValidate } from './hed.ts'
Expand All @@ -28,7 +29,10 @@ const perContextChecks: ContextCheckFunction[] = [
hedValidate,
]

const perDSChecks: DSCheckFunction[] = []
const perDSChecks: DSCheckFunction[] = [
unusedStimulus,
sidecarWithoutDatafile,
]

/**
* Full BIDS schema validation entrypoint
Expand Down
45 changes: 45 additions & 0 deletions bids-validator/src/validators/internal/unusedFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { GenericSchema } from '../../types/schema.ts'
import { BIDSFile, FileTree } from '../../types/filetree.ts'
import { BIDSContextDataset } from '../../schema/context.ts'

function* walkFileTree(fileTree?: FileTree): Generator<BIDSFile> {
if (!fileTree) {
return
}
for (const file of fileTree.files) {
if (!file.ignored) {
yield file
}
}
for (const dir of fileTree.directories) {
if (!dir.ignored) {
yield* walkFileTree(dir)
}
}
}

export async function unusedStimulus(
schema: GenericSchema,
dsContext: BIDSContextDataset,
) {
const stimDir = dsContext.tree.directories.find((dir) => dir.name === 'stimuli')
const unusedStimuli = [...walkFileTree(stimDir)].filter((stimulus) => !stimulus.viewed)
if (unusedStimuli.length) {
dsContext.issues.addNonSchemaIssue('UNUSED_STIMULUS', unusedStimuli)
}

Check warning on line 29 in bids-validator/src/validators/internal/unusedFile.ts

View check run for this annotation

Codecov / codecov/patch

bids-validator/src/validators/internal/unusedFile.ts#L28-L29

Added lines #L28 - L29 were not covered by tests
}

const standalone_json = ['dataset_description.json', 'genetic_info.json']

export async function sidecarWithoutDatafile(
schema: GenericSchema,
dsContext: BIDSContextDataset,
) {
const unusedSidecars = [...walkFileTree(dsContext.tree)].filter(
(file) => (!file.viewed && file.name.endsWith('.json') &&
!standalone_json.includes(file.name)),
)
if (unusedSidecars.length) {
dsContext.issues.addNonSchemaIssue('SIDECAR_WITHOUT_DATAFILE', unusedSidecars)
}

Check warning on line 44 in bids-validator/src/validators/internal/unusedFile.ts

View check run for this annotation

Codecov / codecov/patch

bids-validator/src/validators/internal/unusedFile.ts#L43-L44

Added lines #L43 - L44 were not covered by tests
}