From 3bff7c86e010fe068d9184bf0ffc0b5bfcff3330 Mon Sep 17 00:00:00 2001 From: Jesse Renee Beach Date: Sun, 17 Mar 2019 14:58:19 -0700 Subject: [PATCH] [evcohen/jsx-ast-utils#70] Support OptionalMemberExpression AST nodes --- __tests__/helper.js | 8 ++- __tests__/src/getPropLiteralValue-test.js | 12 ++-- __tests__/src/getPropValue-test.js | 26 +++++-- elementType.js | 2 +- src/elementType.js | 4 +- src/getProp.js | 6 +- src/hasProp.js | 6 +- src/values/Literal.js | 4 +- src/values/expressions/ArrayExpression.js | 4 +- src/values/expressions/BinaryExpression.js | 5 +- src/values/expressions/BindExpression.js | 5 +- src/values/expressions/CallExpression.js | 4 +- .../expressions/ConditionalExpression.js | 4 +- src/values/expressions/LogicalExpression.js | 4 +- src/values/expressions/MemberExpression.js | 4 +- src/values/expressions/ObjectExpression.js | 6 +- .../expressions/OptionalMemberExpression.js | 13 ++++ src/values/expressions/TemplateLiteral.js | 8 ++- src/values/expressions/UnaryExpression.js | 4 +- src/values/expressions/UpdateExpression.js | 4 +- src/values/expressions/index.js | 71 ++++++++++--------- 21 files changed, 122 insertions(+), 82 deletions(-) create mode 100644 src/values/expressions/OptionalMemberExpression.js diff --git a/__tests__/helper.js b/__tests__/helper.js index 5993fe5..364fe04 100644 --- a/__tests__/helper.js +++ b/__tests__/helper.js @@ -4,7 +4,13 @@ const parser = require('babylon'); function parse(code) { return parser.parse(code, { - plugins: ['jsx', 'functionBind', 'estree', 'objectRestSpread'], + plugins: [ + 'estree', + 'functionBind', + 'jsx', + 'objectRestSpread', + 'optionalChaining', + ], }); } diff --git a/__tests__/src/getPropLiteralValue-test.js b/__tests__/src/getPropLiteralValue-test.js index 9c396d0..adfb280 100644 --- a/__tests__/src/getPropLiteralValue-test.js +++ b/__tests__/src/getPropLiteralValue-test.js @@ -258,7 +258,7 @@ describe('getLiteralPropValue', () => { // -"bar" => NaN const expected = true; - const actual = isNaN(getLiteralPropValue(prop)); + const actual = Number.isNaN(getLiteralPropValue(prop)); assert.equal(expected, actual); }); @@ -277,7 +277,7 @@ describe('getLiteralPropValue', () => { // +"bar" => NaN const expected = true; - const actual = isNaN(getLiteralPropValue(prop)); + const actual = Number.isNaN(getLiteralPropValue(prop)); assert.equal(expected, actual); }); @@ -344,7 +344,7 @@ describe('getLiteralPropValue', () => { // ++"bar" => NaN const expected = true; - const actual = isNaN(getLiteralPropValue(prop)); + const actual = Number.isNaN(getLiteralPropValue(prop)); assert.equal(expected, actual); }); @@ -354,7 +354,7 @@ describe('getLiteralPropValue', () => { // --"bar" => NaN const expected = true; - const actual = isNaN(getLiteralPropValue(prop)); + const actual = Number.isNaN(getLiteralPropValue(prop)); assert.equal(expected, actual); }); @@ -364,7 +364,7 @@ describe('getLiteralPropValue', () => { // "bar"++ => NaN const expected = true; - const actual = isNaN(getLiteralPropValue(prop)); + const actual = Number.isNaN(getLiteralPropValue(prop)); assert.equal(expected, actual); }); @@ -374,7 +374,7 @@ describe('getLiteralPropValue', () => { // "bar"-- => NaN const expected = true; - const actual = isNaN(getLiteralPropValue(prop)); + const actual = Number.isNaN(getLiteralPropValue(prop)); assert.equal(expected, actual); }); diff --git a/__tests__/src/getPropValue-test.js b/__tests__/src/getPropValue-test.js index b7e2da5..d3e4479 100644 --- a/__tests__/src/getPropValue-test.js +++ b/__tests__/src/getPropValue-test.js @@ -355,6 +355,20 @@ describe('getPropValue', () => { assert.equal(expected, actual); }); + + it('should evaluate to a correct representation of member expression with a nullable member', () => { + // This tell will not throw when Babel is upgraded from 6 to 7. Remove + // the throw expectation wrapper at that time. + // eslint-disable-next-line no-undef + expect(() => { + const prop = extractProp('
'); + + const expected = 'bar?.baz'; + const actual = getPropValue(prop); + + assert.equal(expected, actual); + }).toThrow(); + }); }); describe('Call expression', () => { @@ -383,7 +397,7 @@ describe('getPropValue', () => { // -"bar" => NaN const expected = true; - const actual = isNaN(getPropValue(prop)); + const actual = Number.isNaN(getPropValue(prop)); assert.equal(expected, actual); }); @@ -402,7 +416,7 @@ describe('getPropValue', () => { // +"bar" => NaN const expected = true; - const actual = isNaN(getPropValue(prop)); + const actual = Number.isNaN(getPropValue(prop)); assert.equal(expected, actual); }); @@ -469,7 +483,7 @@ describe('getPropValue', () => { // ++"bar" => NaN const expected = true; - const actual = isNaN(getPropValue(prop)); + const actual = Number.isNaN(getPropValue(prop)); assert.equal(expected, actual); }); @@ -478,7 +492,7 @@ describe('getPropValue', () => { const prop = extractProp('
'); const expected = true; - const actual = isNaN(getPropValue(prop)); + const actual = Number.isNaN(getPropValue(prop)); assert.equal(expected, actual); }); @@ -488,7 +502,7 @@ describe('getPropValue', () => { // "bar"++ => NaN const expected = true; - const actual = isNaN(getPropValue(prop)); + const actual = Number.isNaN(getPropValue(prop)); assert.equal(expected, actual); }); @@ -497,7 +511,7 @@ describe('getPropValue', () => { const prop = extractProp('
'); const expected = true; - const actual = isNaN(getPropValue(prop)); + const actual = Number.isNaN(getPropValue(prop)); assert.equal(expected, actual); }); diff --git a/elementType.js b/elementType.js index ed4bdd2..96873ac 100644 --- a/elementType.js +++ b/elementType.js @@ -1 +1 @@ -module.exports = require('./lib').elementType; // eslint-disable-line import/no-unresolved +module.exports = require('./lib').elementType; // eslint-disable-line import/no-unresolved diff --git a/src/elementType.js b/src/elementType.js index b3b847f..2f87a19 100644 --- a/src/elementType.js +++ b/src/elementType.js @@ -19,7 +19,9 @@ export default function elementType(node = {}) { if (name.type === 'JSXMemberExpression') { const { object = {}, property = {} } = name; return resolveMemberExpressions(object, property); - } else if (name.type === 'JSXNamespacedName') { + } + + if (name.type === 'JSXNamespacedName') { return `${name.namespace.name}:${name.name.name}`; } diff --git a/src/getProp.js b/src/getProp.js index cc0fc34..4652c38 100644 --- a/src/getProp.js +++ b/src/getProp.js @@ -18,9 +18,9 @@ export default function getProp(props = [], prop = '', options = DEFAULT_OPTIONS return false; } - const currentProp = options.ignoreCase ? - propName(attribute).toUpperCase() : - propName(attribute); + const currentProp = options.ignoreCase + ? propName(attribute).toUpperCase() + : propName(attribute); return propToFind === currentProp; }); diff --git a/src/hasProp.js b/src/hasProp.js index 021232a..e9e4058 100644 --- a/src/hasProp.js +++ b/src/hasProp.js @@ -18,9 +18,9 @@ export default function hasProp(props = [], prop = '', options = DEFAULT_OPTIONS return !options.spreadStrict; } - const currentProp = options.ignoreCase ? - propName(attribute).toUpperCase() : - propName(attribute); + const currentProp = options.ignoreCase + ? propName(attribute).toUpperCase() + : propName(attribute); return propToCheck === currentProp; }); diff --git a/src/values/Literal.js b/src/values/Literal.js index 28fdb7f..a860657 100644 --- a/src/values/Literal.js +++ b/src/values/Literal.js @@ -10,7 +10,9 @@ export default function extractValueFromLiteral(value) { const normalizedStringValue = typeof extractedValue === 'string' && extractedValue.toLowerCase(); if (normalizedStringValue === 'true') { return true; - } else if (normalizedStringValue === 'false') { + } + + if (normalizedStringValue === 'false') { return false; } diff --git a/src/values/expressions/ArrayExpression.js b/src/values/expressions/ArrayExpression.js index ab3810c..c5230a5 100644 --- a/src/values/expressions/ArrayExpression.js +++ b/src/values/expressions/ArrayExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for an ArrayExpression type value node. * An array expression is an expression with [] syntax. @@ -7,5 +5,7 @@ import getValue from './index'; * @returns - An array of the extracted elements. */ export default function extractValueFromArrayExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; return value.elements.map(element => getValue(element)); } diff --git a/src/values/expressions/BinaryExpression.js b/src/values/expressions/BinaryExpression.js index 6f4da94..cab0cbb 100644 --- a/src/values/expressions/BinaryExpression.js +++ b/src/values/expressions/BinaryExpression.js @@ -1,6 +1,3 @@ -import getValue from './index'; - - /** * Extractor function for a BinaryExpression type value node. * A binary expression has a left and right side separated by an operator @@ -10,6 +7,8 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromBinaryExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; const { operator, left, right } = value; const leftVal = getValue(left); const rightVal = getValue(right); diff --git a/src/values/expressions/BindExpression.js b/src/values/expressions/BindExpression.js index 6c5077c..811ee42 100644 --- a/src/values/expressions/BindExpression.js +++ b/src/values/expressions/BindExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for a BindExpression type value node. * A bind expression looks like `::this.foo` @@ -10,7 +8,8 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromBindExpression(value) { - // console.log(value); + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; const callee = getValue(value.callee); // If value.object === null, the callee must be a MemberExpression. diff --git a/src/values/expressions/CallExpression.js b/src/values/expressions/CallExpression.js index ec08ec4..6cdeba9 100644 --- a/src/values/expressions/CallExpression.js +++ b/src/values/expressions/CallExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for a CallExpression type value node. * A call expression looks like `bar()` @@ -10,5 +8,7 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromCallExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; return getValue(value.callee); } diff --git a/src/values/expressions/ConditionalExpression.js b/src/values/expressions/ConditionalExpression.js index a3308f6..4a854e5 100644 --- a/src/values/expressions/ConditionalExpression.js +++ b/src/values/expressions/ConditionalExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for a ConditionalExpression type value node. * @@ -7,6 +5,8 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromConditionalExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; const { test, alternate, diff --git a/src/values/expressions/LogicalExpression.js b/src/values/expressions/LogicalExpression.js index adaf707..5b06cd0 100644 --- a/src/values/expressions/LogicalExpression.js +++ b/src/values/expressions/LogicalExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for a LogicalExpression type value node. * A logical expression is `a && b` or `a || b`, so we evaluate both sides @@ -9,6 +7,8 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromLogicalExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; const { operator, left, right } = value; const leftVal = getValue(left); const rightVal = getValue(right); diff --git a/src/values/expressions/MemberExpression.js b/src/values/expressions/MemberExpression.js index 13d1c84..38e31fe 100644 --- a/src/values/expressions/MemberExpression.js +++ b/src/values/expressions/MemberExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for a MemberExpression type value node. * A member expression is accessing a property on an object `obj.property`. @@ -9,5 +7,7 @@ import getValue from './index'; * and maintaing `obj.property` convention. */ export default function extractValueFromMemberExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; return `${getValue(value.object)}.${getValue(value.property)}`; } diff --git a/src/values/expressions/ObjectExpression.js b/src/values/expressions/ObjectExpression.js index 0e985e3..41ef13b 100644 --- a/src/values/expressions/ObjectExpression.js +++ b/src/values/expressions/ObjectExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for an ObjectExpression type value node. * An object expression is using {}. @@ -7,10 +5,12 @@ import getValue from './index'; * @returns - a representation of the object */ export default function extractValueFromObjectExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; return value.properties.reduce((obj, property) => { const object = Object.assign({}, obj); // Support types: SpreadProperty and ExperimentalSpreadProperty - if (/^(?:Experimental)?SpreadProperty$/.test(property.type)) { + if (/^(?:Experimental)?Spread(?:Property|Element)$/.test(property.type)) { if (property.argument.type === 'ObjectExpression') { return Object.assign(object, extractValueFromObjectExpression(property.argument)); } diff --git a/src/values/expressions/OptionalMemberExpression.js b/src/values/expressions/OptionalMemberExpression.js new file mode 100644 index 0000000..174c99b --- /dev/null +++ b/src/values/expressions/OptionalMemberExpression.js @@ -0,0 +1,13 @@ +/** + * Extractor function for a OptionalMemberExpression type value node. + * A member expression is accessing a property on an object `obj.property`. + * + * @param - value - AST Value object with type `OptionalMemberExpression` + * @returns - The extracted value converted to correct type + * and maintaing `obj?.property` convention. + */ +export default function extractValueFromOptionalMemberExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; + return `${getValue(value.object)}?.${getValue(value.property)}`; +} diff --git a/src/values/expressions/TemplateLiteral.js b/src/values/expressions/TemplateLiteral.js index 717e7aa..58744c5 100644 --- a/src/values/expressions/TemplateLiteral.js +++ b/src/values/expressions/TemplateLiteral.js @@ -19,9 +19,13 @@ export default function extractValueFromTemplateLiteral(value) { } = part; if (type === 'TemplateElement') { return raw + part.value.raw; - } else if (type === 'Identifier') { + } + + if (type === 'Identifier') { return part.name === 'undefined' ? `${raw}${part.name}` : `${raw}{${part.name}}`; - } else if (type.indexOf('Expression') > -1) { + } + + if (type.indexOf('Expression') > -1) { return `${raw}{${type}}`; } diff --git a/src/values/expressions/UnaryExpression.js b/src/values/expressions/UnaryExpression.js index aecdb9b..7190ff4 100644 --- a/src/values/expressions/UnaryExpression.js +++ b/src/values/expressions/UnaryExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for a UnaryExpression type value node. * A unary expression is an expression with a unary operator. @@ -9,6 +7,8 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromUnaryExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; const { operator, argument } = value; switch (operator) { diff --git a/src/values/expressions/UpdateExpression.js b/src/values/expressions/UpdateExpression.js index a78b90a..8d9277a 100644 --- a/src/values/expressions/UpdateExpression.js +++ b/src/values/expressions/UpdateExpression.js @@ -1,5 +1,3 @@ -import getValue from './index'; - /** * Extractor function for an UpdateExpression type value node. * An update expression is an expression with an update operator. @@ -9,6 +7,8 @@ import getValue from './index'; * @returns - The extracted value converted to correct type. */ export default function extractValueFromUpdateExpression(value) { + // eslint-disable-next-line global-require + const getValue = require('./index.js').default; const { operator, argument, prefix } = value; let val = getValue(argument); diff --git a/src/values/expressions/index.js b/src/values/expressions/index.js index 43690ca..7ba66f7 100644 --- a/src/values/expressions/index.js +++ b/src/values/expressions/index.js @@ -6,6 +6,7 @@ import TemplateLiteral from './TemplateLiteral'; import FunctionExpression from './FunctionExpression'; import LogicalExpression from './LogicalExpression'; import MemberExpression from './MemberExpression'; +import OptionalMemberExpression from './OptionalMemberExpression'; import CallExpression from './CallExpression'; import UnaryExpression from './UnaryExpression'; import ThisExpression from './ThisExpression'; @@ -29,6 +30,7 @@ const TYPES = { FunctionExpression, LogicalExpression, MemberExpression, + OptionalMemberExpression, CallExpression, UnaryExpression, ThisExpression, @@ -44,6 +46,39 @@ const TYPES = { const noop = () => null; +const errorMessage = expression => `The prop value with an expression type of ${expression} could not be resolved. Please file issue to get this fixed immediately.`; + +/** + * This function maps an AST value node + * to its correct extractor function for its + * given type. + * + * This will map correctly for *all* possible expression types. + * + * @param - value - AST Value object with type `JSXExpressionContainer` + * @returns The extracted value. + */ +export default function extract(value) { + // Value will not have the expression property when we recurse. + // The type for expression on ArrowFunctionExpression is a boolean. + let expression; + if ( + typeof value.expression !== 'boolean' + && value.expression + ) { + expression = value.expression; // eslint-disable-line prefer-destructuring + } else { + expression = value; + } + const { type } = expression; + + if (TYPES[type] === undefined) { + throw new Error(errorMessage(type)); + } + + return TYPES[type](expression); +} + // Composition map of types to their extractor functions to handle literals. const LITERAL_TYPES = Object.assign({}, TYPES, { Literal: (value) => { @@ -62,6 +97,7 @@ const LITERAL_TYPES = Object.assign({}, TYPES, { FunctionExpression: noop, LogicalExpression: noop, MemberExpression: noop, + OptionalMemberExpression: noop, CallExpression: noop, UnaryExpression: (value) => { const extractedVal = TYPES.UnaryExpression.call(undefined, value); @@ -84,41 +120,6 @@ const LITERAL_TYPES = Object.assign({}, TYPES, { SpreadElement: noop, }); -const errorMessage = expression => - `The prop value with an expression type of ${expression} could not be resolved. - Please file issue to get this fixed immediately.`; - -/** - * This function maps an AST value node - * to its correct extractor function for its - * given type. - * - * This will map correctly for *all* possible expression types. - * - * @param - value - AST Value object with type `JSXExpressionContainer` - * @returns The extracted value. - */ -export default function extract(value) { - // Value will not have the expression property when we recurse. - // The type for expression on ArrowFunctionExpression is a boolean. - let expression; - if ( - typeof value.expression !== 'boolean' - && value.expression - ) { - expression = value.expression; - } else { - expression = value; - } - const { type } = expression; - - if (TYPES[type] === undefined) { - throw new Error(errorMessage(type)); - } - - return TYPES[type](expression); -} - /** * This function maps an AST value node * to its correct extractor function for its