Skip to content

Commit

Permalink
[normalizer] React.createClass
Browse files Browse the repository at this point in the history
Summary:
Before, type-at-pos on `Foo` in
```
const Foo = React.createClass({
  propTypes: {
    foo: React.PropTypes.string,
    bar: React.PropTypes.string.isRequired
  },
  getInitialState() {
    return { baz: 0 };
  },
  getDefaultProps() {
    return {
      foo: "foo"
    };
  }
});
```
returned
```
Class<React$Component>
```

With this change it returns:
```
Class<React$Component<{ bar: string, foo?: string }, {baz: number}>> & {
  +propTypes: {|
    bar: React$PropType$Primitive$Required<string>,
    foo: React$PropType$Primitive<string>
  |},
  defaultProps: { foo: string }
}
```

Implementation-wise, the normalizer:
1. Looks up `"props"` on the newly create instance to fill in the `Props` type
argument to `React$Component`. Will replace with `any` if it's not found.
2. Looks up `"state"` on the newly create instance to fill in the `State` type
argument to `React$Component`. Converts to an inexact version of that type.
Will replace with `any` if it's not found.
3. Looks up and includes all static fields of the instance.

Reviewed By: samwgoldman

Differential Revision: D14486735

fbshipit-source-id: 0ea4259d81b67c1015350878bb41e8badb583fb6
  • Loading branch information
panagosg7 authored and facebook-github-bot committed Mar 20, 2019
1 parent 1159911 commit 94ff166
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 3 deletions.
62 changes: 60 additions & 2 deletions src/typing/ty_normalizer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -888,12 +888,64 @@ end = struct
match desc_of_reason ~unwrap:false r with
| RType name
| RIdentifier name -> return name
| RReactComponent -> return "React$Component"
| r ->
let msg = spf "could not extract name from reason: %s"
(Reason.string_of_desc r) in
terr ~kind:BadInstanceT ~msg None

(* Used for instances of React.createClass(..) *)
and react_component =
let react_props ~env ~default props name =
match SMap.find name props with
| exception Not_found -> return default
| Type.Field (_, t, _) -> type__ ~env t
| _ -> return default
in
let react_static_props ~env static =
let cx = Env.(env.genv.cx) in
match static with
| T.DefT (_, _, T.ObjT { T.props_tmap; _ }) ->
Context.find_props cx props_tmap |>
SMap.bindings |>
mapM (fun (name, p) -> obj_prop ~env name p) >>|
List.concat
| _ -> return []
in
let inexactify = function
| Ty.Obj ({ Ty.obj_exact = true; _ } as obj) ->
Ty.Obj { obj with Ty.obj_exact = false }
| ty -> ty
in
fun ~env static own_props ->
let cx = Env.(env.genv.cx) in
let own_props = Context.find_props cx own_props in
react_props ~env ~default:Ty.implicit_any own_props "props" >>= fun props_ty ->
react_props ~env ~default:Ty.implicit_any own_props "state" >>= fun state_ty ->
react_static_props ~env static >>| fun static_flds ->

(* The inferred type for state is unsealed, which has its exact bit set.
* However, Ty.t does not account for unsealed and exact sealed objects are
* incompatible with exact and unsealed, so making state inexact here. *)
let state_ty = inexactify state_ty in
let parent_instance = generic_builtin_t "React$Component" [props_ty; state_ty] in
let parent_class = Ty.Utility (Ty.Class parent_instance) in
(*
* {
* +propTypes: {
* foo: React$PropType$Primitive$Required<string>,
* bar: React$PropType$Primitive<number>,
* },
* defaultProps: { ... },
* }
*)
let props_obj = Ty.Obj {
Ty.obj_exact = false;
obj_frozen = false;
obj_literal = false;
obj_props = static_flds;
} in
Ty.mk_inter [parent_class; props_obj]

and instance_t ~env r inst =
let open Type in
name_of_instance_reason r >>= fun name ->
Expand Down Expand Up @@ -943,7 +995,13 @@ end = struct
terr ~kind:BadClassT ~msg:(Ty_debug.string_of_ctor ty) None
in
fun ~env t ps ->
type__ ~env t >>= go ~env ps
match t with
| T.DefT (r, _, T.InstanceT (static, _, _, inst))
when desc_of_reason ~unwrap:false r = RReactComponent ->
let { Type.own_props; _ } = inst in
react_component ~env static own_props
| _ ->
type__ ~env t >>= go ~env ps

