diff --git a/examples/Alert.json b/examples/Alert.json
index 40e4ee8..35500c4 100644
--- a/examples/Alert.json
+++ b/examples/Alert.json
@@ -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,
diff --git a/examples/Alert.svelte b/examples/Alert.svelte
index 3fce1a6..25f159d 100644
--- a/examples/Alert.svelte
+++ b/examples/Alert.svelte
@@ -11,7 +11,11 @@
*/
import { createEventDispatcher } from 'svelte';
+
const dispatch = createEventDispatcher();
+ const EVENT = {
+ CLOSE: "close"
+ }
export let closable = false;
@@ -27,7 +31,7 @@
The `close` event fired when user click to X button in the panel.
@event CloseEvent#close
-->
-
+
{/if}
\ No newline at end of file
diff --git a/lib/parser.js b/lib/parser.js
index b578f93..e40bda2 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -459,7 +459,9 @@ 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;
}
@@ -467,7 +469,7 @@ class Parser extends EventEmitter {
continue;
}
- const next = tokens[i + 2];
+ const next = tokens[nextIndex];
const event = {
name: null,
parent: null,
@@ -524,12 +526,22 @@ 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;
@@ -537,7 +549,7 @@ class Parser extends EventEmitter {
}
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];
diff --git a/lib/utils.js b/lib/utils.js
index 19f6f91..88af7ce 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -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);
@@ -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) => {
@@ -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} 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) => {
@@ -299,6 +467,7 @@ 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;
@@ -306,6 +475,10 @@ 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;
diff --git a/lib/v3/parser.js b/lib/v3/parser.js
index ddc3615..962aa37 100644
--- a/lib/v3/parser.js
+++ b/lib/v3/parser.js
@@ -56,6 +56,7 @@ class Parser extends EventEmitter {
// Internal properties
this.componentName = null;
this.eventsEmitted = {};
+ this.identifiers = {};
this.imports = {};
this.dispatcherConstructorNames = [];
this.dispatcherNames = [];
@@ -325,8 +326,14 @@ class Parser extends EventEmitter {
}
if (variable.declarator.init) {
+ const idNode = variable.declarator.id;
const initNode = variable.declarator.init;
+ // Store top level variables in 'identifiers'
+ if (level === 0 && idNode.type === 'Identifier') {
+ this.identifiers[idNode.name] = variable.declarator.init;
+ }
+
if (initNode.type === 'CallExpression') {
const callee = initNode.callee;
@@ -608,16 +615,27 @@ class Parser extends EventEmitter {
const args = node.arguments;
- if (!args && args.length < 1) {
+ if (!args || !args.length) {
return null;
}
const nameNode = args[0];
- return {
- name: nameNode.type === 'Literal'
+ let name;
+
+ try {
+ const chain = utils.buildPropertyAccessorChainFromAst(nameNode);
+
+ // This function can throw if chain is not valid
+ name = utils.getValueForPropertyAccessorChain(this.identifiers, chain);
+ } catch (error) {
+ name = nameNode.type === 'Literal'
? nameNode.value
- : undefined,
+ : undefined;
+ }
+
+ return {
+ name: name,
node: node,
location: {
start: nameNode.start,
@@ -768,6 +786,7 @@ class Parser extends EventEmitter {
this.eventsEmitted[baseEvent.name] = baseEvent;
this.parseKeywords(comment.keywords, baseEvent);
+
this.emit('event', baseEvent);
}
diff --git a/test/svelte2/integration/events/event.method.fire.identifier.svelte b/test/svelte2/integration/events/event.method.fire.identifier.svelte
index 98cd529..d955cc2 100644
--- a/test/svelte2/integration/events/event.method.fire.identifier.svelte
+++ b/test/svelte2/integration/events/event.method.fire.identifier.svelte
@@ -7,9 +7,13 @@
\ No newline at end of file
diff --git a/test/svelte3/integration/events/events.spec.js b/test/svelte3/integration/events/events.spec.js
index 6eb194b..f144290 100644
--- a/test/svelte3/integration/events/events.spec.js
+++ b/test/svelte3/integration/events/events.spec.js
@@ -323,4 +323,27 @@ describe('SvelteDoc v3 - Events', () => {
done(e);
});
});
+
+ it('Dispatch event from code should be found when using an identifier', (done) => {
+ parser.parse({
+ version: 3,
+ filename: path.resolve(__dirname, 'event.dispatcher.identifier.svelte'),
+ features: ['events'],
+ ignoredVisibilities: []
+ }).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(1);
+
+ const event = doc.events[0];
+
+ expect(event, 'Event should be a valid entity').to.exist;
+ expect(event.name).to.equal('notify');
+ expect(event.visibility).to.equal('public');
+
+ done();
+ }).catch(e => {
+ done(e);
+ });
+ });
});
diff --git a/test/unit/helpers/utils.spec.js b/test/unit/helpers/utils.spec.js
index a6fd618..2caa11b 100644
--- a/test/unit/helpers/utils.spec.js
+++ b/test/unit/helpers/utils.spec.js
@@ -1,34 +1,184 @@
const utils = require('../../../lib/utils');
+
+const espree = require('espree');
const { expect } = require('chai');
describe('"utils.js" module', () => {
describe('"buildCamelCase" method', () => {
- it('when input is already camel cased then should return same value', done => {
+ it('when input is already camel cased then should return same value', () => {
const result = utils.buildCamelCase('CamelCasedTestMethodName12');
- expect(result).be.equal('CamelCasedTestMethodName12');
- done();
+ expect(result).to.equal('CamelCasedTestMethodName12');
});
- it('when spaces used in name then should remove them and make next char uppercased', done => {
+ it('when spaces used in name then should remove them and make next char uppercased', () => {
const result = utils.buildCamelCase('Spaces In the name');
- expect(result).be.equal('SpacesInTheName');
- done();
+ expect(result).to.equal('SpacesInTheName');
});
- it('when first letter is lowercased then should be changed to upper case', done => {
+ it('when first letter is lowercased then should be changed to upper case', () => {
const result = utils.buildCamelCase('lowercasedFirstLetter');
- expect(result).be.equal('LowercasedFirstLetter');
- done();
+ expect(result).to.equal('LowercasedFirstLetter');
});
- it('when illegal chars in name then should remove then and make next char uppercased', done => {
+ it('when illegal chars in name then should remove then and make next char uppercased', () => {
const result = utils.buildCamelCase('Illegal-chars-In-the-name');
- expect(result).to.be.equal('IllegalCharsInTheName');
- done();
+ expect(result).to.equal('IllegalCharsInTheName');
+ });
+ });
+
+ describe('buildPropertyAccessorChainFromTokens', () => {
+ it('should correctly parse a single identifier', () => {
+ const expectedChain = ['NOTIFY'];
+ const script = `
+ callee(${expectedChain.join('.')});
+ `;
+ const tokens = espree.tokenize(script);
+ const identifierTokens = tokens.slice(2);
+
+ const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens);
+
+ expect(chain).to.deep.equal(expectedChain);
+ });
+
+ it('should correctly parse chained identifiers', () => {
+ const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY'];
+ const script = `
+ callee(${expectedChain.join('.')});
+ `;
+ const tokens = espree.tokenize(script);
+ const identifierTokens = tokens.slice(2);
+
+ const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens);
+
+ expect(chain).to.deep.equal(expectedChain);
+ });
+ });
+
+ describe('buildPropertyAccessorChainFromAst', () => {
+ describe('should build an array when', () => {
+ it('AST is a single identifier', () => {
+ const expectedChain = ['NOTIFY'];
+ const script = `
+ callee(${expectedChain.join('.')});
+ `;
+ 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 a nested "MemberExpression" node', () => {
+ const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY'];
+ const script = `
+ callee(${expectedChain.join('.')});
+ `;
+ const ast = espree.parse(script);
+ const node = ast.body[0].expression.arguments[0];
+ const chain = utils.buildPropertyAccessorChainFromAst(node);
+
+ expect(chain).to.deep.equal(expectedChain);
+ });
+ });
+ });
+
+ describe('buildObjectFromObjectExpression', () => {
+ it('should build an object from an AST containing a nested "ObjectExpression" node', () => {
+ const expectedObject = {
+ SIGNAL: {
+ NOTIFY: 'notify'
+ }
+ };
+ const script = `
+ var EVENT = {
+ SIGNAL: {
+ NOTIFY: 'notify'
+ }
+ }`;
+ const ast = espree.parse(script);
+ const node = ast.body[0].declarations[0].init;
+ const object = utils.buildObjectFromObjectExpression(node);
+
+ expect(object).to.deep.equal(expectedObject);
+ });
+
+ it('should ignore unsupported node types', () => {
+ const expectedObject = {
+ SIGNAL: {
+ NOTIFY: 'notify'
+ },
+ LITERAL: true,
+ };
+ const script = `
+ var EVENT = {
+ SIGNAL: {
+ NOTIFY: 'notify'
+ },
+ OTHER: ['notify'],
+ LITERAL: true,
+ }`;
+ const ast = espree.parse(script);
+ const node = ast.body[0].declarations[0].init;
+ const object = utils.buildObjectFromObjectExpression(node);
+
+ expect(object).to.deep.equal(expectedObject);
+ });
+ });
+
+ describe('getValueForPropertyAccessorChain', () => {
+ it('should return the default value when value is unreachable', () => {
+ const script = `
+ var EVENT = {
+ SIGNAL: {}
+ }`;
+ const ast = espree.parse(script);
+ const node = ast.body[0].declarations[0].init;
+ const container = {
+ EVENT: node
+ };
+ const chain = ['EVENT', 'SIGNAL', 'NOTIFY'];
+ const value = utils.getValueForPropertyAccessorChain(container, chain);
+
+ expect(value).to.equal(utils.UNHANDLED_EVENT_NAME);
+ });
+
+ it('should return the default value when visiting an unsupported node', () => {
+ const script = `
+ var EVENT = {
+ SIGNAL: ['notify']
+ }`;
+ const ast = espree.parse(script);
+ const node = ast.body[0].declarations[0].init;
+ const container = {
+ EVENT: node
+ };
+ const chain = ['EVENT', 'SIGNAL', '0'];
+ const value = utils.getValueForPropertyAccessorChain(container, chain);
+
+ expect(value).to.equal(utils.UNHANDLED_EVENT_NAME);
+ });
+
+ it('should return the correct value when searching an object', () => {
+ const expectedValue = 'notify';
+ const script = `
+ var EVENT = {
+ SIGNAL: {
+ NOTIFY: 'notify'
+ }
+ }`;
+ const ast = espree.parse(script);
+ const node = ast.body[0].declarations[0].init;
+ const container = {
+ EVENT: node
+ };
+ const chain = ['EVENT', 'SIGNAL', 'NOTIFY'];
+ const value = utils.getValueForPropertyAccessorChain(container, chain);
+
+ expect(value).to.equal(expectedValue);
});
});
});