diff --git a/lib/is_jsdoc_comment.js b/lib/is_jsdoc_comment.js new file mode 100644 index 0000000..0d3ac18 --- /dev/null +++ b/lib/is_jsdoc_comment.js @@ -0,0 +1,16 @@ +'use strict'; + +/** + * Detect whether a comment is a JSDoc comment: it must be a block + * comment which starts with two asterisks, not any other number of asterisks. + * + * The code parser automatically strips out the first asterisk that's + * required for the comment to be a comment at all, so we count the remaining + * comments. + * @param {Object} comment an ast-types node of the comment + * @return {boolean} whether it is valid + */ +module.exports = function isJSDocComment(comment) { + var asterisks = comment.value.match(/^(\*+)/); + return comment.type === 'Block' && asterisks && asterisks[ 1 ].length === 1; +}; diff --git a/streams/infer_membership.js b/streams/infer_membership.js index b07b20b..7839330 100644 --- a/streams/infer_membership.js +++ b/streams/infer_membership.js @@ -2,7 +2,9 @@ var through = require('through'), types = require('ast-types'), - n = types.namedTypes; + n = types.namedTypes, + isJSDocComment = require('../lib/is_jsdoc_comment'), + doctrine = require('doctrine'); /** * Create a transform stream that uses code structure to infer @@ -21,6 +23,10 @@ module.exports = function () { return; } + if (findLendsTag(comment)) { + return; + } + /** * Extract and return the chain of identifiers from the left hand side of expressions * of the forms `Foo = ...`, `Foo.bar = ...`, `Foo.bar.baz = ...`, etc. @@ -85,6 +91,30 @@ module.exports = function () { } } + function findLendsTag(comment) { + for (var i = 0; i < comment.tags.length; i++) { + if (comment.tags[i].title == 'lends') { + return comment.tags[i]; + } + } + } + + function findLendsIdentifiers(node) { + if (!node || !node.leadingComments) { + return; + } + + for (var i = 0; i < node.leadingComments.length; i++) { + var comment = node.leadingComments[i]; + if (isJSDocComment(comment)) { + var lendsTag = findLendsTag(doctrine.parse(comment.value, { unwrap: true })); + if (lendsTag) { + return lendsTag.description.split('.'); + } + } + } + } + var path = comment.context.ast; var identifiers; @@ -106,11 +136,9 @@ module.exports = function () { path = path.get('key'); } - /* - * Foo.bar = ...; - * Foo.prototype.bar = ...; - * Foo.bar.baz = ...; - */ + // Foo.bar = ...; + // Foo.prototype.bar = ...; + // Foo.bar.baz = ...; if (n.MemberExpression.check(path.node)) { identifiers = extractIdentifiers(path); if (identifiers.length >= 2) { @@ -118,11 +146,22 @@ module.exports = function () { } } - /* - * Foo = { bar: ... }; - * Foo.prototype = { bar: ... }; - * Foo.bar = { baz: ... }; - */ + // /** @lends Foo */{ bar: ... } + if (n.Identifier.check(path.node) && + n.Property.check(path.parent.node) && + n.ObjectExpression.check(path.parent.parent.node)) { + // The @lends comment is sometimes attached to the first property rather than + // the object expression itself. + identifiers = findLendsIdentifiers(path.parent.parent.node) || + findLendsIdentifiers(path.parent.parent.node.properties[0]); + if (identifiers) { + inferMembership(identifiers); + } + } + + // Foo = { bar: ... }; + // Foo.prototype = { bar: ... }; + // Foo.bar = { baz: ... }; if (n.Identifier.check(path.node) && n.Property.check(path.parent.node) && n.ObjectExpression.check(path.parent.parent.node) && @@ -133,9 +172,7 @@ module.exports = function () { } } - /* - * var Foo = { bar: ... } - */ + // var Foo = { bar: ... } if (n.Identifier.check(path.node) && n.Property.check(path.parent.node) && n.ObjectExpression.check(path.parent.parent.node) && diff --git a/streams/parse.js b/streams/parse.js index e52e87b..3454099 100644 --- a/streams/parse.js +++ b/streams/parse.js @@ -4,22 +4,8 @@ var doctrine = require('doctrine'), espree = require('espree'), through = require('through'), types = require('ast-types'), - extend = require('extend'); - -/** - * Detect whether a comment is a JSDoc comment: it must be a block - * comment which starts with two asterisks, not any other number of asterisks. - * - * The code parser automatically strips out the first asterisk that's - * required for the comment to be a comment at all, so we count the remaining - * comments. - * @param {Object} comment an ast-types node of the comment - * @return {boolean} whether it is valid - */ -function isJSDocComment(comment) { - var asterisks = comment.value.match(/^(\*+)/); - return comment.type === 'Block' && asterisks && asterisks[ 1 ].length === 1; -} + extend = require('extend'), + isJSDocComment = require('../lib/is_jsdoc_comment'); /** * Comment-out a shebang line that may sit at the top of a file, diff --git a/test/streams/infer_membership.js b/test/streams/infer_membership.js index 350f014..441ee9b 100644 --- a/test/streams/infer_membership.js +++ b/test/streams/infer_membership.js @@ -21,6 +21,7 @@ function evaluate(fn, callback) { } function Foo() {} +function lend() {} test('inferMembership - explicit', function (t) { evaluate(function () { @@ -158,3 +159,67 @@ test('inferMembership - simple', function (t) { t.end(); }); }); + +test('inferMembership - lends, static', function (t) { + evaluate(function () { + lend(/** @lends Foo */{ + /** Test */ + bar: 0 + }); + }, function (result) { + t.equal(result[ 0 ].memberof, 'Foo'); + t.equal(result[ 0 ].scope, 'static'); + t.end(); + }); +}); + +test('inferMembership - lends, static, function', function (t) { + evaluate(function () { + lend(/** @lends Foo */{ + /** Test */ + bar: function () {} + }); + }, function (result) { + t.equal(result[ 0 ].memberof, 'Foo'); + t.equal(result[ 0 ].scope, 'static'); + t.end(); + }); +}); + +test('inferMembership - lends, instance', function (t) { + evaluate(function () { + lend(/** @lends Foo.prototype */{ + /** Test */ + bar: 0 + }); + }, function (result) { + t.equal(result[ 0 ].memberof, 'Foo'); + t.equal(result[ 0 ].scope, 'instance'); + t.end(); + }); +}); + +test('inferMembership - lends, instance, function', function (t) { + evaluate(function () { + lend(/** @lends Foo.prototype */{ + /** Test */ + bar: function () {} + }); + }, function (result) { + t.equal(result[ 0 ].memberof, 'Foo'); + t.equal(result[ 0 ].scope, 'instance'); + t.end(); + }); +}); + +test('inferMembership - lends applies only to following object', function (t) { + evaluate(function () { + lend(/** @lends Foo */{}); + /** Test */ + return 0; + }, function (result) { + t.equal(result.length, 1); + t.equal(result[ 0 ].memberof, undefined); + t.end(); + }); +});