Skip to content

Commit 86b92f0

Browse files
authored
Merge pull request #322 from VisLab/group_bug
Fixed issue with top-level tag used in splice issue #321
2 parents a5d5189 + 6a6d327 commit 86b92f0

File tree

9 files changed

+232
-30
lines changed

9 files changed

+232
-30
lines changed

src/bids/types/json.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class BidsSidecar extends BidsJsonFile {
217217
this.parsedHedData.set(name, sidecarKey.parsedCategoryMap)
218218
}
219219
}
220-
this.#generateSidecarColumnSpliceMap()
220+
this._generateSidecarColumnSpliceMap()
221221
return [errors, warnings]
222222
}
223223

@@ -226,33 +226,33 @@ export class BidsSidecar extends BidsJsonFile {
226226
*
227227
* @private
228228
*/
229-
#generateSidecarColumnSpliceMap() {
229+
_generateSidecarColumnSpliceMap() {
230230
this.columnSpliceMapping = new Map()
231231
this.columnSpliceReferences = new Set()
232232

233233
for (const [sidecarKey, hedData] of this.parsedHedData) {
234234
if (hedData instanceof ParsedHedString) {
235-
this.#parseValueSplice(sidecarKey, hedData)
235+
this._(sidecarKey, hedData)
236236
} else if (hedData instanceof Map) {
237-
this.#parseCategorySplice(sidecarKey, hedData)
237+
this._parseCategorySplice(sidecarKey, hedData)
238238
} else if (hedData) {
239239
IssueError.generateAndThrowInternalError('Unexpected type found in sidecar parsedHedData map.')
240240
}
241241
}
242242
}
243243

244-
#parseValueSplice(sidecarKey, hedData) {
244+
_(sidecarKey, hedData) {
245245
if (hedData.columnSplices.length > 0) {
246-
const keyReferences = this.#processColumnSplices(new Set(), hedData.columnSplices)
246+
const keyReferences = this._processColumnSplices(new Set(), hedData.columnSplices)
247247
this.columnSpliceMapping.set(sidecarKey, keyReferences)
248248
}
249249
}
250250

251-
#parseCategorySplice(sidecarKey, hedData) {
251+
_parseCategorySplice(sidecarKey, hedData) {
252252
let keyReferences = null
253253
for (const valueString of hedData.values()) {
254254
if (valueString?.columnSplices.length > 0) {
255-
keyReferences = this.#processColumnSplices(keyReferences, valueString.columnSplices)
255+
keyReferences = this._processColumnSplices(keyReferences, valueString.columnSplices)
256256
}
257257
}
258258
if (keyReferences instanceof Set) {
@@ -267,7 +267,7 @@ export class BidsSidecar extends BidsJsonFile {
267267
* @returns {Set<string>}
268268
* @private
269269
*/
270-
#processColumnSplices(keyReferences, columnSplices) {
270+
_processColumnSplices(keyReferences, columnSplices) {
271271
keyReferences ??= new Set()
272272
for (const columnSplice of columnSplices) {
273273
keyReferences.add(columnSplice.originalTag)
@@ -358,9 +358,9 @@ export class BidsSidecarKey {
358358
*/
359359
parseHed(hedSchemas) {
360360
if (this.isValueKey) {
361-
return this.#parseValueString(hedSchemas)
361+
return this._parseValueString(hedSchemas)
362362
}
363-
return this.#parseCategory(hedSchemas)
363+
return this._parseCategory(hedSchemas)
364364
}
365365

366366
/**
@@ -373,8 +373,8 @@ export class BidsSidecarKey {
373373
* @returns {Array} - [Issue[], Issue[]] - Errors due for the value.
374374
* @private
375375
*/
376-
#parseValueString(hedSchemas) {
377-
const [parsedString, errorIssues, warningIssues] = parseHedString(this.valueString, hedSchemas, false, true)
376+
_parseValueString(hedSchemas) {
377+
const [parsedString, errorIssues, warningIssues] = parseHedString(this.valueString, hedSchemas, false, true, false)
378378
this.parsedValueString = parsedString
379379
return [errorIssues, warningIssues]
380380
}
@@ -385,7 +385,7 @@ export class BidsSidecarKey {
385385
* @returns {Array} - Array[Issue[], Issue[]] A list of error issues and warning issues.
386386
* @private
387387
*/
388-
#parseCategory(hedSchemas) {
388+
_parseCategory(hedSchemas) {
389389
this.parsedCategoryMap = new Map()
390390
const errors = []
391391
const warnings = []
@@ -399,7 +399,7 @@ export class BidsSidecarKey {
399399
file: this.sidecar?.file?.relativePath,
400400
})
401401
}
402-
const [parsedString, errorIssues, warningIssues] = parseHedString(string, hedSchemas, true, true)
402+
const [parsedString, errorIssues, warningIssues] = parseHedString(string, hedSchemas, true, true, false)
403403
this.parsedCategoryMap.set(value, parsedString)
404404
warnings.push(...warningIssues)
405405
errors.push(...errorIssues)

src/bids/validator/tsvValidator.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ export class BidsHedTsvParser {
353353
this.hedSchemas,
354354
false,
355355
false,
356+
true,
356357
)
357358
element.parsedHedString = parsedHedString
358359
errors.push(...BidsHedIssue.fromHedIssues(errorIssues, this.tsvFile.file, { tsvLine: element.tsvLine }))

src/parser/parsedHedString.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,43 @@ export class ParsedHedString {
1313
* @type {string}
1414
*/
1515
hedString
16+
1617
/**
1718
* The parsed substring data in unfiltered form.
1819
* @type {ParsedHedSubstring[]}
1920
*/
2021
parseTree
22+
2123
/**
2224
* The tag groups in the string (top-level).
2325
* @type {ParsedHedGroup[]}
2426
*/
2527
tagGroups
28+
2629
/**
2730
* All the top-level tags in the string.
2831
* @type {ParsedHedTag[]}
2932
*/
3033
topLevelTags
34+
3135
/**
3236
* All the tags in the string at all levels
3337
* @type {ParsedHedTag[]}
3438
*/
3539
tags
40+
3641
/**
3742
* All the column splices in the string at all levels.
3843
* @type {ParsedHedColumnSplice[]}
3944
*/
4045
columnSplices
46+
4147
/**
4248
* The tags in the top-level tag groups in the string, split into arrays.
4349
* @type {ParsedHedTag[][]}
4450
*/
4551
topLevelGroupTags
52+
4653
/**
4754
* The top-level definition tag groups in the string.
4855
* @type {ParsedHedGroup[]}

src/parser/parser.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ class HedStringParser {
5050
/**
5151
* Parse a full HED string.
5252
*
53+
* @param {boolean} fullValidation - True if full full validation should be performed -- with assemploy
5354
* ###Note: now separates errors and warnings for easier handling.
5455
*
5556
* @returns {Array} - [ParsedHedString|null, Issue[], Issue[]] representing the parsed HED string and any parsing issues.
5657
*/
57-
parse() {
58+
parse(fullValidation) {
5859
if (this.hedString === null || this.hedString === undefined) {
5960
return [null, [generateIssue('invalidTagString', {})], []]
6061
}
@@ -89,7 +90,7 @@ class HedStringParser {
8990
}
9091

9192
// Check the other reserved tags requirements
92-
const checkIssues = ReservedChecker.getInstance().checkHedString(parsedString)
93+
const checkIssues = ReservedChecker.getInstance().checkHedString(parsedString, fullValidation)
9394
if (checkIssues.length > 0) {
9495
return [null, checkIssues, []]
9596
}
@@ -182,10 +183,11 @@ class HedStringParser {
182183
* @param {Schemas} hedSchemas - The collection of HED schemas.
183184
* @param {boolean} definitionsAllowed - True if definitions are allowed.
184185
* @param {boolean} placeholdersAllowed - True if placeholders are allowed.
186+
* @param {boolean} fullValidation - True if full validation is required.
185187
* @returns {Array} - [ParsedHedString, Issue[], Issue[]] representing the parsed HED string and any issues found.
186188
*/
187-
export function parseHedString(hedString, hedSchemas, definitionsAllowed, placeholdersAllowed) {
188-
return new HedStringParser(hedString, hedSchemas, definitionsAllowed, placeholdersAllowed).parse()
189+
export function parseHedString(hedString, hedSchemas, definitionsAllowed, placeholdersAllowed, fullValidation) {
190+
return new HedStringParser(hedString, hedSchemas, definitionsAllowed, placeholdersAllowed).parse(fullValidation)
189191
}
190192

191193
/**
@@ -199,6 +201,12 @@ export function parseHedString(hedString, hedSchemas, definitionsAllowed, placeh
199201
* @param {boolean} placeholdersAllowed - True if placeholders are allowed
200202
* @returns {Array} - [ParsedHedString[], Issue[], Issue[]] representing the parsed HED strings and any issues found.
201203
*/
202-
export function parseHedStrings(hedStrings, hedSchemas, definitionsAllowed, placeholdersAllowed) {
203-
return HedStringParser.parseHedStrings(hedStrings, hedSchemas, definitionsAllowed, placeholdersAllowed)
204+
export function parseHedStrings(hedStrings, hedSchemas, definitionsAllowed, placeholdersAllowed, fullValidation) {
205+
return HedStringParser.parseHedStrings(
206+
hedStrings,
207+
hedSchemas,
208+
definitionsAllowed,
209+
placeholdersAllowed,
210+
fullValidation,
211+
)
204212
}

src/parser/reservedChecker.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ export class ReservedChecker {
4545
* Perform syntactical checks on the provided HED string to detect violations.
4646
*
4747
* @param {ParsedHedString} hedString - The HED string to be checked.
48+
* @param {boolean} fullValidation - If true, perform full validation; otherwise, perform a quick check.
4849
* @returns {Issue[]} - An array of issues if violations are found otherwise, an empty array.
4950
*/
50-
checkHedString(hedString) {
51+
checkHedString(hedString, fullValidation) {
5152
const checks = [
5253
() => this.checkUnique(hedString),
53-
() => this.checkTagGroupLevels(hedString),
54+
() => this.checkTagGroupLevels(hedString, fullValidation),
5455
() => this.checkTopGroupRequirements(hedString),
5556
() => this.checkNonTopGroups(hedString),
5657
]
@@ -85,9 +86,10 @@ export class ReservedChecker {
8586
* Check whether tags are not in groups -- or top-level groups as required
8687
*
8788
* @param {ParsedHedString} hedString - The HED string to be checked for reserved tag syntax.
89+
* @param {boolean} fullValidation - If true, perform full validation; otherwise, perform a quick check.
8890
* @returns {Issue[]} An array of `Issue` objects if there are violations; otherwise, an empty array.
8991
*/
90-
checkTagGroupLevels(hedString) {
92+
checkTagGroupLevels(hedString, fullValidation) {
9193
const issues = []
9294
const topGroupTags = hedString.topLevelGroupTags.flat()
9395

@@ -98,14 +100,22 @@ export class ReservedChecker {
98100
return
99101
}
100102

101-
// If this is a top tag group tag, we know it isn't in a top tag group.
102-
if (ReservedChecker.hasTopLevelTagGroupAttribute(tag)) {
103-
issues.push(generateIssue('invalidTopLevelTagGroupTag', { tag: tag.originalTag, string: hedString.hedString }))
103+
// This is a top-level tag group tag that is in a lower level or ungrouped top level
104+
if (
105+
ReservedChecker.hasTopLevelTagGroupAttribute(tag) &&
106+
(!hedString.topLevelTags.includes(tag) || fullValidation)
107+
) {
108+
issues.push(
109+
generateIssue('invalidTopLevelTagGroupTag', {
110+
tag: tag.originalTag,
111+
string: hedString.hedString,
112+
}),
113+
)
104114
return
105115
}
106116

107117
// In final form --- if not in a group (not just a top group) but has the group tag attribute
108-
if (hedString.topLevelTags.includes(tag) && ReservedChecker.hasGroupAttribute(tag)) {
118+
if (hedString.topLevelTags.includes(tag) && ReservedChecker.hasGroupAttribute(tag) && fullValidation) {
109119
issues.push(generateIssue('missingTagGroup', { tag: tag.originalTag, string: hedString.hedString }))
110120
}
111121
})

tests/bidsTests.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { DefinitionManager } from '../src/parser/definitionManager'
1414
//const skipMap = new Map([['definition-tests', ['invalid-missing-definition-for-def', 'invalid-nested-definition']]])
1515
const skipMap = new Map()
1616
const runAll = true
17-
const runMap = new Map([['curly-brace-tests', ['valid-curly-brace-in-sidecar-with-value-splice']]])
17+
const runMap = new Map([['curly-brace-tests', ['splice-of-top-level-tag']]])
1818

1919
describe('BIDS validation', () => {
2020
const schemaMap = new Map([['8.3.0', undefined]])

tests/stringParserTests.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { shouldRun, getHedString } from './testUtilities'
1313
const skipMap = new Map()
1414
const runAll = true
1515
//const runMap = new Map([['valid-tags', ['single-tag-extension']]])
16-
const runMap = new Map([['valid-tags', ['deprecated-tag']]])
16+
const runMap = new Map([['special-tag-group-tests', ['event-context-in-subgroup']]])
1717

1818
describe('Null schema objects should cause parsing to bail', () => {
1919
it('Should not proceed if no schema and valid string', () => {
@@ -82,6 +82,7 @@ describe('Parse HED string tests', () => {
8282
thisSchema,
8383
test.definitionsAllowed,
8484
test.placeholdersAllowed,
85+
test.fullValidation,
8586
)
8687
} catch (error) {
8788
issues = [error.issue]

tests/testData/bidsTests.data.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,90 @@ export const bidsTestData = [
596596
tsvErrors: [],
597597
comboErrors: [],
598598
},
599+
{
600+
testname: 'splice-of-top-level-tag',
601+
explanation: 'A top-level tag group tag is part of a splice',
602+
schemaVersion: '8.3.0',
603+
definitions: ['(Definition/Acc/#, (Acceleration/# m-per-s^2, Red))', '(Definition/MyColor, (Label/Pie))'],
604+
sidecar: {
605+
duration: {
606+
HED: 'Duration/#',
607+
},
608+
event_code: {
609+
HED: {
610+
face: '({duration}, ((Red, Blue), {ball_type}))',
611+
ball: '{ball_type}, Black',
612+
},
613+
},
614+
ball_type: {
615+
Description: 'Has description with HED',
616+
HED: 'Label/#',
617+
},
618+
},
619+
eventsString: 'onset\tduration\tevent_code\tball_type\n' + '19\t6\tball\tbig-one\n25\t5\tface\tother-one\n',
620+
sidecarErrors: [],
621+
tsvErrors: [],
622+
comboErrors: [],
623+
},
624+
{
625+
testname: 'bad-splice-of-top-level-tag',
626+
explanation: 'A top-level tag group tag is part of a splice but is invalid',
627+
schemaVersion: '8.3.0',
628+
definitions: ['(Definition/Acc/#, (Acceleration/# m-per-s^2, Red))', '(Definition/MyColor, (Label/Pie))'],
629+
sidecar: {
630+
duration: {
631+
HED: '(Duration/#, (Red, Blue))',
632+
},
633+
event_code: {
634+
HED: {
635+
face: '({duration}, ((Red, Blue), {ball_type}))',
636+
ball: '{ball_type}, Black',
637+
},
638+
},
639+
ball_type: {
640+
Description: 'Has description with HED',
641+
HED: 'Label/#',
642+
},
643+
},
644+
eventsString: 'onset\tduration\tevent_code\tball_type\n' + '19\t6\tball\tbig-one\n25\t5\tface\tother-one\n',
645+
sidecarErrors: [],
646+
tsvErrors: [],
647+
comboErrors: [
648+
BidsHedIssue.fromHedIssue(
649+
generateIssue('invalidTopLevelTagGroupTag', {
650+
tag: 'Duration/5',
651+
string: '((Duration/5, (Red, Blue)), ((Red, Blue), Label/other-one))',
652+
}),
653+
{ path: 'bad-splice-of-top-level-tag.tsv', relativePath: 'bad-splice-of-top-level-tag.tsv' },
654+
{ tsvLine: 3 },
655+
),
656+
],
657+
},
658+
{
659+
testname: 'splice-of-non-top-level-tag',
660+
explanation: 'A top-level tag group tag is part of a splice',
661+
schemaVersion: '8.3.0',
662+
definitions: ['(Definition/Acc/#, (Acceleration/# m-per-s^2, Red))', '(Definition/MyColor, (Label/Pie))'],
663+
sidecar: {
664+
duration: {
665+
HED: 'Parameter-value/#',
666+
},
667+
event_code: {
668+
HED: {
669+
face: '({duration}, ((Red, Blue), {ball_type}))',
670+
ball: '{ball_type}, Black',
671+
},
672+
},
673+
ball_type: {
674+
Description: 'Has description with HED',
675+
HED: 'Label/#',
676+
},
677+
},
678+
eventsString: 'onset\tduration\tevent_code\tball_type\n' + '19\t6\tball\tbig-one\n25\t5\tface\tother-one\n',
679+
sidecarErrors: [],
680+
tsvErrors: [],
681+
comboErrors: [],
682+
},
599683
{
600684
testname: 'valid-curly-brace-in-sidecar-with-tsv-n/a',
601685
explanation: 'Valid curly brace in sidecar and valid tsv with n/a',

0 commit comments

Comments
 (0)