From 69eb003cc5cd6e5d9b89862a9b737c9ef9bfc78a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rousseau Date: Wed, 27 Jan 2021 15:33:43 -0500 Subject: [PATCH 1/2] enhance(parser): Support bracket notation when parsing event names (#1) - parser v3: bracket notation for string 'Literal' nodes - parser v2: bracket notation for String tokens - Add unit tests (2 per parser) for bracket notation support --- lib/utils.js | 74 ++++++++++++++++++---------- test/unit/helpers/utils.spec.js | 85 ++++++++++++++++++++++++++------- 2 files changed, 118 insertions(+), 41 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 88af7ce..c6e251a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -173,60 +173,87 @@ const isPunctuatorToken = (token, which = undefined) => { /** * The array of tokens provided must start at the first identifier of the * chain, but it can go beyond the last identifier. Only the chained - * identifiers will be parsed. + * identifiers will be parsed. Supports Dot notation and Bracket notation if + * the brackets contain only a String Token. * * See {@link buildPropertyAccessorChainFromAst} examples for * expected returned values. * * @param {{ type: string; value: string }[]} tokens + * @returns an array of property names built from the tokens array */ const buildPropertyAccessorChainFromTokens = (tokens) => { - const next = tokens[0]; - - if (!next) { + if (!tokens[0]) { return []; } - if (!next.type === 'Identifier') { + if (!tokens[0].type === 'Identifier' && !tokens[0].type === 'String') { return []; } + let next = tokens[0]; + const chain = [next.value]; + // Prepare first Punctuator let punctIndex = 1; - let isChained = isPunctuatorToken(tokens[punctIndex], '.'); - let chained = tokens[punctIndex + 1]; + let isBracket = isPunctuatorToken(tokens[punctIndex], '['); + let isDot = isPunctuatorToken(tokens[punctIndex], '.'); + let isChained = isDot || isBracket; + + // Prepare first Identifier/String + next = tokens[punctIndex + 1]; + let isIdentifier = next.type === 'Identifier'; + let isString = next.type === 'String'; + + while (next && isChained && (isIdentifier || isString)) { + if (isString) { + chain.push(next.value.slice(1, next.value.length - 1)); + } else { + chain.push(next.value); + } - while (isChained && chained && chained.type === 'Identifier') { - chain.push(chained.value); + // Find next Punctuator punctIndex += 2; - isChained = isPunctuatorToken(tokens[punctIndex], '.'); - chained = tokens[punctIndex + 1]; + + if (isBracket && isPunctuatorToken(tokens[punctIndex], ']')) { + punctIndex += 1; // Skip closing bracket ('.' vs '][') + } + + // Prepare next Punctuator + isBracket = isPunctuatorToken(tokens[punctIndex], '['); + isDot = isPunctuatorToken(tokens[punctIndex], '.'); + isChained = isDot || isBracket; + + // Prepare next Identifier/String + next = tokens[punctIndex + 1]; + isIdentifier = next.type === 'Identifier'; + isString = next.type === 'String'; } return chain; }; +const isStringLiteral = (node) => { + return node.type === 'Literal' && + typeof node.value === 'string'; +}; + /** * Builds an array of property names from a 'MemberExpression' node. * - Supports nested 'MemberExpression' and 'Identifier' nodes - * - Does not support bracket notation (computed === true). + * - Supports bracket notation for string 'Literal' only. * * If the ast contains unsupported nodes, an empty array is returned. * * @example * dispatch(PLAIN.NESTED.INNER); - * // Parsing the 'MemberExpression' node - * // corresponding to 'PLAIN.NESTED.INNER' - * // would return: - * ['PLAIN', 'NESTED', 'INNER'] - * - * @example * dispatch(PLAIN['NESTED'].INNER); + * dispatch(PLAIN['NESTED']['INNER']); * // Parsing the 'MemberExpression' node - * // corresponding to 'PLAIN['NESTED'].INNER' + * // corresponding to any of the above * // would return: - * [] + * ['PLAIN', 'NESTED', 'INNER'] * * @param {{ type: string; object: any, property: any, computed: boolean }} node * @returns an array of property names built from the ast @@ -242,12 +269,11 @@ const buildPropertyAccessorChainFromAst = (node) => { const chain = []; - if (!node.computed) { - // Dot notation + if (!node.computed || isStringLiteral(node.property)) { + // Dot notation and minimal bracket notation support chain.push(...buildPropertyAccessorChainFromAst(node.object)); - chain.push(node.property.name); + chain.push(node.computed ? node.property.value : node.property.name); } else { - // TODO: Support bracket notation chain.push(undefined); } diff --git a/test/unit/helpers/utils.spec.js b/test/unit/helpers/utils.spec.js index 2caa11b..36c434f 100644 --- a/test/unit/helpers/utils.spec.js +++ b/test/unit/helpers/utils.spec.js @@ -31,36 +31,63 @@ describe('"utils.js" module', () => { }); describe('buildPropertyAccessorChainFromTokens', () => { - it('should correctly parse a single identifier', () => { - const expectedChain = ['NOTIFY']; - const script = ` + describe('should return an accessor chain when', () => { + it('tokens represent a single identifier', () => { + const expectedChain = ['NOTIFY']; + const script = ` callee(${expectedChain.join('.')}); `; - const tokens = espree.tokenize(script); - const identifierTokens = tokens.slice(2); + const tokens = espree.tokenize(script); + const identifierTokens = tokens.slice(2); - const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); + const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); - expect(chain).to.deep.equal(expectedChain); - }); + expect(chain).to.deep.equal(expectedChain); + }); - it('should correctly parse chained identifiers', () => { - const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; - const script = ` + it('tokens represent chained identifiers using dot notation', () => { + const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; + const script = ` callee(${expectedChain.join('.')}); `; - const tokens = espree.tokenize(script); - const identifierTokens = tokens.slice(2); + const tokens = espree.tokenize(script); + const identifierTokens = tokens.slice(2); + + const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); + + expect(chain).to.deep.equal(expectedChain); + }); + + it('tokens represent chained identifiers using mixed notation', () => { + const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; + const script = ` + callee(${expectedChain[0]}['${expectedChain[1]}'].${expectedChain[2]}); + `; + const tokens = espree.tokenize(script); + const identifierTokens = tokens.slice(2); + + const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); + + expect(chain).to.deep.equal(expectedChain); + }); + it('tokens represent chained identifiers using only bracket notation', () => { + const expectedChain = ['EVENT', '"SIGNAL"', "'NOTIFY'"]; + const script = ` + callee(${expectedChain[0]}['${expectedChain[1]}']["${expectedChain[2]}"]); + `; + const tokens = espree.tokenize(script); + const identifierTokens = tokens.slice(2); - const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); + const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); - expect(chain).to.deep.equal(expectedChain); + expect(chain).to.deep.equal(expectedChain); + }); }); }); describe('buildPropertyAccessorChainFromAst', () => { - describe('should build an array when', () => { - it('AST is a single identifier', () => { + describe('should return an accessor chain when', () => { + it('AST has a single identifier', () => { const expectedChain = ['NOTIFY']; const script = ` callee(${expectedChain.join('.')}); @@ -83,6 +110,30 @@ describe('"utils.js" module', () => { expect(chain).to.deep.equal(expectedChain); }); + + it('AST has a nested computed "MemberExpression" node', () => { + const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; + const script = ` + callee(${expectedChain[0]}['${expectedChain[1]}'].${expectedChain[2]}); + `; + const ast = espree.parse(script); + const node = ast.body[0].expression.arguments[0]; + const chain = utils.buildPropertyAccessorChainFromAst(node); + + expect(chain).to.deep.equal(expectedChain); + }); + + it('AST has only nested computed "MemberExpression" nodes', () => { + const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; + const script = ` + callee(${expectedChain[0]}['${expectedChain[1]}']["${expectedChain[2]}"]); + `; + const ast = espree.parse(script); + const node = ast.body[0].expression.arguments[0]; + const chain = utils.buildPropertyAccessorChainFromAst(node); + + expect(chain).to.deep.equal(expectedChain); + }); }); }); From 689a1742e24d48b53facfc2bdd38aafd8deaff5d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rousseau Date: Wed, 27 Jan 2021 18:27:27 -0500 Subject: [PATCH 2/2] test(svelte3): Enable test for event parsing for bracket notation (#1) --- test/svelte3/integration/events/events.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/svelte3/integration/events/events.spec.js b/test/svelte3/integration/events/events.spec.js index b6f744f..02469e3 100644 --- a/test/svelte3/integration/events/events.spec.js +++ b/test/svelte3/integration/events/events.spec.js @@ -353,7 +353,7 @@ describe('SvelteDoc v3 - Events', () => { }); }); - xit('Dispatch event from code should be found when using an identifier from array', (done) => { + it('Dispatch event from code should be found when using an identifier with bracket notation', (done) => { parser.parse({ version: 3, filename: path.resolve(__dirname, 'event.dispatcher.arrayIdentifier.svelte'), @@ -362,7 +362,7 @@ describe('SvelteDoc v3 - Events', () => { }).then((doc) => { expect(doc, 'Document should be provided').to.exist; expect(doc.events, 'Document events should be parsed').to.exist; - expect(doc.events.length).to.equal(2); + expect(doc.events.length).to.equal(1); const event = doc.events[0];