From 4079563f5dca96477bb13935c1cc30ad8224ae4c Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Wed, 6 Feb 2019 11:56:49 -0800 Subject: More robust/simple signature parsing, using a parse tree --- .../utils/src/abi_encoder/evm_data_type_factory.ts | 87 +++++++++--- .../utils/src/abi_encoder/evm_data_types/method.ts | 5 + packages/utils/src/abi_encoder/index.ts | 1 + .../src/abi_encoder/utils/signature_parser.ts | 151 ++++++++++----------- 4 files changed, 146 insertions(+), 98 deletions(-) diff --git a/packages/utils/src/abi_encoder/evm_data_type_factory.ts b/packages/utils/src/abi_encoder/evm_data_type_factory.ts index 268649148..613eb887f 100644 --- a/packages/utils/src/abi_encoder/evm_data_type_factory.ts +++ b/packages/utils/src/abi_encoder/evm_data_type_factory.ts @@ -2,7 +2,7 @@ import { DataItem, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; -import { generateDataItemsFromSignature } from './utils/signature_parser'; +import { generateDataItemFromSignature } from './utils/signature_parser'; import { DataType } from './abstract_data_types/data_type'; import { DataTypeFactory } from './abstract_data_types/interfaces'; @@ -134,32 +134,87 @@ export class EvmDataTypeFactory implements DataTypeFactory { /** * Convenience function for creating a DataType from different inputs. - * @param input A single or set of DataItem or a DataType signature. - * A signature in the form of '' is interpreted as a `DataItem` - * For example, 'string' is interpreted as {type: 'string'} - * A signature in the form '(, , ..., )' is interpreted as `DataItem[]` - * For eaxmple, '(string, uint256)' is interpreted as [{type: 'string'}, {type: 'uint256'}] + * @param input A single or set of DataItem or a signature for an EVM data type. * @return DataType corresponding to input. */ export function create(input: DataItem | DataItem[] | string): DataType { - // Handle different types of input - const isSignature = typeof input === 'string'; - const isTupleSignature = isSignature && (input as string).startsWith('('); - const shouldParseAsTuple = isTupleSignature || _.isArray(input); - // Create input `dataItem` + const dataItem = consolidateDataItemsIntoSingle(input); + const dataType = EvmDataTypeFactory.getInstance().create(dataItem); + return dataType; +} + +/** + * Convenience function to aggregate a single input or a set of inputs into a single DataItem. + * An array of data items is grouped into a single tuple. + * @param input A single data item; a set of data items; a signature. + * @return A single data item corresponding to input. + */ +function consolidateDataItemsIntoSingle(input: DataItem | DataItem[] | string): DataItem { let dataItem: DataItem; - if (shouldParseAsTuple) { - const dataItems = isSignature ? generateDataItemsFromSignature(input as string) : (input as DataItem[]); + if (_.isArray(input)) { + const dataItems = input as DataItem[]; dataItem = { name: '', type: 'tuple', components: dataItems, }; } else { - dataItem = isSignature ? generateDataItemsFromSignature(input as string)[0] : (input as DataItem); + dataItem = typeof input === 'string' ? generateDataItemFromSignature(input) : (input as DataItem); } - // Create data type - const dataType = EvmDataTypeFactory.getInstance().create(dataItem); + return dataItem; +} + +/** + * Convenience function for creating a Method encoder from different inputs. + * @param methodName name of method. + * @param input A single data item; a set of data items; a signature; or an array of signatures (optional). + * @param output A single data item; a set of data items; a signature; or an array of signatures (optional). + * @return Method corresponding to input. + */ +export function createMethod( + methodName: string, + input?: DataItem | DataItem[] | string | string[], + output?: DataItem | DataItem[] | string | string[], +): Method { + const methodInput = _.isUndefined(input) ? [] : consolidateDataItemsIntoArray(input); + const methodOutput = _.isUndefined(output) ? [] : consolidateDataItemsIntoArray(output); + const methodAbi: MethodAbi = { + name: methodName, + inputs: methodInput, + outputs: methodOutput, + type: 'function', + // default fields not used by ABI + constant: false, + payable: false, + stateMutability: 'nonpayable', + }; + const dataType = new Method(methodAbi); return dataType; } + +/** + * Convenience function that aggregates a single input or a set of inputs into an array of DataItems. + * @param input A single data item; a set of data items; a signature; or an array of signatures. + * @return Array of data items corresponding to input. + */ +function consolidateDataItemsIntoArray(input: DataItem | DataItem[] | string | string[]): DataItem[] { + let dataItems: DataItem[]; + if (_.isArray(input) && _.isEmpty(input)) { + dataItems = []; + } else if (_.isArray(input) && typeof input[0] === 'string') { + dataItems = []; + _.each(input as string[], (signature: string) => { + const dataItem = generateDataItemFromSignature(signature); + dataItems.push(dataItem); + }); + } else if (_.isArray(input)) { + dataItems = input as DataItem[]; + } else if (typeof input === 'string') { + const dataItem = generateDataItemFromSignature(input); + dataItems = [dataItem]; + } else { + dataItems = [input as DataItem]; + } + return dataItems; +} /* tslint:enable no-construct */ diff --git a/packages/utils/src/abi_encoder/evm_data_types/method.ts b/packages/utils/src/abi_encoder/evm_data_types/method.ts index c852a0fdf..93746fa00 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/method.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -65,6 +65,11 @@ export class MethodDataType extends AbstractSetDataType { return this._methodSelector; } + public getReturnValueDataItem(): DataItem { + const returnValueDataItem = this._returnDataType.getDataItem(); + return returnValueDataItem; + } + private _computeSignature(): string { const memberSignature = this._computeSignatureOfMembers(); const methodSignature = `${this.getDataItem().name}${memberSignature}`; diff --git a/packages/utils/src/abi_encoder/index.ts b/packages/utils/src/abi_encoder/index.ts index cfacfe075..976bac8e6 100644 --- a/packages/utils/src/abi_encoder/index.ts +++ b/packages/utils/src/abi_encoder/index.ts @@ -12,5 +12,6 @@ export { Tuple, UInt, create, + createMethod, } from './evm_data_type_factory'; export { DataType } from './abstract_data_types/data_type'; diff --git a/packages/utils/src/abi_encoder/utils/signature_parser.ts b/packages/utils/src/abi_encoder/utils/signature_parser.ts index 315784cea..d3996bf8e 100644 --- a/packages/utils/src/abi_encoder/utils/signature_parser.ts +++ b/packages/utils/src/abi_encoder/utils/signature_parser.ts @@ -1,101 +1,88 @@ import { DataItem } from 'ethereum-types'; import * as _ from 'lodash'; +interface Node { + name: string; + value: string; + children: Node[]; + parent?: Node; +} + +function parseNode(node: Node): DataItem { + const components: DataItem[] = []; + _.each(node.children, (child: Node) => { + const component = parseNode(child); + components.push(component); + }); + const dataItem: DataItem = { + name: node.name, + type: node.value, + }; + if (!_.isEmpty(components)) { + dataItem.components = components; + } + return dataItem; +} + /** - * Returns an array of DataItem's corresponding to the input signature. - * A signature can be in two forms: '' or '(, , ...) - * An example of the first form would be 'address' or 'uint256' - * An example of the second form would be '(address, uint256)' - * Signatures can also include a name field, for example: 'foo address' or '(foo address, bar uint256)' - * @param signature of input DataItems - * @return DataItems derived from input signature + * Returns a DataItem corresponding to the input signature. + * A signature can be in two forms: `type` or `(type_1,type_2,...,type_n)` + * An example of the first form would be 'address' or 'uint256[]' or 'bytes[5][]' + * An example of the second form would be '(address,uint256)' or '(address,uint256)[]' + * @param signature of input DataItem. + * @return DataItem derived from input signature. */ -export function generateDataItemsFromSignature(signature: string): DataItem[] { - let trimmedSignature = signature; - if (signature.startsWith('(')) { - if (!signature.endsWith(')')) { - throw new Error(`Failed to generate data item. Must end with ')'`); - } - trimmedSignature = signature.substr(1, signature.length - 2); +export function generateDataItemFromSignature(signature: string): DataItem { + // No data item corresponds to an empty signature + if (_.isEmpty(signature)) { + throw new Error(`Cannot parse data item from empty signature, ''`); } - trimmedSignature += ','; - let isCurrTokenArray = false; - let currTokenArrayModifier = ''; - let isParsingArrayModifier = false; - let currToken = ''; - let parenCount = 0; - let currTokenName = ''; - const dataItems: DataItem[] = []; - for (const char of trimmedSignature) { - // Tokenize the type string while keeping track of parentheses. + // Create a parse tree for data item + let node: Node = { + name: '', + value: '', + children: [], + }; + for (const char of signature) { switch (char) { case '(': - parenCount += 1; - currToken += char; + const child = { + name: '', + value: '', + children: [], + parent: node, + }; + node.value = 'tuple'; + node.children.push(child); + node = child; break; + case ')': - parenCount -= 1; - currToken += char; + node = node.parent as Node; break; - case '[': - if (parenCount === 0) { - isParsingArrayModifier = true; - isCurrTokenArray = true; - currTokenArrayModifier += '['; - } else { - currToken += char; - } - break; - case ']': - if (parenCount === 0) { - isParsingArrayModifier = false; - currTokenArrayModifier += ']'; - } else { - currToken += char; - } + + case ',': + const sibling = { + name: '', + value: '', + children: [], + parent: node.parent, + }; + (node.parent as Node).children.push(sibling); + node = sibling; break; + case ' ': - if (parenCount === 0) { - currTokenName = currToken; - currToken = ''; - } else { - currToken += char; - } + node.name = node.value; + node.value = ''; break; - case ',': - if (parenCount === 0) { - // Generate new DataItem from token - const components = currToken.startsWith('(') ? generateDataItemsFromSignature(currToken) : []; - const isTuple = !_.isEmpty(components); - const dataItem: DataItem = { name: currTokenName, type: '' }; - if (isTuple) { - dataItem.type = 'tuple'; - dataItem.components = components; - } else { - dataItem.type = currToken; - } - if (isCurrTokenArray) { - dataItem.type += currTokenArrayModifier; - } - dataItems.push(dataItem); - // reset token state - currTokenName = ''; - currToken = ''; - isCurrTokenArray = false; - currTokenArrayModifier = ''; - break; - } else { - currToken += char; - break; - } + default: - if (isParsingArrayModifier) { - currTokenArrayModifier += char; - } else { - currToken += char; - } + node.value += char; break; } } - return dataItems; + // Interpret data item from parse tree + const dataItem = parseNode(node); + return dataItem; } -- cgit From eb5f7e36e9a83f7222b1a98d23c720a7573e7dbf Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Wed, 6 Feb 2019 11:56:58 -0800 Subject: Signature parsing tests --- .../utils/test/abi_encoder/evm_data_types_test.ts | 2 +- packages/utils/test/abi_encoder/signature_test.ts | 393 +++++++++++++++++++++ packages/utils/test/abi_encoder/signature_tests.ts | 0 3 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 packages/utils/test/abi_encoder/signature_test.ts delete mode 100644 packages/utils/test/abi_encoder/signature_tests.ts diff --git a/packages/utils/test/abi_encoder/evm_data_types_test.ts b/packages/utils/test/abi_encoder/evm_data_types_test.ts index c09c0d929..c80290207 100644 --- a/packages/utils/test/abi_encoder/evm_data_types_test.ts +++ b/packages/utils/test/abi_encoder/evm_data_types_test.ts @@ -28,7 +28,7 @@ describe('ABI Encoder: EVM Data Type Encoding/Decoding', () => { const decodedArgs = dataType.decode(encodedArgs); expect(decodedArgs).to.be.deep.equal(args); // Validate signature - const dataTypeFromSignature = AbiEncoder.create(dataType.getSignature(true)); + const dataTypeFromSignature = AbiEncoder.create(dataType.getSignature(false)); const argsEncodedFromSignature = dataTypeFromSignature.encode(args); expect(argsEncodedFromSignature).to.be.deep.equal(expectedEncodedArgs); }); diff --git a/packages/utils/test/abi_encoder/signature_test.ts b/packages/utils/test/abi_encoder/signature_test.ts new file mode 100644 index 000000000..cee6fa5e5 --- /dev/null +++ b/packages/utils/test/abi_encoder/signature_test.ts @@ -0,0 +1,393 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { AbiEncoder } from '../../src'; +import { chaiSetup } from '../utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ABI Encoder: Signatures', () => { + describe('Single type', () => { + it('Elementary', async () => { + const signature = 'uint256'; + const dataType = AbiEncoder.create(signature); + const dataTypeId = dataType.getDataItem().type; + expect(dataTypeId).to.be.equal('uint256'); + expect(dataType.getSignature()).to.be.equal(signature); + }); + it('Array', async () => { + const signature = 'string[]'; + const dataType = AbiEncoder.create(signature); + const dataItem = dataType.getDataItem(); + const expectedDataItem = { + name: '', + type: 'string[]', + }; + expect(dataItem).to.be.deep.equal(expectedDataItem); + expect(dataType.getSignature()).to.be.equal(signature); + }); + it('Multidimensional Array', async () => { + // Decode return value + const signature = 'uint256[4][][5]'; + const dataType = AbiEncoder.create(signature); + const dataTypeId = dataType.getDataItem().type; + expect(dataTypeId).to.be.equal(signature); + expect(dataType.getSignature()).to.be.equal(signature); + }); + it('Tuple with single element', async () => { + const signature = '(uint256)'; + const dataType = AbiEncoder.create(signature); + const dataItem = dataType.getDataItem(); + const expectedDataItem = { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'uint256', + }, + ], + }; + expect(dataItem).to.be.deep.equal(expectedDataItem); + expect(dataType.getSignature()).to.be.equal(signature); + }); + it('Tuple with multiple elements', async () => { + const signature = '(uint256,string,bytes4)'; + const dataType = AbiEncoder.create(signature); + const dataItem = dataType.getDataItem(); + const expectedDataItem = { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'uint256', + }, + { + name: '', + type: 'string', + }, + { + name: '', + type: 'bytes4', + }, + ], + }; + expect(dataItem).to.be.deep.equal(expectedDataItem); + expect(dataType.getSignature()).to.be.equal(signature); + }); + it('Tuple with nested array and nested tuple', async () => { + const signature = '(uint256[],(bytes),string[4],bytes4)'; + const dataType = AbiEncoder.create(signature); + const dataItem = dataType.getDataItem(); + const expectedDataItem = { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'uint256[]', + }, + { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'bytes', + }, + ], + }, + { + name: '', + type: 'string[4]', + }, + { + name: '', + type: 'bytes4', + }, + ], + }; + expect(dataItem).to.be.deep.equal(expectedDataItem); + expect(dataType.getSignature()).to.be.equal(signature); + }); + it('Array of complex tuples', async () => { + const signature = '(uint256[],(bytes),string[4],bytes4)[5][4][]'; + const dataType = AbiEncoder.create(signature); + const dataItem = dataType.getDataItem(); + const expectedDataItem = { + name: '', + type: 'tuple[5][4][]', + components: [ + { + name: '', + type: 'uint256[]', + }, + { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'bytes', + }, + ], + }, + { + name: '', + type: 'string[4]', + }, + { + name: '', + type: 'bytes4', + }, + ], + }; + expect(dataItem).to.be.deep.equal(expectedDataItem); + expect(dataType.getSignature()).to.be.equal(signature); + }); + }); + + describe('Function', () => { + it('No inputs and no outputs', async () => { + // create encoder + const functionName = 'foo'; + const dataType = AbiEncoder.createMethod(functionName); + // create expected values + const expectedSignature = 'foo()'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: [], + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: [], + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + it('No inputs and no outputs (empty arrays as input)', async () => { + // create encoder + const functionName = 'foo'; + const dataType = AbiEncoder.createMethod(functionName, [], []); + // create expected values + const expectedSignature = 'foo()'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: [], + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: [], + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + it('Single DataItem input and single DataItem output', async () => { + // create encoder + const functionName = 'foo'; + const inputDataItem = { + name: 'input', + type: 'uint256', + }; + const outputDataItem = { + name: 'output', + type: 'string', + }; + const dataType = AbiEncoder.createMethod(functionName, inputDataItem, outputDataItem); + // create expected values + const expectedSignature = 'foo(uint256)'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: [inputDataItem], + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: [outputDataItem], + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + it('Single signature input and single signature output', async () => { + // create encoder + const functionName = 'foo'; + const inputSignature = 'uint256'; + const outputSignature = 'string'; + const dataType = AbiEncoder.createMethod(functionName, inputSignature, outputSignature); + // create expected values + const expectedSignature = 'foo(uint256)'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: [ + { + name: '', + type: 'uint256', + }, + ], + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: [ + { + name: '', + type: 'string', + }, + ], + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + it('Single signature tuple input and single signature tuple output', async () => { + // create encoder + const functionName = 'foo'; + const inputSignature = '(uint256,bytes[][4])'; + const outputSignature = '(string,uint32)'; + const dataType = AbiEncoder.createMethod(functionName, inputSignature, outputSignature); + // create expected values + const expectedSignature = 'foo((uint256,bytes[][4]))'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: [ + { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'uint256', + }, + { + name: '', + type: 'bytes[][4]', + }, + ], + }, + ], + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: [ + { + name: '', + type: 'tuple', + components: [ + { + name: '', + type: 'string', + }, + { + name: '', + type: 'uint32', + }, + ], + }, + ], + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + it('Mutiple DataItem input and multiple DataItem output', async () => { + // create encoder + const functionName = 'foo'; + const inputDataItems = [ + { + name: '', + type: 'uint256', + }, + { + name: '', + type: 'bytes[][4]', + }, + ]; + const outputDataItems = [ + { + name: '', + type: 'string', + }, + { + name: '', + type: 'uint32', + }, + ]; + const dataType = AbiEncoder.createMethod(functionName, inputDataItems, outputDataItems); + // create expected values + const expectedSignature = 'foo(uint256,bytes[][4])'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: inputDataItems, + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: outputDataItems, + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + it('Multiple signature input and multiple signature output', async () => { + // create encoder + const functionName = 'foo'; + const inputSignatures = ['uint256', 'bytes[][4]']; + const outputSignatures = ['string', 'uint32']; + const dataType = AbiEncoder.createMethod(functionName, inputSignatures, outputSignatures); + // create expected values + const expectedSignature = 'foo(uint256,bytes[][4])'; + const expectedInputDataItem = { + name: 'foo', + type: 'method', + components: [ + { + name: '', + type: 'uint256', + }, + { + name: '', + type: 'bytes[][4]', + }, + ], + }; + const expectedOutputDataItem = { + name: 'foo', + type: 'tuple', + components: [ + { + name: '', + type: 'string', + }, + { + name: '', + type: 'uint32', + }, + ], + }; + // check expected values + expect(dataType.getSignature()).to.be.equal(expectedSignature); + expect(dataType.getDataItem()).to.be.deep.equal(expectedInputDataItem); + expect(dataType.getReturnValueDataItem()).to.be.deep.equal(expectedOutputDataItem); + }); + }); +}); diff --git a/packages/utils/test/abi_encoder/signature_tests.ts b/packages/utils/test/abi_encoder/signature_tests.ts deleted file mode 100644 index e69de29bb..000000000 -- cgit From 21c3f75efcae5fd4f59df3dffe0af0ec59a163a7 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Wed, 6 Feb 2019 11:57:09 -0800 Subject: Changelog for utils package --- packages/utils/CHANGELOG.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 007591d18..9ce2a4c52 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.0.4", + "changes": [ + { + "note": "Cleaner signature parsing", + "pr": 1592 + } + ] + }, { "version": "4.0.3", "changes": [ -- cgit From b9ee9d2bd5b6aac11ccea5a71291e8885bbe6f7a Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Thu, 7 Feb 2019 14:59:56 -0800 Subject: replaced typeof with _.isString --- packages/utils/src/abi_encoder/evm_data_type_factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/abi_encoder/evm_data_type_factory.ts b/packages/utils/src/abi_encoder/evm_data_type_factory.ts index 613eb887f..8e477f856 100644 --- a/packages/utils/src/abi_encoder/evm_data_type_factory.ts +++ b/packages/utils/src/abi_encoder/evm_data_type_factory.ts @@ -159,7 +159,7 @@ function consolidateDataItemsIntoSingle(input: DataItem | DataItem[] | string): components: dataItems, }; } else { - dataItem = typeof input === 'string' ? generateDataItemFromSignature(input) : (input as DataItem); + dataItem = _.isString(input) ? generateDataItemFromSignature(input) : (input as DataItem); } return dataItem; } @@ -201,7 +201,7 @@ function consolidateDataItemsIntoArray(input: DataItem | DataItem[] | string | s let dataItems: DataItem[]; if (_.isArray(input) && _.isEmpty(input)) { dataItems = []; - } else if (_.isArray(input) && typeof input[0] === 'string') { + } else if (_.isArray(input) && _.isString(input[0])) { dataItems = []; _.each(input as string[], (signature: string) => { const dataItem = generateDataItemFromSignature(signature); -- cgit