diff --git a/.gitignore b/.gitignore index 75419dc2..55190216 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ selection.json # runtime junk npm-debug.log /examples/*/public -/megadoc.sublime-workspace \ No newline at end of file +/megadoc.sublime-workspace +/.local diff --git a/doc/megadoc.conf.js b/doc/megadoc.conf.js index a0841903..ee543d7a 100644 --- a/doc/megadoc.conf.js +++ b/doc/megadoc.conf.js @@ -162,7 +162,7 @@ config.plugins = [ 'ui/**/*.js', ], - exclude: [ /test/, /vendor/, ], + exclude: [ /__tests__/, /vendor/, ], useDirAsNamespace: false, diff --git a/karma.main.js b/karma.main.js index 0fb9eed3..a0f8c185 100644 --- a/karma.main.js +++ b/karma.main.js @@ -9,14 +9,18 @@ require('./ui'); sinon.assert.expose(chai.assert, { prefix: "" }); -const CoreUITests = require.context('./ui', true, /.test.js$/); - -CoreUITests.keys().forEach(CoreUITests); +tap(require.context('./ui', true, /.test.js$/), function(x) { + x.keys().forEach(x); +}); tap(require.context('./packages/megadoc-plugin-markdown/ui', true, /.test.js$/), function(x) { x.keys().forEach(x); }); +tap(require.context('./packages/megadoc-plugin-js/ui', true, /.test.js$/), function(x) { + x.keys().forEach(x); +}); + it('gives us something', function() { chai.assert.ok(true); }); diff --git a/lib/HTMLSerializer__LinkResolver.js b/lib/HTMLSerializer__LinkResolver.js index a7711cac..50ecd34e 100644 --- a/lib/HTMLSerializer__LinkResolver.js +++ b/lib/HTMLSerializer__LinkResolver.js @@ -1,6 +1,7 @@ var assert = require('assert'); var EventEmitter = require('events').EventEmitter; var URI = require('urijs'); +var dumpNodeFilePath = require('megadoc-corpus').Corpus.dumpNodeFilePath; /** * @param {Corpus} corpus @@ -44,10 +45,15 @@ LinkResolver.prototype.lookup = function(params) { assert(typeof params.path === 'string', "A lookup requires at least a @path to be specified."); + if (params.path.length === 0) { + console.warn("Link seems to be empty... Source:", dumpNodeFilePath(params.contextNode)); + return; + } + if (process.env.VERBOSE) { console.log('Resolving link to "%s" from "%s"...', params.path, - params.contextNode ? params.contextNode.uid : '<>' + dumpNodeFilePath(params.contextNode) ); } diff --git a/lib/Renderer__renderCode.js b/lib/Renderer__renderCode.js index 3f0c83c0..e60096f6 100644 --- a/lib/Renderer__renderCode.js +++ b/lib/Renderer__renderCode.js @@ -3,6 +3,7 @@ var assign = require('lodash').assign; function CodeRenderer(userConfig) { var config = assign({ + defaultLanguage: null, languages: [], aliases: {} }, userConfig); @@ -19,7 +20,7 @@ function CodeRenderer(userConfig) { }); return function renderCode(code, language) { - var languageCode = config.aliases[language] || language; + var languageCode = config.aliases[language] || language || config.defaultLanguage; var grammar = Prism.languages[languageCode]; var html = code; diff --git a/lib/Renderer__renderHeading.js b/lib/Renderer__renderHeading.js index 7ee21fc8..b174c06a 100644 --- a/lib/Renderer__renderHeading.js +++ b/lib/Renderer__renderHeading.js @@ -26,6 +26,10 @@ function renderHeading(text, level, state, runOptions) { var id = state.baseURL ? joinBySlash(state.baseURL, scopedId) : scopedId; + if (state.toc.some(function(x) { return x.scopedId === scopedId; })) { + scopedId = level + scopedId; + } + // TODO: gief a markdown analyzer, really... state.toc.push({ id: id, diff --git a/lib/config.js b/lib/config.js index 8c6e11d0..5a1f4f45 100644 --- a/lib/config.js +++ b/lib/config.js @@ -266,6 +266,8 @@ module.exports = { * [Prism.js](http://prismjs.com/). */ syntaxHighlighting: { + defaultLanguage: 'javascript', + /** * @property {Array.} syntaxHighlighting.languages * diff --git a/package.json b/package.json index 289ae1aa..a1018eb2 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,9 @@ "homepage": "https://github.com/megadoc/megadoc", "dependencies": { "async": "^2.0.0-rc.3", - "babel-core": "6.7.6", + "babel-core": "6.9.0", "babel-loader": "6.2.4", - "babel-preset-es2015": "6.6.0", + "babel-preset-es2015": "6.9.0", "babel-preset-react": "6.5.0", "classnames": "^2.1.5", "commander": "^2.8.1", @@ -82,7 +82,7 @@ "karma-mocha": "0.1.10", "karma-sourcemap-loader": "0.3.3", "karma-webpack": "1.7.0", - "mocha": "^2.3.3", + "mocha": "2.4.5", "mocha-lcov-reporter": "^1.0.0", "react-drill": "2.1.1", "react-hot-loader": "1.3.0", diff --git a/packages/megadoc-corpus/defs/core.js b/packages/megadoc-corpus/defs/core.js index fc008e62..d58e6b30 100644 --- a/packages/megadoc-corpus/defs/core.js +++ b/packages/megadoc-corpus/defs/core.js @@ -30,7 +30,6 @@ var t = CorpusTypes.builtInTypes; * }); * ``` */ -var exports; /** * @module T.Corpus @@ -73,7 +72,7 @@ def("Corpus", { def("Namespace", { fields: { /** - * @property {String} + * @property {!String} * * An identifier for this namespace that must be unique at this level in the * corpus. The identifier is utilized in the generation of [CorpusUIDs UIDs]() @@ -83,7 +82,7 @@ def("Namespace", { id: t.string, /** - * @property {String} + * @property {!String} * * A name to be used internally for the namespace. This property is meant to * identify the "class" of the namespace; or in other words, the type of @@ -99,7 +98,7 @@ def("Namespace", { name: t.string, /** - * @property {String} [title=null] + * @property {String?} [title=null] * * A human-friendly title for display for this namespace. The title will be * utilized in the UI any time we need to reference this namespace, like in @@ -112,7 +111,7 @@ def("Namespace", { title: or(t.string, null), /** - * @property {String} [symbol] + * @property {String?} [symbol="/"] * * The symbol is used when generating [[UIDs | CorpusUIDs]] for documents * in the namespace; their UID will effectively be their [[T.Node@id]] @@ -129,8 +128,6 @@ def("Namespace", { * ``` * * Document A above will have a UID of `X/A`. - * - * _Defaults to: `/`_ */ symbol: or(t.string, null), @@ -214,6 +211,8 @@ def("Node", { */ filePath: or(t.string, null), + loc: or(t.object, null), + /** * @inheritdoc T.Corpus@meta */ diff --git a/packages/megadoc-corpus/lib/Corpus.js b/packages/megadoc-corpus/lib/Corpus.js index af7b2095..b31b098d 100644 --- a/packages/megadoc-corpus/lib/Corpus.js +++ b/packages/megadoc-corpus/lib/Corpus.js @@ -184,8 +184,8 @@ function Corpus() { if (node.uid in nodes) { assert(false, 'IntegrityViolation: a node with the UID "' + node.uid + '" already exists.' + - '\nOriginal node is defined in: ' + (nodes[node.uid].filePath || '<>') + - '\nCurrent node is defined in: ' + (node.filePath || '<>') + '\nPast definition: ' + dumpNodeFilePath(nodes[node.uid]) + + '\nThis definition: ' + dumpNodeFilePath(node) ); } @@ -269,5 +269,19 @@ function hasValidNamespaceId(node) { return node.id && node.id[0] !== '/' && node.id[0] !== '.'; } -module.exports = Corpus; +function dumpNodeFilePath(node) { + var buffer = '<>'; + + if (node && node.filePath) { + buffer = node.filePath; + + if (node.loc && node.loc.start && node.loc.start.line) { + buffer += ':' + node.loc.start.line; + } + } + + return buffer; +} +module.exports = Corpus; +module.exports.dumpNodeFilePath = dumpNodeFilePath; \ No newline at end of file diff --git a/packages/megadoc-corpus/lib/CorpusResolver.js b/packages/megadoc-corpus/lib/CorpusResolver.js index bdb26194..12eafdcf 100644 --- a/packages/megadoc-corpus/lib/CorpusResolver.js +++ b/packages/megadoc-corpus/lib/CorpusResolver.js @@ -162,7 +162,7 @@ function resolveByFilePath(anchor) { filePath = filePath.slice(0, -1 * entityId.length); } - targetPath = path.join(path.dirname(contextNode.filePath), filePath); + targetPath = ensureLeadingSlash(path.join(path.dirname(contextNode.filePath), filePath)); var node = resolve({ text: targetPath, contextNode: anchor.contextNode }); @@ -191,4 +191,8 @@ function createListOfFriendNodes(node) { } return map; +} + +function ensureLeadingSlash(s) { + return s[0] === '/' ? s : '/' + s; } \ No newline at end of file diff --git a/packages/megadoc-docstring/lib/utils/__tests__/neutralizeWhitespace.test.js b/packages/megadoc-docstring/lib/utils/__tests__/neutralizeWhitespace.test.js index de2b358d..a7865861 100644 --- a/packages/megadoc-docstring/lib/utils/__tests__/neutralizeWhitespace.test.js +++ b/packages/megadoc-docstring/lib/utils/__tests__/neutralizeWhitespace.test.js @@ -63,4 +63,24 @@ describe('utils::neutralizeWhitespace', function() { assert.deepEqual(input, output); }); + + it('strips leading whitespace from description', function() { + var string = multiline(function() {; + // This + // is + // a + // multiline + // description. + }); + + assert.equal( + neutralizeWhitespace(string) + // this silly hack is to work around istanbul's instrumentor / + // multiline-slash where when NOT instrumenting, we'll have a leading + // newline + .replace(/^\n{0,1}/, '') + , + 'This\nis\na\nmultiline\ndescription.\n ' + ); + }); }); \ No newline at end of file diff --git a/packages/megadoc-docstring/lib/utils/neutralizeWhitespace.js b/packages/megadoc-docstring/lib/utils/neutralizeWhitespace.js index f513ea2a..613136e8 100644 --- a/packages/megadoc-docstring/lib/utils/neutralizeWhitespace.js +++ b/packages/megadoc-docstring/lib/utils/neutralizeWhitespace.js @@ -9,7 +9,7 @@ var RE_MATCH_INDENT = /(?:^|\n)([ \t]*)[^\s]/; * @param {String} str * @return {String} */ -module.exports = function(src) { +module.exports = function neutralizeWhitespace(src) { var indent = src.match(RE_MATCH_INDENT); if (indent && indent[1].length !== CODE_BLOCK_PADDING) { return src.replace(new RegExp('(^|\n)' + indent[1], 'g'), '$1'); diff --git a/packages/megadoc-docstring/package.json b/packages/megadoc-docstring/package.json index 4780a243..27f6b1e1 100644 --- a/packages/megadoc-docstring/package.json +++ b/packages/megadoc-docstring/package.json @@ -29,8 +29,5 @@ "homepage": "https://github.com/megadoc/megadoc#readme", "dependencies": { "jsdoctypeparser": "1.2.0" - }, - "devDependencies": { - "pegjs": "0.9.0" } } diff --git a/packages/megadoc-plugin-js/lib/Parser/Utils.js b/packages/megadoc-plugin-js/lib/Parser/ASTUtils.js similarity index 73% rename from packages/megadoc-plugin-js/lib/Parser/Utils.js rename to packages/megadoc-plugin-js/lib/Parser/ASTUtils.js index 1867a08f..50051fce 100644 --- a/packages/megadoc-plugin-js/lib/Parser/Utils.js +++ b/packages/megadoc-plugin-js/lib/Parser/ASTUtils.js @@ -1,16 +1,17 @@ var nodejsPath = require('path'); var t = require('babel-types'); +var K = require('./constants'); +var DocUtils = require('./DocUtils'); var Utils = exports; Utils.isModuleExports = function(node) { return ( - t.isMemberExpression(node.left) - && node.left.object.name === 'module' - && node.left.property.name === 'exports' - && ( - t.isIdentifier(node.right) - || t.isFunctionExpression(node.right) + t.isMemberExpression(node.left) && + node.left.object.name === 'module' && + node.left.property.name === 'exports' && ( + t.isIdentifier(node.right) || + t.isFunctionExpression(node.right) ) ); }; @@ -40,13 +41,14 @@ Utils.isInstanceEntity = function(node) { }; Utils.isFactoryModuleReturnEntity = function(node, startingPath, registry) { + var modulePath = Utils.findAncestorPath(startingPath, function(path) { var doc = registry.getModuleDocAtPath(path); return ( doc && doc.isModule() && - doc.nodeInfo.ctx.type === 'function' + DocUtils.isOfType(doc, K.TYPE_FUNCTION) ); }); @@ -101,7 +103,7 @@ Utils.findScope = function(path) { do { scope = path.scope; - } while (!scope && path && (path = path.parentPath)); + } while (!scope && ((path = path.parentPath))); return scope; }; @@ -114,7 +116,7 @@ Utils.findNearestPathWithComments = function(startingPath) { }; Utils.dumpLocation = function(node, filePath) { - return [filePath, node.loc.start.line].join(':'); + return [filePath, Utils.getLocation(node).start.line].join(':'); }; /** @@ -133,16 +135,7 @@ Utils.findIdentifierInScope = function(identifierName, path) { if (currentScope) { var targetScope = currentScope.getBinding(identifierName); - // for (var x in currentScope) { - // if (typeof currentScope[x] === 'function') - // console.log('#' + x); - // } return targetScope && targetScope.path; - // console.log(targetScope) - // if (targetScope) { - // console.log(targetScope) - // return targetScope.getBinding(identifierName)[0]; - // } } }; @@ -156,5 +149,28 @@ Utils.getLocation = function(node) { loc = node.loc; } - return loc || { start: {}, end: {} }; -}; \ No newline at end of file + return loc || { + start: { line: '?' }, + end: { line: '?' } + }; +}; + +// Whether there's a commenet for such a node: +// +// /** +// * @module +// */ +// var { Assertion } = require('chai'); +// +Utils.isCommentedDestructuredProperty = function(path) { + return ( + t.isIdentifier(path.node) && + path.node.leadingComments && + path.parentPath && + t.isVariableDeclarator(path.parentPath) && + path.parentPath.parentPath && + t.isVariableDeclaration(path.parentPath.parentPath) && + path.parentPath.parentPath.node.leadingComments && + path.parentPath.parentPath.node.leadingComments[0] === path.node.leadingComments[0] + ); +}; diff --git a/packages/megadoc-plugin-js/lib/Parser/Doc.js b/packages/megadoc-plugin-js/lib/Parser/Doc.js index 70d27381..d54be57b 100644 --- a/packages/megadoc-plugin-js/lib/Parser/Doc.js +++ b/packages/megadoc-plugin-js/lib/Parser/Doc.js @@ -1,65 +1,84 @@ var K = require('./constants'); var DocClassifier = require('./DocClassifier'); var _ = require('lodash'); -var findWhere = _.findWhere; +var DocUtils = require('./DocUtils'); +var ASTUtils = require('./ASTUtils'); var assign = _.assign; -var assert = require('assert'); -var debuglog = require('megadoc/lib/Logger')('megadoc').info; /** * @param {Docstring} docstring * @param {NodeInfo} nodeInfo * @param {String} filePath - * @param {String} absoluteFilePath */ -function Doc(docstring, nodeInfo, filePath, absoluteFilePath) { +function Doc(docstring, nodeInfo, filePath) { this.consumeDocstring(docstring); this.consumeNodeInfo(nodeInfo); - this.id = this.generateId(); - this.name = this.generateName(); this.filePath = filePath; - this.absoluteFilePath = absoluteFilePath; - this.customAliases = []; return this; } -Doc.prototype.toJSON = function() { - var doc = assign({}, this.docstring.toJSON(), this.nodeInfo.toJSON()); +Object.defineProperty(Doc.prototype, 'id', { + get: function() { + return DocUtils.getIdOf(this); + } +}); + +Doc.prototype.toJSON = function(registry) { + var nodeInfo = this.nodeInfo; + var doc = assign({}, + this.docstring.toJSON(), + this.nodeInfo.toJSON() + ); + + doc.type = DocUtils.getTypeNameOf(this); + doc.nodeInfo = this.nodeInfo.ctx; doc.id = this.id; - doc.name = this.generateName(); + doc.name = DocUtils.getNameOf(this); doc.filePath = this.filePath; - doc.absoluteFilePath = this.absoluteFilePath; doc.isModule = this.isModule(); - doc.receiver = this.getReceiver(); - doc.mixinTargets = doc.tags.filter(function(tag) { - return tag.type === 'mixes'; - }).reduce(function(list, tag) { - return list.concat(tag.mixinTargets); - }, []); - - doc.aliases = doc.tags.filter(function(tag) { - return tag.type === 'alias'; - }).map(function(tag) { - return tag.alias; - }).concat(this.customAliases); - - // support for explicit typing using tags like @method or @type - if (this.docstring.hasTypeOverride()) { - doc.ctx.type = this.docstring.getTypeOverride(); - } if (!doc.isModule) { - doc.ctx.symbol = this.generateSymbol(doc.ctx.type); - doc.id = [ doc.receiver, doc.id ].join(doc.ctx.symbol); - doc.path = [ doc.receiver, doc.name ].join(doc.ctx.symbol); - } - else { - doc.path = doc.id; + var resolvedContext = DocUtils.getReceiverAndScopeFor(this, registry); + + doc.receiver = resolvedContext.receiver; + + // scope + if (resolvedContext.scope) { + doc.nodeInfo.scope = resolvedContext.scope; + } + else { + if (nodeInfo.isInstanceEntity()) { + doc.nodeInfo.scope = K.SCOPE_INSTANCE; + } + else if (nodeInfo.isPrototypeEntity()) { + doc.nodeInfo.scope = K.SCOPE_PROTOTYPE; + } + // blegh + else if ( + ASTUtils.isFactoryModuleReturnEntity( + this.$path.node, + this.$path, + registry + ) + ) { + doc.nodeInfo.scope = K.SCOPE_FACTORY_EXPORTS; + } + } + + doc.symbol = generateSymbol(this); + doc.id = doc.receiver + doc.symbol + this.id; } + doc.mixinTargets = doc.tags + .filter(function(tag) { return tag.type === 'mixes'; }) + .map(function(tag) { return tag.typeInfo.name; }) + ; + + doc.aliases = Object.keys(this.docstring.aliases); + // we'll need this for @preserveOrder support if (doc.loc) { doc.line = doc.loc.start.line; @@ -68,8 +87,6 @@ Doc.prototype.toJSON = function() { }); } - this.useSourceNameWhereNeeded(doc.name, doc); - return doc; }; @@ -81,26 +98,6 @@ Doc.prototype.consumeNodeInfo = function(nodeInfo) { this.nodeInfo = nodeInfo; }; -Doc.prototype.generateId = function() { - var id = this.docstring.id || this.nodeInfo.id; - var namespace = this.docstring.namespace; - - if (id && namespace && id.indexOf(namespace) === -1) { - id = [ namespace, id ].join(K.NAMESPACE_SEP); - } - - return id; -}; - -Doc.prototype.generateName = function() { - if (this.docstring.namespace && this.id) { - return this.id.replace(this.docstring.namespace + K.NAMESPACE_SEP, ''); - } - else { - return this.id; - } -}; - Doc.prototype.markAsExported = function() { this.$isExported = true; }; @@ -110,87 +107,19 @@ Doc.prototype.isExported = function() { }; Doc.prototype.isModule = function() { - return !this.docstring.hasMemberOf() && ( - this.isExported() || - this.docstring.isModule() || - this.nodeInfo.isModule() - ); + return DocUtils.isModule(this); }; -Doc.prototype.generateSymbol = function(type) { - var symbol; - - switch(type) { - case K.TYPE_FUNCTION: - if (DocClassifier.isStaticMethod(this)) { - symbol = '.'; - } - else { - symbol = '#'; - } - break; - - default: - if (DocClassifier.isObjectProperty(this)) { - symbol = '@'; - } - else { - symbol = '.'; - } - break; +function generateSymbol(doc) { + if (DocClassifier.isStaticMember(doc)) { + return '.'; } - - if (this.docstring.hasTag('property') && !this.docstring.hasTag('static')) { - symbol = '@'; + else if (DocClassifier.isMethod(doc)) { + return '#'; } - - if (this.docstring.hasTag('property') && this.docstring.hasTag('static')) { - symbol = '.'; + else if (DocClassifier.isMember(doc)) { + return '@'; } - - return symbol; -}; - -/** - * Set the correct receiver for this doc. The receiver might not have been - * resolved correctly in NodeInfo due to @lends or module aliases. - * - * @param {String} correctedReceiver - * Name of the new receiver. - */ -Doc.prototype.overrideReceiver = function(correctedReceiver) { - assert(!!correctedReceiver, - "You are attempting to override a receiver with an undefined one!" - ); - - debuglog( - 'Adjusting receiver from "%s" to "%s" for "%s" (scope: %s)', - this.nodeInfo.receiver, - correctedReceiver, - this.id, - this.nodeInfo.ctx && this.nodeInfo.ctx.scope - ); - - this.$correctedReceiver = correctedReceiver; -}; - -Doc.prototype.getReceiver = function() { - return this.$correctedReceiver || this.nodeInfo.receiver; -}; - -Doc.prototype.hasReceiver = function() { - return Boolean(this.getReceiver()); -}; - -Doc.prototype.useSourceNameWhereNeeded = function(name, doc) { - var propertyTag = findWhere(doc.tags, { type: 'property' }); - if (propertyTag && !propertyTag.typeInfo.name) { - propertyTag.typeInfo.name = name; - } -}; - -Doc.prototype.addAlias = function(name) { - this.customAliases.push(name); }; module.exports = Doc; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/DocClassifier.js b/packages/megadoc-plugin-js/lib/Parser/DocClassifier.js index 3029f8bf..498d9c93 100644 --- a/packages/megadoc-plugin-js/lib/Parser/DocClassifier.js +++ b/packages/megadoc-plugin-js/lib/Parser/DocClassifier.js @@ -1,4 +1,5 @@ var K = require('./constants'); +var DocUtils = require('./DocUtils'); function isModule(doc) { return doc.isModule(); @@ -8,28 +9,34 @@ function isEntity(doc) { return !doc.isModule(); } -function isMethod(doc) { +function isMember(doc) { var ctx = doc.nodeInfo.ctx; - return ctx.type === K.TYPE_FUNCTION && isObjectProperty(doc); -} - -function isObjectProperty(doc) { - var ctx = doc.nodeInfo.ctx; - - return ( + return !doc.docstring.hasTag('static') && ( ctx.scope === K.SCOPE_FACTORY_EXPORTS || ctx.scope === K.SCOPE_INSTANCE || ctx.scope === K.SCOPE_PROTOTYPE ); } +function isStaticMember(doc) { + return ( + doc.nodeInfo.ctx.scope === K.SCOPE_UNSCOPED || + doc.docstring.hasTag('static') + ); +} + +function isMethod(doc) { + return DocUtils.isOfType(doc, K.TYPE_FUNCTION) && isMember(doc); +} + function isStaticMethod(doc) { - return doc.nodeInfo.ctx.type === K.TYPE_FUNCTION && !isMethod(doc); + return DocUtils.isOfType(doc, K.TYPE_FUNCTION) && isStaticMember(doc); } exports.isModule = isModule; exports.isEntity = isEntity; exports.isMethod = isMethod; exports.isStaticMethod = isStaticMethod; -exports.isObjectProperty = isObjectProperty; +exports.isMember = isMember; +exports.isStaticMember = isStaticMember; diff --git a/packages/megadoc-plugin-js/lib/Parser/DocUtils.js b/packages/megadoc-plugin-js/lib/Parser/DocUtils.js new file mode 100644 index 00000000..a47e64a1 --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/DocUtils.js @@ -0,0 +1,138 @@ +var K = require('./constants'); + +exports.getIdOf = function(doc) { + var name = doc.docstring.name || doc.nodeInfo.id; + var namespace = doc.docstring.namespace; + + if (name && namespace) { + return namespace + K.NAMESPACE_SEP + name; + } + else { + return name; + } +}; + +exports.getNameOf = function(doc) { + return doc.docstring.name || doc.nodeInfo.id; +}; + +exports.getTypeOf = function(doc) { + if (doc.docstring.hasTypeOverride()) { + return doc.docstring.getTypeOverride(); + } + else if (doc.nodeInfo.ctx.type) { + return doc.nodeInfo.ctx.type; + } + + return K.TYPE_UNKNOWN; +}; + +exports.getTypeNameOf = function(doc) { + var type = exports.getTypeOf(doc); + + return type.name || type; +}; + +exports.isOfType = function(doc, expectedTypeName) { + var typeName = exports.getTypeNameOf(doc); + + return typeName === expectedTypeName; +}; + +exports.isModule = function(doc) { + return !doc.docstring.hasMemberOf() && ( + doc.isExported() || + doc.docstring.isModule() || + doc.nodeInfo.isModule() + ); +}; + +exports.getReceiverAndScopeFor = function(doc, registry) { + var receiver = doc.nodeInfo.receiver; + var correctedScope, exportedModule, enclosingModule, receivingModule; + + // Resolve @memberOf receiver aliasing: + if (doc.docstring.hasMemberOf()) { + receiver = doc.docstring.getExplicitReceiver(); + } + + // Resolve @lends using either the original receiver or the one pointed to + // by @memberOf: + var lendEntry = ( + registry.findAliasedLendTarget(doc.$path, receiver) || + registry.findClosestLend(doc.$path) + ); + + // TODO: this needs a bit of rethinking really + if (lendEntry) { + receiver = lendEntry.receiver; + } + // CommonJS specific scenario; the entity is attached to the special "exports" + // variable. + // + // We need to watch out and not confuse it with "exports" Identifier nodes + // that are not defined in the global Program scope. + // + // var Something = exports; + // ^^^^^^^^^ + // + // // ... + // + // /** yep */ + // exports.something = function() {} + // ^^^^^^^ + else if (receiver === 'exports') { + exportedModule = registry.findExportedModule(doc.filePath); + + if (exportedModule) { + console.log('Correcting receiver from "exports" to "%s". Source: %s', + exportedModule.id, + exports.getLocationOf(doc) + ); + + receiver = exportedModule.id; + } + } + else { + // TODO: this too + enclosingModule = ( + // registry.findAliasedReceiver(receiver) || + registry.findClosestModule(doc.$path) + ); + + if (enclosingModule) { + receiver = enclosingModule; + } + } + + if (receiver) { + if (receiver.match(/(.*)\.prototype$/)) { + receiver = RegExp.$1; + correctedScope = K.SCOPE_PROTOTYPE; + } + + // Now search the Registry for a module that either has an ID or a name with + // our resolved receiver string: + receivingModule = registry.get(receiver); + + if (receivingModule) { + receiver = receivingModule.id; + } + } + + if (!receiver) { + console.warn( + "No receiver was found for the document '%s', it will be discarded.", + exports.getLocationOf(doc) + ); + } + + return { receiver: receiver, scope: correctedScope }; +}; + +exports.getLocationOf = function(doc) { + return ( + (doc.id || '<>') + ' in ' + + doc.filePath + ':' + doc.nodeInfo.loc.start.line + ); +}; diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring.js b/packages/megadoc-plugin-js/lib/Parser/Docstring.js index fce4f6fb..3a03f262 100644 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring.js +++ b/packages/megadoc-plugin-js/lib/Parser/Docstring.js @@ -1,11 +1,11 @@ -var dox = require('dox'); +// var dox = require('dox'); +var parseComment = require('./parseComment'); var assert = require('assert'); var _ = require('lodash'); var K = require('./constants'); -var Tag = require('./Docstring/Tag'); -var extractIdInfo = require('./Docstring/extractIdInfo'); -var collectDescription = require('./Docstring/collectDescription'); -var findWhere = _.findWhere; +var Tag = require('./Docstring__Tag'); +var extractIdInfo = require('./Docstring__extractIdInfo'); +var collectDescription = require('./Docstring__collectDescription'); /** * An object representing a JSDoc comment (parsed using dox). @@ -14,23 +14,46 @@ var findWhere = _.findWhere; * The JSDoc-compatible comment string to build from. */ function Docstring(comment, options, filePath) { - var doxDocs = dox.parseComments(comment, { raw: true }); - var idInfo; + var commentNode, idInfo; - assert(doxDocs.length === 1, - 'Dox should not extract more than 1 doc from an expression.' + try { + commentNode = parseComment(comment); + } + catch(e) { + throw new Error('Comment parse failed: "' + e.message + '" Source:\n' + comment); + } + + if (commentNode.length === 0) { + throw new Error('Invalid annotation in comment block. Source:\n' + comment); + } + + assert(commentNode.length === 1, + 'Comment parser should yield a single node, not ' + commentNode.length + '! ' + + 'Source:\n' + comment ); - this._doxDocs = doxDocs; - this.tags = doxDocs[0].tags.map(function(doxTag) { + this.tags = commentNode[0].tags.map(function(doxTag) { return new Tag(doxTag, options || {}, filePath); }); idInfo = extractIdInfo(this.tags); - this.id = idInfo.id; + // this.id = idInfo.id; + this.name = idInfo.name; this.namespace = idInfo.namespace; - this.description = collectDescription(doxDocs[0], this.id, this.tags); + this.description = collectDescription(commentNode[0], this.id, this.tags); + this.aliases = this.tags.filter(function(tag) { + return tag.type === 'alias'; + }).reduce(function(map, tag) { + map[tag.typeInfo.name] = true; + return map; + }, {}); + + this.$location = ( + (this.namespace ? this.namespace + K.NAMESPACE_SEP : '') + + (this.name || '') + ' in ' + + filePath + ); return this; } @@ -40,8 +63,10 @@ var Dpt = Docstring.prototype; /** * @return {Object} doc * - * @return {String} doc.id - * The explicit module id found in a @module tag, if any. + * @return {String} doc.name + * If this is a module, it will be the name of the module found in a + * @module tag, or a @name tag. Otherwise, it's what may be found in a + * @name tag, a @property tag, or a @method tag. See [extractIdInfo](). * * @return {String} doc.namespace * The namespace name found in a @namespace tag, if any. @@ -54,7 +79,7 @@ var Dpt = Docstring.prototype; */ Docstring.prototype.toJSON = function() { var docstring = _.pick(this, [ - 'id', + 'name', 'namespace', 'description', ]); @@ -74,16 +99,12 @@ Dpt.isInternal = function() { return this.hasTag('internal'); }; -Dpt.isMethod = function() { - return this.hasTag('method'); -}; - Dpt.doesLend = function() { return this.hasTag('lends'); }; Dpt.getLentTo = function() { - return this.getTag('lends').lendReceiver; + return this.getTag('lends').typeInfo.name; }; Dpt.hasMemberOf = function() { @@ -91,39 +112,55 @@ Dpt.hasMemberOf = function() { }; Dpt.getExplicitReceiver = function() { - return this.getTag('memberOf').explicitReceiver; + return this.getTag('memberOf').typeInfo.name; }; Dpt.hasTag = function(type) { - return !!findWhere(this.tags, { type: type }); + return this.tags.some(findByType(type)); }; Dpt.getTag = function(type) { - return findWhere(this.tags, { type: type }); + return this.tags.filter(findByType(type))[0]; +}; + +Dpt.hasAlias = function(alias) { + return alias in this.aliases; }; Dpt.hasTypeOverride = function() { - return this.tags.filter(function(tag) { - return !!tag.explicitType; - }).length > 0; + return getTypeOverridingTags(this.tags).length > 0; }; Dpt.getTypeOverride = function() { - return this.tags.filter(function(tag) { - return !!tag.explicitType; - })[0].explicitType; + var typedTags = getTypeOverridingTags(this.tags); + + if (typedTags.length > 1) { + console.warn("Document has multiple type overrides! Source: %s", + this.$location + ); + } + + return typedTags[0].typeInfo.type; }; Dpt.overrideNamespace = function(namespace) { - var oldNamespace = this.namespace; - this.namespace = namespace; +}; - if (oldNamespace && this.id) { - this.id = this.id.replace(oldNamespace + K.NAMESPACE_SEP, ''); - } - - // this will FUBAR if we have @namespace tags ... +Dpt.addAlias = function(name) { + this.aliases[name] = true; }; -module.exports = Docstring; \ No newline at end of file +module.exports = Docstring; + +function getTypeOverridingTags(tags) { + return tags.filter(function(tag) { + return tag.type in K.TYPE_OVERRIDING_TAGS; + }); +} + +function findByType(type) { + return function(x) { + return x.type === type; + } +} \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag.js deleted file mode 100644 index 8fe39f1c..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag.js +++ /dev/null @@ -1,257 +0,0 @@ -var parseProperty = require('./Tag/parseProperty'); -var extractDefaultValue = require('./Tag/extractDefaultValue'); -var neutralizeWhitespace = require('./Tag/neutralizeWhitespace'); -var K = require('../constants'); -var assert = require('assert'); - -var TypeAliases = { - 'returns': 'return' -}; - -/** - * @param {Object} doxTag - * @param {Object} options - * @param {Object} options.customTags - * @param {Boolean} [options.namedReturnTags=true] - * - * @param {String} filePath - */ -function Tag(doxTag, options, filePath) { - var customTags = options.customTags; - - /** - * @property {String} - * The type of this tag. This is always present. - */ - this.type = TypeAliases[doxTag.type] || doxTag.type; - - /** - * @property {String} - * The raw text. - */ - this.string = String(doxTag.string || '') - - /** - * @property {String} - * Module namepath pointed to by @alias. - */ - this.alias = null; - - /** - * @property {String} visibility - */ - this.visibility = null; - - /** - * @property {String} - * Module namepath pointed to by @memberOf. - */ - this.explicitReceiver = null; - - /** - * @property {String} - * Would be "function" for @method tags, or whatever is specified - * by a @type tag. - */ - this.explicitType = null; - - /** - * @property {String} - * Name of the module that is lent to by the enclosing doc. - * - * Available only for @lends tags. - */ - this.lendReceiver = null; - - this.mixinTargets = []; - - /** - * @property {String} - * Available on @property, @type, @param, and @live_example tags. - */ - this.typeInfo = { - /** - * @property {String} - */ - name: null, - - /** - * @property {String} - */ - description: null, - - /** - * @property {Boolean} - */ - isOptional: null, - - /** - * @property {String} - */ - defaultValue: null, - - /** - * @property {String[]} - */ - types: [] - }; - - switch(this.type) { - case 'property': - case 'param': - case 'return': - case 'throws': - case 'example': - case 'interface': - this.typeInfo = TypeInfo(doxTag); - - // fixup for return tags when we're not expecting them to be named - if (this.type === 'return' && this.typeInfo.name && options.namedReturnTags === false) { - this.typeInfo.description = this.typeInfo.name + ' ' + this.typeInfo.description; - this.typeInfo.name = undefined; - } - - break; - - case 'type': - // console.assert(doxTag.types.length === 1, - // "Expected @type tag to contain only a single type, but it contained %d.", - // doxTag.types.length - // ); - this.typeInfo = TypeInfo(doxTag); - this.string = this.string.replace(doxTag.string, ''); - - if (doxTag.types.length === 1) { - this.explicitType = renamePrimitiveType(doxTag.types[0].trim()); - } - - break; - - // if it was marked @method, treat it as such (not stupid "property" type - // on object modules) - case 'method': - this.explicitType = K.TYPE_FUNCTION; - this.typeInfo = TypeInfo(doxTag); - - break; - - case 'protected': - this.visibility = K.VISIBILITY_PROTECTED; - break; - - case 'private': - this.visibility = K.VISIBILITY_PRIVATE; - break; - - case 'memberOf': - this.explicitReceiver = doxTag.parent; - - // @memberOf's "parent" property (which is the target class name) will be - // present in the string so we remove it: - this.string = this.string.replace(this.explicitReceiver, ''); - - break; - - case 'alias': - this.alias = this.string.split('\n')[0].trim(); - - // same deal with @memberOf - this.string = this.string.replace(this.alias, ''); - - break; - - case 'lends': - this.lendReceiver = doxTag.parent; - break; - - case 'mixes': - var firstLine = this.string.split('\n')[0].trim(); - this.mixinTargets = firstLine.split(/\s+/); - this.string = this.string.replace(firstLine, ''); - break; - - default: - if (customTags && customTags.hasOwnProperty(doxTag.type)) { - this.useCustomTagDefinition(doxTag, customTags[doxTag.type], filePath); - } - } - - return this; -} - -Tag.prototype.adjustString = function(newString) { - this.string = newString; -}; - -Tag.prototype.toJSON = function() { - return Object.keys(this).reduce(function(json, key) { - if (this[key] !== null && typeof this[key] !== 'function') { - json[key] = this[key]; - } - - return json; - }.bind(this), {}); -}; - -Tag.prototype.useCustomTagDefinition = function(doxTag, customTag, filePath) { - var customAttributes = customTag.attributes || []; - - if (customTag.withTypeInfo) { - this.typeInfo = TypeInfo(doxTag); - } - - if (customTag.process instanceof Function) { - customTag.process(createCustomTagAPI(this, customAttributes), filePath); - } -}; - -function createCustomTagAPI(tag, attrWhitelist) { - var api = tag.toJSON(); - - api.setCustomAttribute = function(name, value) { - assert(attrWhitelist.indexOf(name) > -1, - "Unrecognized custom attribute '" + name + "'. Make sure you " + - "you specify it in the @attributes array for the customTag." - ); - - tag[name] = value; - }; - - return api; -} - -module.exports = Tag; - -function renamePrimitiveType(type) { - if (type === 'Function') { - return 'function'; - } - else { - return type; - } -} - -function TypeInfo(doxTag) { - var typeInfo = parseProperty(doxTag.string); - - if (typeInfo.description) { - typeInfo.description = neutralizeWhitespace(typeInfo.description); - } - - if (typeInfo.name) { - if (doxTag.name && doxTag.name !== typeInfo.name) { - typeInfo.name = doxTag.name; - typeInfo.description = doxTag.description; - } - - var nameFragments = extractDefaultValue(typeInfo.name); - - if (nameFragments) { - typeInfo.name = nameFragments.name; - typeInfo.defaultValue = nameFragments.defaultValue; - } - } - - - return typeInfo; -} diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/__tests__/neutralizeWhitespace.test.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/__tests__/neutralizeWhitespace.test.js deleted file mode 100644 index f1711e75..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/__tests__/neutralizeWhitespace.test.js +++ /dev/null @@ -1,29 +0,0 @@ -var neutralizeWhitespace = require('../neutralizeWhitespace'); -var assert = require('chai').assert; -var multiline = require('multiline-slash'); - -var parse = function(strGenerator) { - return neutralizeWhitespace(multiline(strGenerator)); -}; - -describe('CJS::Parser::Docstring::Tag::neutralizeWhitespace', function() { - it('strips leading whitespace from description', function() { - var string = parse(function() {; - // This - // is - // a - // multiline - // description. - }); - - assert.equal( - string - // this silly hack is to work around istanbul's instrumentor / - // multiline-slash where when NOT instrumenting, we'll have a leading - // newline - .replace(/^\n{0,1}/, '') - , - 'This\nis\na\nmultiline\ndescription.\n' - ); - }); -}); \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/extractDefaultValue.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/extractDefaultValue.js deleted file mode 100644 index c8dd84db..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/extractDefaultValue.js +++ /dev/null @@ -1,17 +0,0 @@ -var RE = new RegExp('\s*([^=]+)=(.+)\s*'); - -module.exports = function extractDefaultValue(string) { - if (string[0] === '[' && string[string.length-1] === ']') { - if (string.slice(1,-1).match(RE)) { - return { name: RegExp.$1, defaultValue: RegExp.$2 }; - } - else { - return { - name: string.slice(1,-1), - defaultValue: null - } - } - } - - return null; -}; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/neutralizeWhitespace.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/neutralizeWhitespace.js deleted file mode 100644 index cee74bef..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/neutralizeWhitespace.js +++ /dev/null @@ -1,36 +0,0 @@ -var CODE_BLOCK_PADDING = 4; - -module.exports = function(src) { - if (!src) { - return; - } - - var lines = src.split('\n'); - - var padding; - var firstNonBlankLine; - var lineIter, line; - - for (lineIter = 0; lineIter < lines.length; ++lineIter) { - line = lines[lineIter]; - - if (line.trim().length > 0) { - firstNonBlankLine = line; - break; - } - } - - if (firstNonBlankLine) { - if (firstNonBlankLine.match(/^(\s+)/)) { - padding = RegExp.$1.length; - - if (padding > 0 && padding !== CODE_BLOCK_PADDING) { - return lines.map(function(_line) { - return _line.substr(padding); - }).join('\n'); - } - } - } - - return src; -}; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/parseProperty.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/parseProperty.js deleted file mode 100644 index de021635..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/Tag/parseProperty.js +++ /dev/null @@ -1,99 +0,0 @@ -var TYPE_SPLITTER = /,|\|/ - -function parseProperty(docstring) { - var typeInfo = {}; - - var STATE_PARSING_NONE = 0; - var STATE_PARSING_TYPE = 1; - var STATE_PARSING_NAME = 2; - var STATE_PARSING_DEFAULT_VALUE = 3; - var STATE_PARSING_DESCRIPTION = 4; - var state = STATE_PARSING_NONE; - - var typeStr; - var nameStr; - var descStr; - - docstring.split('').forEach(function(char) { - switch (state) { - case STATE_PARSING_NONE: - if (char === '{') { - typeStr = ''; - state = STATE_PARSING_TYPE; - } - else if (/\s/.test(char)) { - state = STATE_PARSING_DESCRIPTION; - } - else { - nameStr = char; - state = STATE_PARSING_NAME; - } - - break; - - case STATE_PARSING_TYPE: - if (char === '}') { - state = STATE_PARSING_NAME; - nameStr = ''; - } - else { - typeStr += char; - } - - break; - - case STATE_PARSING_NAME: - if (char === '[') { - typeInfo.isOptional = true; - } - else if (char === '=') { - state = STATE_PARSING_DEFAULT_VALUE; - typeInfo.defaultValue = ''; - } - else if (char === '\n') { - state = STATE_PARSING_DESCRIPTION; - } - else if (char !== ']') { - nameStr += char; - } - - break; - - case STATE_PARSING_DEFAULT_VALUE: - if (char === '\n') { - state = STATE_PARSING_DESCRIPTION; - } - else if (char !== ']') { - typeInfo.defaultValue += char; - } - break; - - case STATE_PARSING_DESCRIPTION: - if (!descStr) { - descStr = ''; - } - - descStr += char; - - break; - } - }); - - if (typeStr) { - typeInfo.types = typeStr.split(TYPE_SPLITTER); - } - else { - typeInfo.types = []; - } - - typeInfo.name = nameStr && nameStr.trim().length > 0 ? - nameStr.trim() : - null - ; - - typeInfo.description = descStr || null; - - return typeInfo; -} - -module.exports = parseProperty; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/collectDescription.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/collectDescription.js deleted file mode 100644 index ad3db46a..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/collectDescription.js +++ /dev/null @@ -1,61 +0,0 @@ -var NO_DESCRIPTION_TAGS = [ - 'memberOf', - 'constructor', - 'class', - 'module', - 'extends', - 'private', - 'mixin', - 'mixes', - 'preserveOrder', - 'alias', - 'type', - 'method', - 'inheritdoc', -]; - -function extractSwallowedDescriptionInTag(tag, fragments) { - // Extract class description; sometimes it's kept in description.full, - // other times it's swalloed inside a tag of type "class" or "constructor" - if (NO_DESCRIPTION_TAGS.indexOf(tag.type) > -1) { - if (tag.explicitReceiver) { - tag.adjustString(tag.string.replace(tag.explicitReceiver, '')); - } - - fragments.push(tag.string.trim()); - } - - // if there was a @namespace tag with some description of the module below - // it, it would "consume" that description so we need to rewrite it to - // the module's doc. Example docstring: - // - // /** - // * @namespace Core - // * - // * My module description. - // */ - // function MyModule() {} - else if (tag.type === 'namespace') { - var nsDescription = tag.string.split('\n').slice(1).join('\n'); - fragments.push(nsDescription); - } -} - -module.exports = function(doxDoc, id, tags) { - var description = String(doxDoc.description.full); - - var fragments = tags.reduce(function(_fragments, tag) { - extractSwallowedDescriptionInTag(tag, _fragments); - return _fragments; - }, []); - - if (fragments.length > 0) { - description = [ description ].concat(fragments).join(''); - } - - if (id && description.substr(0, id.length) === id) { - description = description.replace(id, ''); - } - - return description.trim(); -}; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring/extractIdInfo.js b/packages/megadoc-plugin-js/lib/Parser/Docstring/extractIdInfo.js deleted file mode 100644 index 879ae2da..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/Docstring/extractIdInfo.js +++ /dev/null @@ -1,56 +0,0 @@ -var K = require('../constants'); -var findWhere = require('lodash').findWhere; - -function extractIdInfo(tags) { - var id, namespace; - - // extract the module id from a @module tag: - var moduleTag = findWhere(tags, { type: 'module' }); - if (moduleTag) { - var moduleId = moduleTag.string.match(/^([\w\.]+)\s*$|^([\w\.]+)\s*\n/); - - if (moduleId) { - id = moduleId[1] || moduleId[2]; - } - } - - // extract the namespace from a @namespace tag: - var nsTag = findWhere(tags, { type: 'namespace' }); - if (nsTag) { - var namespaceString = nsTag.string.split('\n')[0].trim(); - - if (namespaceString.length) { - namespace = namespaceString; - } - else { - console.warn('@namespace tag must have a name.'); - } - } - // check for inline namespaces found in a module id string - else if (id && id.indexOf(K.NAMESPACE_SEP) > -1) { - namespace = id - .split(K.NAMESPACE_SEP) - .slice(0, -1) - .join(K.NAMESPACE_SEP) - ; - } - - var methodTag = findWhere(tags, { type: 'method' }); - - if (methodTag && methodTag.typeInfo.name) { - id = methodTag.typeInfo.name; - } - - var propertyTag = findWhere(tags, { type: 'property' }); - - if (propertyTag && propertyTag.typeInfo.name) { - id = propertyTag.typeInfo.name; - } - - return { - id: id, - namespace: namespace - }; -} - -module.exports = extractIdInfo; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring__Tag.js b/packages/megadoc-plugin-js/lib/Parser/Docstring__Tag.js new file mode 100644 index 00000000..8da06a0a --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/Docstring__Tag.js @@ -0,0 +1,173 @@ +var K = require('./constants'); +var TypeInfo = require('./Docstring__TagTypeInfo'); +var assert = require('assert'); + +var TypeAliases = { + 'returns': 'return' +}; + +/** + * @param {Object} commentNode + * @param {Object} options + * @param {Object} options.customTags + * @param {Boolean} [options.namedReturnTags=true] + * + * @param {String} filePath + */ +function Tag(commentNode, options, filePath) { + var customTags = options.customTags; + + if (commentNode.errors && commentNode.errors.length) { + throw new Error(commentNode.errors[0]); + } + + /** + * @property {String} + * The type of this tag. This is always present. + */ + this.type = TypeAliases[commentNode.tag] || commentNode.tag; + + /** + * @property {String} + * The raw text. + */ + this.string = String(commentNode.description || ''); + + /** + * @property {String} + * Available on @property, @type, @param, and @live_example tags. + */ + this.typeInfo = { + /** + * @property {String} + */ + name: null, + + /** + * @property {String} + */ + description: null, + + /** + * @property {Boolean} + */ + isOptional: null, + + /** + * @property {String} + */ + defaultValue: null, + + /** + * @property {TagTypeInfo} typeInfo.type + */ + type: null + }; + + switch(this.type) { + case 'property': + case 'param': + case 'return': + case 'throws': + case 'example': + case 'interface': + this.typeInfo = TypeInfo(commentNode); + + // fixup for return tags when we're not expecting them to be named + if (this.type === 'return' && this.typeInfo.name && options.namedReturnTags === false) { + this.typeInfo.description = this.typeInfo.name + ' ' + this.typeInfo.description; + delete this.typeInfo.name; + } + + break; + + case 'type': + this.typeInfo = TypeInfo(commentNode); + + break; + + case 'method': + this.typeInfo = TypeInfo(commentNode); + this.typeInfo.type = { name: K.TYPE_FUNCTION }; + + break; + + case 'protected': + case 'private': + break; + + case 'memberOf': + this.typeInfo.name = commentNode.name; + + break; + + case 'module': + if (commentNode.name.trim().length > 0) { + this.typeInfo.name = commentNode.name.trim(); + } + break; + + case 'namespace': + case 'name': + case 'alias': + case 'lends': + case 'mixes': + case 'see': + this.typeInfo.name = commentNode.name; + break; + } + + if (customTags && customTags.hasOwnProperty(this.type)) { + this.useCustomTagDefinition(commentNode, customTags[this.type], filePath); + } + + return this; +} + +Tag.prototype.toJSON = function() { + return Object.keys(this).reduce(function(json, key) { + if (this[key] !== null && typeof this[key] !== 'function' && key[0] !== '$') { + json[key] = this[key]; + } + + return json; + }.bind(this), {}); +}; + +Tag.prototype.useCustomTagDefinition = function(commentNode, customTag, filePath) { + var customAttributes = customTag.attributes || []; + + if (customTag.withTypeInfo) { + this.typeInfo = TypeInfo(commentNode); + } + + if (customTag.process instanceof Function) { + customTag.process(createCustomTagAPI(this, customAttributes), filePath); + } +}; + +function createCustomTagAPI(tag, attrWhitelist) { + var api = tag.toJSON(); + + api.setCustomAttribute = function(name, value) { + assert(attrWhitelist.indexOf(name) > -1, + "Unrecognized custom attribute '" + name + "'. Make sure you " + + "you specify it in the @attributes array for the customTag." + ); + + tag[name] = value; + }; + + return api; +} + +module.exports = Tag; + +// function renamePrimitiveType(type) { +// if (type === 'Function') { +// return 'function'; +// } +// else { +// return type; +// } +// } diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring__TagTypeInfo.js b/packages/megadoc-plugin-js/lib/Parser/Docstring__TagTypeInfo.js new file mode 100644 index 00000000..5e45d685 --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/Docstring__TagTypeInfo.js @@ -0,0 +1,73 @@ +var parseTypes = require('./Docstring__parseTypeString'); + +function TypeInfo(commentNode) { + var description = commentNode.description; + var name = (commentNode.name || '').trim(); + var typeInfo = {}; + + if (name.length > 0) { + typeInfo.name = name; + } + + var hasType = commentNode.type && commentNode.type.length > 0; + + // Example tags without a type but with a name: + // + // /** + // * @example Do something. + // */ + // + // Gives us: + // + // { + // name: 'Do', + // description: 'something' + // } + // + // But we want: + // + // { + // description: 'Do something.' + // } + // + if (commentNode.tag === 'example' && !hasType) { + if (commentNode.name.trim().length > 0) { + console.warn("Invalid @example tag: this tag does not support a name."); + } + // description = commentNode.source.split('\n').slice(1).join('\n') + // .replace(/^\n+/, '\n') + // .replace(/\n+$/, '\n') + ; + // description = ( + // commentNode.name + + // (commentNode.name.match(/\s$/) ? '' : ' ') + + // commentNode.description + // ); + + delete typeInfo.name; + } + + if (description && description.length > 0) { + typeInfo.description = description; + } + + if (hasType) { + typeInfo.type = parseTypes(commentNode.type); + + if (!typeInfo.type || !typeInfo.type.name) { + delete typeInfo.type; + } + } + + if (commentNode.optional) { + typeInfo.isOptional = true; + + if (commentNode.default) { + typeInfo.defaultValue = commentNode.default; + } + } + + return typeInfo; +} + +module.exports = TypeInfo; diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring__collectDescription.js b/packages/megadoc-plugin-js/lib/Parser/Docstring__collectDescription.js new file mode 100644 index 00000000..14125d3b --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/Docstring__collectDescription.js @@ -0,0 +1,12 @@ +var NO_DESCRIPTION_TAGS = require('./constants').NO_DESCRIPTION_TAGS; + +module.exports = function(commentNode, id, tags) { + var description = String(commentNode.description || '').trim(); + + return tags + .filter(function(x) { return x.type in NO_DESCRIPTION_TAGS }) + .reduce(function(buffer, x) { + return buffer + x.string.trim(); + }, description) + ; +}; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring__extractIdInfo.js b/packages/megadoc-plugin-js/lib/Parser/Docstring__extractIdInfo.js new file mode 100644 index 00000000..ebf5f5a2 --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/Docstring__extractIdInfo.js @@ -0,0 +1,57 @@ +var K = require('./constants'); +var findWhere = require('lodash').findWhere; + +function extractIdInfo(tags) { + var name, id; + var namespace = getNameFromTag(tags, 'namespace'); + var fqid = id = getNameFromTag(tags, 'module'); + + // check for inline namespaces found in a module id string + if (fqid && fqid.indexOf(K.NAMESPACE_SEP) > -1) { + if (namespace) { + console.warn( + "Document '%s' already has a namespace specified using the " + + "@namespace tag, it should not have a namespace in its name as well.", + fqid + ); + } + + namespace = fqid + .split(K.NAMESPACE_SEP) + .slice(0, -1) + .join(K.NAMESPACE_SEP) + ; + } + + // try getting the ID from other tags, but these do not allow implicit + // namespacing: + if (!id) { + id = ( + getNameFromTag(tags, 'name') || + getNameFromTag(tags, 'method') || + getNameFromTag(tags, 'property') + ); + } + + if (id && namespace && id.indexOf(namespace + K.NAMESPACE_SEP) === 0) { + name = id.substr(namespace.length + 1); + } + else { + name = id; + } + + return { + name: name, + namespace: namespace + }; +} + +module.exports = extractIdInfo; + +function getNameFromTag(tags, tagType) { + var tag = findWhere(tags, { type: tagType }); + + if (tag && tag.typeInfo.name) { + return tag.typeInfo.name; + } +} \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Docstring__parseTypeString.js b/packages/megadoc-plugin-js/lib/Parser/Docstring__parseTypeString.js new file mode 100644 index 00000000..89fba436 --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/Docstring__parseTypeString.js @@ -0,0 +1,84 @@ +var catharsis = require('catharsis'); +var K = require('./constants'); +var CATHARSIS_OPTIONS = { jsdoc: true, useCache: false }; + +module.exports = function parseTypeString(typeString) { + return reduceCatharsis(catharsis.parse(typeString, CATHARSIS_OPTIONS)); +}; + +/** + * @typedef {TagTypeInfo} + * @property {String} name + * @property {?Boolean} nullable + * @property {?Boolean} optional + * @property {?Boolean} repeatable + * @property {?Array.} elements + * @property {?Array.} params + * @property {?TagTypeInfo} returnValue + * @property {?TagTypeInfo} key + * @property {?TagTypeInfo} value + */ +function reduceCatharsis(typeInfo) { + var info = {}; + + if (typeInfo.type === 'NameExpression') { + info.name = typeInfo.name; + + if (typeInfo.nullable) { + info.nullable = true; + } + else if (typeInfo.nullable === false) { + info.nullable = false; + } + + if (typeInfo.optional) { + info.optional = true; + } + else if (typeInfo.optional === false) { + info.optional = false; + } + + if (typeInfo.repeatable) { + info.repeatable = true; + } + } + else if (typeInfo.type === 'TypeApplication') { + info = reduceCatharsis(typeInfo.expression); + + if (typeInfo.applications) { + info.elements = typeInfo.applications.map(reduceCatharsis); + } + } + else if (typeInfo.type === 'TypeUnion') { + info.name = K.TYPE_UNION; + info.elements = typeInfo.elements.map(reduceCatharsis); + } + else if (typeInfo.type === 'FunctionType') { + info.name = K.TYPE_FUNCTION; + + if (typeInfo.params && typeInfo.params.length) { + info.params = typeInfo.params.map(reduceCatharsis); + } + + if (typeInfo.result) { + info.returnType = reduceCatharsis(typeInfo.result); + } + } + else if (typeInfo.type === 'AllLiteral') { + info.name = K.TYPE_ALL_LITERAL; + } + else if (typeInfo.type === 'UnknownLiteral') { + info.name = K.TYPE_UNKNOWN_LITERAL; + } + else if (typeInfo.type === 'RecordType') { + info.name = K.TYPE_OBJECT; + info.elements = typeInfo.fields.map(reduceCatharsis); + } + else if (typeInfo.type === 'FieldType') { + info.name = K.TYPE_OBJECT_PROPERTY; + info.key = reduceCatharsis(typeInfo.key); + info.value = reduceCatharsis(typeInfo.value); + } + + return info; +} \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer.js b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer.js index b9405269..1be948eb 100644 --- a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer.js +++ b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer.js @@ -1,5 +1,5 @@ var runAllSync = require('../utils/runAllSync'); -var analyzeNode = require('./NodeAnalyzer/analyzeNode'); +var analyzeNode = require('./NodeAnalyzer__analyzeNode'); var t = require('babel-types'); var NodeAnalyzer = exports; @@ -7,7 +7,9 @@ var NodeAnalyzer = exports; NodeAnalyzer.analyze = function(node, path, filePath, config) { var nodeInfo = analyzeNode(node, path, filePath, config); - runAllSync(config.nodeAnalyzers || [], [ t, node, path, nodeInfo ]); + if (config.nodeAnalyzers && config.nodeAnalyzers.length) { + runAllSync(config.nodeAnalyzers, [ t, node, path, nodeInfo ]); + } return nodeInfo; }; \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/generateContext.js b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/generateContext.js deleted file mode 100644 index 3a795425..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/generateContext.js +++ /dev/null @@ -1,77 +0,0 @@ -var K = require('../constants'); -var t = require('babel-types'); - -module.exports = function generateContext(contextNode) { - var ctx; - - if ( - t.isFunctionExpression(contextNode) || - t.isFunctionDeclaration(contextNode) || - t.isObjectMethod(contextNode) - ) { - ctx = parseFunctionExpression(contextNode); - } - else if (t.isVariableDeclarator(contextNode)) { - return generateContext(contextNode.init); - } - else if (t.isVariableDeclaration(contextNode)) { - return generateContext(contextNode.declarations[0]); - } - else if (t.isLiteral(contextNode)) { - ctx = parseLiteral(contextNode); - } - else if (t.isObjectExpression(contextNode)) { - ctx = parseObjectExpression(contextNode); - } - else if (t.isArrayExpression(contextNode)) { - ctx = parseArrayExpression(contextNode); - } - else if (t.isClassDeclaration(contextNode)) { - ctx = { type: K.TYPE_CLASS }; // TODO - } - - return ctx; -}; - -function castExpressionValue(expr) { - if (t.isArrayExpression(expr)) { - return expr.elements.map(castExpressionValue); - } - else if (t.isLiteral(expr)) { - return expr.value; - } -} - -function parseFunctionExpression(node) { - var defaults = node.defaults || []; - - return { - type: K.TYPE_FUNCTION, - params: node.params.map(function(param, i) { - return { - name: param.name, - defaultValue: castExpressionValue(defaults[i]) - } - }) - }; -} - -function parseLiteral(node) { - return { - type: K.TYPE_LITERAL, - value: castExpressionValue(node) - }; -} - -function parseObjectExpression(/*expr*/) { - return { - type: K.TYPE_OBJECT, - properties: [] // TODO - } -} -function parseArrayExpression(/*expr*/) { - return { - type: K.TYPE_ARRAY, - values: [] // TODO - } -} diff --git a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/NodeInfo.js b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__NodeInfo.js similarity index 95% rename from packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/NodeInfo.js rename to packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__NodeInfo.js index 309c1538..8f2b5b93 100644 --- a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/NodeInfo.js +++ b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__NodeInfo.js @@ -1,6 +1,6 @@ -var K = require('../constants'); +var K = require('./constants'); var assign = require('lodash').assign; -var getLocation = require('../Utils').getLocation; +var getLocation = require('./ASTUtils').getLocation; /** * @param {recast.ast} node @@ -27,7 +27,6 @@ function NodeInfo(node, filePath) { NodeInfo.prototype.toJSON = function() { return { id: this.id, - ctx: this.ctx, receiver: this.receiver, loc: this.loc, }; diff --git a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/analyzeNode.js b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__analyzeNode.js similarity index 88% rename from packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/analyzeNode.js rename to packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__analyzeNode.js index f840e8ac..0fdaa85a 100644 --- a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer/analyzeNode.js +++ b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__analyzeNode.js @@ -1,7 +1,7 @@ -var Utils = require('../Utils'); -var NodeInfo = require('./NodeInfo'); -var K = require('../constants'); -var generateContext = require('./generateContext'); +var ASTUtils = require('./ASTUtils'); +var NodeInfo = require('./NodeAnalyzer__NodeInfo'); +var K = require('./constants'); +var generateContext = require('./NodeAnalyzer__generateContext'); var debuglog = require('megadoc/lib/Logger')('megadoc').info; var t = require('babel-types'); @@ -36,6 +36,16 @@ function analyzeNode(node, path, filePath, config) { info.id = node.id.name; info.$contextNode = node; } + // TODO: when do we reach this? it seems FunctionExpression nodes are only + // really valid as an argument to a ReturnStatement, or as an init for a + // VariableDeclaration + // + // Maybe something like this? + // + // ( + // /** Hello */ + // function() {} + // )(this) else if (t.isFunctionExpression(node)) { info.id = node.id.name; info.$contextNode = node; @@ -62,10 +72,6 @@ function analyzeNode(node, path, filePath, config) { if (info.id) { info.addContextInfo(generateContext(info.$contextNode)); - - if (info.isExports() || info.isDestructuredObject()) { - info.addContextInfo({ type: K.TYPE_OBJECT }); - } } return info; @@ -74,6 +80,7 @@ function analyzeNode(node, path, filePath, config) { function analyzeVariableDeclaration(node, path, info) { var decl = node.declarations[0]; + // var Something = 'a'; // var Something = SomeFunc(); // var Something = {}; @@ -115,8 +122,9 @@ function analyzeVariableDeclaration(node, path, info) { // * Something. // */ // var SomeModule = exports; - if (Utils.isExports(node)) { + if (ASTUtils.isExports(node)) { info.markAsExports(); + info.addContextInfo({ type: K.TYPE_OBJECT }); } } @@ -133,7 +141,6 @@ function analyzeExpressionStatement(node, path, info, filePath, config) { // // SomeModule.foo = 'a'; if (t.isIdentifier(lhs.object) && t.isIdentifier(lhs.property)) { - info.type = 'property'; info.id = lhs.property.name; info.receiver = lhs.object.name; } @@ -163,14 +170,14 @@ function analyzeExpressionStatement(node, path, info, filePath, config) { // CommonJS special scenario: a named function assigned to `module.exports` // // module.exports = function namedFunction() {}; - if (Utils.isModuleExports(expr) && t.isIdentifier(rhs.id)) { + if (ASTUtils.isModuleExports(expr) && t.isIdentifier(rhs.id)) { info.id = rhs.id.name; } // Unnamed CommonJS module.exports: // // module.exports = function() {}; - else if (Utils.isModuleExports(expr) && config.inferModuleIdFromFileName) { - info.id = Utils.getVariableNameFromFilePath(filePath); + else if (ASTUtils.isModuleExports(expr) && config.inferModuleIdFromFileName) { + info.id = ASTUtils.getVariableNameFromFilePath(filePath); } // A function assigned to some object property: // @@ -211,7 +218,7 @@ function analyzeExpressionStatement(node, path, info, filePath, config) { // SomeModule.prototype.someFunction = function() {}; else if (t.isMemberExpression(lhs.object) && t.isIdentifier(lhs.property)) { info.id = lhs.property.name; - info.receiver = Utils.flattenNodePath(lhs.object); + info.receiver = ASTUtils.flattenNodePath(lhs.object); } } else if (t.isMemberExpression(lhs)) { @@ -233,7 +240,7 @@ function analyzeExpressionStatement(node, path, info, filePath, config) { console.info("Unrecognized ExpressionStatement '%s' => '%s' (Source: %s).", lhs ? lhs.type : expr.type, rhs ? rhs.type : expr.type, - Utils.dumpLocation(node, filePath) + ASTUtils.dumpLocation(node, filePath) ); } } @@ -247,7 +254,7 @@ function analyzeProperty(node, path, info) { // someFunc: fn // <-- // }; if (t.isIdentifier(node.key) && t.isIdentifier(node.value)) { - var identifierPath = Utils.findIdentifierInScope(node.value.name, path); + var identifierPath = ASTUtils.findIdentifierInScope(node.value.name, path); if (identifierPath) { info.id = node.key.name; info.$contextNode = identifierPath.parentPath.node; diff --git a/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__generateContext.js b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__generateContext.js new file mode 100644 index 00000000..7ab5278a --- /dev/null +++ b/packages/megadoc-plugin-js/lib/Parser/NodeAnalyzer__generateContext.js @@ -0,0 +1,99 @@ +var K = require('./constants'); +var t = require('babel-types'); + +module.exports = parseNode; + +function parseNode(contextNode) { + if ( + t.isFunctionExpression(contextNode) || + t.isFunctionDeclaration(contextNode) || + t.isObjectMethod(contextNode) + ) { + return parseFunctionExpression(contextNode); + } + else if (t.isVariableDeclarator(contextNode)) { + return parseNode(contextNode.init); + } + else if (t.isVariableDeclaration(contextNode)) { + return parseNode(contextNode.declarations[0]); + } + else if (t.isLiteral(contextNode)) { + return parseLiteral(contextNode); + } + else if (t.isObjectExpression(contextNode)) { + return parseObjectExpression(contextNode); + } + else if (t.isArrayExpression(contextNode)) { + return parseArrayExpression(contextNode); + } + else if (t.isClassDeclaration(contextNode)) { + return { type: K.TYPE_CLASS }; // TODO + } + else if (t.isObjectProperty(contextNode) && t.isIdentifier(contextNode.key)) { + return { + type: K.OBJECT_PROPERTY, + key: contextNode.key.name, + value: parseNode(contextNode.value), + } + } +}; + +function parseFunctionExpression(node) { + return { + type: K.TYPE_FUNCTION, + params: node.params.map(function(param) { + return parseFunctionParameter(param); + }) + }; +} + +function parseLiteral(node) { + return { + type: K.TYPE_LITERAL, + value: node.value + }; +} + +function parseObjectExpression(expr) { + return { + type: K.TYPE_OBJECT, + properties: expr.properties.map(parseNode) + } +} + +function parseArrayExpression(expr) { + return { + type: K.TYPE_ARRAY, + elements: expr.elements.map(parseNode) + } +} + +// ESprima seems to annotate FunctionDeclaration nodes with a "defaults" property +// that is an array of Literal nodes that map to parameter default values, +// however, babel doesn't seem to do it this way; instead, it's using an +// AssignmentPattern to reflect the defaults. So for: +// +// function x(a, b = 5) {} +// +// We get something like: +// +// [FunctionDeclaration | +// params: [Array | +// [Node.Identifier], +// [Node.AssignmentPattern| +// left: Node, +// right: Node +// ] +// ] +// ] +function parseFunctionParameter(node) { + if (t.isIdentifier(node)) { + return { name: node.name }; + } + else if (t.isAssignmentPattern(node)) { + return { + name: node.left.name, + defaultValue: node.right.value + }; + } +} \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/PostProcessor.js b/packages/megadoc-plugin-js/lib/Parser/PostProcessor.js deleted file mode 100644 index 4e699f5f..00000000 --- a/packages/megadoc-plugin-js/lib/Parser/PostProcessor.js +++ /dev/null @@ -1,213 +0,0 @@ -var path = require('path'); -var K = require('./constants'); -var DocClassifier = require('./DocClassifier'); -var Utils = require('./Utils'); -var debuglog = require('megadoc/lib/Logger')('megadoc').info; - -exports.run = function(registry, config) { - var docs = registry.docs; - var initialCount = docs.length; - - debuglog('Post-processing %d docs.', docs.length); - - if (config.namespaceDirMap) { - var namespaceDirMap = Object.keys(config.namespaceDirMap); - - docs.filter(DocClassifier.isModule).forEach(function(doc) { - var docstring = doc.docstring; - var filePath = doc.filePath; - - if (!docstring.namespace) { - var dirName = path.dirname(filePath); - - namespaceDirMap.some(function(pattern) { - if (dirName.match(pattern)) { - // gotta <3 mutable state.. :) - docstring.overrideNamespace(config.namespaceDirMap[pattern]); - doc.id = doc.generateId(); - doc.name = doc.generateName(); - - return true; - } - }); - } - }); - } - - docs.filter(DocClassifier.isEntity).forEach(function(doc) { - resolveReceiver(registry, doc); - }); - - removeBadDocs(registry); - - docs.filter(DocClassifier.isEntity).forEach(function(doc) { - identifyScope(registry, doc); - }); - - // Remove @lends docs, we don't need them. - docs - .filter(function(doc) { return doc.docstring.doesLend(); }) - .forEach(function(doc) { - registry.remove(doc); - }) - ; - - warnAboutUnknownContexts(registry); - - debuglog('Post-processing complete. %d/%d docs remain.', docs.length, initialCount); -}; - -function identifyScope(registry, doc) { - var nodeInfo = doc.nodeInfo; - - if (nodeInfo.isInstanceEntity()) { - nodeInfo.ctx.scope = K.SCOPE_INSTANCE; - } - else if (nodeInfo.isPrototypeEntity()) { - nodeInfo.ctx.scope = K.SCOPE_PROTOTYPE; - } - else if ( - Utils.isFactoryModuleReturnEntity( - doc.$path.node, - doc.$path, - registry - ) - ) { - nodeInfo.ctx.scope = K.SCOPE_FACTORY_EXPORTS; - } -} - -function resolveReceiver(registry, doc) { - var receiver = doc.nodeInfo.receiver; - var actualReceiver; - - // @memberOf support - // - // Note that this might still need alias-resolving. - if (doc.docstring.hasMemberOf()) { - receiver = doc.docstring.getExplicitReceiver(); - - if (receiver.match(/(.*)\.prototype$/)) { - doc.overrideReceiver(RegExp.$1); - - doc.nodeInfo.addContextInfo({ - scope: K.SCOPE_PROTOTYPE - }); - - // @method or @type ? we use that instead - if (doc.docstring.hasTypeOverride()) { - doc.nodeInfo.addContextInfo({ - type: doc.docstring.getTypeOverride() - }); - } - - // For something like: - // - // Object.defineProperty(someObj, 'someProp', { - // /** @memberOf someObj */ - // get: function() { - // } - // }) - // - // we don't want the context type to be function, because it isn't - else if (doc.docstring.hasTag('property')) { - doc.nodeInfo.addContextInfo({ - type: K.TYPE_UNKNOWN - }); - } - } - else { - doc.overrideReceiver(receiver); - } - } - - // Resolve @lends - var lendEntry = ( - registry.findAliasedLendTarget(doc.$path, receiver) || - registry.findClosestLend(doc.$path) - ); - - // TODO: this needs a bit of rethinking really - if (lendEntry) { - actualReceiver = registry.get(lendEntry.receiver); - - if (actualReceiver) { - doc.overrideReceiver(actualReceiver.id); - - if (lendEntry.scope) { - doc.nodeInfo.ctx.scope = lendEntry.scope; - } - } - } - else { - // var Something = exports; - // ^^^^^^^^^ - // - // // ... - // - // /** yep */ - // exports.something = function() {} - // ^^^^^^^ - if (receiver === 'exports') { - actualReceiver = registry.findExportedModule(doc.filePath); - - if (actualReceiver) { - doc.overrideReceiver(actualReceiver.id); - } - } - else { - // TODO: this too - actualReceiver = ( - registry.findAliasedReceiver(doc.$path, receiver) || - registry.findClosestModule(doc.$path) - ); - - if (actualReceiver) { - doc.overrideReceiver(actualReceiver); - } - } - } -} - -function removeBadDocs(registry) { - var badDocs = []; - - registry.docs - .filter(function(doc) { - return ( - !doc.isModule() && - (!doc.hasReceiver() || !registry.get(doc.getReceiver())) - ); - }) - .forEach(function(doc) { - console.warn( - 'Unable to map "%s" to any module, it will be discarded. (Source: %s)', - doc.id, - doc.nodeInfo.fileLoc - ); - - badDocs.push(doc); - }) - ; - - badDocs.forEach(function(doc) { - registry.remove(doc); - }); - - badDocs = null; -} - -function warnAboutUnknownContexts(registry) { - registry.docs.forEach(function(doc) { - var ctx = doc.nodeInfo.ctx; - - if (!ctx.type || ctx.type === K.TYPE_UNKNOWN) { - debuglog( - 'Entity "%s" has no context. This probably means megadoc does not know ' + - 'how to handle it yet. (Source: %s)', - doc.id, - doc.nodeInfo.fileLoc - ); - } - }); -} \ No newline at end of file diff --git a/packages/megadoc-plugin-js/lib/Parser/Registry.js b/packages/megadoc-plugin-js/lib/Parser/Registry.js index 974e540d..d62d4472 100644 --- a/packages/megadoc-plugin-js/lib/Parser/Registry.js +++ b/packages/megadoc-plugin-js/lib/Parser/Registry.js @@ -1,6 +1,6 @@ var WeakMap = require('weakmap'); -var Utils = require('./Utils'); -var K = require('./constants'); +var ASTUtils = require('./ASTUtils'); +var DocUtils = require('./DocUtils'); var assert = require('assert'); /** @@ -28,7 +28,7 @@ Rpt.addModuleDoc = function(doc, path, filePath) { if (this.get(doc.id)) { console.warn('You are attempting to overwrite an existing doc entry! This is very bad.', doc.id, - filePath + ':' + Utils.getLocation(path.node).start.line + filePath + ':' + ASTUtils.getLocation(path.node).start.line ); } @@ -52,19 +52,11 @@ Rpt.addEntityDoc = function(doc, path) { }; Rpt.trackLend = function(lendsTo, path) { - var targetPath = Utils.findNearestPathWithComments(path); + var targetPath = ASTUtils.findNearestPathWithComments(path); - if (lendsTo.match(/(.*)\.prototype$/)) { - this.lends.set(targetPath, { - receiver: RegExp.$1, - scope: K.SCOPE_PROTOTYPE - }); - } - else { - this.lends.set(targetPath, { - receiver: lendsTo - }); - } + this.lends.set(targetPath, { + receiver: lendsTo + }); }; Rpt.remove = function(doc) { @@ -76,9 +68,20 @@ Rpt.remove = function(doc) { Rpt.get = function(id, filePath) { return this.docs.filter(function(doc) { - return (doc.id === id || doc.name === id) && ( - filePath ? doc.filePath === filePath : true + if (filePath && doc.filePath !== filePath) { + return false; + } + + return ( + DocUtils.getIdOf(doc) === id || + doc.docstring.hasAlias(id) ); + })[0] || this.docs.filter(function(doc) { // TODO: optimize + if (filePath && doc.filePath !== filePath) { + return false; + } + + return DocUtils.getNameOf(doc) === id; })[0]; }; @@ -93,7 +96,7 @@ Rpt.get = function(id, filePath) { Rpt.findClosestModule = function(path) { var receiverDoc = this.findEnclosingDoc(path, this.docPaths); - if (receiverDoc && receiverDoc.isModule()) { + if (receiverDoc && DocUtils.isModule(receiverDoc)) { return receiverDoc.id; } }; @@ -135,10 +138,10 @@ Rpt.findClosestLend = function(path) { * @return {Object} lendEntry */ Rpt.findAliasedLendTarget = function(path, alias) { - var identifierPath = Utils.findIdentifierInScope(alias, path); + var identifierPath = ASTUtils.findIdentifierInScope(alias, path); if (identifierPath) { - var targetPath = Utils.findNearestPathWithComments(identifierPath); + var targetPath = ASTUtils.findNearestPathWithComments(identifierPath); if (targetPath) { return this.lends.get(targetPath); @@ -150,27 +153,25 @@ Rpt.findAliasedLendTarget = function(path, alias) { * @return {String} * The actual/resolved receiver. */ -Rpt.findAliasedReceiver = function(path, alias) { - var identifierPath = Utils.findIdentifierInScope(alias, path); +Rpt.findAliasedReceiver = function(alias) { + var docs = this.docs.filter(function(doc) { + return doc.docstring.hasAlias(alias); + }); - if (identifierPath) { - var receiverPath = Utils.findNearestPathWithComments(identifierPath); - - if (receiverPath) { - var receiverDoc = this.getModuleDocAtPath(receiverPath); - if (receiverDoc) { - if (receiverDoc.id !== alias) { - return receiverDoc.id; - } - } - } + if (docs.length > 1) { + console.warn("Multiple documents are using the same alias '%s': %s", + alias, + JSON.stringify(docs.map(function(x) { return x.id; })) + ); } + + return docs[0] && docs[0].id; }; Rpt.findEnclosingDoc = function(startingPath, map) { var doc; - Utils.findAncestorPath(startingPath, function(path) { + ASTUtils.findAncestorPath(startingPath, function(path) { return Boolean(doc = map.get(path)); }); diff --git a/packages/megadoc-plugin-js/lib/Parser/TestUtils.js b/packages/megadoc-plugin-js/lib/Parser/TestUtils.js index 8172c528..f8fd1543 100644 --- a/packages/megadoc-plugin-js/lib/Parser/TestUtils.js +++ b/packages/megadoc-plugin-js/lib/Parser/TestUtils.js @@ -1,16 +1,29 @@ var multiline = require('multiline-slash'); var ASTParser = require('./'); +exports.parseNode = function(strGenerator, config, filePath) { + var parser = new ASTParser(); + var body = typeof strGenerator === 'function' ? multiline(strGenerator) : strGenerator; + + config = config || {}; + config.alias = config.alias || {}; + config.strict = true; + + parser.parseString(body, config, filePath || '__test__'); + + return parser.registry.docs; +} + function parseInline(strGenerator, config, filePath) { var parser = new ASTParser(); - var body = multiline(strGenerator); + var body = typeof strGenerator === 'function' ? multiline(strGenerator) : strGenerator; var database; config = config || {}; config.alias = config.alias || {}; + config.strict = true; parser.parseString(body, config, filePath || '__test__'); - parser.seal(config); database = parser.toJSON(); @@ -31,10 +44,9 @@ function parseFiles(filePaths, config, commonPrefix) { config.alias = config.alias || {}; filePaths.forEach(function(filePath) { - parser.parseFile(filePath, config || {}, commonPrefix); + parser.parseFile(filePath, config || {}, commonPrefix || __dirname); }); - parser.seal(config); database = parser.toJSON(); if (config.postProcessors) { diff --git a/packages/megadoc-plugin-js/lib/Parser/__tests__/Docstring.test.js b/packages/megadoc-plugin-js/lib/Parser/__tests__/Docstring.test.js index dcc99b78..df146916 100644 --- a/packages/megadoc-plugin-js/lib/Parser/__tests__/Docstring.test.js +++ b/packages/megadoc-plugin-js/lib/Parser/__tests__/Docstring.test.js @@ -9,9 +9,6 @@ var parse = function(strGenerator, customTags, filePath) { }; describe('CJS::Parser::Docstring', function() { -}); - -describe('CJS::Parser::Docstring::Tag', function() { it('parses inline "description"', function() { var docstring = parse(function() {; // /** @@ -25,16 +22,6 @@ describe('CJS::Parser::Docstring::Tag', function() { }); describe('@module', function() { - it('parses the module path', function() { - var docstring = parse(function() {; - // /** - // * @module Dragon - // */ - }); - - assert.equal(docstring.id, 'Dragon'); - }); - it('parses "description" and omits the module path from it', function() { var docstring = parse(function() {; // /** @@ -66,355 +53,8 @@ describe('CJS::Parser::Docstring::Tag', function() { // */ }); + assert.equal(docstring.name, 'Dragon'); assert.equal(docstring.namespace, 'Hairy'); }); }); - - describe('@property', function() { - it('parses a single type', function() { - var docstring = parse(function() {; - // /** - // * @property {String} - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.deepEqual(docstring.tags[0].typeInfo.types, [ 'String' ]); - }); - - it('parses multiple types', function() { - var docstring = parse(function() {; - // /** - // * @property {String|Object} - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.deepEqual(docstring.tags[0].typeInfo.types, [ 'String', 'Object' ]); - }); - - it('parses the name', function() { - var docstring = parse(function() {; - // /** - // * @property {String} foo - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.name, 'foo'); - }); - - it('parses an optional property', function() { - var docstring = parse(function() {; - // /** - // * @property {String} [foo] - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.isOptional, true); - assert.equal(docstring.tags[0].typeInfo.name, 'foo'); - }); - - it('parses the default value', function() { - var docstring = parse(function() {; - // /** - // * @property {String} [foo='bar'] - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.defaultValue, "'bar'"); - }); - - it('parses the description', function() { - var docstring = parse(function() {; - // /** - // * @property {String} foo - // * Something. - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.description, 'Something.'); - }); - - it('strips leading whitespace from description', function() { - var docstring = parse(function() {; - // /** - // * @property {String} foo - // * This - // * is - // * a - // * multiline - // * description. - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.description, - 'This\nis\na\nmultiline\ndescription.' - ); - }); - }); - - describe('@return', function() { - it('parses a single type', function() { - var docstring = parse(function() {; - // /** - // * @return {String} - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.deepEqual(docstring.tags[0].typeInfo.types, [ 'String' ]); - }); - - it('parses multiple types', function() { - var docstring = parse(function() {; - // /** - // * @return {String|Object} - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.deepEqual(docstring.tags[0].typeInfo.types, [ 'String', 'Object' ]); - }); - - it('parses the name', function() { - var docstring = parse(function() {; - // /** - // * @return {String} foo - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.name, 'foo'); - assert.equal(docstring.tags[0].typeInfo.description, undefined); - }); - - it('parses the description', function() { - var docstring = parse(function() {; - // /** - // * @return {String} foo - // * Something. - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.name, 'foo'); - assert.equal(docstring.tags[0].typeInfo.description, 'Something.'); - }); - - it('parses the description without a name', function() { - var docstring = parse(function() {; - // /** - // * @return {String} - // * Something. - // */ - }); - - assert.equal(docstring.tags.length, 1); - assert.equal(docstring.tags[0].typeInfo.name, undefined); - assert.equal(docstring.tags[0].typeInfo.description, 'Something.'); - }); - }); - - describe('custom tag: @live_example', function() { - var customTags = { - live_example: { - withTypeInfo: true - } - }; - - it('parses the example type', function() { - var docstring = parse(function() {; - // /** - // * @live_example {jsx} - // */ - }, { customTags: customTags }); - - assert.equal(docstring.tags.length, 1); - assert.deepEqual(docstring.tags[0].typeInfo.types, [ 'jsx' ]); - }); - - it('accepts a custom processor', function(done) { - parse(function() {; - // /** - // * @live_example {jsx} - // */ - }, { - customTags: { - live_example: { - withTypeInfo: true, - process: function(tag) { - assert.ok(tag); - done(); - } - } - } - }); - }); - - it('accepts custom attributes', function(done) { - parse(function() {; - // /** - // * @live_example {jsx} - // */ - }, { - customTags: { - live_example: { - withTypeInfo: true, - attributes: [ 'width' ], - process: function(tag) { - assert.doesNotThrow(function() { - tag.setCustomAttribute('width', 240); - }); - - done(); - } - } - } - }); - }); - - it('whines if attempting to write to an unspecified attribute', function(done) { - parse(function() {; - // /** - // * @live_example {jsx} - // * - // *