Repository: brigand/babel-plugin-flow-react-proptypes
Pull request #98 (2017-05-30 06:12:48)

Fix issue 96 type annotations with member expressions

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11739586/badge)](https://coveralls.io/builds/11739586)

Coverage decreased (-0.4%) to 92.917% when pulling **1bc2f7da17a44a812d01ba18c52af82ad66f0cb0 on mhaas:fix-issue-96-type-annotations-with-member-expressions** into **56ef1e4b57183324034bb87105dd668e4bb41e2f on brigand:master**.

brigand: You added a test, but I don't see the snapshot. Let me know when it's ready to merge.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/11750432/badge)](https://coveralls.io/builds/11750432)

Coverage decreased (-0.4%) to 92.917% when pulling **3e5d0d50bca5e7a0a6e08f3890d0454ba744178e on mhaas:fix-issue-96-type-annotations-with-member-expressions** into **0bce70b38643c51ceb7ed807e177f9e9a9aa835e on brigand:master**.

mhaas:  @brigand I added the missing file - commit is amended.
brigand: I'd like to be extra safe and guard against each property in the chain being undefined/null.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e3aee2..14e6955 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 3.1.3
+  - Fix error on attempted propTypes generation for non-component function (#66)
+
+## 3.1.2
+  -  Fix bug with functions defaulting to react components (#97)
+
 ## 3.1.0
 
   - Add support for top-level propTypes assignment of imported types (#88)
diff --git a/package.json b/package.json
index 5b75ef4..d11fb20 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "babel-plugin-flow-react-proptypes",
-  "version": "3.1.2",
+  "version": "3.1.3",
   "description": "converts flow types to react proptypes",
   "main": "lib/index.js",
   "scripts": {
diff --git a/src/convertToPropTypes.js b/src/convertToPropTypes.js
index 3f9bf0e..2f272c7 100644
--- a/src/convertToPropTypes.js
+++ b/src/convertToPropTypes.js
@@ -96,7 +96,7 @@ export default function convertToPropTypes(node, importedTypes, internalTypes) {
       resultPropType = {type: 'func'};
     }
     else {
-      resultPropType = {type: 'possible-class', value: node.id.name};
+      resultPropType = {type: 'possible-class', value: node.id};
     }
   }
   else if (node.type === 'UnionTypeAnnotation') {
diff --git a/src/index.js b/src/index.js
index 5dae5a0..cf84bc5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -35,6 +35,9 @@ const getPropsForTypeAnnotation = typeAnnotation => {
       || typeAnnotation.type === 'AnyTypeAnnotation') {
     props = convertNodeToPropTypes(typeAnnotation);
   }
+  else if (typeAnnotation.properties != null || typeAnnotation.type != null) {
+    $debug('typeAnnotation not of expected type, not generating propTypes: ', typeAnnotation);
+  }
   else {
     throw new Error(`Expected prop types, but found none. This is a bug in ${PLUGIN_NAME}`);
   }
diff --git a/src/makePropTypesAst.js b/src/makePropTypesAst.js
index b2b8df5..c145d43 100644
--- a/src/makePropTypesAst.js
+++ b/src/makePropTypesAst.js
@@ -170,9 +170,36 @@ function makePropTypeImportNode() {
 function makeFunctionCheckAST(variableNode) {
   return t.binaryExpression('===', t.unaryExpression('typeof', variableNode), t.stringLiteral('function'));
 }
+
+function makeNullCheckAST(variableNode) {
+  return t.binaryExpression('==', variableNode, t.nullLiteral());
+}
+
 function markNodeAsRequired(node) {
   return t.memberExpression(node, t.identifier('isRequired'));
 }
+
+function processQualifiedTypeIdentifierIntoMemberExpression(qualifiedTypeIdentifier) {
+  const qualification = qualifiedTypeIdentifier.qualification;
+
+  let objectAST;
+  if (qualification.type === 'QualifiedTypeIdentifier') {
+    objectAST = processQualifiedTypeIdentifierIntoMemberExpression(qualification);
+  }
+  else if (qualification.type === 'Identifier') {
+    objectAST = t.identifier(qualification.name);
+  }
+  else {
+    throw new Error('Cannot handle type of qualification property:', qualification);
+  }
+  const propertyAST = t.identifier(qualifiedTypeIdentifier.id.name);
+
+  const memberExpression = t.memberExpression(objectAST, propertyAST);
+
+  return t.conditionalExpression(makeNullCheckAST(memberExpression), t.objectExpression([]), memberExpression)
+
+}
+
 /**
  * Handles all prop types.
  *
@@ -275,18 +302,28 @@ function makePropType(data, isExact) {
   }
   else if (method === 'possible-class') {
     markFullExpressionAsRequired = false;
-
+    let classSpec;
+    if (data.value.name != null) {
+      classSpec = t.identifier(data.value.name);
+    }
+    else if (data.value.type === 'QualifiedTypeIdentifier') {
+      classSpec = processQualifiedTypeIdentifierIntoMemberExpression(data.value);
+    }
+    else {
+      throw new Error('Unknown node type in possible-class for node:', data.value);
+    }
     let instanceOfNode = t.callExpression(
         t.memberExpression(node, t.identifier('instanceOf')),
-        [t.identifier(data.value)]
-      );
+        [classSpec]
+    );
     let anyNode = makeAnyPropTypeAST();
     if (data.isRequired) {
       instanceOfNode = markNodeAsRequired(instanceOfNode);
       anyNode = markNodeAsRequired(anyNode);
     }
-    const functionCheckNode = makeFunctionCheckAST(t.identifier(data.value));
+    const functionCheckNode = makeFunctionCheckAST(classSpec);
     node = t.conditionalExpression(functionCheckNode, instanceOfNode, anyNode);
+
     // Don't add .required on the full expression; we already handled this ourselves
     // for any proptypes we generated
   }


Test patch:
diff --git a/src/__tests__/__snapshots__/instance-of-object-properties.js.snap b/src/__tests__/__snapshots__/instance-of-object-properties.js.snap
new file mode 100644
index 0000000..d5f0f1c
--- /dev/null
+++ b/src/__tests__/__snapshots__/instance-of-object-properties.js.snap
@@ -0,0 +1,36 @@
+exports[`test instance-of 1`] = `
+"\'use strict\';
+
+var _some_class = require(\'./some_class\');
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }
+
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(\"this hasn\'t been initialised - super() hasn\'t been called\"); } return call && (typeof call === \"object\" || typeof call === \"function\") ? call : self; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function, not \" + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+if (typeof exports !== \'undefined\') Object.defineProperty(exports, \'babelPluginFlowReactPropTypes_proptype_Fact\', {
+  value: typeof (Foo.Bar == null ? {} : Foo.Bar) === \'function\' ? require(\'prop-types\').instanceOf(Foo.Bar == null ? {} : Foo.Bar) : require(\'prop-types\').any
+});
+if (typeof exports !== \'undefined\') Object.defineProperty(exports, \'babelPluginFlowReactPropTypes_proptype_FactMap\', {
+  value: typeof (Foo.Map == null ? {} : Foo.Map) === \'function\' ? require(\'prop-types\').instanceOf(Foo.Map == null ? {} : Foo.Map) : require(\'prop-types\').any
+});
+
+var MyComponent = function (_React$Component) {
+  _inherits(MyComponent, _React$Component);
+
+  function MyComponent() {
+    _classCallCheck(this, MyComponent);
+
+    return _possibleConstructorReturn(this, (MyComponent.__proto__ || Object.getPrototypeOf(MyComponent)).apply(this, arguments));
+  }
+
+  return MyComponent;
+}(React.Component);
+
+MyComponent.propTypes = {
+  some_class: typeof (_some_class.SomeClass.property == null ? {} : _some_class.SomeClass.property) === \'function\' ? require(\'prop-types\').instanceOf(_some_class.SomeClass.property == null ? {} : _some_class.SomeClass.property).isRequired : require(\'prop-types\').any.isRequired,
+  generics: typeof (_some_class.SomeClass.Map == null ? {} : _some_class.SomeClass.Map) === \'function\' ? require(\'prop-types\').instanceOf(_some_class.SomeClass.Map == null ? {} : _some_class.SomeClass.Map).isRequired : require(\'prop-types\').any.isRequired,
+  deep: typeof (((_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting == null ? {} : (_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting).Bar == null ? {} : ((_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting == null ? {} : (_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting).Bar) === \'function\' ? require(\'prop-types\').instanceOf(((_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting == null ? {} : (_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting).Bar == null ? {} : ((_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting == null ? {} : (_some_class.SomeClass.deeper == null ? {} : _some_class.SomeClass.deeper).nesting).Bar).isRequired : require(\'prop-types\').any.isRequired
+};"
+`;
diff --git a/src/__tests__/__snapshots__/test-issue-66.js.snap b/src/__tests__/__snapshots__/test-issue-66.js.snap
new file mode 100644
index 0000000..6b05c7e
--- /dev/null
+++ b/src/__tests__/__snapshots__/test-issue-66.js.snap
@@ -0,0 +1,14 @@
+exports[`test issue 66 1`] = `
+"\"use strict\";
+
+Object.defineProperty(exports, \"__esModule\", {
+  value: true
+});
+
+exports.default = function (url, options) {
+  var Html = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DefaultHtml;
+
+
+  return React.createElement(\"div\", null);
+};"
+`;
diff --git a/src/__tests__/instance-of-object-properties.js b/src/__tests__/instance-of-object-properties.js
new file mode 100644
index 0000000..8a1be6c
--- /dev/null
+++ b/src/__tests__/instance-of-object-properties.js
@@ -0,0 +1,25 @@
+const babel = require('babel-core');
+const content = `
+
+export type Fact = Foo.Bar;
+export type FactMap = Foo.Map<string, string>;
+
+import {SomeClass} from './some_class';
+
+class MyComponent extends React.Component {
+    props: {
+      some_class: SomeClass.property,
+      generics: SomeClass.Map<string, string>,
+      deep: SomeClass.deeper.nesting.Bar,
+    }
+}
+`;
+
+it('instance-of', () => {
+  const res = babel.transform(content, {
+    babelrc: false,
+    presets: ['es2015', 'stage-1', 'react'],
+    plugins: ['syntax-flow', require('../')],
+  }).code;
+  expect(res).toMatchSnapshot();
+});
diff --git a/src/__tests__/test-issue-66.js b/src/__tests__/test-issue-66.js
new file mode 100644
index 0000000..9d2e384
--- /dev/null
+++ b/src/__tests__/test-issue-66.js
@@ -0,0 +1,25 @@
+const babel = require('babel-core');
+const content = `
+
+// @flow
+
+export default function(
+  url: string,
+  options: PhenomicStaticConfig,
+  Html: Function = DefaultHtml
+): Promise<string> {
+
+  return <div/>;
+}
+
+
+`;
+
+it('issue 66', () => {
+  const res = babel.transform(content, {
+    babelrc: false,
+    presets: ['es2015', 'stage-1', 'react'],
+    plugins: ['syntax-flow', require('../')],
+  }).code;
+  expect(res).toMatchSnapshot();
+});