Skip to content

Commit baaabd0

Browse files
authored
feat: Add stringKeys parse option (#581)
1 parent f2fa108 commit baaabd0

File tree

8 files changed

+69
-7
lines changed

8 files changed

+69
-7
lines changed

docs/03_options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `new Composer()`,
2929
| lineCounter | `LineCounter` | | If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` to provide the `{ line, col }` positions within the input. |
3030
| prettyErrors | `boolean` | `true` | Include line/col position in errors, along with an extract of the source string. |
3131
| strict | `boolean` | `true` | When parsing, do not ignore errors [required](#silencing-errors-and-warnings) by the YAML 1.2 spec, but caused by unambiguous content. |
32+
| stringKeys | `boolean` | `false` | Parse all mapping keys as strings. Treat all non-scalar keys as errors. |
3233
| uniqueKeys | `boolean ⎮ (a, b) => boolean` | `true` | Whether key uniqueness is checked, or customised. If set to be a function, it will be passed two parsed nodes and should return a boolean value indicating their equality. |
3334

3435
[bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt

docs/08_errors.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ To identify errors for special handling, you should primarily use `code` to diff
2626
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2727
| `ALIAS_PROPS` | Unlike scalars and collections, alias nodes cannot have an anchor or tag associated with it. |
2828
| `BAD_ALIAS` | An alias identifier must be a non-empty sequence of valid characters. |
29+
| `BAD_COLLECTION_TYPE` | Explicit collection tag used on a collection type it does not support. |
2930
| `BAD_DIRECTIVE` | Only the `%YAML` and `%TAG` directives are supported, and they need to follow the specified structure. |
3031
| `BAD_DQ_ESCAPE` | Double-quotes strings may include `\` escaped content, but that needs to be valid. |
3132
| `BAD_INDENT` | Indentation is important in YAML, and collection items need to all start at the same level. Block scalars are also picky about their leading content. |
@@ -36,12 +37,12 @@ To identify errors for special handling, you should primarily use `code` to diff
3637
| `DUPLICATE_KEY` | Map keys must be unique. Use the `uniqueKeys` option to disable or customise this check when parsing. |
3738
| `IMPOSSIBLE` | This really should not happen. If you encounter this error code, please file a bug. |
3839
| `KEY_OVER_1024_CHARS` | Due to legacy reasons, implicit keys must have their following `:` indicator after at most 1k characters. |
39-
| `MISSING_ANCHOR` | Aliases can only dereference anchors that are before them in the document. |
4040
| `MISSING_CHAR` | Some character or characters are missing here. See the error message for what you need to add. |
4141
| `MULTILINE_IMPLICIT_KEY` | Implicit keys need to be on a single line. Does the input include a plain scalar with a `:` followed by whitespace, which is getting parsed as a map key? |
4242
| `MULTIPLE_ANCHORS` | A node is only allowed to have one anchor. |
4343
| `MULTIPLE_DOCS` | A YAML stream may include multiple documents. If yours does, you'll need to use `parseAllDocuments()` to work with it. |
4444
| `MULTIPLE_TAGS` | A node is only allowed to have one tag. |
45+
| `NON_STRING_KEY` | With the `stringKeys` option, all mapping keys must be strings |
4546
| `TAB_AS_INDENT` | Only spaces are allowed as indentation. |
4647
| `TAG_RESOLVE_FAILED` | Something went wrong when resolving a node's tag with the current schema. |
4748
| `UNEXPECTED_TOKEN` | A token was encountered in a place where it wasn't expected. |

src/compose/compose-node.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Directives } from '../doc/directives.js'
22
import { Alias } from '../nodes/Alias.js'
3+
import { isScalar } from '../nodes/identity.js'
34
import type { ParsedNode } from '../nodes/Node.js'
45
import type { ParseOptions } from '../options.js'
56
import type { FlowScalar, SourceToken, Token } from '../parse/cst.js'
@@ -36,6 +37,7 @@ export function composeNode(
3637
props: Props,
3738
onError: ComposeErrorHandler
3839
) {
40+
const atKey = ctx.atKey
3941
const { spaceBefore, comment, anchor, tag } = props
4042
let node: ParsedNode
4143
let isSrcToken = true
@@ -81,6 +83,16 @@ export function composeNode(
8183
}
8284
if (anchor && node.anchor === '')
8385
onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string')
86+
if (
87+
atKey &&
88+
ctx.options.stringKeys &&
89+
(!isScalar(node) ||
90+
typeof node.value !== 'string' ||
91+
(node.tag && node.tag !== 'tag:yaml.org,2002:str'))
92+
) {
93+
const msg = 'With stringKeys, all keys must be strings'
94+
onError(tag ?? token, 'NON_STRING_KEY', msg)
95+
}
8496
if (spaceBefore) node.spaceBefore = true
8597
if (comment) {
8698
if (token.type === 'scalar' && token.source === '') node.comment = comment

src/compose/compose-scalar.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ export function composeScalar(
2424
onError(tagToken, 'TAG_RESOLVE_FAILED', msg)
2525
)
2626
: null
27-
const tag =
28-
tagToken && tagName
29-
? findScalarTagByName(ctx.schema, value, tagName, tagToken, onError)
30-
: token.type === 'scalar'
31-
? findScalarTagByTest(ctx, value, token, onError)
32-
: ctx.schema[SCALAR]
27+
28+
let tag: ScalarTag
29+
if (ctx.options.stringKeys && ctx.atKey) {
30+
tag = ctx.schema[SCALAR]
31+
} else if (tagName)
32+
tag = findScalarTagByName(ctx.schema, value, tagName, tagToken!, onError)
33+
else if (token.type === 'scalar')
34+
tag = findScalarTagByTest(ctx, value, token, onError)
35+
else tag = ctx.schema[SCALAR]
3336

3437
let scalar: Scalar
3538
try {

src/doc/Document.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export class Document<
122122
logLevel: 'warn',
123123
prettyErrors: true,
124124
strict: true,
125+
stringKeys: false,
125126
uniqueKeys: true,
126127
version: '1.2'
127128
},

src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type ErrorCode =
1818
| 'MULTIPLE_ANCHORS'
1919
| 'MULTIPLE_DOCS'
2020
| 'MULTIPLE_TAGS'
21+
| 'NON_STRING_KEY'
2122
| 'TAB_AS_INDENT'
2223
| 'TAG_RESOLVE_FAILED'
2324
| 'UNEXPECTED_TOKEN'

src/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export type ParseOptions = {
4848
*/
4949
strict?: boolean
5050

51+
/**
52+
* Parse all mapping keys as strings. Treat all non-scalar keys as errors.
53+
*
54+
* Default: `false`
55+
*/
56+
stringKeys?: boolean
57+
5158
/**
5259
* YAML requires map keys to be unique. By default, this is checked by
5360
* comparing scalar values with `===`; deep equality is not checked for

tests/doc/parse.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,3 +880,39 @@ describe('CRLF line endings', () => {
880880
expect(res).toBe('foo bar')
881881
})
882882
})
883+
884+
describe('stringKeys', () => {
885+
test('success', () => {
886+
const doc = YAML.parseDocument<any>(
887+
source`
888+
x: x
889+
!!str y: y
890+
42: 42
891+
true: true
892+
null: null
893+
~: ~
894+
:
895+
`,
896+
{ stringKeys: true }
897+
)
898+
expect(doc.contents.items).toMatchObject([
899+
{ key: { value: 'x' }, value: { value: 'x' } },
900+
{ key: { value: 'y' }, value: { value: 'y' } },
901+
{ key: { value: '42' }, value: { value: 42 } },
902+
{ key: { value: 'true' }, value: { value: true } },
903+
{ key: { value: 'null' }, value: { value: null } },
904+
{ key: { value: '~' }, value: { value: null } },
905+
{ key: { value: '' }, value: { value: null } }
906+
])
907+
})
908+
909+
test('explicit non-string tag', () => {
910+
const doc = YAML.parseDocument('!!int 42: 42', { stringKeys: true })
911+
expect(doc.errors).toMatchObject([{ code: 'NON_STRING_KEY' }])
912+
})
913+
914+
test('collection key', () => {
915+
const doc = YAML.parseDocument('{ x, y }: 42', { stringKeys: true })
916+
expect(doc.errors).toMatchObject([{ code: 'NON_STRING_KEY' }])
917+
})
918+
})

0 commit comments

Comments
 (0)