diff --git a/.eslintrc.js b/.eslintrc.js index ec9a5352bea..2d229af2f02 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,5 +27,9 @@ module.exports = { 'no-unused-vars': 'off', }, }, + { + files: 'src/**/__tests__/*-test.js', + env: { jest: true }, + }, ], }; diff --git a/src/__tests__/__snapshots__/main-test.js.snap b/src/__tests__/__snapshots__/main-test.js.snap index 8dfcedd139c..b72e4bcc86e 100644 --- a/src/__tests__/__snapshots__/main-test.js.snap +++ b/src/__tests__/__snapshots__/main-test.js.snap @@ -1066,3 +1066,24 @@ Object { }, } `; + +exports[`main fixtures processes component "component_20.js" without errors 1`] = ` +Object { + "description": "", + "displayName": "Button", + "methods": Array [], + "props": Object { + "@computed#children": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"default\\"", + }, + "description": "This is a test", + "required": false, + "type": Object { + "name": "string", + }, + }, + }, +} +`; diff --git a/src/__tests__/fixtures/component_20.js b/src/__tests__/fixtures/component_20.js new file mode 100644 index 00000000000..71876904f4b --- /dev/null +++ b/src/__tests__/fixtures/component_20.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Button = () => ( +
+); + +Button.propTypes = { + /** This is a test */ + [children]: PropTypes.string.isRequired, +}; + +Button.defaultProps = { + [children]: "default", +}; + +export default Button; diff --git a/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap b/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap new file mode 100644 index 00000000000..f31d43fa66f --- /dev/null +++ b/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`componentMethodsHandler should handle and ignore computed methods 1`] = ` +Array [ + Object { + "docblock": "The foo method", + "modifiers": Array [], + "name": "@computed#foo", + "params": Array [ + Object { + "name": "bar", + "optional": undefined, + "type": Object { + "name": "number", + }, + }, + ], + "returns": Object { + "type": Object { + "name": "number", + }, + }, + }, +] +`; diff --git a/src/handlers/__tests__/__snapshots__/defaultPropsHandler-test.js.snap b/src/handlers/__tests__/__snapshots__/defaultPropsHandler-test.js.snap new file mode 100644 index 00000000000..f7c4a11a2a8 --- /dev/null +++ b/src/handlers/__tests__/__snapshots__/defaultPropsHandler-test.js.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`defaultPropsHandler ClassDeclaration with static defaultProps should find prop default values that are imported variables 1`] = ` +Object { + "foo": Object { + "defaultValue": Object { + "computed": true, + "value": "ImportedComponent", + }, + }, +} +`; + +exports[`defaultPropsHandler ClassDeclaration with static defaultProps should find prop default values that are literals 1`] = ` +Object { + "abc": Object { + "defaultValue": Object { + "computed": false, + "value": "{xyz: abc.def, 123: 42}", + }, + }, + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "baz": Object { + "defaultValue": Object { + "computed": false, + "value": "[\\"foo\\", \\"bar\\"]", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler ClassExpression with static defaultProps should find prop default values that are literals 1`] = ` +Object { + "abc": Object { + "defaultValue": Object { + "computed": false, + "value": "{xyz: abc.def, 123: 42}", + }, + }, + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "baz": Object { + "defaultValue": Object { + "computed": false, + "value": "[\\"foo\\", \\"bar\\"]", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler Functional components with default params should find default props that are literals 1`] = ` +Object { + "abc": Object { + "defaultValue": Object { + "computed": false, + "value": "{xyz: abc.def, 123: 42}", + }, + }, + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "baz": Object { + "defaultValue": Object { + "computed": false, + "value": "[\\"foo\\", \\"bar\\"]", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler Functional components with default params should find prop default values that are imported variables 1`] = ` +Object { + "foo": Object { + "defaultValue": Object { + "computed": true, + "value": "ImportedComponent", + }, + }, +} +`; + +exports[`defaultPropsHandler Functional components with default params should override with defaultProps if available 1`] = ` +Object { + "abc": Object { + "defaultValue": Object { + "computed": false, + "value": "{xyz: abc.def, 123: 42}", + }, + }, + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "baz": Object { + "defaultValue": Object { + "computed": false, + "value": "[\\"foo\\", \\"bar\\"]", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler Functional components with default params should work with aliases 1`] = ` +Object { + "abc": Object { + "defaultValue": Object { + "computed": false, + "value": "{xyz: abc.def, 123: 42}", + }, + }, + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "baz": Object { + "defaultValue": Object { + "computed": false, + "value": "[\\"foo\\", \\"bar\\"]", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler Functional components with default params should work with no defaults 1`] = `Object {}`; + +exports[`defaultPropsHandler ObjectExpression handles computed properties 1`] = ` +Object { + "@computed#bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler ObjectExpression ignores complex computed properties 1`] = ` +Object { + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler ObjectExpression should find prop default values that are literals 1`] = ` +Object { + "abc": Object { + "defaultValue": Object { + "computed": false, + "value": "{xyz: abc.def, 123: 42}", + }, + }, + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, + "baz": Object { + "defaultValue": Object { + "computed": false, + "value": "[\\"foo\\", \\"bar\\"]", + }, + }, + "foo": Object { + "defaultValue": Object { + "computed": false, + "value": "\\"bar\\"", + }, + }, +} +`; + +exports[`defaultPropsHandler should only consider Property nodes, not e.g. spread properties 1`] = ` +Object { + "bar": Object { + "defaultValue": Object { + "computed": false, + "value": "42", + }, + }, +} +`; diff --git a/src/handlers/__tests__/__snapshots__/flowTypeHandler-test.js.snap b/src/handlers/__tests__/__snapshots__/flowTypeHandler-test.js.snap new file mode 100644 index 00000000000..adad2c36f13 --- /dev/null +++ b/src/handlers/__tests__/__snapshots__/flowTypeHandler-test.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`flowTypeHandler TypeAlias class definition for flow <0.53 ignores hash map entry 1`] = ` +Object { + "bar": Object { + "description": "", + "flowType": Object {}, + "required": false, + }, +} +`; + +exports[`flowTypeHandler TypeAlias class definition for flow >=0.53 with State ignores hash map entry 1`] = ` +Object { + "bar": Object { + "description": "", + "flowType": Object {}, + "required": false, + }, +} +`; + +exports[`flowTypeHandler TypeAlias class definition for flow >=0.53 without State ignores hash map entry 1`] = ` +Object { + "bar": Object { + "description": "", + "flowType": Object {}, + "required": false, + }, +} +`; + +exports[`flowTypeHandler TypeAlias class definition with inline props ignores hash map entry 1`] = ` +Object { + "bar": Object { + "description": "", + "flowType": Object {}, + "required": false, + }, +} +`; + +exports[`flowTypeHandler TypeAlias stateless component ignores hash map entry 1`] = ` +Object { + "bar": Object { + "description": "", + "flowType": Object {}, + "required": false, + }, +} +`; diff --git a/src/handlers/__tests__/__snapshots__/propTypeHandler-test.js.snap b/src/handlers/__tests__/__snapshots__/propTypeHandler-test.js.snap new file mode 100644 index 00000000000..59d2b63e348 --- /dev/null +++ b/src/handlers/__tests__/__snapshots__/propTypeHandler-test.js.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`propTypeHandler React.createClass handles computed properties 1`] = ` +Object { + "@computed#foo": Object { + "required": true, + "type": Object {}, + }, + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler React.createClass ignores complex computed properties 1`] = ` +Object { + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler class definition class property handles computed properties 1`] = ` +Object { + "@computed#foo": Object { + "required": true, + "type": Object {}, + }, + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler class definition class property ignores complex computed properties 1`] = ` +Object { + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler class definition static getter handles computed properties 1`] = ` +Object { + "@computed#foo": Object { + "required": true, + "type": Object {}, + }, + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler class definition static getter ignores complex computed properties 1`] = ` +Object { + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler stateless component handles computed properties 1`] = ` +Object { + "@computed#foo": Object { + "required": true, + "type": Object {}, + }, + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; + +exports[`propTypeHandler stateless component ignores complex computed properties 1`] = ` +Object { + "complex_prop": Object { + "required": true, + "type": Object {}, + }, +} +`; diff --git a/src/handlers/__tests__/componentMethodsHandler-test.js b/src/handlers/__tests__/componentMethodsHandler-test.js index 63555f206f6..39cf34703aa 100644 --- a/src/handlers/__tests__/componentMethodsHandler-test.js +++ b/src/handlers/__tests__/componentMethodsHandler-test.js @@ -132,6 +132,35 @@ describe('componentMethodsHandler', () => { test(parse(src).get('body', 0)); }); + it('should handle and ignore computed methods', () => { + const src = ` + class Test { + /** + * The foo method + */ + [foo](bar: number): number { + return bar; + } + + /** + * Should not show up + */ + [() => {}](bar: number): number { + return bar; + } + + componentDidMount() {} + + render() { + return null; + } + } + `; + + componentMethodsHandler(documentation, parse(src).get('body', 0)); + expect(documentation.methods).toMatchSnapshot(); + }); + it('should not find methods for stateless components', () => { const src = ` (props) => {} diff --git a/src/handlers/__tests__/defaultPropsHandler-test.js b/src/handlers/__tests__/defaultPropsHandler-test.js index a97a32e6de7..8e65c0fb607 100644 --- a/src/handlers/__tests__/defaultPropsHandler-test.js +++ b/src/handlers/__tests__/defaultPropsHandler-test.js @@ -8,9 +8,6 @@ * */ -/*global jest, describe, beforeEach, it, expect*/ - -jest.disableAutomock(); jest.mock('../../Documentation'); describe('defaultPropsHandler', () => { @@ -24,36 +21,6 @@ describe('defaultPropsHandler', () => { defaultPropsHandler = require('../defaultPropsHandler').default; }); - function test(definition) { - defaultPropsHandler(documentation, definition); - expect(documentation.descriptors).toEqual({ - foo: { - defaultValue: { - value: '"bar"', - computed: false, - }, - }, - bar: { - defaultValue: { - value: '42', - computed: false, - }, - }, - baz: { - defaultValue: { - value: '["foo", "bar"]', - computed: false, - }, - }, - abc: { - defaultValue: { - value: '{xyz: abc.def, 123: 42}', - computed: false, - }, - }, - }); - } - describe('ObjectExpression', () => { it('should find prop default values that are literals', () => { const src = ` @@ -68,7 +35,47 @@ describe('defaultPropsHandler', () => { } }) `; - test(parse(src).get('body', 0, 'expression')); + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'expression'), + ); + expect(documentation.descriptors).toMatchSnapshot(); + }); + + it('handles computed properties', () => { + const src = ` + ({ + getDefaultProps: function() { + return { + foo: "bar", + [bar]: 42, + }; + } + }) + `; + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'expression'), + ); + expect(documentation.descriptors).toMatchSnapshot(); + }); + + it('ignores complex computed properties', () => { + const src = ` + ({ + getDefaultProps: function() { + return { + foo: "bar", + [() => {}]: 42, + }; + } + }) + `; + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'expression'), + ); + expect(documentation.descriptors).toMatchSnapshot(); }); }); @@ -84,7 +91,8 @@ describe('defaultPropsHandler', () => { }; } `; - test(parse(src).get('body', 0)); + defaultPropsHandler(documentation, parse(src).get('body', 0)); + expect(documentation.descriptors).toMatchSnapshot(); }); it('should find prop default values that are imported variables', () => { @@ -98,14 +106,7 @@ describe('defaultPropsHandler', () => { } `; defaultPropsHandler(documentation, parse(src).get('body', 1)); - expect(documentation.descriptors).toEqual({ - foo: { - defaultValue: { - value: 'ImportedComponent', - computed: true, - }, - }, - }); + expect(documentation.descriptors).toMatchSnapshot(); }); }); @@ -120,7 +121,11 @@ describe('defaultPropsHandler', () => { abc: {xyz: abc.def, 123: 42} }; }`; - test(parse(src).get('body', 0, 'declarations', 0, 'init')); + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'declarations', 0, 'init'), + ); + expect(documentation.descriptors).toMatchSnapshot(); }); }); @@ -137,14 +142,7 @@ describe('defaultPropsHandler', () => { `; const definition = parse(src).get('body', 0, 'expression'); expect(() => defaultPropsHandler(documentation, definition)).not.toThrow(); - expect(documentation.descriptors).toEqual({ - bar: { - defaultValue: { - value: '42', - computed: false, - }, - }, - }); + expect(documentation.descriptors).toMatchSnapshot(); }); describe('Functional components with default params', () => { @@ -157,7 +155,11 @@ describe('defaultPropsHandler', () => { abc = {xyz: abc.def, 123: 42} }) =>
`; - test(parse(src).get('body', 0, 'expression')); + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'expression'), + ); + expect(documentation.descriptors).toMatchSnapshot(); }); it('should override with defaultProps if available', () => { @@ -170,7 +172,11 @@ describe('defaultPropsHandler', () => { }) =>
Foo.defaultProps = { abc: {xyz: abc.def, 123: 42} }; `; - test(parse(src).get('body', 0, 'declarations', 0, 'init')); + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'declarations', 0, 'init'), + ); + expect(documentation.descriptors).toMatchSnapshot(); }); it('should work with aliases', () => { @@ -182,7 +188,11 @@ describe('defaultPropsHandler', () => { abc: defg = {xyz: abc.def, 123: 42} }) =>
`; - test(parse(src).get('body', 0, 'expression')); + defaultPropsHandler( + documentation, + parse(src).get('body', 0, 'expression'), + ); + expect(documentation.descriptors).toMatchSnapshot(); }); it('should find prop default values that are imported variables', () => { @@ -197,15 +207,7 @@ describe('defaultPropsHandler', () => { documentation, parse(src).get('body', 1, 'expression'), ); - - expect(documentation.descriptors).toEqual({ - foo: { - defaultValue: { - value: 'ImportedComponent', - computed: true, - }, - }, - }); + expect(documentation.descriptors).toMatchSnapshot(); }); it('should work with no defaults', () => { @@ -216,7 +218,7 @@ describe('defaultPropsHandler', () => { documentation, parse(src).get('body', 0, 'expression'), ); - expect(documentation.descriptors).toEqual({}); + expect(documentation.descriptors).toMatchSnapshot(); }); }); }); diff --git a/src/handlers/__tests__/flowTypeHandler-test.js b/src/handlers/__tests__/flowTypeHandler-test.js index 699ebd974de..a61a025df9d 100644 --- a/src/handlers/__tests__/flowTypeHandler-test.js +++ b/src/handlers/__tests__/flowTypeHandler-test.js @@ -96,6 +96,20 @@ describe('flowTypeHandler', () => { }); }); + it('ignores hash map entry', () => { + const flowTypesSrc = ` + { + [key: string]: string, + bar?: number, + } + `; + const definition = getSrc(flowTypesSrc); + + flowTypeHandler(documentation, definition); + + expect(documentation.descriptors).toMatchSnapshot(); + }); + it('detects union types', () => { const flowTypesSrc = ` { diff --git a/src/handlers/__tests__/propTypeHandler-test.js b/src/handlers/__tests__/propTypeHandler-test.js index fd0a4de8232..1f8c3546f6b 100644 --- a/src/handlers/__tests__/propTypeHandler-test.js +++ b/src/handlers/__tests__/propTypeHandler-test.js @@ -8,8 +8,6 @@ * */ -/*global jest, describe, it, expect, beforeEach*/ - jest.mock('../../Documentation'); jest.mock('../../utils/getPropType', () => jest.fn(() => ({}))); @@ -123,6 +121,36 @@ describe('propTypeHandler', () => { }); }); + it('handles computed properties', () => { + const definition = parse( + getSrc( + `{ + [foo]: PropTypes.array.isRequired, + complex_prop: + PropTypes.oneOfType([PropTypes.number, PropTypes.bool]).isRequired, + }`, + ), + ); + + propTypeHandler(documentation, definition); + expect(documentation.descriptors).toMatchSnapshot(); + }); + + it('ignores complex computed properties', () => { + const definition = parse( + getSrc( + `{ + [() => {}]: PropTypes.array.isRequired, + complex_prop: + PropTypes.oneOfType([PropTypes.number, PropTypes.bool]).isRequired, + }`, + ), + ); + + propTypeHandler(documentation, definition); + expect(documentation.descriptors).toMatchSnapshot(); + }); + it('only considers definitions from React or ReactPropTypes', () => { const definition = parse( getSrc( diff --git a/src/handlers/componentMethodsHandler.js b/src/handlers/componentMethodsHandler.js index 2fcc29fd63d..976cad116c3 100644 --- a/src/handlers/componentMethodsHandler.js +++ b/src/handlers/componentMethodsHandler.js @@ -69,5 +69,8 @@ export default function componentMethodsHandler( } } - documentation.set('methods', methodPaths.map(getMethodDocumentation)); + documentation.set( + 'methods', + methodPaths.map(getMethodDocumentation).filter(Boolean), + ); } diff --git a/src/handlers/defaultPropsHandler.js b/src/handlers/defaultPropsHandler.js index 7d410323265..61aa6e0ea92 100644 --- a/src/handlers/defaultPropsHandler.js +++ b/src/handlers/defaultPropsHandler.js @@ -99,10 +99,11 @@ function getDefaultValuesFromProps( !isStateless || types.AssignmentPattern.check(propertyPath.get('value').node), ) - .forEach(function(propertyPath) { - const propDescriptor = documentation.getPropDescriptor( - getPropertyName(propertyPath), - ); + .forEach(propertyPath => { + const propName = getPropertyName(propertyPath); + if (!propName) return; + + const propDescriptor = documentation.getPropDescriptor(propName); const defaultValue = getDefaultValue( isStateless ? propertyPath.get('value', 'right') diff --git a/src/handlers/flowTypeHandler.js b/src/handlers/flowTypeHandler.js index 1dbca53fcaa..c38a33adf63 100644 --- a/src/handlers/flowTypeHandler.js +++ b/src/handlers/flowTypeHandler.js @@ -55,9 +55,10 @@ function setPropDescriptor(documentation: Documentation, path: NodePath): void { } } else if (types.ObjectTypeProperty.check(path.node)) { const type = getFlowType(path.get('value')); - const propDescriptor = documentation.getPropDescriptor( - getPropertyName(path), - ); + const propName = getPropertyName(path); + if (!propName) return; + + const propDescriptor = documentation.getPropDescriptor(propName); propDescriptor.required = !path.node.optional; propDescriptor.flowType = type; diff --git a/src/handlers/propTypeHandler.js b/src/handlers/propTypeHandler.js index 0a1fa2f34a4..6cad9a62b4c 100644 --- a/src/handlers/propTypeHandler.js +++ b/src/handlers/propTypeHandler.js @@ -39,10 +39,13 @@ function amendPropTypes(getDescriptor, path) { return; } - path.get('properties').each(function(propertyPath) { + path.get('properties').each(propertyPath => { switch (propertyPath.node.type) { case types.Property.name: { - const propDescriptor = getDescriptor(getPropertyName(propertyPath)); + const propName = getPropertyName(propertyPath); + if (!propName) return; + + const propDescriptor = getDescriptor(propName); const valuePath = propertyPath.get('value'); const type = isPropTypesExpression(valuePath) ? getPropType(valuePath) diff --git a/src/utils/__tests__/__snapshots__/getMethodDocumentation-test.js.snap b/src/utils/__tests__/__snapshots__/getMethodDocumentation-test.js.snap new file mode 100644 index 00000000000..3a95cebf940 --- /dev/null +++ b/src/utils/__tests__/__snapshots__/getMethodDocumentation-test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getMethodDocumentation name handles computed method name 1`] = ` +Object { + "docblock": null, + "modifiers": Array [], + "name": "@computed#foo", + "params": Array [], + "returns": null, +} +`; + +exports[`getMethodDocumentation name ignores complex computed method name 1`] = `null`; diff --git a/src/utils/__tests__/__snapshots__/getPropType-test.js.snap b/src/utils/__tests__/__snapshots__/getPropType-test.js.snap index 0e2af1e794a..f41957ab084 100644 --- a/src/utils/__tests__/__snapshots__/getPropType-test.js.snap +++ b/src/utils/__tests__/__snapshots__/getPropType-test.js.snap @@ -102,6 +102,34 @@ Object { } `; +exports[`getPropType handles computed properties 1`] = ` +Object { + "name": "exact", + "value": Object { + "@computed#foo": Object { + "name": "string", + "required": true, + }, + "bar": Object { + "name": "bool", + "required": false, + }, + }, +} +`; + +exports[`getPropType ignores complex computed properties 1`] = ` +Object { + "name": "exact", + "value": Object { + "bar": Object { + "name": "bool", + "required": false, + }, + }, +} +`; + exports[`getPropType resolve identifier to their values correctly resolves SpreadElements in arrays 1`] = ` Object { "name": "enum", diff --git a/src/utils/__tests__/getMethodDocumentation-test.js b/src/utils/__tests__/getMethodDocumentation-test.js index 2dc9a3224f0..712cce9bac8 100644 --- a/src/utils/__tests__/getMethodDocumentation-test.js +++ b/src/utils/__tests__/getMethodDocumentation-test.js @@ -37,6 +37,26 @@ describe('getMethodDocumentation', () => { params: [], }); }); + + it('handles computed method name', () => { + const def = statement(` + class Foo { + [foo]() {} + } + `); + const method = def.get('body', 'body', 0); + expect(getMethodDocumentation(method)).toMatchSnapshot(); + }); + + it('ignores complex computed method name', () => { + const def = statement(` + class Foo { + [() => {}]() {} + } + `); + const method = def.get('body', 'body', 0); + expect(getMethodDocumentation(method)).toMatchSnapshot(); + }); }); describe('docblock', () => { diff --git a/src/utils/__tests__/getPropType-test.js b/src/utils/__tests__/getPropType-test.js index b52fc425d3f..ea42e6e3f66 100644 --- a/src/utils/__tests__/getPropType-test.js +++ b/src/utils/__tests__/getPropType-test.js @@ -340,4 +340,26 @@ describe('getPropType', () => { ), ).toMatchSnapshot(); }); + + it('handles computed properties', () => { + expect( + getPropType( + expression(`exact({ + [foo]: string.isRequired, + bar: bool + })`), + ), + ).toMatchSnapshot(); + }); + + it('ignores complex computed properties', () => { + expect( + getPropType( + expression(`exact({ + [() => {}]: string.isRequired, + bar: bool + })`), + ), + ).toMatchSnapshot(); + }); }); diff --git a/src/utils/__tests__/getPropertyName-test.js b/src/utils/__tests__/getPropertyName-test.js new file mode 100644 index 00000000000..39c42fa465e --- /dev/null +++ b/src/utils/__tests__/getPropertyName-test.js @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +describe('getPropertyName', () => { + let getPropertyName; + let expression; + + beforeEach(() => { + getPropertyName = require('../getPropertyName').default; + ({ expression } = require('../../../tests/utils')); + }); + + it('returns the name for a normal property', () => { + const def = expression('{ foo: 1 }'); + const param = def.get('properties', 0); + + expect(getPropertyName(param)).toBe('foo'); + }); + + it('returns the name of a object type spread property', () => { + const def = expression('(a: { ...foo })'); + const param = def.get('typeAnnotation', 'typeAnnotation', 'properties', 0); + + expect(getPropertyName(param)).toBe('foo'); + }); + + it('creates name for computed properties', () => { + const def = expression('{ [foo]: 21 }'); + const param = def.get('properties', 0); + + expect(getPropertyName(param)).toBe('@computed#foo'); + }); + + it('creates name for computed properties from string', () => { + const def = expression('{ ["foo"]: 21 }'); + const param = def.get('properties', 0); + + expect(getPropertyName(param)).toBe('@computed#foo'); + }); + + it('creates name for computed properties from int', () => { + const def = expression('{ [31]: 21 }'); + const param = def.get('properties', 0); + + expect(getPropertyName(param)).toBe('@computed#31'); + }); + + it('returns null for computed properties from regex', () => { + const def = expression('{ [/31/]: 21 }'); + const param = def.get('properties', 0); + + expect(getPropertyName(param)).toBe(null); + }); + + it('returns null for to complex computed properties', () => { + const def = expression('{ [() => {}]: 21 }'); + const param = def.get('properties', 0); + + expect(getPropertyName(param)).toBe(null); + }); +}); diff --git a/src/utils/getMethodDocumentation.js b/src/utils/getMethodDocumentation.js index 328c12be653..75d78199971 100644 --- a/src/utils/getMethodDocumentation.js +++ b/src/utils/getMethodDocumentation.js @@ -104,8 +104,10 @@ function getMethodModifiers(methodPath) { export default function getMethodDocumentation( methodPath: NodePath, -): MethodDocumentation { +): ?MethodDocumentation { const name = getPropertyName(methodPath); + if (!name) return null; + const docblock = getDocblock(methodPath); return { diff --git a/src/utils/getPropType.js b/src/utils/getPropType.js index 96f89d322d1..de7b3629774 100644 --- a/src/utils/getPropType.js +++ b/src/utils/getPropType.js @@ -149,6 +149,8 @@ function getPropTypeShapish(name, argumentPath) { return; } + const propertyName = getPropertyName(propertyPath); + if (!propertyName) return; const descriptor: PropDescriptor | PropTypeDescriptor = getPropType( propertyPath.get('value'), ); @@ -157,7 +159,7 @@ function getPropTypeShapish(name, argumentPath) { descriptor.description = docs; } descriptor.required = isRequiredPropType(propertyPath.get('value')); - value[getPropertyName(propertyPath)] = descriptor; + value[propertyName] = descriptor; }); type.value = value; } diff --git a/src/utils/getPropertyName.js b/src/utils/getPropertyName.js index adc16e7336f..a5d625dd282 100644 --- a/src/utils/getPropertyName.js +++ b/src/utils/getPropertyName.js @@ -17,16 +17,31 @@ const { types: { namedTypes: types }, } = recast; +export const COMPUTED_PREFIX = '@computed#'; + /** * In an ObjectExpression, the name of a property can either be an identifier * or a literal (or dynamic, but we don't support those). This function simply * returns the value of the literal or name of the identifier. */ -export default function getPropertyName(propertyPath: NodePath): string { +export default function getPropertyName(propertyPath: NodePath): ?string { if (types.ObjectTypeSpreadProperty.check(propertyPath.node)) { return getNameOrValue(propertyPath.get('argument').get('id'), false); } else if (propertyPath.node.computed) { - throw new TypeError('Property name must be an Identifier or a Literal'); + if (types.Identifier.check(propertyPath.node.key)) { + return `${COMPUTED_PREFIX}${getNameOrValue( + propertyPath.get('key'), + false, + )}`; + } else if ( + types.Literal.check(propertyPath.node.key) && + (typeof propertyPath.node.key.value === 'string' || + typeof propertyPath.node.key.value === 'number') + ) { + return `${COMPUTED_PREFIX}${propertyPath.node.key.value}`; + } + + return null; } return getNameOrValue(propertyPath.get('key'), false); diff --git a/src/utils/isReactComponentMethod.js b/src/utils/isReactComponentMethod.js index 822e3672ecf..263ef611607 100644 --- a/src/utils/isReactComponentMethod.js +++ b/src/utils/isReactComponentMethod.js @@ -44,5 +44,5 @@ export default function(methodPath: NodePath): boolean { } const name = getPropertyName(methodPath); - return componentMethods.indexOf(name) !== -1; + return !!name && componentMethods.indexOf(name) !== -1; } diff --git a/src/utils/setPropDescription.js b/src/utils/setPropDescription.js index 1fbe11f699c..60e87b2185c 100644 --- a/src/utils/setPropDescription.js +++ b/src/utils/setPropDescription.js @@ -7,23 +7,18 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @flow - * */ import type Documentation from '../Documentation'; import getPropertyName from './getPropertyName'; import { getDocblock } from './docblock'; -/** - * - */ export default (documentation: Documentation, propertyPath: NodePath) => { const propName = getPropertyName(propertyPath); - const propDescriptor = documentation.getPropDescriptor(propName); + if (!propName) return; - if (propDescriptor.description) { - return; - } + const propDescriptor = documentation.getPropDescriptor(propName); + if (propDescriptor.description) return; propDescriptor.description = getDocblock(propertyPath) || ''; };