Skip to content

Commit 0a445c0

Browse files
committed
Add soft errors
1 parent bda342e commit 0a445c0

File tree

11 files changed

+296
-61
lines changed

11 files changed

+296
-61
lines changed

CHANGELOG.md

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

3+
- Added
4+
- Implemented soft errors.
5+
- From this version onward, it will try to generate special variables like `TODO_this` to indicate errors and continue transformation.
6+
- It allows you to automate more transformation while using manual rewriting when necessary.
7+
- Not all errors are soft errors, but we will continue to make as many errors soft errors as possible.
8+
39
## 0.1.10
410

511
- Misc

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,16 @@ export const C = props => {
161161
};
162162
```
163163

164+
## Errors
165+
166+
Hard errors are indicated by `/* react-declassify-disable Cannot perform transformation */`.
167+
168+
Soft errors are indicated by special variable names including:
169+
170+
- `TODO_this`
171+
172+
Hard errors stop transformation of the whole class while stop errors do not. You need to fix the errors to conclude transformation.
173+
164174
## Configuration
165175

166176
### Disabling transformation

src/analysis.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
TSType,
99
TSTypeParameterDeclaration,
1010
} from "@babel/types";
11-
import { AnalysisError } from "./analysis/error.js";
11+
import { AnalysisError, SoftErrorRepository } from "./analysis/error.js";
1212
import { BindThisSite, analyzeClassFields } from "./analysis/class_fields.js";
1313
import { analyzeState, StateObjAnalysis } from "./analysis/state.js";
1414
import { getAndDelete } from "./utils.js";
@@ -23,7 +23,7 @@ import type { PreAnalysisResult } from "./analysis/pre.js";
2323
import type { LibRef } from "./analysis/lib.js";
2424
import { EffectAnalysis, analyzeEffects } from "./analysis/effect.js";
2525

26-
export { AnalysisError } from "./analysis/error.js";
26+
export { AnalysisError, SoftErrorRepository } from "./analysis/error.js";
2727

2828
export type { LibRef } from "./analysis/lib.js";
2929
export type { PreAnalysisResult } from "./analysis/pre.js";
@@ -63,14 +63,15 @@ export type AnalysisResult = {
6363

6464
export function analyzeClass(
6565
path: NodePath<ClassDeclaration>,
66-
preanalysis: PreAnalysisResult
66+
preanalysis: PreAnalysisResult,
67+
softErrors: SoftErrorRepository
6768
): AnalysisResult {
6869
const locals = new LocalManager(path);
6970
const {
7071
instanceFields: sites,
7172
staticFields,
7273
bindThisSites,
73-
} = analyzeClassFields(path);
74+
} = analyzeClassFields(path, softErrors);
7475

7576
const propsObjAnalysis = getAndDelete(sites, "props") ?? { sites: [] };
7677
const defaultPropsObjAnalysis = getAndDelete(
@@ -84,6 +85,7 @@ export function analyzeClass(
8485
stateObjAnalysis,
8586
setStateAnalysis,
8687
locals,
88+
softErrors,
8789
preanalysis
8890
);
8991

@@ -102,8 +104,10 @@ export function analyzeClass(
102104
analyzeOuterCapturings(path, locals);
103105
let renderPath: NodePath<ClassMethod> | undefined = undefined;
104106
{
105-
if (renderAnalysis.sites.some((site) => site.type === "expr")) {
106-
throw new AnalysisError(`do not use this.render`);
107+
for (const site of renderAnalysis.sites) {
108+
if (site.type === "expr") {
109+
softErrors.addThisError(site.thisPath);
110+
}
107111
}
108112
const init = renderAnalysis.sites.find((site) => site.init);
109113
if (init) {
@@ -112,7 +116,7 @@ export function analyzeClass(
112116
}
113117
}
114118
}
115-
const userDefined = analyzeUserDefined(sites);
119+
const userDefined = analyzeUserDefined(sites, softErrors);
116120
for (const [name] of staticFields) {
117121
if (!SPECIAL_STATIC_NAMES.has(name)) {
118122
throw new AnalysisError(`Cannot transform static ${name}`);
@@ -127,6 +131,7 @@ export function analyzeClass(
127131
propsObjAnalysis,
128132
defaultPropsObjAnalysis,
129133
locals,
134+
softErrors,
130135
preanalysis
131136
);
132137
postAnalyzeCallbackDependencies(userDefined, props, states, sites);

src/analysis/class_fields.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
memberRefName,
3333
nonNullPath,
3434
} from "../utils.js";
35-
import { AnalysisError } from "./error.js";
35+
import { AnalysisError, SoftErrorRepository } from "./error.js";
3636

3737
/**
3838
* Aggregated result of class field analysis.
@@ -103,6 +103,7 @@ export type ClassFieldExprSite = {
103103
* The node that accesses the field (both read and write)
104104
*/
105105
path: NodePath<MemberExpression>;
106+
thisPath: NodePath<ThisExpression>;
106107
owner: string | undefined;
107108
typing: undefined;
108109
init: undefined;
@@ -168,7 +169,8 @@ export type BindThisSite = {
168169
* - Static fields ... `C.foo`, where `C` is the class
169170
*/
170171
export function analyzeClassFields(
171-
path: NodePath<ClassDeclaration>
172+
path: NodePath<ClassDeclaration>,
173+
softErrors: SoftErrorRepository
172174
): ClassFieldsAnalysis {
173175
const instanceFields = new Map<string, ClassFieldAnalysis>();
174176
const getInstanceField = (name: string) =>
@@ -188,7 +190,12 @@ export function analyzeClassFields(
188190
const isStatic = itemPath.node.static;
189191
const name = memberName(itemPath.node);
190192
if (name == null) {
191-
throw new AnalysisError(`Unnamed class element`);
193+
if (isStatic) {
194+
throw new AnalysisError(`Unnamed class element`);
195+
} else {
196+
softErrors.addDeclError(itemPath);
197+
continue;
198+
}
192199
}
193200
const field = isStatic ? getStaticField(name) : getInstanceField(name);
194201
if (isClassPropertyLike(itemPath)) {
@@ -405,12 +412,14 @@ export function analyzeClassFields(
405412
});
406413
return;
407414
}
408-
throw new AnalysisError(`Stray this`);
415+
softErrors.addThisError(thisPath);
416+
return;
409417
}
410418

411419
const name = memberRefName(thisMemberPath.node);
412420
if (name == null) {
413-
throw new AnalysisError(`Unrecognized this-property reference`);
421+
softErrors.addThisError(thisPath);
422+
return;
414423
}
415424

416425
const field = getInstanceField(name)!;
@@ -431,6 +440,7 @@ export function analyzeClassFields(
431440
type: "expr",
432441
owner,
433442
path: thisMemberPath,
443+
thisPath,
434444
typing: undefined,
435445
init: undefined,
436446
hasWrite,
@@ -521,6 +531,25 @@ export function analyzeClassFields(
521531
return { instanceFields, staticFields, bindThisSites };
522532
}
523533

534+
export function addClassFieldError(
535+
site: ClassFieldSite,
536+
softErrors: SoftErrorRepository
537+
) {
538+
if (site.type === "decl") {
539+
if (isNamedClassElement(site.path)) {
540+
softErrors.addDeclError(site.path);
541+
} else if (site.path.isAssignmentExpression()) {
542+
const left = site.path.get("left") as NodePath<MemberExpression>;
543+
const object = left.get("object") as NodePath<ThisExpression>;
544+
softErrors.addThisError(object);
545+
} else {
546+
throw new Error(`Unreachable: invalid type: ${site.path.node.type}`);
547+
}
548+
} else {
549+
softErrors.addThisError(site.thisPath);
550+
}
551+
}
552+
524553
function traverseThis(
525554
path: NodePath,
526555
visit: (path: NodePath<ThisExpression>) => void

src/analysis/error.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,56 @@
1+
import type { NodePath } from "@babel/traverse";
2+
import type {
3+
ClassAccessorProperty,
4+
ClassMethod,
5+
ClassPrivateMethod,
6+
ClassPrivateProperty,
7+
ClassProperty,
8+
TSDeclareMethod,
9+
ThisExpression,
10+
} from "@babel/types";
11+
12+
export type SoftError = SoftErrorThisExpr | SoftErrorDecl;
13+
export type SoftErrorThisExpr = {
14+
type: "invalid_this";
15+
path: NodePath<ThisExpression>;
16+
};
17+
export type SoftErrorDecl = {
18+
type: "invalid_decl";
19+
path: NodePath<
20+
| ClassProperty
21+
| ClassPrivateProperty
22+
| ClassMethod
23+
| ClassPrivateMethod
24+
| TSDeclareMethod
25+
| ClassAccessorProperty
26+
>;
27+
};
28+
29+
export class SoftErrorRepository {
30+
errors: SoftError[] = [];
31+
addThisError(thisPath: NodePath<ThisExpression>) {
32+
this.errors.push({
33+
type: "invalid_this",
34+
path: thisPath,
35+
});
36+
}
37+
addDeclError(
38+
declPath: NodePath<
39+
| ClassProperty
40+
| ClassPrivateProperty
41+
| ClassMethod
42+
| ClassPrivateMethod
43+
| TSDeclareMethod
44+
| ClassAccessorProperty
45+
>
46+
) {
47+
this.errors.push({
48+
type: "invalid_decl",
49+
path: declPath,
50+
});
51+
}
52+
}
53+
154
export class AnalysisError extends Error {
255
static {
356
this.prototype.name = "AnalysisError";

src/analysis/prop.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type {
77
TSPropertySignature,
88
} from "@babel/types";
99
import { getOr, memberName } from "../utils.js";
10-
import { AnalysisError } from "./error.js";
10+
import { AnalysisError, SoftErrorRepository } from "./error.js";
1111
import type { LocalManager } from "./local.js";
12-
import { ClassFieldAnalysis } from "./class_fields.js";
12+
import { ClassFieldAnalysis, addClassFieldError } from "./class_fields.js";
1313
import { trackMember } from "./track_member.js";
1414
import { PreAnalysisResult } from "./pre.js";
1515

@@ -67,6 +67,7 @@ export function analyzeProps(
6767
propsObjAnalysis: ClassFieldAnalysis,
6868
defaultPropsObjAnalysis: ClassFieldAnalysis,
6969
locals: LocalManager,
70+
softErrors: SoftErrorRepository,
7071
preanalysis: PreAnalysisResult
7172
): PropsObjAnalysis {
7273
const defaultProps = analyzeDefaultProps(defaultPropsObjAnalysis);
@@ -80,7 +81,8 @@ export function analyzeProps(
8081

8182
for (const site of propsObjAnalysis.sites) {
8283
if (site.type !== "expr" || site.hasWrite) {
83-
throw new AnalysisError(`Invalid use of this.props`);
84+
addClassFieldError(site, softErrors);
85+
continue;
8486
}
8587
const memberAnalysis = trackMember(site.path);
8688
const parentSite: PropsObjSite = {
@@ -89,8 +91,8 @@ export function analyzeProps(
8991
decomposedAsAliases: false,
9092
child: undefined,
9193
};
92-
newObjSites.push(parentSite);
9394
if (memberAnalysis.fullyDecomposed && memberAnalysis.memberAliases) {
95+
newObjSites.push(parentSite);
9496
for (const [name, aliasing] of memberAnalysis.memberAliases) {
9597
getProp(name).aliases.push({
9698
scope: aliasing.scope,
@@ -102,10 +104,10 @@ export function analyzeProps(
102104
parentSite.decomposedAsAliases = true;
103105
} else {
104106
if (defaultProps && !memberAnalysis.memberExpr) {
105-
throw new AnalysisError(
106-
`Non-analyzable this.props in presence of defaultProps`
107-
);
107+
addClassFieldError(site, softErrors);
108+
continue;
108109
}
110+
newObjSites.push(parentSite);
109111
if (memberAnalysis.memberExpr) {
110112
const child: PropSite = {
111113
path: memberAnalysis.memberExpr.path,
@@ -145,7 +147,7 @@ function analyzeDefaultProps(
145147
): Map<string, NodePath<Expression>> | undefined {
146148
for (const site of defaultPropsAnalysis.sites) {
147149
if (!site.init) {
148-
throw new AnalysisError(`Invalid use of static defaultState`);
150+
throw new AnalysisError(`Invalid use of static defaultProps`);
149151
}
150152
}
151153

0 commit comments

Comments
 (0)