@safeaccess/inline 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -1
- package/CHANGELOG.md +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +26 -56
- package/dist/inline.js +43 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +4 -1
- package/dist/security/security-guard.js +7 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +25 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +46 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +10 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +8 -479
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { YamlParser } from '../../src/parser/yaml-parser.js';
|
|
3
|
+
import { YamlParseException } from '../../src/exceptions/yaml-parse-exception.js';
|
|
4
|
+
|
|
5
|
+
function makeParser(): YamlParser {
|
|
6
|
+
return new YamlParser();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe(`${YamlParser.name} > mutation killing - assertNoUnsafeConstructs`, () => {
|
|
10
|
+
// Kills L50 trimStart→trimEnd: indented comment with tag syntax must be skipped
|
|
11
|
+
it('does not throw for indented comment containing !! tag syntax', () => {
|
|
12
|
+
expect(() => makeParser().parse('key: value\n # !!python/object foo')).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('does not throw for indented comment containing & anchor syntax', () => {
|
|
16
|
+
expect(() => makeParser().parse('key: value\n # &anchor reference')).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('does not throw for indented comment containing * alias syntax', () => {
|
|
20
|
+
expect(() => makeParser().parse('key: value\n # *alias reference')).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Kills L52 || → && and BlockStatement → {}: comment with tag is skipped
|
|
24
|
+
it('does not throw for root comment line with !! tag', () => {
|
|
25
|
+
expect(() => makeParser().parse('# !!str tagged\nkey: value')).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does not throw for root comment line with anchor', () => {
|
|
29
|
+
expect(() => makeParser().parse('# &ref anchored\nkey: value')).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not throw for root comment line with alias', () => {
|
|
33
|
+
expect(() => makeParser().parse('# *ref aliased\nkey: value')).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Kills L52:29 StringLiteral '' → "Stryker was here!": whitespace-only line
|
|
37
|
+
// with unsafe construct on same line (would fail if empty check is broken AND
|
|
38
|
+
// the blank line somehow reaches the regex)
|
|
39
|
+
it('parses correctly with blank line between keys', () => {
|
|
40
|
+
const result = makeParser().parse('a: 1\n\nb: 2');
|
|
41
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe(`${YamlParser.name} > mutation killing - parseLines comment/blank handling`, () => {
|
|
46
|
+
// Kills L106 ConditionalExpression→false: comment with colon must be ignored
|
|
47
|
+
it('ignores comment lines containing colons in nested blocks', () => {
|
|
48
|
+
const yaml = 'root:\n # comment: with colon\n key: value';
|
|
49
|
+
expect(makeParser().parse(yaml)).toEqual({ root: { key: 'value' } });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Kills L106:60-L109:14 BlockStatement → {}: same as above
|
|
53
|
+
it('skips comment containing key-value syntax in root level', () => {
|
|
54
|
+
const yaml = '# fake: entry\nreal: value';
|
|
55
|
+
expect(makeParser().parse(yaml)).toEqual({ real: 'value' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Kills L106 || → && : empty line followed by comment with colon
|
|
59
|
+
it('handles empty line then comment-with-colon in nested block', () => {
|
|
60
|
+
const yaml = 'root:\n\n # x: y\n actual: data';
|
|
61
|
+
expect(makeParser().parse(yaml)).toEqual({ root: { actual: 'data' } });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe(`${YamlParser.name} > mutation killing - key:value regex`, () => {
|
|
66
|
+
// Kills L130 \s*→\s: key:value with no space after colon in sequence
|
|
67
|
+
it('parses sequence item key:value without space after colon', () => {
|
|
68
|
+
const yaml = 'items:\n - key:value';
|
|
69
|
+
const result = makeParser().parse(yaml);
|
|
70
|
+
expect(result).toEqual({ items: [{ key: 'value' }] });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Kills L164 \s*→\s: mergeChildLines key:value without space after colon
|
|
74
|
+
it('parses mergeChildLines key:value without space after colon', () => {
|
|
75
|
+
const yaml = 'items:\n - name: Alice\n role:admin';
|
|
76
|
+
expect(makeParser().parse(yaml)).toEqual({
|
|
77
|
+
items: [{ name: 'Alice', role: 'admin' }],
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Kills L216 \s*→\s: resolveValue/map key:value without space
|
|
82
|
+
it('parses root map key:value without space after colon', () => {
|
|
83
|
+
const yaml = 'key:value';
|
|
84
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'value' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Kills \s*→\S*: leading space in value should be trimmed
|
|
88
|
+
it('trims value correctly when there are multiple spaces after colon', () => {
|
|
89
|
+
const yaml = 'key: spaced';
|
|
90
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'spaced' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Kills L127 ConditionalExpression→false: empty item content should parse as null
|
|
94
|
+
it('parses bare dash producing null when no block children follow', () => {
|
|
95
|
+
const yaml = 'items:\n -\nnext: key';
|
|
96
|
+
const result = makeParser().parse(yaml);
|
|
97
|
+
expect((result['items'] as unknown[])[0]).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Kills L127:49 StringLiteral '' → "": itemContent check for dash-only
|
|
101
|
+
it('distinguishes bare dash from dash with content', () => {
|
|
102
|
+
const yaml = 'items:\n - \n - value';
|
|
103
|
+
const result = makeParser().parse(yaml);
|
|
104
|
+
const items = result['items'] as unknown[];
|
|
105
|
+
expect(items[0]).toBeNull();
|
|
106
|
+
expect(items[1]).toBe('value');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Kills L130:21 ConditionalExpression→true: non-empty content should try regex
|
|
110
|
+
it('handles sequence item with pure scalar (no colon)', () => {
|
|
111
|
+
const yaml = 'items:\n - justtext';
|
|
112
|
+
expect(makeParser().parse(yaml)).toEqual({ items: ['justtext'] });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Kills L130:37 StringLiteral '' → "Stryker was here!": empty check
|
|
116
|
+
it('correctly handles dash with space but no content', () => {
|
|
117
|
+
const yaml = 'list:\n - \n - real';
|
|
118
|
+
const result = makeParser().parse(yaml);
|
|
119
|
+
const items = result['list'] as unknown[];
|
|
120
|
+
expect(items[0]).toBeNull();
|
|
121
|
+
expect(items[1]).toBe('real');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe(`${YamlParser.name} > mutation killing - resolveValue`, () => {
|
|
126
|
+
it('resolves empty value with child block as nested map', () => {
|
|
127
|
+
const yaml = 'parent:\n child: deep';
|
|
128
|
+
expect(makeParser().parse(yaml)).toEqual({ parent: { child: 'deep' } });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('resolves truly empty value after colon as nested block', () => {
|
|
132
|
+
const yaml = 'wrapper:\n inner:\n leaf: val';
|
|
133
|
+
expect(makeParser().parse(yaml)).toEqual({
|
|
134
|
+
wrapper: { inner: { leaf: 'val' } },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns null for empty value when no children follow', () => {
|
|
139
|
+
const yaml = 'top:\nempty:';
|
|
140
|
+
const result = makeParser().parse(yaml);
|
|
141
|
+
expect(result['empty']).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('treats non-empty trimmed value as scalar (not nested block)', () => {
|
|
145
|
+
const yaml = 'key: simple';
|
|
146
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'simple' });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe(`${YamlParser.name} > mutation killing - assertNoUnsafeConstructs skip`, () => {
|
|
151
|
+
it('does not throw for comment with anchor syntax preceded by whitespace', () => {
|
|
152
|
+
expect(() => makeParser().parse(' # &ref anchor\nkey: val')).not.toThrow();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('does not throw for standalone blank line between valid YAML', () => {
|
|
156
|
+
expect(makeParser().parse('a: 1\n\nb: 2')).toEqual({ a: 1, b: 2 });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not throw for comment containing alias syntax', () => {
|
|
160
|
+
expect(() => makeParser().parse('# *alias text\nkey: value')).not.toThrow();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('does not throw for comment containing tag syntax in assertNoUnsafeConstructs', () => {
|
|
164
|
+
expect(() => makeParser().parse('# !!str tagged\nkey: value')).not.toThrow();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('treats blank trimmed line as skip target in assertNoUnsafeConstructs', () => {
|
|
168
|
+
expect(() => makeParser().parse('\n\n\nkey: value')).not.toThrow();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe(`${YamlParser.name} > mutation killing - parseLines comment/blank skip`, () => {
|
|
173
|
+
it('skips comment with colon at root level in parseLines', () => {
|
|
174
|
+
expect(makeParser().parse('# key: fakevalue\nreal: data')).toEqual({ real: 'data' });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('skips blank line followed by comment inside nested block', () => {
|
|
178
|
+
const yaml = 'root:\n\n # comment: fake\n key: value';
|
|
179
|
+
expect(makeParser().parse(yaml)).toEqual({ root: { key: 'value' } });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('treats bare blank line inside block as ignorable', () => {
|
|
183
|
+
const yaml = 'items:\n - a\n\n - b';
|
|
184
|
+
expect(makeParser().parse(yaml)).toEqual({ items: ['a', 'b'] });
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe(`${YamlParser.name} > mutation killing - sequence key-value detection`, () => {
|
|
189
|
+
it('detects key:value in sequence item vs plain scalar', () => {
|
|
190
|
+
const yaml = 'items:\n - key: val\n - plaintext';
|
|
191
|
+
const result = makeParser().parse(yaml);
|
|
192
|
+
const items = result['items'] as unknown[];
|
|
193
|
+
expect(items[0]).toEqual({ key: 'val' });
|
|
194
|
+
expect(items[1]).toBe('plaintext');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('does not apply key:value regex to empty sequence content', () => {
|
|
198
|
+
const yaml = 'items:\n -\n - value';
|
|
199
|
+
const result = makeParser().parse(yaml);
|
|
200
|
+
const items = result['items'] as unknown[];
|
|
201
|
+
expect(items[0]).toBeNull();
|
|
202
|
+
expect(items[1]).toBe('value');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('parses sequence item with only scalar (no colon) as plain value', () => {
|
|
206
|
+
expect(makeParser().parse('list:\n - nocolon')).toEqual({ list: ['nocolon'] });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe(`${YamlParser.name} > mutation killing - block scalar regex anchors`, () => {
|
|
211
|
+
it('does not treat value ending with pipe as block scalar', () => {
|
|
212
|
+
expect(makeParser().parse('text: a|')).toEqual({ text: 'a|' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('does not treat value ending with > as block scalar', () => {
|
|
216
|
+
expect(makeParser().parse('text: a>')).toEqual({ text: 'a>' });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does not treat value starting with pipe followed by text as block scalar', () => {
|
|
220
|
+
expect(makeParser().parse('text: |text')).toEqual({ text: '|text' });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not treat value starting with > followed by text as block scalar', () => {
|
|
224
|
+
expect(makeParser().parse('text: >text')).toEqual({ text: '>text' });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe(`${YamlParser.name} > mutation killing - octal regex anchors`, () => {
|
|
229
|
+
it('does not parse value with prefix before 0o as octal', () => {
|
|
230
|
+
expect(makeParser().parse('val: 100o7')).toEqual({ val: '100o7' });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('does not parse 0o with trailing non-octal chars as octal', () => {
|
|
234
|
+
expect(makeParser().parse('val: 0o77x')).toEqual({ val: '0o77x' });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not parse single octal digit suffix as octal', () => {
|
|
238
|
+
expect(makeParser().parse('val: x0o7')).toEqual({ val: 'x0o7' });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe(`${YamlParser.name} > mutation killing - hex regex anchors`, () => {
|
|
243
|
+
it('does not parse value with prefix before 0x as hex', () => {
|
|
244
|
+
expect(makeParser().parse('val: x0xFF')).toEqual({ val: 'x0xFF' });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('does not parse 0x with trailing non-hex chars as hex', () => {
|
|
248
|
+
expect(makeParser().parse('val: 0xFFg')).toEqual({ val: '0xFFg' });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('does not parse single hex digit standalone', () => {
|
|
252
|
+
expect(makeParser().parse('val: 0xG')).toEqual({ val: '0xG' });
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe(`${YamlParser.name} > mutation killing - float regex edge cases`, () => {
|
|
257
|
+
it('parses float with uppercase E notation', () => {
|
|
258
|
+
expect(makeParser().parse('val: 2.0E5')).toEqual({ val: 200000 });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('parses float without sign in exponent', () => {
|
|
262
|
+
expect(makeParser().parse('val: 1.0e2')).toEqual({ val: 100 });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('parses float with explicit positive exponent', () => {
|
|
266
|
+
expect(makeParser().parse('val: 1.0e+2')).toEqual({ val: 100 });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('parses float with negative exponent', () => {
|
|
270
|
+
expect(makeParser().parse('val: 1.0e-2')).toEqual({ val: 0.01 });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('parses float with multi-digit exponent', () => {
|
|
274
|
+
expect(makeParser().parse('val: 1.0e10')).toEqual({ val: 1.0e10 });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('does not parse value with dot but no digits after as float', () => {
|
|
278
|
+
expect(makeParser().parse('val: 1.')).toEqual({ val: '1.' });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('float regex requires includes dot in value', () => {
|
|
282
|
+
const result = makeParser().parse('val: 42');
|
|
283
|
+
expect(result['val']).toBe(42);
|
|
284
|
+
expect(Number.isInteger(result['val'] as number)).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe(`${YamlParser.name} > mutation killing - stripInlineComment fast-path`, () => {
|
|
289
|
+
it('strips hash after double-quoted value without space before hash', () => {
|
|
290
|
+
expect(makeParser().parse('key: "hello"# comment')).toEqual({ key: 'hello' });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('strips hash after single-quoted value without space before hash', () => {
|
|
294
|
+
expect(makeParser().parse("key: 'hello'# comment")).toEqual({ key: 'hello' });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('does not strip when first char repeats and hash follows second occurrence', () => {
|
|
298
|
+
expect(makeParser().parse('key: a xa # comment')).toEqual({ key: 'a xa' });
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('preserves double-quote-started value when no closing quote found', () => {
|
|
302
|
+
expect(makeParser().parse('key: "unclosed')).toEqual({ key: '"unclosed' });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('preserves single-quote-started value when no closing quote found', () => {
|
|
306
|
+
expect(makeParser().parse("key: 'unclosed")).toEqual({ key: "'unclosed" });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('returns full quoted value when nothing follows closing quote', () => {
|
|
310
|
+
expect(makeParser().parse('key: "complete"')).toEqual({ key: 'complete' });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('returns full single-quoted value when nothing follows closing quote', () => {
|
|
314
|
+
expect(makeParser().parse("key: 'complete'")).toEqual({ key: 'complete' });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('falls through to general loop when afterQuote is not empty or hash', () => {
|
|
318
|
+
expect(makeParser().parse('key: "word" extra')).toEqual({ key: '"word" extra' });
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe(`${YamlParser.name} > mutation killing - stripInlineComment general loop`, () => {
|
|
323
|
+
it('does not strip hash at position 0 even with following space', () => {
|
|
324
|
+
expect(makeParser().parse('key: #value rest')).toEqual({ key: '#value rest' });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('strips hash preceded by space in unquoted value', () => {
|
|
328
|
+
expect(makeParser().parse('key: abc # rest')).toEqual({ key: 'abc' });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('does not strip hash immediately after text without space', () => {
|
|
332
|
+
expect(makeParser().parse('key: test#value')).toEqual({ key: 'test#value' });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('does not strip hash when preceded by non-space (hash after text# space)', () => {
|
|
336
|
+
expect(makeParser().parse('key: test# value')).toEqual({ key: 'test# value' });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('single-quote protects hash in general loop', () => {
|
|
340
|
+
expect(makeParser().parse("key: 'x # y'")).toEqual({ key: 'x # y' });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('double-quote protects hash in general loop', () => {
|
|
344
|
+
expect(makeParser().parse('key: "x # y"')).toEqual({ key: 'x # y' });
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe(`${YamlParser.name} > mutation killing - folded block edge cases`, () => {
|
|
349
|
+
it('folded block with leading blank line joins subsequent lines with space', () => {
|
|
350
|
+
const yaml = 'text: >\n\n a\n b';
|
|
351
|
+
expect(makeParser().parse(yaml)).toEqual({ text: '\na b\n' });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('folded block endsWith newline check prevents double space after newline', () => {
|
|
355
|
+
const yaml = 'text: >\n first\n\n second\n third';
|
|
356
|
+
const result = makeParser().parse(yaml);
|
|
357
|
+
expect(result['text']).toBe('first\nsecond third\n');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('folded block single line gets trailing newline with clip chomping', () => {
|
|
361
|
+
expect(makeParser().parse('text: >\n only')).toEqual({ text: 'only\n' });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('literal block preserves exact indentation stripping from first line', () => {
|
|
365
|
+
const yaml = 'text: |\n deep\n also';
|
|
366
|
+
const result = makeParser().parse(yaml);
|
|
367
|
+
expect(result['text']).toBe('deep\nalso\n');
|
|
368
|
+
expect((result['text'] as string).startsWith(' ')).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('block scalar trim vs trimStart matters for indent detection', () => {
|
|
372
|
+
const yaml = 'text: |\n with trailing \n spaces ';
|
|
373
|
+
const result = makeParser().parse(yaml);
|
|
374
|
+
expect(result['text']).toBe('with trailing \nspaces \n');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe(`${YamlParser.name} > mutation killing - parseLines sequence edge cases`, () => {
|
|
379
|
+
it('bare dash (no space) treated as sequence item', () => {
|
|
380
|
+
const yaml = 'items:\n -\n -\n - c';
|
|
381
|
+
const result = makeParser().parse(yaml);
|
|
382
|
+
const items = result['items'] as unknown[];
|
|
383
|
+
expect(items[0]).toBeNull();
|
|
384
|
+
expect(items[1]).toBeNull();
|
|
385
|
+
expect(items[2]).toBe('c');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('dash with space and trailing whitespace parses content correctly', () => {
|
|
389
|
+
const yaml = 'items:\n - value ';
|
|
390
|
+
const result = makeParser().parse(yaml);
|
|
391
|
+
expect((result['items'] as unknown[])[0]).toBe('value');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('sequence item regex match captures key and value groups', () => {
|
|
395
|
+
const yaml = 'items:\n - k1: v1\n - k2: v2';
|
|
396
|
+
const result = makeParser().parse(yaml);
|
|
397
|
+
const items = result['items'] as Record<string, unknown>[];
|
|
398
|
+
expect(items[0]).toEqual({ k1: 'v1' });
|
|
399
|
+
expect(items[1]).toEqual({ k2: 'v2' });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('sequence item with empty value after colon parses nested block', () => {
|
|
403
|
+
const yaml = 'items:\n - parent:\n child: value';
|
|
404
|
+
const result = makeParser().parse(yaml);
|
|
405
|
+
const items = result['items'] as Record<string, unknown>[];
|
|
406
|
+
expect(items[0]['parent']).toEqual({ child: 'value' });
|
|
407
|
+
expect(items[0]['child']).toBe('value');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe(`${YamlParser.name} > mutation killing - splitFlowItems trailing`, () => {
|
|
412
|
+
it('does not push whitespace-only trailing item (trim check)', () => {
|
|
413
|
+
const result = makeParser().parse('items: [a, ]');
|
|
414
|
+
expect((result['items'] as unknown[]).length).toBe(1);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('does not push empty trailing item after last comma', () => {
|
|
418
|
+
const result = makeParser().parse('items: [x, y,]');
|
|
419
|
+
expect((result['items'] as unknown[]).length).toBe(2);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('pushes non-empty trailing item without comma', () => {
|
|
423
|
+
const result = makeParser().parse('items: [x, y]');
|
|
424
|
+
expect((result['items'] as unknown[]).length).toBe(2);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe(`${YamlParser.name} > mutation killing - flow map value trim`, () => {
|
|
429
|
+
it('trims value after colon in flow map entry', () => {
|
|
430
|
+
expect(makeParser().parse('m: {k: v }')).toEqual({ m: { k: 'v' } });
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('flow map entry with no colon is skipped', () => {
|
|
434
|
+
const result = makeParser().parse('m: {nocolon}');
|
|
435
|
+
expect(result).toEqual({ m: {} });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('flow map with multiple entries including colon-less', () => {
|
|
439
|
+
expect(makeParser().parse('m: {skip, a: 1, skip2, b: 2}')).toEqual({
|
|
440
|
+
m: { a: 1, b: 2 },
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe(`${YamlParser.name} > mutation killing - castScalar trim and length`, () => {
|
|
446
|
+
it('castScalar trims whitespace from value before type detection', () => {
|
|
447
|
+
expect(makeParser().parse('val: true ')).toEqual({ val: true });
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('castScalar handles exactly length 2 quoted string ""', () => {
|
|
451
|
+
const result = makeParser().parse('val: ""');
|
|
452
|
+
expect(result['val']).toBe('');
|
|
453
|
+
expect(typeof result['val']).toBe('string');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("castScalar handles exactly length 2 quoted string ''", () => {
|
|
457
|
+
const result = makeParser().parse("val: ''");
|
|
458
|
+
expect(result['val']).toBe('');
|
|
459
|
+
expect(typeof result['val']).toBe('string');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('castScalar does not unquote length 1 strings', () => {
|
|
463
|
+
expect(makeParser().parse("val: '")).toEqual({ val: "'" });
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe(`${YamlParser.name} > mutation killing - mergeChildLines regex`, () => {
|
|
468
|
+
it('child key:value regex does not match bare text in sibling', () => {
|
|
469
|
+
const yaml = 'items:\n - key: val\n bareword';
|
|
470
|
+
const result = makeParser().parse(yaml);
|
|
471
|
+
const items = result['items'] as Record<string, unknown>[];
|
|
472
|
+
expect(items[0]['key']).toBe('val');
|
|
473
|
+
expect(items[0]['bareword']).toBeUndefined();
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe(`${YamlParser.name} > mutation killing - parseLines indentation and comment skip`, () => {
|
|
478
|
+
it('comment line is skipped at all indent levels in parseLines', () => {
|
|
479
|
+
const yaml = '# L1 comment\nkey: val\n # L3 nested comment';
|
|
480
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'val' });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('blank line is skipped at all indent levels in parseLines', () => {
|
|
484
|
+
const yaml = '\nkey: val\n\n';
|
|
485
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'val' });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('comment line between sequence items is skipped', () => {
|
|
489
|
+
const yaml = 'items:\n - a\n # comment\n - b';
|
|
490
|
+
expect(makeParser().parse(yaml)).toEqual({ items: ['a', 'b'] });
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('blank line between map keys is skipped', () => {
|
|
494
|
+
const yaml = 'a: 1\n\nb: 2\n\nc: 3';
|
|
495
|
+
expect(makeParser().parse(yaml)).toEqual({ a: 1, b: 2, c: 3 });
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe(`${YamlParser.name} > mutation killing - resolveValue trimming`, () => {
|
|
500
|
+
it('resolveValue trims rawValue before checking block scalar indicator', () => {
|
|
501
|
+
const yaml = 'text: | \n content';
|
|
502
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content\n' });
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('resolveValue trims rawValue before checking flow sequence', () => {
|
|
506
|
+
const yaml = 'items: [a, b] ';
|
|
507
|
+
expect(makeParser().parse(yaml)).toEqual({ items: ['a', 'b'] });
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('resolveValue trims rawValue before checking flow map', () => {
|
|
511
|
+
const yaml = 'config: {a: 1} ';
|
|
512
|
+
expect(makeParser().parse(yaml)).toEqual({ config: { a: 1 } });
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe(`${YamlParser.name} > mutation killing - assertNoUnsafeConstructs startsWith`, () => {
|
|
517
|
+
it('rejects ! tag syntax on non-comment non-empty line', () => {
|
|
518
|
+
expect(() => makeParser().parse('val: !ruby/hash {}')).toThrow(YamlParseException);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('does not reject ! inside string value (no tag syntax)', () => {
|
|
522
|
+
expect(() => makeParser().parse('val: hello!')).not.toThrow();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('rejects !! tag on first line', () => {
|
|
526
|
+
expect(() => makeParser().parse('!!map\nkey: val')).toThrow(YamlParseException);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe(`${YamlParser.name} > mutation killing - flow sequence item casting`, () => {
|
|
531
|
+
it('casts each flow sequence item individually', () => {
|
|
532
|
+
expect(makeParser().parse('items: [1, true, null, hello]')).toEqual({
|
|
533
|
+
items: [1, true, null, 'hello'],
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('flow sequence item trim removes whitespace before casting', () => {
|
|
538
|
+
expect(makeParser().parse('items: [ 42 ]')).toEqual({ items: [42] });
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe(`${YamlParser.name} > mutation killing - block scalar empty line push`, () => {
|
|
543
|
+
it('pushes empty string for blank line in block scalar', () => {
|
|
544
|
+
const yaml = 'text: |\n line1\n\n line3';
|
|
545
|
+
const result = makeParser().parse(yaml);
|
|
546
|
+
expect(result['text']).toBe('line1\n\nline3\n');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('block scalar with only blank lines and strip chomping returns empty', () => {
|
|
550
|
+
const yaml = 'text: |-\n\n\n';
|
|
551
|
+
const result = makeParser().parse(yaml);
|
|
552
|
+
expect(result['text']).toBe('');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('block scalar detects indentation from first non-blank content line', () => {
|
|
556
|
+
const yaml = 'text: |\n\n first\n second';
|
|
557
|
+
const result = makeParser().parse(yaml);
|
|
558
|
+
expect(result['text']).toBe('\nfirst\nsecond\n');
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe(`${YamlParser.name} > mutation killing - scientific notation without dot`, () => {
|
|
563
|
+
it('does not parse 1e5 as number (no dot, returned as string)', () => {
|
|
564
|
+
const result = makeParser().parse('val: 1e5');
|
|
565
|
+
expect(result['val']).toBe('1e5');
|
|
566
|
+
expect(typeof result['val']).toBe('string');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('does not parse -1e5 as number (no dot)', () => {
|
|
570
|
+
expect(makeParser().parse('val: -1e5')).toEqual({ val: '-1e5' });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('does not parse 2E3 as number (no dot)', () => {
|
|
574
|
+
expect(makeParser().parse('val: 2E3')).toEqual({ val: '2E3' });
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
describe(`${YamlParser.name} > mutation killing - assertNoUnsafeConstructs condition`, () => {
|
|
579
|
+
it('skips blank line before a line with !! tag', () => {
|
|
580
|
+
expect(() => makeParser().parse('\n!!seq [a]')).toThrow(YamlParseException);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('skips comment-only line before a line with !! tag', () => {
|
|
584
|
+
expect(() => makeParser().parse('# safe\n!!str hello')).toThrow(YamlParseException);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('skips comment containing tag syntax followed by real tag', () => {
|
|
588
|
+
expect(() => makeParser().parse('# !!safe comment\n!!str real')).toThrow(
|
|
589
|
+
YamlParseException,
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('does not throw for blank-then-comment-then-valid YAML', () => {
|
|
594
|
+
const result = makeParser().parse('\n# comment\nkey: val');
|
|
595
|
+
expect(result).toEqual({ key: 'val' });
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('does not throw when only blank and comment lines are present with unsafe syntax in them', () => {
|
|
599
|
+
const result = makeParser().parse('# !!tag\n# &anchor\n# *alias\nkey: val');
|
|
600
|
+
expect(result).toEqual({ key: 'val' });
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
describe(`${YamlParser.name} > mutation killing - parseLines blank/comment condition`, () => {
|
|
605
|
+
it('skips comment with sequence syntax inside nested block', () => {
|
|
606
|
+
const yaml = 'root:\n # - fake item\n key: val';
|
|
607
|
+
expect(makeParser().parse(yaml)).toEqual({ root: { key: 'val' } });
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('skips blank line then processes next valid line at same indent', () => {
|
|
611
|
+
const yaml = 'root:\n a: 1\n\n b: 2';
|
|
612
|
+
expect(makeParser().parse(yaml)).toEqual({ root: { a: 1, b: 2 } });
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('comment in sequence position does not create item', () => {
|
|
616
|
+
const yaml = 'items:\n - a\n # not an item\n - b';
|
|
617
|
+
const result = makeParser().parse(yaml);
|
|
618
|
+
expect((result['items'] as unknown[]).length).toBe(2);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
describe(`${YamlParser.name} > mutation killing - sequence bare dash handling`, () => {
|
|
623
|
+
it('bare dash without following content becomes null', () => {
|
|
624
|
+
const yaml = 'list:\n -';
|
|
625
|
+
const result = makeParser().parse(yaml);
|
|
626
|
+
expect((result['list'] as unknown[])[0]).toBeNull();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('bare dash with following indented block parses as child', () => {
|
|
630
|
+
const yaml = 'list:\n -\n key: val';
|
|
631
|
+
const result = makeParser().parse(yaml);
|
|
632
|
+
expect((result['list'] as unknown[])[0]).toEqual({ key: 'val' });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('bare dash at end of document becomes null', () => {
|
|
636
|
+
const yaml = 'list:\n - a\n -';
|
|
637
|
+
const result = makeParser().parse(yaml);
|
|
638
|
+
const items = result['list'] as unknown[];
|
|
639
|
+
expect(items[0]).toBe('a');
|
|
640
|
+
expect(items[1]).toBeNull();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('dash with space trims content for scalar', () => {
|
|
644
|
+
const yaml = 'list:\n - hello world';
|
|
645
|
+
const result = makeParser().parse(yaml);
|
|
646
|
+
expect((result['list'] as unknown[])[0]).toBe('hello world');
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('sequence item content with quotes is preserved', () => {
|
|
650
|
+
const yaml = "list:\n - 'quoted value'";
|
|
651
|
+
const result = makeParser().parse(yaml);
|
|
652
|
+
expect((result['list'] as unknown[])[0]).toBe('quoted value');
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
describe(`${YamlParser.name} > mutation killing - sequence item regex vs scalar`, () => {
|
|
657
|
+
it('empty itemContent does not attempt regex match', () => {
|
|
658
|
+
const yaml = 'items:\n -\n a: 1';
|
|
659
|
+
const result = makeParser().parse(yaml);
|
|
660
|
+
expect((result['items'] as unknown[])[0]).toEqual({ a: 1 });
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('non-empty itemContent without colon is pushed as scalar', () => {
|
|
664
|
+
const yaml = 'items:\n - plainvalue';
|
|
665
|
+
expect(makeParser().parse(yaml)).toEqual({ items: ['plainvalue'] });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('non-empty itemContent with colon is parsed as key-value', () => {
|
|
669
|
+
const yaml = 'items:\n - k: v';
|
|
670
|
+
expect(makeParser().parse(yaml)).toEqual({ items: [{ k: 'v' }] });
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
describe(`${YamlParser.name} > mutation killing - block scalar indent stripping`, () => {
|
|
675
|
+
it('block scalar strips indentation based on first content line', () => {
|
|
676
|
+
const yaml = 'text: |\n indented content\n more content';
|
|
677
|
+
const result = makeParser().parse(yaml);
|
|
678
|
+
expect(result['text']).toBe('indented content\nmore content\n');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('block scalar does not include leading indentation in output', () => {
|
|
682
|
+
const yaml = 'text: |\n hello\n world';
|
|
683
|
+
const result = makeParser().parse(yaml);
|
|
684
|
+
expect((result['text'] as string).startsWith('hello')).toBe(true);
|
|
685
|
+
expect((result['text'] as string).includes(' hello')).toBe(false);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
describe(`${YamlParser.name} > mutation killing - flow items initial state`, () => {
|
|
690
|
+
it('flow sequence first item has no prefix', () => {
|
|
691
|
+
const result = makeParser().parse('items: [first]');
|
|
692
|
+
const items = result['items'] as string[];
|
|
693
|
+
expect(items[0]).toBe('first');
|
|
694
|
+
expect(items[0].length).toBe(5);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('flow map first key has no prefix', () => {
|
|
698
|
+
const result = makeParser().parse('m: {first: val}');
|
|
699
|
+
expect(Object.keys(result['m'] as Record<string, unknown>)[0]).toBe('first');
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('flow sequence with single numeric has correct value', () => {
|
|
703
|
+
expect(makeParser().parse('items: [42]')).toEqual({ items: [42] });
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('flow map with single entry has correct key and value', () => {
|
|
707
|
+
const result = makeParser().parse('m: {key: 42}');
|
|
708
|
+
expect((result['m'] as Record<string, unknown>)['key']).toBe(42);
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe(`${YamlParser.name} > mutation killing - stripInlineComment closePos`, () => {
|
|
713
|
+
it('quoted value without trailing text is returned as-is', () => {
|
|
714
|
+
expect(makeParser().parse('key: "value"')).toEqual({ key: 'value' });
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('quoted value with only whitespace after closing quote is trimmed', () => {
|
|
718
|
+
expect(makeParser().parse('key: "value" ')).toEqual({ key: 'value' });
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('quoted value followed by # is trimmed to quoted value', () => {
|
|
722
|
+
expect(makeParser().parse('key: "value" # comment')).toEqual({ key: 'value' });
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('quoted value followed by text is returned including text', () => {
|
|
726
|
+
expect(makeParser().parse('key: "value" text')).toEqual({ key: '"value" text' });
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('single-quoted value followed by # is trimmed to quoted value', () => {
|
|
730
|
+
expect(makeParser().parse("key: 'value' # comment")).toEqual({ key: 'value' });
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
describe(`${YamlParser.name} > mutation killing - stripInlineComment loop bounds`, () => {
|
|
735
|
+
it('value with no hash returns entire value', () => {
|
|
736
|
+
expect(makeParser().parse('key: nohash')).toEqual({ key: 'nohash' });
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('hash at first position with space after is not stripped (i=0 guard)', () => {
|
|
740
|
+
expect(makeParser().parse('key: #start')).toEqual({ key: '#start' });
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('hash at second position with preceding space is stripped', () => {
|
|
744
|
+
expect(makeParser().parse('key: x #rest')).toEqual({ key: 'x' });
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('substring before comment is trimmed of trailing whitespace', () => {
|
|
748
|
+
expect(makeParser().parse('key: value # comment')).toEqual({ key: 'value' });
|
|
749
|
+
});
|
|
750
|
+
});
|