Skip to content

Commit ac77a5e

Browse files
Keen Yee Liaumgechev
Keen Yee Liau
authored andcommitted
fix(@schematics/angular): JsonUtils should respect indent
This commit fixes a few issues with json-utils: 1. The implementation is lacking tests 2. The implementation hardcodes indent = 2 in many places and does not respect the `indent` parameter passed by users 3. The implementation is buggy when passed an empty object or array
1 parent a5738db commit ac77a5e

File tree

2 files changed

+155
-70
lines changed

2 files changed

+155
-70
lines changed

packages/schematics/angular/utility/json-utils.ts

+24-19
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,22 @@ export function appendPropertyInAstObject(
2222
indent: number,
2323
) {
2424
const indentStr = _buildIndent(indent);
25-
25+
let index = node.start.offset + 1;
2626
if (node.properties.length > 0) {
2727
// Insert comma.
2828
const last = node.properties[node.properties.length - 1];
29-
recorder.insertRight(last.start.offset + last.text.replace(/\s+$/, '').length, ',');
29+
const {text, end} = last;
30+
const commaIndex = text.endsWith('\n') ? end.offset - 1 : end.offset;
31+
recorder.insertRight(commaIndex, ',');
32+
index = end.offset;
3033
}
31-
32-
recorder.insertLeft(
33-
node.end.offset - 1,
34-
' '
35-
+ `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}`
36-
+ indentStr.slice(0, -2),
34+
const content = JSON.stringify(value, null, indent).replace(/\n/g, indentStr);
35+
recorder.insertRight(
36+
index,
37+
(node.properties.length === 0 && indent ? '\n' : '')
38+
+ ' '.repeat(indent)
39+
+ `"${propertyName}":${indent ? ' ' : ''}${content}`
40+
+ indentStr.slice(0, -indent),
3741
);
3842
}
3943

@@ -77,15 +81,14 @@ export function insertPropertyInAstObjectInOrder(
7781
}
7882

7983
const indentStr = _buildIndent(indent);
80-
8184
const insertIndex = insertAfterProp === null
8285
? node.start.offset + 1
8386
: insertAfterProp.end.offset + 1;
84-
87+
const content = JSON.stringify(value, null, indent).replace(/\n/g, indentStr);
8588
recorder.insertRight(
8689
insertIndex,
8790
indentStr
88-
+ `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}`
91+
+ `"${propertyName}":${indent ? ' ' : ''}${content}`
8992
+ ',',
9093
);
9194
}
@@ -98,18 +101,20 @@ export function appendValueInAstArray(
98101
indent = 4,
99102
) {
100103
const indentStr = _buildIndent(indent);
101-
104+
let index = node.start.offset + 1;
102105
if (node.elements.length > 0) {
103106
// Insert comma.
104107
const last = node.elements[node.elements.length - 1];
105-
recorder.insertRight(last.start.offset + last.text.replace(/\s+$/, '').length, ',');
108+
recorder.insertRight(last.end.offset, ',');
109+
index = indent ? last.end.offset + 1 : last.end.offset;
106110
}
107111

108-
recorder.insertLeft(
109-
node.end.offset - 1,
110-
' '
111-
+ JSON.stringify(value, null, 2).replace(/\n/g, indentStr)
112-
+ indentStr.slice(0, -2),
112+
recorder.insertRight(
113+
index,
114+
(node.elements.length === 0 && indent ? '\n' : '')
115+
+ ' '.repeat(indent)
116+
+ JSON.stringify(value, null, indent).replace(/\n/g, indentStr)
117+
+ indentStr.slice(0, -indent),
113118
);
114119
}
115120

@@ -129,5 +134,5 @@ export function findPropertyInAstObject(
129134
}
130135

131136
function _buildIndent(count: number): string {
132-
return '\n' + new Array(count + 1).join(' ');
137+
return count ? '\n' + ' '.repeat(count) : '';
133138
}

packages/schematics/angular/utility/json-utils_spec.ts

+131-51
Original file line numberDiff line numberDiff line change
@@ -5,77 +5,157 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import { parseJsonAst } from '@angular-devkit/core';
9-
import { HostTree } from '@angular-devkit/schematics';
8+
import { JsonAstArray, JsonAstObject, JsonValue, parseJsonAst } from '@angular-devkit/core';
9+
import { HostTree, UpdateRecorder } from '@angular-devkit/schematics';
1010
import { UnitTestTree } from '@angular-devkit/schematics/testing';
11-
import { insertPropertyInAstObjectInOrder } from './json-utils';
12-
13-
type Pojso = {
14-
[key: string]: string;
15-
};
11+
import {
12+
appendPropertyInAstObject,
13+
appendValueInAstArray,
14+
insertPropertyInAstObjectInOrder,
15+
} from './json-utils';
1616

1717
describe('json-utils', () => {
18+
const a = 'a';
19+
const b = 'b';
20+
const m = 'm';
21+
const z = 'z';
1822
const filePath = '/temp';
1923
let tree: UnitTestTree;
24+
25+
function runTest(testFn: Function, obj: JsonValue, indent: number): string {
26+
const content = JSON.stringify(obj, null, indent);
27+
tree.create(filePath, content);
28+
const ast = parseJsonAst(content);
29+
const rec = tree.beginUpdate(filePath);
30+
testFn(rec, ast);
31+
tree.commitUpdate(rec);
32+
33+
const result = tree.readContent(filePath);
34+
// Clean up the tree by deleting the file.
35+
tree.delete(filePath);
36+
37+
return result;
38+
}
39+
2040
beforeEach(() => {
2141
tree = new UnitTestTree(new HostTree());
2242
});
2343

24-
describe('insertPropertyInAstObjectInOrder', () => {
25-
function runTest(obj: Pojso, prop: string, val: string): Pojso {
26-
const content = JSON.stringify(obj, null, 2);
27-
tree.create(filePath, content);
28-
const ast = parseJsonAst(content);
29-
const rec = tree.beginUpdate(filePath);
30-
if (ast.kind === 'object') {
31-
insertPropertyInAstObjectInOrder(rec, ast, prop, val, 2);
44+
describe('appendPropertyInAstObject', () => {
45+
it('should insert multiple props with different indentations', () => {
46+
const cases: Array<[{}, string, {}, number]> = [
47+
// initial | prop | want | indent
48+
[{}, z, {z}, 0],
49+
[{z}, m, {z, m}, 0],
50+
[{m, z}, a, {m, z, a}, 0],
51+
[{a, m, z}, b, {a, m, z, b}, 0],
52+
[{}, z, {z}, 2],
53+
[{z}, m, {z, m}, 2],
54+
[{m, z}, a, {m, z, a}, 2],
55+
[{a, m, z}, b, {a, m, z, b}, 2],
56+
[{}, z, {z}, 4],
57+
[{z}, m, {z, m}, 4],
58+
[{m, z}, a, {m, z, a}, 4],
59+
[{a, m, z}, b, {a, m, z, b}, 4],
60+
];
61+
for (const c of cases) {
62+
const [initial, prop, want, indent] = c;
63+
const got = runTest((rec: UpdateRecorder, ast: JsonAstObject) => {
64+
expect(ast.kind).toBe('object');
65+
appendPropertyInAstObject(rec, ast, prop, prop, indent);
66+
}, initial, indent);
67+
expect(got).toBe(JSON.stringify(want, null, indent));
68+
expect(JSON.parse(got)).toEqual(want);
3269
}
33-
tree.commitUpdate(rec);
34-
35-
const result = JSON.parse(tree.readContent(filePath));
36-
// Clean up the tree by deleting the file.
37-
tree.delete(filePath);
38-
39-
return result;
40-
}
70+
});
71+
});
4172

73+
describe('insertPropertyInAstObjectInOrder', () => {
4274
it('should insert a first prop', () => {
43-
const obj = {
44-
m: 'm',
45-
z: 'z',
46-
};
47-
const result = runTest(obj, 'a', 'val');
48-
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
75+
const indent = 2;
76+
const result = runTest((rec: UpdateRecorder, ast: JsonAstObject) => {
77+
expect(ast.kind).toBe('object');
78+
insertPropertyInAstObjectInOrder(rec, ast, a, a, indent);
79+
}, {m, z}, indent);
80+
expect(result).toBe(JSON.stringify({a, m, z}, null, indent));
81+
expect(JSON.parse(result)).toEqual({a, m, z});
4982
});
5083

5184
it('should insert a middle prop', () => {
52-
const obj = {
53-
a: 'a',
54-
z: 'z',
55-
};
56-
const result = runTest(obj, 'm', 'val');
57-
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
85+
const indent = 2;
86+
const result = runTest((rec: UpdateRecorder, ast: JsonAstObject) => {
87+
expect(ast.kind).toBe('object');
88+
insertPropertyInAstObjectInOrder(rec, ast, m, m, indent);
89+
}, {a, z}, indent);
90+
expect(result).toBe(JSON.stringify({a, m, z}, null, indent));
91+
expect(JSON.parse(result)).toEqual({a, m, z});
5892
});
5993

6094
it('should insert a last prop', () => {
61-
const obj = {
62-
a: 'a',
63-
m: 'm',
64-
};
65-
const result = runTest(obj, 'z', 'val');
66-
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
95+
const indent = 2;
96+
const result = runTest((rec: UpdateRecorder, ast: JsonAstObject) => {
97+
expect(ast.kind).toBe('object');
98+
insertPropertyInAstObjectInOrder(rec, ast, z, z, indent);
99+
}, {a, m}, indent);
100+
expect(result).toBe(JSON.stringify({a, m, z}, null, indent));
101+
expect(JSON.parse(result)).toEqual({a, m, z});
102+
});
103+
104+
it('should insert multiple props with different indentations', () => {
105+
const cases: Array<[{}, string, {}, number]> = [
106+
// initial | prop | want | indent
107+
[{}, z, {z}, 0],
108+
[{z}, m, {m, z}, 0],
109+
[{m, z}, a, {a, m, z}, 0],
110+
[{a, m, z}, b, {a, b, m, z}, 0],
111+
[{}, z, {z}, 2],
112+
[{z}, m, {m, z}, 2],
113+
[{m, z}, a, {a, m, z}, 2],
114+
[{a, m, z}, b, {a, b, m, z}, 2],
115+
[{}, z, {z}, 4],
116+
[{z}, m, {m, z}, 4],
117+
[{m, z}, a, {a, m, z}, 4],
118+
[{a, m, z}, b, {a, b, m, z}, 4],
119+
];
120+
for (const c of cases) {
121+
const [initial, prop, want, indent] = c;
122+
const got = runTest((rec: UpdateRecorder, ast: JsonAstObject) => {
123+
expect(ast.kind).toBe('object');
124+
insertPropertyInAstObjectInOrder(rec, ast, prop, prop, indent);
125+
}, initial, indent);
126+
expect(got).toBe(JSON.stringify(want, null, indent));
127+
expect(JSON.parse(got)).toEqual(want);
128+
}
67129
});
130+
});
68131

69-
it('should insert multiple props', () => {
70-
let obj = {};
71-
obj = runTest(obj, 'z', 'val');
72-
expect(Object.keys(obj)).toEqual(['z']);
73-
obj = runTest(obj, 'm', 'val');
74-
expect(Object.keys(obj)).toEqual(['m', 'z']);
75-
obj = runTest(obj, 'a', 'val');
76-
expect(Object.keys(obj)).toEqual(['a', 'm', 'z']);
77-
obj = runTest(obj, 'b', 'val');
78-
expect(Object.keys(obj)).toEqual(['a', 'b', 'm', 'z']);
132+
describe('appendValueInAstArray', () => {
133+
it('should insert multiple props with different indentations', () => {
134+
const cases: Array<[string[], string, {}, number]> = [
135+
// initial | value | want | indent
136+
[[], z, [z], 0],
137+
[[z], m, [z, m], 0],
138+
[[m, z], a, [m, z, a], 0],
139+
[[a, m, z], b, [a, m, z, b], 0],
140+
[[], z, [z], 2],
141+
[[z], m, [z, m], 2],
142+
[[m, z], a, [m, z, a], 2],
143+
[[a, m, z], b, [a, m, z, b], 2],
144+
[[], z, [z], 4],
145+
[[z], m, [z, m], 4],
146+
[[m, z], a, [m, z, a], 4],
147+
[[a, m, z], b, [a, m, z, b], 4],
148+
];
149+
for (const c of cases) {
150+
const [initial, value, want, indent] = c;
151+
const got = runTest((rec: UpdateRecorder, ast: JsonAstArray) => {
152+
expect(ast.kind).toBe('array');
153+
appendValueInAstArray(rec, ast, value, indent);
154+
}, initial, indent);
155+
expect(got).toBe(JSON.stringify(want, null, indent));
156+
expect(JSON.parse(got)).toEqual(want);
157+
}
79158
});
80159
});
160+
81161
});

0 commit comments

Comments
 (0)