and this_class_t ~env t ps =
type__ ~env t >>= fun ty ->
Expand Down
36 changes: 36 additions & 0 deletions tests/type-at-pos_react/create_class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @flow

const React = require("react");
const Foo = React.createClass({
propTypes: {
foo: React.PropTypes.string,
bar: React.PropTypes.string.isRequired
},
getInitialState() {
return { baz: 0 };
},
getDefaultProps() {
return {
foo: "foo"
};
}
});

const NoState = React.createClass({
propTypes: {
foo: React.PropTypes.string,
bar: React.PropTypes.string.isRequired
},
getDefaultProps() {
return {
foo: "foo"
};
}
});

const NoDefaultProps = React.createClass({
propTypes: {
foo: React.PropTypes.string,
bar: React.PropTypes.string.isRequired
},
});
9 changes: 8 additions & 1 deletion tests/type-at-pos_react/test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/bin/bash

# create_class.js
printf "create_calss.js:3:7 = "
assert_ok "$FLOW" type-at-pos create_class.js 4 7 --strip-root --pretty
printf "create_calss.js:19:7 = "
assert_ok "$FLOW" type-at-pos create_class.js 19 7 --strip-root --pretty
printf "create_calss.js:31:7 = "
assert_ok "$FLOW" type-at-pos create_class.js 31 7 --strip-root --pretty

# react_component.js
printf "react_component.js:3:9 = "
assert_ok "$FLOW" type-at-pos react_component.js 3 9 --strip-root --pretty
Expand All @@ -20,4 +28,3 @@ assert_ok "$FLOW" type-at-pos react.js 2 7 --strip-root --pretty

printf "react_abstract_component.js:3:15 ="
assert_ok "$FLOW" type-at-pos react_abstract_component.js 3 15 --strip-root --pretty

