Skip to content

Commit 6a92683

Browse files
author
Josh Goldberg
authored
Used TypeScript transform for JSON patching (#1160)
* WIP: JSON patching * Fixed up tests * Looks like my Prettier hook is not running locally or in CI
1 parent 7a48cb6 commit 6a92683

File tree

2 files changed

+219
-68
lines changed

2 files changed

+219
-68
lines changed

src/converters/editorConfigs/converters/convertVSCodeConfig.test.ts

Lines changed: 101 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,87 @@ describe("convertVSCodeConfig", () => {
1010
const editorSettings = { unrelated: true };
1111

1212
// Act
13-
const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings);
13+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
1414

1515
// Assert
16-
expect(result).toEqual({
17-
contents: JSON.stringify(editorSettings, null, 4),
18-
missing: [],
19-
});
16+
expect(result).toMatchInlineSnapshot(`
17+
Object {
18+
"contents": "{
19+
\\"unrelated\\": true
20+
}",
21+
"missing": Array [],
22+
}
23+
`);
2024
});
2125

22-
it("includes eslint.autoFixOnSave when source.fixAll.tslint exists", () => {
26+
it("preserves original settings when the input structure is not an object", () => {
27+
// Arrange
28+
const editorSettings: never[] = [];
29+
30+
// Act
31+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
32+
33+
// Assert
34+
expect(result).toMatchInlineSnapshot(`
35+
Object {
36+
"contents": "[]",
37+
"missing": Array [],
38+
}
39+
`);
40+
});
41+
42+
it("does not include eslint.autoFixOnSave when source.fixAll.tslint is false", () => {
2343
// Arrange
2444
const editorSettings = {
2545
"editor.codeActionsOnSave": {
26-
"source.fixAll.tslint": true,
46+
"source.fixAll.tslint": false,
2747
},
2848
unrelated: true,
2949
};
3050

3151
// Act
32-
const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings);
52+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
3353

3454
// Assert
35-
expect(result).toEqual({
36-
contents: JSON.stringify(
37-
{
38-
"editor.codeActionsOnSave": {
39-
"source.fixAll.tslint": true,
40-
"eslint.autoFixOnSave": true,
41-
},
42-
unrelated: true,
43-
},
44-
null,
45-
4,
46-
),
47-
missing: [],
48-
});
55+
expect(result).toMatchInlineSnapshot(`
56+
Object {
57+
"contents": "{
58+
\\"editor.codeActionsOnSave\\": {
59+
\\"source.fixAll.tslint\\": false
60+
},
61+
\\"unrelated\\": true
62+
}",
63+
"missing": Array [],
64+
}
65+
`);
66+
});
67+
68+
it("includes eslint.autoFixOnSave when source.fixAll.tslint is true", () => {
69+
// Arrange
70+
const editorSettings = {
71+
"editor.codeActionsOnSave": {
72+
"source.fixAll.tslint": true,
73+
},
74+
unrelated: false,
75+
};
76+
77+
// Act
78+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
79+
80+
// Assert
81+
expect(result).toMatchInlineSnapshot(`
82+
Object {
83+
"contents": "{
84+
\\"editor.codeActionsOnSave\\": {
85+
\\"source.fixAll.tslint\\": true,
86+
\\"eslint.autoFixOnSave\\": true
87+
},
88+
\\"unrelated\\": false
89+
}
90+
",
91+
"missing": Array [],
92+
}
93+
`);
4994
});
5095

5196
it("does not include configFile when tslint.configFile does not match the output config", () => {
@@ -56,7 +101,7 @@ describe("convertVSCodeConfig", () => {
56101
};
57102

58103
// Act
59-
const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings);
104+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
60105

61106
// Assert
62107
expect(result).toEqual({
@@ -73,23 +118,22 @@ describe("convertVSCodeConfig", () => {
73118
};
74119

75120
// Act
76-
const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings);
121+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
77122

78123
// Assert
79-
expect(result).toEqual({
80-
contents: JSON.stringify(
81-
{
82-
"tslint.configFile": "./tslint.json",
83-
unrelated: true,
84-
"eslint.options": {
85-
configFile: stubSettings.config,
86-
},
87-
},
88-
null,
89-
4,
90-
),
91-
missing: [],
92-
});
124+
expect(result).toMatchInlineSnapshot(`
125+
Object {
126+
"contents": "{
127+
\\"tslint.configFile\\": \\"./tslint.json\\",
128+
\\"unrelated\\": true,
129+
\\"eslint.options\\": {
130+
\\"configFile\\": \\".eslintrc.js\\"
131+
}
132+
}
133+
",
134+
"missing": Array [],
135+
}
136+
`);
93137
});
94138

95139
it("includes missing notices when known missing settings are included", () => {
@@ -103,18 +147,26 @@ describe("convertVSCodeConfig", () => {
103147
};
104148

105149
// Act
106-
const result = convertVSCodeConfig(JSON.stringify(editorSettings), stubSettings);
150+
const result = convertVSCodeConfig(JSON.stringify(editorSettings, null, 4), stubSettings);
107151

108152
// Assert
109-
expect(result).toEqual({
110-
contents: JSON.stringify(editorSettings, null, 4),
111-
missing: [
112-
"tslint.alwaysShowRuleFailuresAsWarnings",
113-
"tslint.exclude",
114-
"tslint.ignoreDefinitionFiles",
115-
"tslint.jsEnable",
116-
"tslint.suppressWhileTypeErrorsPresent",
117-
],
118-
});
153+
expect(result).toMatchInlineSnapshot(`
154+
Object {
155+
"contents": "{
156+
\\"tslint.alwaysShowRuleFailuresAsWarnings\\": true,
157+
\\"tslint.exclude\\": true,
158+
\\"tslint.ignoreDefinitionFiles\\": true,
159+
\\"tslint.jsEnable\\": true,
160+
\\"tslint.suppressWhileTypeErrorsPresent\\": true
161+
}",
162+
"missing": Array [
163+
"tslint.alwaysShowRuleFailuresAsWarnings",
164+
"tslint.exclude",
165+
"tslint.ignoreDefinitionFiles",
166+
"tslint.jsEnable",
167+
"tslint.suppressWhileTypeErrorsPresent",
168+
],
169+
}
170+
`);
119171
});
120172
});
Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { merge } from "lodash";
21
import * as path from "path";
2+
import * as ts from "typescript";
33

44
import { parseJson } from "../../../utils";
55
import { EditorConfigConverter } from "../types";
@@ -14,6 +14,8 @@ const knownMissingSettings = [
1414

1515
export const convertVSCodeConfig: EditorConfigConverter = (rawEditorSettings, settings) => {
1616
const editorSettings: Record<string, string | number | symbol> = parseJson(rawEditorSettings);
17+
const missing = knownMissingSettings.filter((setting) => editorSettings[setting]);
18+
1719
const autoFixOnSave =
1820
editorSettings["editor.codeActionsOnSave"] &&
1921
typeof editorSettings["editor.codeActionsOnSave"] === "object" &&
@@ -28,26 +30,123 @@ export const convertVSCodeConfig: EditorConfigConverter = (rawEditorSettings, se
2830
path.dirname(settings.config),
2931
);
3032

31-
const contents = JSON.stringify(
32-
merge(
33-
{},
34-
editorSettings,
35-
autoFixOnSave !== undefined && {
36-
"editor.codeActionsOnSave": {
37-
"eslint.autoFixOnSave": autoFixOnSave,
38-
},
39-
},
40-
eslintPathMatches && {
41-
"eslint.options": {
42-
configFile: settings.config,
43-
},
44-
},
45-
),
46-
null,
47-
4,
33+
// We can bail without making changes if there are no changes we need to make
34+
if (!autoFixOnSave && !eslintPathMatches) {
35+
return { contents: rawEditorSettings, missing };
36+
}
37+
38+
// Since we've found at least one matching setting, we know the source structure is a proper {}
39+
const sourceFile = ts.createSourceFile(
40+
"settings.json",
41+
rawEditorSettings,
42+
ts.ScriptTarget.Latest,
43+
/*setParentNodes*/ true,
44+
ts.ScriptKind.JSON,
4845
);
46+
const jsonRoot = (sourceFile.statements[0] as ts.ExpressionStatement)
47+
.expression as ts.ObjectLiteralExpression;
4948

50-
const missing = knownMissingSettings.filter((setting) => editorSettings[setting]);
49+
const propertyIndexByName = (
50+
properties: ts.NodeArray<ts.ObjectLiteralElementLike>,
51+
name: string,
52+
) =>
53+
properties.findIndex(
54+
(property) =>
55+
property.name && ts.isStringLiteral(property.name) && property.name.text === name,
56+
);
57+
58+
const transformer =
59+
(context: ts.TransformationContext) =>
60+
(rootNode: ts.SourceFile): ts.SourceFile => {
61+
const upsertProperties = (
62+
node: ts.ObjectLiteralExpression,
63+
additions: readonly [string, string, unknown][],
64+
) => {
65+
const originalProperties = node.properties;
66+
67+
for (const [parent, setting, value] of additions) {
68+
const createNewChild = (
69+
properties?: ts.NodeArray<ts.ObjectLiteralElementLike>,
70+
) => {
71+
return context.factory.createPropertyAssignment(
72+
`"${parent}"`,
73+
context.factory.createObjectLiteralExpression(
74+
[
75+
...(properties ?? []),
76+
context.factory.createPropertyAssignment(
77+
`"${setting}"`,
78+
typeof value === "string"
79+
? context.factory.createStringLiteral(value)
80+
: context.factory.createTrue(),
81+
),
82+
],
83+
true,
84+
),
85+
);
86+
};
87+
88+
const existingIndex = propertyIndexByName(originalProperties, parent);
89+
90+
if (existingIndex !== -1) {
91+
const existingProperty = originalProperties[existingIndex];
92+
const updatedProperties = [...node.properties];
93+
94+
// We know these casts should be safe because we previously found a matching parent object for the property
95+
updatedProperties[existingIndex] = createNewChild(
96+
(
97+
(existingProperty as ts.PropertyAssignment)
98+
.initializer as ts.ObjectLiteralExpression
99+
).properties as ts.NodeArray<ts.ObjectLiteralElementLike> | undefined,
100+
);
101+
node = context.factory.createObjectLiteralExpression(
102+
updatedProperties,
103+
true,
104+
);
105+
} else {
106+
node = context.factory.createObjectLiteralExpression(
107+
[...node.properties, createNewChild()],
108+
true,
109+
);
110+
}
111+
}
112+
113+
return node;
114+
};
115+
116+
const visit = (node: ts.Node) => {
117+
node = ts.visitEachChild(node, visit, context);
118+
119+
if (node !== jsonRoot) {
120+
return node;
121+
}
122+
123+
const additions: [string, string, unknown][] = [];
124+
125+
if (autoFixOnSave !== undefined) {
126+
additions.push([
127+
"editor.codeActionsOnSave",
128+
"eslint.autoFixOnSave",
129+
autoFixOnSave,
130+
]);
131+
}
132+
133+
if (eslintPathMatches !== undefined) {
134+
additions.push(["eslint.options", "configFile", settings.config]);
135+
}
136+
137+
return upsertProperties(jsonRoot, additions);
138+
};
139+
140+
return ts.visitNode(rootNode, visit);
141+
};
142+
143+
const printer = ts.createPrinter(undefined);
144+
const result = ts.transform(sourceFile, [transformer]);
145+
const contents = printer
146+
.printFile(result.transformed[0])
147+
.replace(/^\(/giu, "")
148+
.replace(/\);(\r\n|\r|\n)*$/giu, "$1");
149+
result.dispose();
51150

52151
return { contents, missing };
53152
};

0 commit comments

Comments
 (0)