1+ // This file contains analysis for class fields (`this.foo` and `C.foo`) where `C` is the class,
2+ // regardless of whether this is a special one (`this.props`) or a user-defined one (`this.foo`).
3+ //
4+ // Both the declarations and the usages are collected.
5+
16import type { NodePath } from "@babel/core" ;
27import type { AssignmentExpression , CallExpression , ClassAccessorProperty , ClassDeclaration , ClassMethod , ClassPrivateMethod , ClassPrivateProperty , ClassProperty , Expression , ExpressionStatement , MemberExpression , ThisExpression , TSDeclareMethod , TSType } from "@babel/types" ;
38import { getOr , isClassAccessorProperty , isClassMethodLike , isClassMethodOrDecl , isClassPropertyLike , isNamedClassElement , isStaticBlock , memberName , memberRefName , nonNullPath } from "../utils.js" ;
49import { AnalysisError } from "./error.js" ;
510
11+ /**
12+ * Aggregated result of class field analysis.
13+ */
614export type ThisFields = {
15+ /** Access to instance fields (`this.foo`), indexed by their names. */
716 thisFields : Map < string , ThisFieldSite [ ] > ;
17+ /** Access to static fields (`C.foo`, where `C` is the class), indexed by their names. */
818 staticFields : Map < string , StaticFieldSite [ ] > ;
919} ;
1020
21+ /**
22+ * A place where the instance field is declared or used.
23+ */
1124export type ThisFieldSite = {
1225 type : "class_field" ;
26+ /**
27+ * Declaration. One of:
28+ *
29+ * - Class element (methods, fields, etc.)
30+ * - Assignment to `this` in the constructor
31+ */
1332 path : NodePath < ClassProperty | ClassPrivateProperty | ClassMethod | ClassPrivateMethod | ClassAccessorProperty | TSDeclareMethod | AssignmentExpression > ;
33+ /**
34+ * Type annotation, if any.
35+ *
36+ * Param/return annotations attached to function-like implementations are ignored.
37+ */
1438 typing : FieldTyping | undefined ;
39+ /**
40+ * Initializing expression, if any.
41+ */
1542 init : FieldInit | undefined ;
1643 hasWrite : undefined ;
44+ /**
45+ * true if the initializer has a side effect.
46+ */
1747 hasSideEffect : boolean ;
1848} | {
1949 type : "expr" ;
50+ /**
51+ * The node that accesses the field (both read and write)
52+ */
2053 path : NodePath < MemberExpression > ;
2154 typing : undefined ;
2255 init : undefined ;
56+ /**
57+ * true if it involves writing. This includes:
58+ *
59+ * - Assignment `this.foo = 42`
60+ * - Compound assignment `this.foo += 42`
61+ * - Delete `delete this.foo`
62+ */
2363 hasWrite : boolean ;
2464 hasSideEffect : undefined ;
2565} ;
2666
67+ /**
68+ * Essentially a TSTypeAnnotation, but accounts for TSDeclareMethod as well.
69+ */
2770export type FieldTyping = {
2871 type : "type_value" ;
2972 valueTypePath : NodePath < TSType > ;
@@ -32,6 +75,9 @@ export type FieldTyping = {
3275 methodDeclPath : NodePath < TSDeclareMethod > ;
3376}
3477
78+ /**
79+ * Essentially an Expression, but accounts for ClassMethod as well.
80+ */
3581export type FieldInit = {
3682 type : "init_value" ;
3783 valuePath : NodePath < Expression >
@@ -40,15 +86,41 @@ export type FieldInit = {
4086 methodPath : NodePath < ClassMethod | ClassPrivateMethod > ;
4187} ;
4288
89+ /**
90+ * A place where the static field is declared or used.
91+ */
4392export type StaticFieldSite = {
4493 type : "class_field" ;
94+ /**
95+ * Declaration. One of:
96+ *
97+ * - Class element (methods, fields, etc.)
98+ * - Assignment to `this` in a static initialization block
99+ */
45100 path : NodePath < ClassProperty | ClassPrivateProperty | ClassMethod | ClassPrivateMethod | ClassAccessorProperty | TSDeclareMethod | AssignmentExpression > ;
101+ /**
102+ * Type annotation, if any.
103+ *
104+ * Param/return annotations attached to function-like implementations are ignored.
105+ */
46106 typing : FieldTyping | undefined ;
107+ /**
108+ * Initializing expression, if any.
109+ */
47110 init : FieldInit | undefined ;
48111 hasWrite : undefined ;
112+ /**
113+ * true if the initializer has a side effect.
114+ */
49115 hasSideEffect : boolean ;
50116} ;
51117
118+ /**
119+ * Collect declarations and uses of the following:
120+ *
121+ * - Instance fields ... `this.foo`
122+ * - Static fields ... `C.foo`, where `C` is the class
123+ */
52124export function analyzeThisFields ( path : NodePath < ClassDeclaration > ) : ThisFields {
53125 const thisFields = new Map < string , ThisFieldSite [ ] > ( ) ;
54126 const getThisField = ( name : string ) => getOr ( thisFields , name , ( ) => [ ] ) ;
@@ -59,13 +131,17 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
59131 // 1st pass: look for class field definitions
60132 for ( const itemPath of path . get ( "body" ) . get ( "body" ) ) {
61133 if ( isNamedClassElement ( itemPath ) ) {
134+ // The element is a class method or a class field (in a general sense)
62135 const isStatic = itemPath . node . static ;
63136 const name = memberName ( itemPath . node ) ;
64137 if ( name == null ) {
65138 throw new AnalysisError ( `Unnamed class element` ) ;
66139 }
67140 const field = isStatic ? getStaticField ( name ) : getThisField ( name ) ;
68141 if ( isClassPropertyLike ( itemPath ) ) {
142+ // Class field.
143+ // - May have an initializer: `foo = 42;` or not: `foo;`
144+ // - May have a type annotation: `foo: number;` or not: `foo;`
69145 const valuePath = nonNullPath < Expression > ( itemPath . get ( "value" ) ) ;
70146 const typeAnnotation = itemPath . get ( "typeAnnotation" ) ;
71147 const typeAnnotation_ = typeAnnotation . isTSTypeAnnotation ( ) ? typeAnnotation : undefined ;
@@ -83,14 +159,19 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
83159 hasSideEffect : ! ! itemPath . node . value && estimateSideEffect ( itemPath . node . value ) ,
84160 } ) ;
85161 if ( valuePath ) {
162+ // Initializer should be analyzed in step 2 too (considered to be in the constructor)
86163 bodies . push ( valuePath ) ;
87164 }
88165 } else if ( isClassMethodOrDecl ( itemPath ) ) {
166+ // Class method, constructor, getter/setter, or an accessor (those that will be introduced in the decorator proposal).
167+ //
168+ // - In TS, it may lack the implementation (i.e. TSDeclareMethod)
89169 const kind = itemPath . node . kind ?? "method" ;
90170 if ( kind === "method" ) {
91171 field . push ( {
92172 type : "class_field" ,
93173 path : itemPath ,
174+ // We put `typing` here only when it is type-only
94175 typing : itemPath . isTSDeclareMethod ( )
95176 ? {
96177 type : "type_method" ,
@@ -103,6 +184,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
103184 hasWrite : undefined ,
104185 hasSideEffect : false ,
105186 } ) ;
187+ // Analysis for step 2
106188 if ( isClassMethodLike ( itemPath ) ) {
107189 for ( const paramPath of itemPath . get ( "params" ) ) {
108190 bodies . push ( paramPath ) ;
@@ -133,6 +215,8 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
133215
134216 // 1st pass additional work: field initialization in constructor
135217 if ( constructor ) {
218+ // Only `constructor(props)` is allowed.
219+ // TODO: accept context as well
136220 if ( constructor . node . params . length > 1 ) {
137221 throw new AnalysisError ( `Constructor has too many parameters` ) ;
138222 } else if ( constructor . node . params . length < 1 ) {
@@ -146,6 +230,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
146230 const stmts = constructor . get ( "body" ) . get ( "body" ) ;
147231
148232 // Check super() call
233+ // Must be super(props) or super(props, context)
149234 const superCallIndex = stmts . findIndex ( ( stmt ) =>
150235 stmt . node . type === "ExpressionStatement"
151236 && stmt . node . expression . type === "CallExpression"
@@ -169,6 +254,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
169254 throw new AnalysisError ( `Invalid argument for super()` ) ;
170255 }
171256
257+ // Analyze init statements (must be in the form of `this.foo = expr;`)
172258 const initStmts = stmts . slice ( superCallIndex + 1 ) ;
173259 for ( const stmt of initStmts ) {
174260 if ( ! (
@@ -247,6 +333,7 @@ export function analyzeThisFields(path: NodePath<ClassDeclaration>): ThisFields
247333 traverseItem ( body ) ;
248334 }
249335
336+ // Post validation
250337 for ( const [ name , fieldSites ] of thisFields ) {
251338 if ( fieldSites . length === 0 ) {
252339 thisFields . delete ( name ) ;
@@ -312,7 +399,7 @@ function estimateSideEffect(expr: Expression): boolean {
312399 case "MemberExpression" :
313400 // Assume `foo.bar` to be pure
314401 return estimateSideEffect ( expr . object ) || ( expr . property . type !== "PrivateName" && estimateSideEffect ( expr . property ) ) ;
315-
402+
316403 case "UnaryExpression" :
317404 switch ( expr . operator ) {
318405 case "void" :
0 commit comments