Skip to content

Commit 05a554f

Browse files
committed
Temporarily add invariant codemod script
I'm adding this codemod to the repo temporarily, but I'll revert it in the same PR. That way we don't have to check it in but it's still accessible (via the PR) if we need it later.
1 parent e1d6cd6 commit 05a554f

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

.eslintrc.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,16 @@ module.exports = {
249249
TaskController: true,
250250
},
251251
},
252+
// TODO: I'm adding this plugin to the repo temporarily, but I'll revert it
253+
// in the same PR. That way we don't have to check it in but it's still
254+
// accessible (via the PR) if we need it later.
255+
{
256+
files: ['packages/**/*.js'],
257+
plugins: ['unused-imports'],
258+
rules: {
259+
'unused-imports/no-unused-imports': ERROR,
260+
},
261+
},
252262
],
253263

254264
globals: {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
6262
"eslint-plugin-react": "^6.7.1",
6363
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
64+
"eslint-plugin-unused-imports": "^1.1.5",
6465
"fbjs-scripts": "1.2.0",
6566
"filesize": "^6.0.1",
6667
"flow-bin": "0.97",

scripts/codemod-invariant.js

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
'use strict';
2+
3+
export const parser = 'flow';
4+
5+
export default function transformer(file, api) {
6+
const j = api.jscodeshift;
7+
8+
function evalToStringArray(ast, allArgs) {
9+
let result;
10+
switch (ast.type) {
11+
case 'StringLiteral':
12+
case 'Literal': {
13+
const formatString = ast.value;
14+
const quasis = formatString.split('%s').map(raw => {
15+
// This isn't a generally correct solution for escaping backticks
16+
// because it doesn't account for backticks that are already escape
17+
// but it's is good enough for this codemod since none of our existing
18+
// invariant messages do that. And the error code lint rule will
19+
// catch mistakes.
20+
const stringWithBackticksEscaped = raw.replace(/`/g, '\\`');
21+
return j.templateElement(
22+
{
23+
raw: stringWithBackticksEscaped,
24+
cooked: stringWithBackticksEscaped,
25+
},
26+
false
27+
);
28+
});
29+
const numberOfQuasis = quasis.length;
30+
if (numberOfQuasis === 1) {
31+
result = ast;
32+
break;
33+
}
34+
const numberOfArgs = numberOfQuasis - 1;
35+
const args = allArgs.slice(0, numberOfArgs);
36+
allArgs.splice(0, numberOfArgs);
37+
result = j.templateLiteral(quasis, args);
38+
break;
39+
}
40+
case 'BinaryExpression': // `+`
41+
if (ast.operator !== '+') {
42+
throw new Error('Unsupported binary operator ' + ast.operator);
43+
}
44+
result = j.binaryExpression(
45+
'+',
46+
evalToStringArray(ast.left, allArgs),
47+
evalToStringArray(ast.right, allArgs)
48+
);
49+
break;
50+
default:
51+
throw new Error('Unsupported type ' + ast.type);
52+
}
53+
54+
result.comments = ast.comments;
55+
return result;
56+
}
57+
58+
function invertCondition(cond) {
59+
let invertedCond;
60+
let isUnsafeInversion = false;
61+
if (cond.type === 'UnaryExpression' && cond.operator === '!') {
62+
invertedCond = cond.argument;
63+
} else if (cond.type === 'BinaryExpression') {
64+
switch (cond.operator) {
65+
case '==': {
66+
invertedCond = j.binaryExpression('!=', cond.left, cond.right);
67+
break;
68+
}
69+
case '!=': {
70+
invertedCond = j.binaryExpression('==', cond.left, cond.right);
71+
break;
72+
}
73+
case '===': {
74+
invertedCond = j.binaryExpression('!==', cond.left, cond.right);
75+
break;
76+
}
77+
case '!==': {
78+
invertedCond = j.binaryExpression('===', cond.left, cond.right);
79+
break;
80+
}
81+
case '<': {
82+
invertedCond = j.binaryExpression('>=', cond.left, cond.right);
83+
isUnsafeInversion = true;
84+
break;
85+
}
86+
case '<=': {
87+
invertedCond = j.binaryExpression('>', cond.left, cond.right);
88+
isUnsafeInversion = true;
89+
break;
90+
}
91+
case '>': {
92+
invertedCond = j.binaryExpression('<=', cond.left, cond.right);
93+
isUnsafeInversion = true;
94+
break;
95+
}
96+
case '>=': {
97+
invertedCond = j.binaryExpression('<', cond.left, cond.right);
98+
isUnsafeInversion = true;
99+
break;
100+
}
101+
default: {
102+
invertedCond = j.unaryExpression('!', cond);
103+
break;
104+
}
105+
}
106+
} else if (cond.type === 'LogicalExpression') {
107+
switch (cond.operator) {
108+
case '&&': {
109+
const [invertedLeft, leftInversionIsUnsafe] = invertCondition(
110+
cond.left
111+
);
112+
const [invertedRight, rightInversionIsUnsafe] = invertCondition(
113+
cond.right
114+
);
115+
if (leftInversionIsUnsafe || rightInversionIsUnsafe) {
116+
isUnsafeInversion = true;
117+
}
118+
invertedCond = j.logicalExpression('||', invertedLeft, invertedRight);
119+
break;
120+
}
121+
case '||': {
122+
const [invertedLeft, leftInversionIsUnsafe] = invertCondition(
123+
cond.left
124+
);
125+
const [invertedRight, rightInversionIsUnsafe] = invertCondition(
126+
cond.right
127+
);
128+
if (leftInversionIsUnsafe || rightInversionIsUnsafe) {
129+
isUnsafeInversion = true;
130+
}
131+
invertedCond = j.logicalExpression('&&', invertedLeft, invertedRight);
132+
break;
133+
}
134+
default: {
135+
invertedCond = j.unaryExpression('!', cond);
136+
break;
137+
}
138+
}
139+
} else {
140+
invertedCond = j.unaryExpression('!', cond);
141+
}
142+
invertedCond.comments = cond.comments;
143+
return [invertedCond, isUnsafeInversion];
144+
}
145+
146+
let didTransform = false;
147+
const transformed = j(file.source)
148+
.find(j.ExpressionStatement)
149+
.forEach(path => {
150+
const invariantCall = path.node.expression;
151+
if (
152+
invariantCall.type !== 'CallExpression' ||
153+
invariantCall.callee.name !== 'invariant'
154+
) {
155+
return;
156+
}
157+
didTransform = true;
158+
const [cond, msgFormatAst, ...args] = invariantCall.arguments;
159+
const msgFormatStrings = evalToStringArray(msgFormatAst, args);
160+
161+
const throwStatement = j.throwStatement(
162+
j.newExpression(j.identifier('Error'), [msgFormatStrings])
163+
);
164+
165+
const [invertedCond, isUnsafeInversion] = invertCondition(cond);
166+
167+
const originalComments = path.node.comments;
168+
if (cond.type === 'Literal' && cond.value === false) {
169+
throwStatement.comments = originalComments;
170+
j(path).replaceWith(throwStatement);
171+
} else {
172+
const ifStatement = j.ifStatement(
173+
invertedCond,
174+
j.blockStatement([throwStatement])
175+
);
176+
if (isUnsafeInversion) {
177+
ifStatement.comments = [
178+
...(originalComments || []),
179+
j.line(' FIXME: Review this condition before merging '),
180+
j.line(
181+
` Should be equivalent to: ${j(
182+
j.unaryExpression('!', cond)
183+
).toSource()} `
184+
),
185+
];
186+
} else {
187+
ifStatement.comments = originalComments;
188+
}
189+
j(path).replaceWith(ifStatement);
190+
}
191+
});
192+
193+
if (didTransform) {
194+
return transformed.toSource();
195+
}
196+
return null;
197+
}

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6447,6 +6447,7 @@ [email protected]:
64476447

64486448
"eslint-plugin-react-internal@link:./scripts/eslint-rules":
64496449
version "0.0.0"
6450+
uid ""
64506451

64516452
eslint-plugin-react@^6.7.1:
64526453
version "6.10.3"
@@ -6459,6 +6460,13 @@ eslint-plugin-react@^6.7.1:
64596460
jsx-ast-utils "^1.3.4"
64606461
object.assign "^4.0.4"
64616462

6463+
eslint-plugin-unused-imports@^1.1.5:
6464+
version "1.1.5"
6465+
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.1.5.tgz#a2b992ef0faf6c6c75c3815cc47bde76739513c2"
6466+
integrity sha512-TeV8l8zkLQrq9LBeYFCQmYVIXMjfHgdRQLw7dEZp4ZB3PeR10Y5Uif11heCsHRmhdRIYMoewr1d9ouUHLbLHew==
6467+
dependencies:
6468+
eslint-rule-composer "^0.3.0"
6469+
64626470
eslint-rule-composer@^0.3.0:
64636471
version "0.3.0"
64646472
resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"

0 commit comments

Comments
 (0)