Repository: brigand/babel-plugin-flow-react-proptypes
Pull request #92 (2017-05-28 13:04:31)

Support instanceOf - reloaded
This is a re-implementation of #6 .


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 451c112..2753f0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
   - Add support for top-level propTypes assignment of imported types (#88)
+  - Add support for instanceOf (see PR #6, #92)
 
 ## 3.0.0
 
diff --git a/src/convertToPropTypes.js b/src/convertToPropTypes.js
index 777424c..3f9bf0e 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: 'any'};
+      resultPropType = {type: 'possible-class', value: node.id.name};
     }
   }
   else if (node.type === 'UnionTypeAnnotation') {
diff --git a/src/makePropTypesAst.js b/src/makePropTypesAst.js
index c3b4913..ab4555f 100644
--- a/src/makePropTypesAst.js
+++ b/src/makePropTypesAst.js
@@ -54,14 +54,19 @@ export function makePropTypesAstForExport(propTypeData) {
 };
 
 
+function makeAnyPropTypeAST() {
+  const importNode = makePropTypeImportNode();
+  const anyNode = t.memberExpression(importNode, t.identifier('any'));
+  return anyNode;
+}
+
 function makeObjectAstForRaw(propTypeSpec, propTypeObjects) {
   let propTypeObject = t.identifier(propTypeSpec.value);
 
   // This will just be a variable, referencing an import we
   // generated above. This variable may contain prop-types.any,
   // which will not work when used in an intersection.
-  const importNode = makePropTypeImportNode();
-  const anyNode = t.memberExpression(importNode, t.identifier('any'));
+  const anyNode = makeAnyPropTypeAST();
   const testExpression = t.binaryExpression('===', propTypeObject, anyNode);
   propTypeObject = t.conditionalExpression(testExpression, t.objectExpression([]), propTypeObject);
   return propTypeObject;
@@ -162,6 +167,12 @@ function makePropTypeImportNode() {
     return t.memberExpression(reactNode, t.identifier('PropTypes'));
   }
 }
+function makeFunctionCheckAST(variableNode) {
+  return t.binaryExpression('===', t.unaryExpression('typeof', variableNode), t.stringLiteral('function'));
+}
+function markNodeAsRequired(node) {
+  return t.memberExpression(node, t.identifier('isRequired'));
+}
 /**
  * Handles all prop types.
  *
@@ -190,18 +201,18 @@ function makePropType(data, isExact) {
     return makePropType(data.properties, true);
   }
 
-  let isRequired = true;
   let node = makePropTypeImportNode();
+  let markFullExpressionAsRequired = true;
 
   if (method === 'any' || method === 'string' || method === 'number' || method === 'bool' || method === 'object' ||
       method === 'array' || method === 'func' || method === 'node') {
     node = t.memberExpression(node, t.identifier(method));
   }
   else if (method === 'raw') {
+    markFullExpressionAsRequired = false;
     // In 'raw', we handle variables - typically derived from imported types.
     // These are either - at run-time - objects or functions. Objects are wrapped in a shape;
     // for functions, we assume that the variable already contains a proptype assertion
-
     const variableNode = t.identifier(data.value);
     const shapeNode = t.callExpression(
         t.memberExpression(
@@ -210,9 +221,8 @@ function makePropType(data, isExact) {
         ),
         [variableNode],
     );
-    const functionCheckNode = t.binaryExpression('===', t.unaryExpression('typeof', variableNode), t.stringLiteral('function'));
+    const functionCheckNode = makeFunctionCheckAST(variableNode);
     node = t.conditionalExpression(functionCheckNode, variableNode, shapeNode);
-    isRequired = false;
   }
   else if (method === 'shape') {
     const shapeObjectProperties = data.properties.map(({key, value}) => {
@@ -257,8 +267,26 @@ function makePropType(data, isExact) {
     );
   }
   else if (method === 'void') {
+    markFullExpressionAsRequired = false;
     node = dontSetTemplate().expression;
   }
+  else if (method === 'possible-class') {
+    markFullExpressionAsRequired = false;
+
+    let instanceOfNode = t.callExpression(
+        t.memberExpression(node, t.identifier('instanceOf')),
+        [t.identifier(data.value)]
+      );
+    let anyNode = makeAnyPropTypeAST();
+    if (data.isRequired) {
+      instanceOfNode = markNodeAsRequired(instanceOfNode);
+      anyNode = markNodeAsRequired(anyNode);
+    }
+    const functionCheckNode = makeFunctionCheckAST(t.identifier(data.value));
+    node = t.conditionalExpression(functionCheckNode, instanceOfNode, anyNode);
+    // Don't add .required on the full expression; we already handled this ourselves
+    // for any proptypes we generated
+  }
   else {
     const bugData = JSON.stringify(data, null, 2);
     $debug('Unknown node ' + bugData);
@@ -266,8 +294,8 @@ function makePropType(data, isExact) {
       `Report it immediately with the source file and babel config. Data: ${bugData}`);
   }
 
-  if (isRequired && data.isRequired && method !== 'void') {
-    node = t.memberExpression(node, t.identifier('isRequired'));
+  if (markFullExpressionAsRequired && data.isRequired) {
+    node = markNodeAsRequired(node);
   }
 
   return node;


Test patch:
diff --git a/src/__tests__/__snapshots__/basic-test.js.snap b/src/__tests__/__snapshots__/basic-test.js.snap
index be0150b..953f9c0 100644
--- a/src/__tests__/__snapshots__/basic-test.js.snap
+++ b/src/__tests__/__snapshots__/basic-test.js.snap
@@ -31,7 +31,7 @@ Foo.propTypes = {
   a_boolean: require(\'prop-types\').bool.isRequired,
   a_generic_object: require(\'prop-types\').object.isRequired,
   array_of_strings: require(\'prop-types\').arrayOf(require(\'prop-types\').string).isRequired,
-  instance_of_Bar: require(\'prop-types\').any.isRequired,
+  instance_of_Bar: typeof Bar === \'function\' ? require(\'prop-types\').instanceOf(Bar).isRequired : require(\'prop-types\').any.isRequired,
   anything: require(\'prop-types\').any.isRequired,
   mixed: require(\'prop-types\').any.isRequired,
   one_of: require(\'prop-types\').oneOf([\'QUACK\', \'BARK\', 5]).isRequired,
diff --git a/src/__tests__/__snapshots__/import-non-relative.js.snap b/src/__tests__/__snapshots__/import-non-relative.js.snap
index 2405700..82fe2d5 100644
--- a/src/__tests__/__snapshots__/import-non-relative.js.snap
+++ b/src/__tests__/__snapshots__/import-non-relative.js.snap
@@ -26,8 +26,8 @@ var C = function (_React$Component) {
 C.propTypes = {
   an_imported_named_type: typeof babelPluginFlowReactPropTypes_proptype_NamedType === \'function\' ? babelPluginFlowReactPropTypes_proptype_NamedType : require(\'prop-types\').shape(babelPluginFlowReactPropTypes_proptype_NamedType),
   an_imported_default_type: typeof babelPluginFlowReactPropTypes_proptype_DefaultType === \'function\' ? babelPluginFlowReactPropTypes_proptype_DefaultType : require(\'prop-types\').shape(babelPluginFlowReactPropTypes_proptype_DefaultType),
-  a_global_type: require(\'prop-types\').any.isRequired,
-  a_undefined_type: require(\'prop-types\').any.isRequired
+  a_global_type: typeof Date === \'function\' ? require(\'prop-types\').instanceOf(Date).isRequired : require(\'prop-types\').any.isRequired,
+  a_undefined_type: typeof FooBarBaz === \'function\' ? require(\'prop-types\').instanceOf(FooBarBaz).isRequired : require(\'prop-types\').any.isRequired
 };
 ;"
 `;
diff --git a/src/__tests__/__snapshots__/import-object-test.js.snap b/src/__tests__/__snapshots__/import-object-test.js.snap
index 607cf45..c61071c 100644
--- a/src/__tests__/__snapshots__/import-object-test.js.snap
+++ b/src/__tests__/__snapshots__/import-object-test.js.snap
@@ -26,8 +26,8 @@ var C = function (_React$Component) {
 C.propTypes = {
   an_imported_named_type: typeof babelPluginFlowReactPropTypes_proptype_NamedType === \'function\' ? babelPluginFlowReactPropTypes_proptype_NamedType : require(\'prop-types\').shape(babelPluginFlowReactPropTypes_proptype_NamedType),
   an_imported_default_type: typeof babelPluginFlowReactPropTypes_proptype_DefaultType === \'function\' ? babelPluginFlowReactPropTypes_proptype_DefaultType : require(\'prop-types\').shape(babelPluginFlowReactPropTypes_proptype_DefaultType),
-  a_global_type: require(\'prop-types\').any.isRequired,
-  a_undefined_type: require(\'prop-types\').any.isRequired
+  a_global_type: typeof Date === \'function\' ? require(\'prop-types\').instanceOf(Date).isRequired : require(\'prop-types\').any.isRequired,
+  a_undefined_type: typeof FooBarBaz === \'function\' ? require(\'prop-types\').instanceOf(FooBarBaz).isRequired : require(\'prop-types\').any.isRequired
 };
 ;"
 `;
diff --git a/src/__tests__/__snapshots__/instance-of.js.snap b/src/__tests__/__snapshots__/instance-of.js.snap
new file mode 100644
index 0000000..c1b32c9
--- /dev/null
+++ b/src/__tests__/__snapshots__/instance-of.js.snap
@@ -0,0 +1,38 @@
+exports[`test instance-of 1`] = `
+"\'use strict\';
+
+var _some_class = require(\'./some_class\');
+
+var _some_class2 = _interopRequireDefault(_some_class);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+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; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }
+
+var SomeOtherClass = function SomeOtherClass() {
+  _classCallCheck(this, SomeOtherClass);
+};
+
+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_class2.default === \'function\' ? require(\'prop-types\').instanceOf(_some_class2.default).isRequired : require(\'prop-types\').any.isRequired,
+  some_other_class: typeof SomeOtherClass === \'function\' ? require(\'prop-types\').instanceOf(SomeOtherClass).isRequired : require(\'prop-types\').any.isRequired,
+  key_can_be_omitted: typeof _some_class2.default === \'function\' ? require(\'prop-types\').instanceOf(_some_class2.default) : require(\'prop-types\').any,
+  value_can_be_null: typeof SomeOtherClass === \'function\' ? require(\'prop-types\').instanceOf(SomeOtherClass) : require(\'prop-types\').any
+};"
+`;
diff --git a/src/__tests__/__snapshots__/stateless-inline-test.js.snap b/src/__tests__/__snapshots__/stateless-inline-test.js.snap
index 699a83c..2d5e950 100644
--- a/src/__tests__/__snapshots__/stateless-inline-test.js.snap
+++ b/src/__tests__/__snapshots__/stateless-inline-test.js.snap
@@ -22,7 +22,7 @@ Foo.propTypes = {
   a_boolean: require(\'prop-types\').bool.isRequired,
   a_generic_object: require(\'prop-types\').object.isRequired,
   array_of_strings: require(\'prop-types\').arrayOf(require(\'prop-types\').string).isRequired,
-  instance_of_Bar: require(\'prop-types\').any.isRequired,
+  instance_of_Bar: typeof Bar === \'function\' ? require(\'prop-types\').instanceOf(Bar).isRequired : require(\'prop-types\').any.isRequired,
   anything: require(\'prop-types\').any.isRequired,
   one_of: require(\'prop-types\').oneOf([\'QUACK\', \'BARK\', 5]).isRequired,
   onw_of_type: require(\'prop-types\').oneOfType([require(\'prop-types\').number, require(\'prop-types\').string]).isRequired,
diff --git a/src/__tests__/__snapshots__/stateless-test.js.snap b/src/__tests__/__snapshots__/stateless-test.js.snap
index d528098..0f8b64c 100644
--- a/src/__tests__/__snapshots__/stateless-test.js.snap
+++ b/src/__tests__/__snapshots__/stateless-test.js.snap
@@ -21,7 +21,7 @@ Foo.propTypes = {
   a_boolean: require(\'prop-types\').bool.isRequired,
   a_generic_object: require(\'prop-types\').object.isRequired,
   array_of_strings: require(\'prop-types\').arrayOf(require(\'prop-types\').string).isRequired,
-  instance_of_Bar: require(\'prop-types\').any.isRequired,
+  instance_of_Bar: typeof Bar === \'function\' ? require(\'prop-types\').instanceOf(Bar).isRequired : require(\'prop-types\').any.isRequired,
   anything: require(\'prop-types\').any.isRequired,
   one_of: require(\'prop-types\').oneOf([\'QUACK\', \'BARK\', 5]).isRequired,
   onw_of_type: require(\'prop-types\').oneOfType([require(\'prop-types\').number, require(\'prop-types\').string]).isRequired,
diff --git a/src/__tests__/instance-of.js b/src/__tests__/instance-of.js
new file mode 100644
index 0000000..d04a390
--- /dev/null
+++ b/src/__tests__/instance-of.js
@@ -0,0 +1,25 @@
+const babel = require('babel-core');
+const content = `
+import SomeClass from './some_class';
+class SomeOtherClass {
+
+}
+
+class MyComponent extends React.Component {
+    props: {
+      some_class: SomeClass,
+      some_other_class: SomeOtherClass,
+      key_can_be_omitted?: SomeClass,
+      value_can_be_null: ?SomeOtherClass,
+    }
+}
+`;
+
+it('instance-of', () => {
+  const res = babel.transform(content, {
+    babelrc: false,
+    presets: ['es2015', 'stage-1', 'react'],
+    plugins: ['syntax-flow', require('../')],
+  }).code;
+  expect(res).toMatchSnapshot();
+});