45 changes: 45 additions & 0 deletions tests/type-at-pos_react/type-at-pos_react.exp
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
create_calss.js:3:7 = {
"type":"Class<React$Component<{bar: string, foo?: string}, {baz: number}>> & {defaultProps: {foo: string}, +getDefaultProps: () => {|foo: string|}, +propTypes: {|bar: React$PropType$Primitive$Required<string>, foo: React$PropType$Primitive<string>|}}",
"reasons":[],
"loc":{
"source":"create_class.js",
"type":"SourceFile",
"start":{"line":4,"column":7,"offset":48},
"end":{"line":4,"column":9,"offset":51}
},
"path":"create_class.js",
"line":4,
"endline":4,
"start":7,
"end":9
}
create_calss.js:19:7 = {
"type":"Class<React$Component<{bar: string, foo?: string}, {}>> & {defaultProps: {foo: string}, +getDefaultProps: () => {|foo: string|}, +propTypes: {|bar: React$PropType$Primitive$Required<string>, foo: React$PropType$Primitive<string>|}}",
"reasons":[],
"loc":{
"source":"create_class.js",
"type":"SourceFile",
"start":{"line":19,"column":7,"offset":294},
"end":{"line":19,"column":13,"offset":301}
},
"path":"create_class.js",
"line":19,
"endline":19,
"start":7,
"end":13
}
create_calss.js:31:7 = {
"type":"Class<React$Component<{bar: string, foo?: string}, {}>> & {defaultProps: void, +propTypes: {|bar: React$PropType$Primitive$Required<string>, foo: React$PropType$Primitive<string>|}}",
"reasons":[],
"loc":{
"source":"create_class.js",
"type":"SourceFile",
"start":{"line":31,"column":7,"offset":494},
"end":{"line":31,"column":20,"offset":508}
},
"path":"create_class.js",
"line":31,
"endline":31,
"start":7,
"end":20
}
react_component.js:3:9 = {
"type":"{|+AbstractComponent: type AbstractComponent<-Config, +Instance = mixed> = React$AbstractComponent<Config, Instance>, +Children: {+count: (children: ChildrenArray<any>) => number, +forEach: <T>(children: ChildrenArray<T>, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: <T, U>(children: ChildrenArray<T>, fn: (child: $NonMaybeType<T>, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType<U>>, +only: <T>(children: ChildrenArray<T>) => $NonMaybeType<T>, +toArray: <T>(children: ChildrenArray<T>) => Array<$NonMaybeType<T>>}, +ChildrenArray: type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T, +Component: class React$Component<Props, State = void>, +ComponentType: type ComponentType<-P> = React$ComponentType<P>, +ConcurrentMode: ({children: ?React$Node}) => React$Node, +Config: type Config<Props, DefaultProps> = React$Config<Props, DefaultProps>, +Context: type Context<T> = React$Context<T>, +DOM: any, +Element: type Element<+C> = React$Element<C>, +ElementConfig: type ElementConfig<C> = React$ElementConfig<C>, +ElementProps: type ElementProps<C> = React$ElementProps<C>, +ElementRef: type ElementRef<C> = React$ElementRef<C>, +ElementType: type ElementType = React$ElementType, +Fragment: ({children: ?React$Node}) => React$Node, +Key: type Key = React$Key, +Node: type Node = React$Node, +Portal: type Portal = React$Portal, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent<Props, State = void>, +Ref: type Ref<C> = React$Ref<C>, +StatelessFunctionalComponent: type StatelessFunctionalComponent<P> = React$StatelessFunctionalComponent<P>, +StrictMode: ({children: ?React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: ?React$Node, fallback?: React$Node, maxDuration?: number}>, +checkPropTypes: <V>(propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: <T>(defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context<T>, +createElement: React$CreateElement, +createFactory: <ElementType: React$ElementType>(type: ElementType) => React$ElementFactory<ElementType>, +createRef: <T>() => {|current: (null | T)|}, +default: {|+Children: {+count: (children: ChildrenArray<any>) => number, +forEach: <T>(children: ChildrenArray<T>, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: <T, U>(children: ChildrenArray<T>, fn: (child: $NonMaybeType<T>, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType<U>>, +only: <T>(children: ChildrenArray<T>) => $NonMaybeType<T>, +toArray: <T>(children: ChildrenArray<T>) => Array<$NonMaybeType<T>>}, +Component: class React$Component<Props, State = void>, +ConcurrentMode: ({children: ?React$Node}) => React$Node, +DOM: any, +Fragment: ({children: ?React$Node}) => React$Node, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent<Props, State = void>, +StrictMode: ({children: ?React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: ?React$Node, fallback?: React$Node, maxDuration?: number}>, +checkPropTypes: <V>(propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: <T>(defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context<T>, +createElement: React$CreateElement, +createFactory: <ElementType: React$ElementType>(type: ElementType) => React$ElementFactory<ElementType>, +createRef: <T>() => {|current: (null | T)|}, +forwardRef: <Config, Instance>(render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent<Config, Instance>, +isValidElement: (element: any) => boolean, +lazy: <P>(component: () => Promise<{default: React$ComponentType<P>}>) => React$ComponentType<P>, +memo: <P>(component: React$ComponentType<P>, equal?: (P, P) => boolean) => React$ComponentType<P>, +useCallback: <T: (...args: $ReadOnlyArray<empty>) => mixed>(callback: T, inputs: ?$ReadOnlyArray<mixed>) => T, +useContext: <T>(context: React$Context<T>, observedBits: (void | number | boolean)) => T, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useImperativeHandle: <T>(ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray<mixed>) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useMemo: <T>(create: () => T, inputs: ?$ReadOnlyArray<mixed>) => T, +useReducer: ((<S, A>(reducer: (S, A) => S, initialState: S) => [S, Dispatch<A>]) & (<S, A>(reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch<A>]) & (<S, A, I>(reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch<A>])), +useRef: <T>(initialValue: T) => {|current: T|}, +useState: <S>(initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}, +forwardRef: <Config, Instance>(render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent<Config, Instance>, +isValidElement: (element: any) => boolean, +lazy: <P>(component: () => Promise<{default: React$ComponentType<P>}>) => React$ComponentType<P>, +memo: <P>(component: React$ComponentType<P>, equal?: (P, P) => boolean) => React$ComponentType<P>, +useCallback: <T: (...args: $ReadOnlyArray<empty>) => mixed>(callback: T, inputs: ?$ReadOnlyArray<mixed>) => T, +useContext: <T>(context: React$Context<T>, observedBits: (void | number | boolean)) => T, +useDebugValue: (value: any) => void, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useImperativeHandle: <T>(ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray<mixed>) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useMemo: <T>(create: () => T, inputs: ?$ReadOnlyArray<mixed>) => T, +useReducer: ((<S, A>(reducer: (S, A) => S, initialState: S) => [S, Dispatch<A>]) & (<S, A>(reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch<A>]) & (<S, A, I>(reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch<A>])), +useRef: <T>(initialValue: T) => {|current: T|}, +useState: <S>(initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}",
"reasons":[],
Expand Down

0 comments on commit 94ff166

Please sign in to comment.