Skip to content

Commit 0b90d06

Browse files
committed
Add support for ref types
1 parent ce001f4 commit 0b90d06

File tree

6 files changed

+169
-23
lines changed

6 files changed

+169
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## Unreleased
22

33
- Added
4-
- Add support for refs
4+
- Add support for refs (types are supported as well)
55
- Add support for state types
66
- Add support for opt-out in one of:
77
- `@abstract` JSDoc comment

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const C = props => {
7070
- [x] Add `React.FC` annotation
7171
- [x] Transform `P` type argument
7272
- [x] Transform `S` type argument
73-
- [ ] Transform ref types
73+
- [x] Transform ref types
7474
- [ ] Transform generic components
7575
- [ ] Modify Props appropriately if defaultProps is present
7676
- [ ] Modify Props appropriately if `children` seems to be used

src/analysis/this_fields.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { NodePath } from "@babel/core";
2-
import type { AssignmentExpression, CallExpression, ClassAccessorProperty, ClassDeclaration, ClassMethod, ClassPrivateMethod, ClassPrivateProperty, ClassProperty, Expression, ExpressionStatement, MemberExpression, ThisExpression, TSDeclareMethod } from "@babel/types";
2+
import type { AssignmentExpression, CallExpression, ClassAccessorProperty, ClassDeclaration, ClassMethod, ClassPrivateMethod, ClassPrivateProperty, ClassProperty, Expression, ExpressionStatement, MemberExpression, ThisExpression, TSDeclareMethod, TSType } from "@babel/types";
33
import { getOr, isClassAccessorProperty, isClassMethodLike, isClassMethodOrDecl, isClassPropertyLike, isNamedClassElement, isStaticBlock, memberName, memberRefName, nonNullPath } from "../utils.js";
44
import { AnalysisError } from "./error.js";
55

@@ -11,19 +11,27 @@ export type ThisFields = {
1111
export type ThisFieldSite = {
1212
type: "class_field";
1313
path: NodePath<ClassProperty | ClassPrivateProperty | ClassMethod | ClassPrivateMethod | ClassAccessorProperty | TSDeclareMethod | AssignmentExpression>;
14-
hasType: boolean;
14+
typing: FieldTyping | undefined;
1515
init: FieldInit | undefined;
1616
hasWrite: undefined;
1717
hasSideEffect: boolean;
1818
} | {
1919
type: "expr";
2020
path: NodePath<MemberExpression>;
21-
hasType: undefined;
21+
typing: undefined;
2222
init: undefined;
2323
hasWrite: boolean;
2424
hasSideEffect: undefined;
2525
};
2626

27+
export type FieldTyping = {
28+
type: "type_value";
29+
valueTypePath: NodePath<TSType>;
30+
} | {
31+
type: "type_method";
32+
methodDeclPath: NodePath<TSDeclareMethod>;
33+
}
34+
2735
export type FieldInit = {
2836
type: "init_value";
2937
valuePath: NodePath<Expression>
@@ -35,7 +43,7 @@ export type FieldInit = {
3543
export type StaticFieldSite = {
3644
type: "class_field";
3745
path: NodePath<ClassProperty | ClassPrivateProperty | ClassMethod | ClassPrivateMethod | ClassAccessorProperty | TSDeclareMethod | AssignmentExpression>;
38-
hasType: boolean;
46+
typing: FieldTyping | undefined;
3947
init: FieldInit | undefined;
4048
hasWrite: undefined;
4149
hasSideEffect: boolean;
@@ -59,10 +67,17 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
5967
const field = isStatic ? getStaticField(name) : getThisField(name);
6068
if (isClassPropertyLike(itemPath)) {
6169
const valuePath = nonNullPath<Expression>(itemPath.get("value"));
70+
const typeAnnotation = itemPath.get("typeAnnotation");
71+
const typeAnnotation_ = typeAnnotation.isTSTypeAnnotation() ? typeAnnotation : undefined;
6272
field.push({
6373
type: "class_field",
6474
path: itemPath,
65-
hasType: !!itemPath.node.typeAnnotation,
75+
typing: typeAnnotation_
76+
? {
77+
type: "type_value",
78+
valueTypePath: typeAnnotation_.get("typeAnnotation"),
79+
}
80+
: undefined,
6681
init: valuePath ? { type: "init_value", valuePath } : undefined,
6782
hasWrite: undefined,
6883
hasSideEffect: !!itemPath.node.value && estimateSideEffect(itemPath.node.value),
@@ -76,7 +91,12 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
7691
field.push({
7792
type: "class_field",
7893
path: itemPath,
79-
hasType: itemPath.isTSDeclareMethod(),
94+
typing: itemPath.isTSDeclareMethod()
95+
? {
96+
type: "type_method",
97+
methodDeclPath: itemPath,
98+
}
99+
: undefined,
80100
init: isClassMethodLike(itemPath)
81101
? { type: "init_method", methodPath: itemPath }
82102
: undefined,
@@ -171,7 +191,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
171191
field.push({
172192
type: "class_field",
173193
path: exprPath,
174-
hasType: false,
194+
typing: undefined,
175195
init: {
176196
type: "init_value",
177197
valuePath: exprPath.get("right"),
@@ -216,7 +236,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
216236
field.push({
217237
type: "expr",
218238
path: thisMemberPath,
219-
hasType: undefined,
239+
typing: undefined,
220240
init: undefined,
221241
hasWrite,
222242
hasSideEffect: undefined
@@ -235,7 +255,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
235255
if (numInits > 1) {
236256
throw new AnalysisError(`${name} is initialized more than once`);
237257
}
238-
const numTypes = fieldSites.reduce((n, site) => n + Number(!!site.hasType), 0);
258+
const numTypes = fieldSites.reduce((n, site) => n + Number(!!site.typing), 0);
239259
if (numTypes > 1) {
240260
throw new AnalysisError(`${name} is declared more than once`);
241261
}
@@ -248,7 +268,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
248268
if (numInits > 1) {
249269
throw new AnalysisError(`static ${name} is initialized more than once`);
250270
}
251-
const numTypes = fieldSites.reduce((n, site) => n + Number(!!site.hasType), 0);
271+
const numTypes = fieldSites.reduce((n, site) => n + Number(!!site.typing), 0);
252272
if (numTypes > 1) {
253273
throw new AnalysisError(`static ${name} is declared more than once`);
254274
}

src/analysis/user_defined.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { NodePath } from "@babel/core";
2-
import { ArrowFunctionExpression, ClassMethod, ClassPrivateMethod, Expression, FunctionExpression } from "@babel/types";
3-
import { isClassMethodLike } from "../utils.js";
2+
import { ArrowFunctionExpression, ClassMethod, ClassPrivateMethod, Expression, FunctionExpression, TSType } from "@babel/types";
3+
import { isClassMethodLike, nonNullPath } from "../utils.js";
44
import { AnalysisError } from "./error.js";
55
import { analyzeLibRef, isReactRef } from "./lib.js";
66
import type { ThisFieldSite } from "./this_fields.js";
@@ -48,12 +48,14 @@ export type UserDefined =
4848
export type UserDefinedRef = {
4949
type: "user_defined_ref";
5050
localName?: string | undefined;
51+
typeAnnotation?: NodePath<TSType> | undefined;
5152
sites: ThisFieldSite[];
5253
};
5354
export type UserDefinedDirectRef = {
5455
type: "user_defined_direct_ref";
5556
localName?: string | undefined;
5657
init: NodePath<Expression>;
58+
typeAnnotation?: NodePath<TSType> | undefined;
5759
sites: ThisFieldSite[];
5860
};
5961
export type UserDefinedFn = {
@@ -81,7 +83,10 @@ export function analyzeUserDefined(
8183
}
8284
let fnInit: FnInit | undefined = undefined;
8385
let isRefInit = false;
86+
let refInitType1: NodePath<TSType> | undefined = undefined;
87+
let refInitType2: NodePath<TSType> | undefined = undefined;
8488
let valInit: NodePath<Expression> | undefined = undefined;
89+
let valInitType: NodePath<TSType> | undefined = undefined;
8590
const initSite = fieldSites.find((site) => site.init);
8691
if (initSite) {
8792
const init = initSite.init!;
@@ -108,11 +113,48 @@ export function analyzeUserDefined(
108113
throw new AnalysisError("Extra arguments to createRef");
109114
}
110115
isRefInit = true;
116+
const typeParameters = nonNullPath(initPath.get("typeParameters"));
117+
if (typeParameters) {
118+
const params = typeParameters.get("params");
119+
if (params.length > 0) {
120+
// this.foo = React.createRef<HTMLDivElement>();
121+
// ^^^^^^^^^^^^^^
122+
refInitType1 = params[0]!;
123+
}
124+
}
111125
}
112126
}
113127
valInit = initPath;
114128
}
115129
}
130+
const typeSite = fieldSites.find((site) => site.typing);
131+
if (typeSite) {
132+
const typing = typeSite.typing!;
133+
if (typing.type === "type_value") {
134+
if (typing.valueTypePath.isTSTypeReference()) {
135+
const lastName =
136+
typing.valueTypePath.node.typeName.type === "Identifier"
137+
? typing.valueTypePath.node.typeName.name
138+
: typing.valueTypePath.node.typeName.right.name;
139+
const typeParameters = nonNullPath(typing.valueTypePath.get("typeParameters"));
140+
if (lastName === "RefObject" && typeParameters) {
141+
const params = typeParameters.get("params");
142+
if (params.length > 0) {
143+
// class C {
144+
// foo: React.RefObject<HTMLDivElement>;
145+
// ^^^^^^^^^^^^^^
146+
// }
147+
refInitType2 = params[0]!;
148+
}
149+
}
150+
}
151+
// class C {
152+
// foo: HTMLDivElement | null;
153+
// ^^^^^^^^^^^^^^^^^^^^^
154+
// }
155+
valInitType = typing.valueTypePath;
156+
}
157+
}
116158
const hasWrite = fieldSites.some((site) => site.hasWrite);
117159
if (fnInit && !hasWrite) {
118160
fields.set(name, {
@@ -123,12 +165,14 @@ export function analyzeUserDefined(
123165
} else if (isRefInit && !hasWrite) {
124166
fields.set(name, {
125167
type: "user_defined_ref",
168+
typeAnnotation: refInitType1 ?? refInitType2,
126169
sites: fieldSites,
127170
});
128171
} else if (valInit) {
129172
fields.set(name, {
130173
type: "user_defined_direct_ref",
131174
init: valInit,
175+
typeAnnotation: valInitType,
132176
sites: fieldSites,
133177
});
134178
} else {

src/index.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function transform(code: string, options: {
1212
babelrc: false,
1313
filename: ts ? "file.tsx" : "file.jsx",
1414
parserOpts: {
15-
plugins: ["jsx", "typescript"],
15+
plugins: ts ? ["jsx", "typescript"] : ["jsx"],
1616
},
1717
plugins: [plugin],
1818
});
@@ -771,6 +771,40 @@ describe("react-declassify", () => {
771771
expect(transform(input)).toBe(output);
772772
});
773773

774+
it("transforms typed createRef as useRef", () => {
775+
const input = dedent`\
776+
class C extends React.Component {
777+
button: React.RefObject<HTMLButtonElement>
778+
constructor(props) {
779+
super(props);
780+
this.div = React.createRef<HTMLDivElement>();
781+
this.button = React.createRef();
782+
}
783+
784+
foo() {
785+
console.log(this.div.current);
786+
}
787+
788+
render() {
789+
return <div ref={this.div} />;
790+
}
791+
}
792+
`;
793+
const output = dedent`\
794+
const C: React.FC = () => {
795+
const button = React.useRef<HTMLButtonElement>(null);
796+
797+
function foo() {
798+
console.log(div.current);
799+
}
800+
801+
const div = React.useRef<HTMLDivElement>(null);
802+
return <div ref={div} />;
803+
};
804+
`;
805+
expect(transform(input, { ts: true })).toBe(output);
806+
});
807+
774808
it("transforms class field as useRef", () => {
775809
const input = dedent`\
776810
class C extends React.Component {
@@ -801,6 +835,38 @@ describe("react-declassify", () => {
801835
expect(transform(input)).toBe(output);
802836
});
803837

838+
it("transforms typed class field as useRef", () => {
839+
const input = dedent`\
840+
class C extends React.Component {
841+
div: HTMLDivElement | null;
842+
constructor(props) {
843+
super(props);
844+
this.div = null;
845+
}
846+
847+
foo() {
848+
console.log(this.div);
849+
}
850+
851+
render() {
852+
return <div ref={(elem) => this.div = elem} />;
853+
}
854+
}
855+
`;
856+
const output = dedent`\
857+
const C: React.FC = () => {
858+
const div = React.useRef<HTMLDivElement | null>(null);
859+
860+
function foo() {
861+
console.log(div.current);
862+
}
863+
864+
return <div ref={(elem) => div.current = elem} />;
865+
};
866+
`;
867+
expect(transform(input, { ts: true })).toBe(output);
868+
});
869+
804870
it("transforms ref initializer", () => {
805871
const input = dedent`\
806872
class C extends React.Component {

src/index.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,26 +246,42 @@ function transformClass(head: ComponentHead, body: ComponentBody, options: { ts:
246246
}
247247
} else if (field.type === "user_defined_ref") {
248248
// const foo = useRef(null);
249+
const call = t.callExpression(
250+
getReactImport("useRef", babel, head.superClassRef),
251+
[t.nullLiteral()]
252+
);
249253
preamble.push(t.variableDeclaration(
250254
"const",
251255
[t.variableDeclarator(
252256
t.identifier(field.localName!),
253-
t.callExpression(
254-
getReactImport("useRef", babel, head.superClassRef),
255-
[t.nullLiteral()]
256-
),
257+
ts && field.typeAnnotation
258+
? assignTypeParameters(
259+
call,
260+
t.tsTypeParameterInstantiation([
261+
field.typeAnnotation.node
262+
])
263+
)
264+
: call
257265
)]
258266
))
259267
} else if (field.type === "user_defined_direct_ref") {
260268
// const foo = useRef(init);
269+
const call = t.callExpression(
270+
getReactImport("useRef", babel, head.superClassRef),
271+
[field.init.node]
272+
);
261273
preamble.push(t.variableDeclaration(
262274
"const",
263275
[t.variableDeclarator(
264276
t.identifier(field.localName!),
265-
t.callExpression(
266-
getReactImport("useRef", babel, head.superClassRef),
267-
[field.init.node]
268-
),
277+
ts && field.typeAnnotation
278+
? assignTypeParameters(
279+
call,
280+
t.tsTypeParameterInstantiation([
281+
field.typeAnnotation.node
282+
])
283+
)
284+
: call
269285
)]
270286
))
271287
}

0 commit comments

Comments
 (0)