diff --git a/src/typing/ty_normalizer.ml b/src/typing/ty_normalizer.ml index 84fe9919bf7..11555bdadfa 100644 --- a/src/typing/ty_normalizer.ml +++ b/src/typing/ty_normalizer.ml @@ -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, + * bar: React$PropType$Primitive, + * }, + * 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 -> @@ -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 -> diff --git a/tests/type-at-pos_react/create_class.js b/tests/type-at-pos_react/create_class.js new file mode 100644 index 00000000000..075cd94e325 --- /dev/null +++ b/tests/type-at-pos_react/create_class.js @@ -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 + }, +}); diff --git a/tests/type-at-pos_react/test.sh b/tests/type-at-pos_react/test.sh index 842f5dfc21b..11532413082 100644 --- a/tests/type-at-pos_react/test.sh +++ b/tests/type-at-pos_react/test.sh @@ -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 @@ -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 - diff --git a/tests/type-at-pos_react/type-at-pos_react.exp b/tests/type-at-pos_react/type-at-pos_react.exp index 317dbeeb195..0d921b413b9 100644 --- a/tests/type-at-pos_react/type-at-pos_react.exp +++ b/tests/type-at-pos_react/type-at-pos_react.exp @@ -1,3 +1,48 @@ +create_calss.js:3:7 = { + "type":"Class> & {defaultProps: {foo: string}, +getDefaultProps: () => {|foo: string|}, +propTypes: {|bar: React$PropType$Primitive$Required, foo: React$PropType$Primitive|}}", + "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> & {defaultProps: {foo: string}, +getDefaultProps: () => {|foo: string|}, +propTypes: {|bar: React$PropType$Primitive$Required, foo: React$PropType$Primitive|}}", + "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> & {defaultProps: void, +propTypes: {|bar: React$PropType$Primitive$Required, foo: React$PropType$Primitive|}}", + "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, +Children: {+count: (children: ChildrenArray) => number, +forEach: (children: ChildrenArray, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: (children: ChildrenArray, fn: (child: $NonMaybeType, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType>, +only: (children: ChildrenArray) => $NonMaybeType, +toArray: (children: ChildrenArray) => Array<$NonMaybeType>}, +ChildrenArray: type ChildrenArray<+T> = $ReadOnlyArray> | T, +Component: class React$Component, +ComponentType: type ComponentType<-P> = React$ComponentType

, +ConcurrentMode: ({children: ?React$Node}) => React$Node, +Config: type Config = React$Config, +Context: type Context = React$Context, +DOM: any, +Element: type Element<+C> = React$Element, +ElementConfig: type ElementConfig = React$ElementConfig, +ElementProps: type ElementProps = React$ElementProps, +ElementRef: type ElementRef = React$ElementRef, +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, +Ref: type Ref = React$Ref, +StatelessFunctionalComponent: type StatelessFunctionalComponent

= React$StatelessFunctionalComponent

, +StrictMode: ({children: ?React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: ?React$Node, fallback?: React$Node, maxDuration?: number}>, +checkPropTypes: (propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: (defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context, +createElement: React$CreateElement, +createFactory: (type: ElementType) => React$ElementFactory, +createRef: () => {|current: (null | T)|}, +default: {|+Children: {+count: (children: ChildrenArray) => number, +forEach: (children: ChildrenArray, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: (children: ChildrenArray, fn: (child: $NonMaybeType, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType>, +only: (children: ChildrenArray) => $NonMaybeType, +toArray: (children: ChildrenArray) => Array<$NonMaybeType>}, +Component: class React$Component, +ConcurrentMode: ({children: ?React$Node}) => React$Node, +DOM: any, +Fragment: ({children: ?React$Node}) => React$Node, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent, +StrictMode: ({children: ?React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: ?React$Node, fallback?: React$Node, maxDuration?: number}>, +checkPropTypes: (propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: (defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context, +createElement: React$CreateElement, +createFactory: (type: ElementType) => React$ElementFactory, +createRef: () => {|current: (null | T)|}, +forwardRef: (render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent, +isValidElement: (element: any) => boolean, +lazy:

(component: () => Promise<{default: React$ComponentType

}>) => React$ComponentType

, +memo:

(component: React$ComponentType

, equal?: (P, P) => boolean) => React$ComponentType

, +useCallback: ) => mixed>(callback: T, inputs: ?$ReadOnlyArray) => T, +useContext: (context: React$Context, observedBits: (void | number | boolean)) => T, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray) => void, +useImperativeHandle: (ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray) => void, +useMemo: (create: () => T, inputs: ?$ReadOnlyArray) => T, +useReducer: (((reducer: (S, A) => S, initialState: S) => [S, Dispatch]) & ((reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch]) & ((reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch])), +useRef: (initialValue: T) => {|current: T|}, +useState: (initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}, +forwardRef: (render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent, +isValidElement: (element: any) => boolean, +lazy:

(component: () => Promise<{default: React$ComponentType

}>) => React$ComponentType

, +memo:

(component: React$ComponentType

, equal?: (P, P) => boolean) => React$ComponentType

, +useCallback: ) => mixed>(callback: T, inputs: ?$ReadOnlyArray) => T, +useContext: (context: React$Context, observedBits: (void | number | boolean)) => T, +useDebugValue: (value: any) => void, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray) => void, +useImperativeHandle: (ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray) => void, +useMemo: (create: () => T, inputs: ?$ReadOnlyArray) => T, +useReducer: (((reducer: (S, A) => S, initialState: S) => [S, Dispatch]) & ((reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch]) & ((reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch])), +useRef: (initialValue: T) => {|current: T|}, +useState: (initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}", "reasons":[],