Skip to content

Commit

Permalink
Merge pull request #54 from soft-decay/master
Browse files Browse the repository at this point in the history
enhance(parser): Support bracket notation when parsing event names (#1)
  • Loading branch information
alexprey authored Jan 28, 2021
2 parents 054ef70 + 689a174 commit 6b3ea3d
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 43 deletions.
74 changes: 50 additions & 24 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions test/svelte3/integration/events/events.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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];

Expand Down
85 changes: 68 additions & 17 deletions test/unit/helpers/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('.')});
Expand All @@ -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);
});
});
});

Expand Down

0 comments on commit 6b3ea3d

Please sign in to comment.