Skip to content

Commit c251fcd

Browse files
committed
Add support for useCallback
1 parent e818d9e commit c251fcd

File tree

9 files changed

+407
-64
lines changed

9 files changed

+407
-64
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Unreleased
22

3+
- Added
4+
- Add support for `useCallback`
35
- Fixed
46
- Use function declaration instead of function expression when possible
57

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ If you need to enforce specific styles, use Prettier or ESLint or whatever is yo
239239
- [ ] Support for user-defined methods
240240
- [x] Transform methods to `function`s
241241
- [x] Transform class fields initialized as functions to `function`s
242-
- [ ] Use `useCallback` if deemed necessary
243-
- [ ] Auto-expand direct callback call (like `this.props.onClick()`) to indirect call
242+
- [x] Use `useCallback` if deemed necessary
243+
- [x] Auto-expand direct callback call (like `this.props.onClick()`) to indirect call
244244
- [x] Rename methods if necessary
245245
- [ ] Skip method-binding expressions (e.g. `onClick={this.onClick.bind(this)}`)
246246
- [ ] Skip method-binding statements (e.g. `this.onClick = this.onClick.bind(this)`)

src/analysis.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { AnalysisError } from "./analysis/error.js";
55
import { analyzeClassFields } from "./analysis/class_fields.js";
66
import { analyzeState, StateObjAnalysis } from "./analysis/state.js";
77
import { getAndDelete } from "./utils.js";
8-
import { analyzeProps, PropsObjAnalysis } from "./analysis/prop.js";
8+
import { analyzeProps, needAlias, PropsObjAnalysis } from "./analysis/prop.js";
99
import { LocalManager, RemovableNode } from "./analysis/local.js";
10-
import { analyzeUserDefined, UserDefinedAnalysis } from "./analysis/user_defined.js";
10+
import { analyzeUserDefined, postAnalyzeCallbackDependencies, UserDefinedAnalysis } from "./analysis/user_defined.js";
1111
import type { PreAnalysisResult } from "./analysis/pre.js";
1212
import type { LibRef } from "./analysis/lib.js";
1313

