Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(parser): Parse event name when using an identifier (#1) #46

Merged
merged 2 commits into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/Alert.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@
"type": "any"
}
},
{
"visibility": "private",
"description": null,
"keywords": [],
"name": "EVENT",
"kind": "const",
"static": false,
"readonly": true,
"type": {
"kind": "type",
"text": "any",
"type": "any"
}
},
{
"visibility": "public",
"description": null,
Expand Down
6 changes: 5 additions & 1 deletion examples/Alert.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
*/

import { createEventDispatcher } from 'svelte';

const dispatch = createEventDispatcher();
const EVENT = {
CLOSE: "close"
}

export let closable = false;
</script>
Expand All @@ -27,7 +31,7 @@
The `close` event fired when user click to X button in the panel.
@event CloseEvent#close
-->
<button on:click={() => dispatch('close')}>&times;</button>
<button on:click={() => dispatch(EVENT.CLOSE)}>&times;</button>
</div>
{/if}
</div>
4 changes: 2 additions & 2 deletions examples/Button.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@
"type": "string"
},
"name": "question",
"description": "a question about life, the universe, everything",
"optional": true,
"defaultValue": "Why?",
"description": "a question about life, the universe, everything"
"defaultValue": "Why?"
}
],
"return": {
Expand Down
28 changes: 20 additions & 8 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,15 +459,17 @@ class Parser extends EventEmitter {
const token = tokens[i];

if (token.type === 'Identifier' && token.value === 'fire') {
if (!tokens[i + 2]) {
const nextIndex = i + 2;

if (!tokens[nextIndex]) {
break;
}

if (!this.features.includes('events')) {
continue;
}

const next = tokens[i + 2];
const next = tokens[nextIndex];
const event = {
name: null,
parent: null,
Expand Down Expand Up @@ -524,20 +526,30 @@ class Parser extends EventEmitter {
break;

case 'Identifier':
event.name = utils.getIdentifierValue(
tokens, next.value, next.range[0]);
if (next.value in this.identifiers) {
const startingAtFirstArg = tokens.slice(nextIndex);

const chain = utils.buildPropertyAccessorChainFromTokens(startingAtFirstArg);

event.name = utils.getValueForPropertyAccessorChain(this.identifiers, chain);
}

if (!event.name) {
event.name = utils.getIdentifierValue(
tokens, next.value, next.range[0]);

if (typeof event.name === 'object') {
event.name = utils.getIdentifierValueFromStart(
this.ast.tokens, event.name.notFoundIdentifier);
if (typeof event.name === 'object') {
event.name = utils.getIdentifierValueFromStart(
this.ast.tokens, event.name.notFoundIdentifier);
}
}

break;
}
}

if (!event.name) {
event.name = '****unhandled-event-name****';
event.name = utils.UNHANDLED_EVENT_NAME;
} else {
if (hasOwnProperty(this.eventsEmitted, event.name)) {
const emitedEvent = this.eventsEmitted[event.name];
Expand Down
189 changes: 181 additions & 8 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const RE_VISIBILITY = new RegExp(`^(${VISIBILITIES.join('|')})$`);
const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤€%µ,?;.:/!§<>+¨-]+))?/ig;

const DEFAULT_VISIBILITY = 'public';
const UNHANDLED_EVENT_NAME = '****unhandled-event-name****';

const isVisibilitySupported = (v) => RE_VISIBILITY.test(v);

Expand Down Expand Up @@ -112,30 +113,30 @@ class NodeFunction {
}

const value = (property) => {
if (property.key.type === 'Literal') {
property.key.name = property.key.value;
}
const keyName = property.key.type === 'Literal'
? property.key.value
: property.key.name;

switch (property.value.type) {
case 'Literal':
return { [property.key.name]: property.value.value };
return { [keyName]: property.value.value };

case 'Identifier':
return {
[property.key.name]: property.value.name === 'undefined'
[keyName]: property.value.name === 'undefined'
? undefined
: property.value.name
};

case 'ObjectExpression':
return { [property.key.name]: values(property) };
return { [keyName]: values(property) };

case 'FunctionExpression':
case 'ArrowFunctionExpression':
return { [property.key.name]: new NodeFunction(property.value) };
return { [keyName]: new NodeFunction(property.value) };
}

return { [property.key.name]: property.value };
return { [keyName]: property.value };
};

const values = (entry) => {
Expand All @@ -153,6 +154,173 @@ const values = (entry) => {

return values;
};
/**
* @param {{ type: string; value: string }} token the Node token that needs to be tested
* @param {string} which a punctuator value to compare the token's value against
* @returns true if token is a punctuator with the correct value (if provided)
*/
const isPunctuatorToken = (token, which = undefined) => {
if (!token) {
return false;
}

const isPunctuator = token.type === 'Punctuator';
const isSpecific = which === undefined || token.value === which;

return isPunctuator && isSpecific;
};

/**
* 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.
*
* See {@link buildPropertyAccessorChainFromAst} examples for
* expected returned values.
*
* @param {{ type: string; value: string }[]} tokens
*/
const buildPropertyAccessorChainFromTokens = (tokens) => {
const next = tokens[0];

if (!next) {
return [];
}

if (!next.type === 'Identifier') {
return [];
}

const chain = [next.value];

let punctIndex = 1;
let isChained = isPunctuatorToken(tokens[punctIndex], '.');
let chained = tokens[punctIndex + 1];

while (isChained && chained && chained.type === 'Identifier') {
chain.push(chained.value);
punctIndex += 2;
isChained = isPunctuatorToken(tokens[punctIndex], '.');
chained = tokens[punctIndex + 1];
}

return chain;
};

/**
* Builds an array of property names from a 'MemberExpression' node.
* - Supports nested 'MemberExpression' and 'Identifier' nodes
* - Does not support bracket notation (computed === true).
*
* 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);
* // Parsing the 'MemberExpression' node
* // corresponding to 'PLAIN['NESTED'].INNER'
* // would return:
* []
*
* @param {{ type: string; object: any, property: any, computed: boolean }} node
* @returns an array of property names built from the ast
*/
const buildPropertyAccessorChainFromAst = (node) => {
if (node.type === 'Identifier') {
return [node.name];
}

if (node.type !== 'MemberExpression') {
return [];
}

const chain = [];

if (!node.computed) {
// Dot notation
chain.push(...buildPropertyAccessorChainFromAst(node.object));
chain.push(node.property.name);
} else {
// TODO: Support bracket notation
chain.push(undefined);
}

return chain.includes(undefined) ? [] : chain;
};

/**
* Builds an object expression (i.e. { ... }) from an 'ObjectExpression' node.
* Supports a limited range of property types:
* - 'ObjectExpression' (nested)
* - 'Literal' (string, int, boolean, etc)
*
* @param {{ type: 'ObjectExpression'; properties: any[] }} node
*/
const buildObjectFromObjectExpression = (node) => {
if (node.type !== 'ObjectExpression') {
throw new TypeError("Node must be of type 'ObjectExpression' but is", node.type);
}

const obj = {};

node.properties.forEach((property) => {
if (property.value.type === 'ObjectExpression') {
obj[property.key.name] = buildObjectFromObjectExpression(property.value);
} else if (property.value.type === 'Literal') {
obj[property.key.name] = property.value.value;
}
});

return obj;
};

/**
* Supports a limited range of property types:
* - 'ObjectExpression' (nested)
* - 'Literal' (string, int, boolean, etc)
*
* If the `chain` visits an unsupported node type or tries to access a
* non-existing node, a default value is returned instead.
*
* @throws TypeError when argument `chain` is not an array of strings
* @param {Record<string, { type: string; value?: any }>} record identifier keys mapped to ast node values
* @param {string[]} chain an array of string used to access a value in `record`
* @returns the value found in `record` for the provided accessor `chain`
*/
const getValueForPropertyAccessorChain = (record, chain) => {
if (!chain.every(s => typeof s === 'string')) {
throw new TypeError('Unsupported PropertyAccessorChain:' +
`Expected 'chain' to be an array of strings but it was ${chain}`);
}

const rootExpression = record[chain[0]];

if (rootExpression.type === 'Literal') {
return rootExpression.value;
}

if (rootExpression.type !== 'ObjectExpression') {
return UNHANDLED_EVENT_NAME;
}

let current = buildObjectFromObjectExpression(rootExpression);

for (const identifier of chain.slice(1)) {
current = current[identifier];

if (!current) {
return UNHANDLED_EVENT_NAME;
}
}

return current;
};

const tokensInterval = (tokens, range) => {
return tokens.filter((item) => {
Expand Down Expand Up @@ -299,13 +467,18 @@ const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj,

module.exports.VISIBILITIES = VISIBILITIES;
module.exports.DEFAULT_VISIBILITY = DEFAULT_VISIBILITY;
module.exports.UNHANDLED_EVENT_NAME = UNHANDLED_EVENT_NAME;
module.exports.isVisibilitySupported = isVisibilitySupported;
module.exports.getVisibility = getVisibility;
module.exports.parseComment = parseComment;
module.exports.getCommentFromSourceCode = getCommentFromSourceCode;
module.exports.NodeFunction = NodeFunction;
module.exports.value = value;
module.exports.values = values;
module.exports.buildObjectFromObjectExpression = buildObjectFromObjectExpression;
module.exports.buildPropertyAccessorChainFromAst = buildPropertyAccessorChainFromAst;
module.exports.buildPropertyAccessorChainFromTokens = buildPropertyAccessorChainFromTokens;
module.exports.getValueForPropertyAccessorChain = getValueForPropertyAccessorChain;
module.exports.tokensInterval = tokensInterval;
module.exports.getIdentifierValue = getIdentifierValue;
module.exports.getIdentifierValueFromStart = getIdentifierValueFromStart;
Expand Down
Loading