@@ -20,6 +20,7 @@ export type {
2020
export { preanalyzeClass } from "./analysis/pre.js";
2121
export type { LocalManager } from "./analysis/local.js";
2222
export type { StateObjAnalysis } from "./analysis/state.js";
23+
export { needAlias } from "./analysis/prop.js";
2324
export type { PropsObjAnalysis } from "./analysis/prop.js";
2425

2526
const SPECIAL_STATIC_NAMES = new Set<string>([
@@ -85,8 +86,15 @@ export function analyzeClass(
8586
throw new AnalysisError(`Missing render method`);
8687
}
8788
const props = analyzeProps(propsObjAnalysis, defaultPropsObjAnalysis, locals, preanalysis);
89+
postAnalyzeCallbackDependencies(
90+
userDefined,
91+
props,
92+
states,
93+
sites,
94+
);
95+
8896
for (const [name, propAnalysis] of props.props) {
89-
if (propAnalysis.needsAlias) {
97+
if (needAlias(propAnalysis)) {
9098
propAnalysis.newAliasName = locals.newLocal(
9199
name,
92100
propAnalysis.sites.map((site) => site.path)

src/analysis/class_fields.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export type ClassFieldDeclSite = {
4242
* - Assignment to `this` in a static initialization block (static case)
4343
*/
4444
path: NodePath<ClassProperty | ClassPrivateProperty | ClassMethod | ClassPrivateMethod | ClassAccessorProperty | TSDeclareMethod | AssignmentExpression>;
45+
/**
46+
* Where is it referenced in?
47+
*/
48+
owner: string | undefined;
4549
/**
4650
* Type annotation, if any.
4751
*
@@ -65,6 +69,7 @@ export type ClassFieldExprSite = {
6569
* The node that accesses the field (both read and write)
6670
*/
6771
path: NodePath<MemberExpression>;
72+
owner: string | undefined;
6873
typing: undefined;
6974
init: undefined;
7075
/**
@@ -112,7 +117,10 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
112117
const staticFields = new Map<string, ClassFieldAnalysis>();
113118
const getStaticField = (name: string) => getOr(staticFields, name, () => ({ sites: [] }));
114119
let constructor: NodePath<ClassMethod> | undefined = undefined;
115-
const bodies: NodePath[] = [];
120+
const bodies: {
121+
owner: string | undefined,
122+
path: NodePath,
123+
}[] = [];
116124
// 1st pass: look for class field definitions
117125
for (const itemPath of path.get("body").get("body")) {
118126
if (isNamedClassElement(itemPath)) {
@@ -133,6 +141,7 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
133141
field.sites.push({
134142
type: "decl",
135143
path: itemPath,
144+
owner: undefined,
136145
typing: typeAnnotation_
137146
? {
138147
type: "type_value",
@@ -145,7 +154,13 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
145154
});
146155
if (valuePath) {
147156
// Initializer should be analyzed in step 2 too (considered to be in the constructor)
148-
bodies.push(valuePath);
157+
bodies.push({
158+
owner:
159+
valuePath.isFunctionExpression() || valuePath.isArrowFunctionExpression()
160+
? name
161+
: undefined,
162+
path: valuePath,
163+
});
149164
}
150165
} else if (isClassMethodOrDecl(itemPath)) {
151166
// Class method, constructor, getter/setter, or an accessor (those that will be introduced in the decorator proposal).
@@ -156,6 +171,7 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
156171
field.sites.push({
157172
type: "decl",
158173
path: itemPath,
174+
owner: undefined,
159175
// We put `typing` here only when it is type-only
160176
typing: itemPath.isTSDeclareMethod()
161177
? {
@@ -172,9 +188,15 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
172188
// Analysis for step 2
173189
if (isClassMethodLike(itemPath)) {
174190
for (const paramPath of itemPath.get("params")) {
175-
bodies.push(paramPath);
191+
bodies.push({
192+
owner: name,
193+
path: paramPath,
194+
});
176195
}
177-
bodies.push(itemPath.get("body"));
196+
bodies.push({
197+
owner: name,
198+
path: itemPath.get("body"),
199+
});
178200
}
179201
} else if (kind === "get" || kind === "set") {
180202
throw new AnalysisError(`Not implemented yet: getter / setter`);
@@ -262,6 +284,7 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
262284
field.sites.push({
263285
type: "decl",
264286
path: exprPath,
287+
owner: undefined,
265288
typing: undefined,
266289
init: {
267290
type: "init_value",
@@ -270,12 +293,15 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
270293
hasWrite: undefined,
271294
hasSideEffect: estimateSideEffect(stmt.node.expression.right),
272295
});
273-
bodies.push(exprPath.get("right"));
296+
bodies.push({
297+
owner: name,
298+
path: exprPath.get("right"),
299+
});
274300
}
275301
}
276302

277303
// 2nd pass: look for uses within items
278-
function traverseItem(path: NodePath) {
304+
function traverseItem(owner: string | undefined, path: NodePath) {
279305
traverseThis(path, (thisPath) => {
280306
// Ensure this is part of `this.foo`
281307
const thisMemberPath = thisPath.parentPath;
@@ -306,6 +332,7 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
306332

307333
field.sites.push({
308334
type: "expr",
335+
owner,
309336
path: thisMemberPath,
310337
typing: undefined,
311338
init: undefined,
@@ -315,7 +342,7 @@ export function analyzeClassFields(path: NodePath<ClassDeclaration>): ClassField
315342
});
316343
}
317344
for (const body of bodies) {
318-
traverseItem(body);
345+
traverseItem(body.owner, body.path);
319346
}
320347

321348
// Post validation

src/analysis/prop.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,30 @@ export type PropsObjAnalysis = {
1818
export type PropAnalysis = {
1919
newAliasName?: string | undefined;
2020
defaultValue?: NodePath<Expression>;
21-
/**
22-
* present only when there is defaultProps.
23-
*/
2421
sites: PropSite[];
2522
aliases: PropAlias[];
26-
needsAlias: boolean;
2723
typing?: NodePath<TSPropertySignature | TSMethodSignature> | undefined;
2824
};
2925

26+
// These are mutually linked
3027
export type PropsObjSite = {
3128
path: NodePath<MemberExpression>;
29+
owner: string | undefined;
30+
decomposedAsAliases: boolean;
31+
child: PropSite | undefined;
3232
};
3333

3434
export type PropSite = {
3535
path: NodePath<MemberExpression>;
36+
parent: PropsObjSite;
37+
owner: string | undefined;
38+
enabled: boolean;
3639
};
3740

3841
export type PropAlias = {
3942
scope: Scope,
4043
localName: string,
44+
owner: string | undefined,
4145
};
4246

4347
/**
@@ -66,31 +70,45 @@ export function analyzeProps(
6670
const getProp = (name: string) => getOr(props, name, () => ({
6771
sites: [],
6872
aliases: [],
69-
needsAlias: false,
7073
}));
7174

7275
for (const site of propsObjAnalysis.sites) {
7376
if (site.type !== "expr" || site.hasWrite) {
7477
throw new AnalysisError(`Invalid use of this.props`);
7578
}
76-
newObjSites.push({
77-
path: site.path,
78-
});
7979
const memberAnalysis = trackMember(site.path);
80+
const parentSite: PropsObjSite = {
81+
path: site.path,
82+
owner: site.owner,
83+
decomposedAsAliases: false,
84+
child: undefined,
85+
};
86+
newObjSites.push(parentSite);
8087
if (memberAnalysis.fullyDecomposed && memberAnalysis.memberAliases) {
8188
for (const [name, aliasing] of memberAnalysis.memberAliases) {
8289
getProp(name).aliases.push({
8390
scope: aliasing.scope,
8491
localName: aliasing.localName,
92+
owner: site.owner,
8593
});
8694
locals.reserveRemoval(aliasing.idPath);
8795
}
88-
} else if (defaultProps) {
89-
if (memberAnalysis.memberExpr) {
90-
getProp(memberAnalysis.memberExpr.name).sites.push({ path: memberAnalysis.memberExpr.path });
91-
} else {
96+
parentSite.decomposedAsAliases = true;
97+
} else {
98+
if (defaultProps && !memberAnalysis.memberExpr) {
9299
throw new AnalysisError(`Non-analyzable this.props in presence of defaultProps`);
93100
}
101+
if (memberAnalysis.memberExpr) {
102+
const child: PropSite = {
103+
path: memberAnalysis.memberExpr.path,
104+
parent: parentSite,
105+
owner: site.owner,
106+
// `enabled` will also be turned on later in callback analysis
107+
enabled: !!defaultProps,
108+
};
109+
parentSite.child = child;
110+
getProp(memberAnalysis.memberExpr.name).sites.push(child);
111+
}
94112
}
95113
}
96114
for (const [name, propTyping] of preanalysis.propsEach) {
@@ -101,9 +119,6 @@ export function analyzeProps(
101119
getProp(name).defaultValue = defaultValue;
102120
}
103121
}
104-
for (const [, prop] of props) {
105-
prop.needsAlias = prop.aliases.length > 0 || prop.sites.length > 0;
106-
}
107122
const allAliases = Array.from(props.values()).flatMap((prop) => prop.aliases);
108123
return {
109124
hasDefaults: !!defaultProps,
@@ -113,6 +128,10 @@ export function analyzeProps(
113128
};
114129
}
115130

131+
export function needAlias(prop: PropAnalysis): boolean {
132+
return prop.aliases.length > 0 || prop.sites.some((s) => s.enabled);
133+
}
134+
116135
function analyzeDefaultProps(
117136
defaultPropsAnalysis: ClassFieldAnalysis,
118137
): Map<string, NodePath<Expression>> | undefined {

src/analysis/state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type StateInitSite = {
2727
export type StateExprSite = {
2828
type: "expr";
2929
path: NodePath<Expression>;
30+
owner: string | undefined;
3031
};
3132
export type SetStateSite = {
3233
type: "setState";
@@ -103,13 +104,15 @@ export function analyzeState(
103104
getState(name).sites.push({
104105
type: "expr",
105106
path,
107+
owner: site.owner,
106108
});
107109
}
108110
}
109111
} else if (memberAnalysis.memberExpr) {
110112
getState(memberAnalysis.memberExpr.name).sites.push({
111113
type: "expr",
112114
path: memberAnalysis.memberExpr.path,
115+
owner: site.owner,
113116
});
114117
} else {
115118
throw new AnalysisError(`Non-analyzable this.state`);

0 commit comments

Comments
 (0)