Repository: marijnh/getdocs Branch: master Commit: 7b8a575b3f1f Files: 49 Total size: 54.7 KB Directory structure: gitextract__rpuop8z/ ├── .gitignore ├── LICENSE ├── README.md ├── bin/ │ └── getdocs.js ├── package.json ├── src/ │ ├── doccomments.js │ ├── index.js │ └── parsetype.js └── test/ ├── class_addmethod.js ├── class_addmethod.json ├── class_ctor.js ├── class_ctor.json ├── class_expr.js ├── class_expr.json ├── class_simple.js ├── class_simple.json ├── class_static.js ├── class_static.json ├── class_this.js ├── class_this.json ├── defaultarg.js ├── defaultarg.json ├── diffargname.js ├── diffargname.json ├── exported.js ├── exported.json ├── function.js ├── function.json ├── functionsub.js ├── functionsub.json ├── infer.js ├── infer.json ├── literal.js ├── literal.json ├── namedprop.js ├── namedprop.json ├── obj.js ├── obj.json ├── phantom.js ├── phantom.json ├── restarg.js ├── restarg.json ├── run.js ├── subcomment.js ├── subcomment.json ├── tags.js ├── tags.json ├── union.js └── union.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /node_modules .tern-* ================================================ FILE: LICENSE ================================================ Copyright (C) 2013 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Getdocs Getdocs is like JSDoc or documentation.js, running over ES6 code to extract information and inline documentation in order to generate docs, but without all the @s. It takes source files and outputs JSON. For example, if you have this file, `foo.js`: ```javascript // :: (number, number) → number // Add two numbers export function plus(a, b = 2) { return a + b } ``` You can say `getdocs foo.js` to get this JSON: ```json { "plus": { "type": "Function", "params": [ { "type": "number", "name": "a" }, { "type": "number", "default": "2", "optional": true, "name": "b" } ], "returns": { "type": "number" }, "description": "Add two numbers", "exported": true } } ``` The idea is to then feed this into a system (can be a simple set of templates) that massages it into actual human-readable documention files. A getdocs doc comment starts with a double colon, optionally prefixed with a name (`foo::`) and followed by a type. It can be either a block comment or a continuous sequence of line comments. When you don't want to specify a type, for example because the type can be inferred from the code (as with a class declaration), you can write a single dash after the colons, instead of a type. When no name is given, such a doc comment applies to the next program element after it. That element should be something with a name, like a variable, function, or class declaration, or an assignment that can be statically resolved. The documented items found in the files passed to getdocs will be returned as part of a big JSON object. Nesting is only applied for class and object properties, where the properties are moved under the `properties` object of the item they are part of. _A single namespace is assumed for the documented identifiers in the group of files._ Inside a doc comment, properties of the thing being defined can be added by writing nested, indented doc comments. For example: ``` // Plugin:: interface // // Objects conforming to the plugin interface can be plugged into a // Foo // // mount:: (Foo) → bool // Mount the plugin in this Foo. The return value indicates whether // the mount succeeded. // // unmount:: (Foo) // Unmount the plugin from a Foo. ``` Further nesting below such a property (by adding more indentation) is supported. ## Type syntax A type can be: * A JavaScript identifier, optionally followed by any number of properties, which are a dot character followed by a JavaScript identifier. A type name can be followed by a list of type parameters, between angle brackets, as in `Object` (an object whose properties hold string values). * An array type, which is a type wrapped in `[` and `]`. `[x]` is equivalent to `Array`. * A function type, which is written as a parenthesized list of argument types. Each argument type may optionally be prefixed with an argument name, which is an identifier followed by a colon. When an argument is prefixed by the string `...`, it is marked as a `rest` argument. After the closing parenthesis, an optional return type may appear after an arrow, written either `→` or `->`. * A nullable type, written as a question mark followed by a type. * An unspecified or “any” type, written as an asterisk `*`. * An object type, written as a list of properties wrapped in `{` and `}` braces. Each property must start with an identifier, followed by a colon, followed by a type. * A string literal, enclosed by double quotes, or a number literal. * A type followed by `extends` followed by another type, to indicate a sub-type. Here are some examples of types: * `Math.pow`: `(base: number, exponent: number) → number` * `Element.insertBefore`: `(newNode: Node, before: ?Node) → Node` * `console.log`: `(...data: *)` * A pair of coordinates: `{x: number, y: number}` * An array of strings: `[string]` * An array of numbers or a string: `union<[number], string>` (what the name `union` means isn't something getdocs is aware of, but you could use it for union types, and maybe render it as `[number] | string` in your output). ## Tags It is possible to add tags to a documented item. These are words prefixed with a `#` character, appearing at the start of the comment — that is, immediately after the type. A tag like `#deprecated`, for example, will result in a `$deprecated: "true"` property on the given item. The property is named by prepending the tag's name with a dollar sign. You can give tags an explicit value other than `"true"` by writing an `=` character followed either by a word (a sequence of characters without whitespace) or a quoted JavaScript-style string. For example `#chapter=selection` or `#added="2.1.0"`. The `#static` tag can be used to indicate that a given class member is static (which is only necessary for doc comments that aren't tied to a syntactic element in the code). ## Output JSON The returned object maps item names to item descriptions. The following properties can appear in a description for a documented item: * **description**: The doc comment for the item. * **loc**: A `{line, column, file}` object pointing at the start of the item. * **exported**: Set if the item is exported using ES6 module syntax. * **constructor**: For classes with a documented constructor, this points at the constructor function. * **extends**: Holds the type of the supertype of a class or other sub-type. * **staticProperties**: For classes, this holds properties and methods that appear directly on the constructor. In addition, they may have these properties, which can also appear on nested types: * **type**: The name of the type. Instances of classes should use the (capitalized) class name. Builtin types will have names like `Array` or `Function`. Getdocs does not prescribe a naming of primitive types, but for consistency I recommend you use `number`, `string`, and `bool`. * **properties**: An object mapping property names to types. * **params**: For function types, this holds an array of parameter types. Parameter types can have these additional properties: * **name**: The name of the parameter. * **rest**: Set when this is a rest parameter. * **default**: The default value of the parameter (as a raw source string). * **returns**: For function types, this holds the type that is returned. * **typeParams**: For array types or named types with parameters (angle bracket syntax), this holds an array of parameter types. * **optional**: Set for nullable types. * **id**: The path to this type. For a top-level variable `foo` this'll be `"foo"`, for the type of the property `bar` under `foo`, it'll be `"foo.bar"`, and so on. ## Interface The module exports the following function: **`gather`**`: (code: string, options: Object) → Object` It takes a code file, extracts the docs, and returns an object describing the documented items. Options can have the following properties: * **`filename`**`: string` The filename of the given code. Required. * **`items`**`: ?Object` An existing items object to add the items found in the given code to. * **`onComment`**`: ?(block: bool, text: string, start: number, end: number, startPos: Object, endPos: Object)` Will be called for each comment in the code, if given. **`parseType`**`: (input: string, start: number, loc: {file: string, line: number}) → {type: Object, end: number}` Parse a type in getdocs syntax into its object representation. `start` indicates where in the string the parsing should start. The returned object tells you where the type ended. Will throw a `SyntaxError` when the type isn't valid. **`stripComment`**`: (comment: string) → string` Strips leading indentation and asterisks (as in the common block comment style where each line gets an asterisk) from a string. ================================================ FILE: bin/getdocs.js ================================================ #!/usr/bin/env node var fs = require("fs") var glob = require("glob") var getdocs = require("../src") var items = {} process.argv.slice(2).forEach(function(arg) { glob.sync(arg).forEach(function(filename) { getdocs.gather(fs.readFileSync(filename, "utf8"), {filename: filename, items: items}) }) }) console.log(JSON.stringify(items, null, 2)) ================================================ FILE: package.json ================================================ { "name": "getdocs", "version": "0.6.1", "description": "An extractor for succinct documentation comments in JavaScript code", "main": "src/index.js", "bin": { "getdocs": "bin/getdocs.js" }, "scripts": { "test": "node test/run.js" }, "repository": { "type": "git", "url": "git://github.com/marijnh/getdocs.git" }, "keywords": [ "documentation", "comment" ], "maintainers": [ { "name": "Marijn Haverbeke", "email": "marijn@haverbeke.berlin", "web": "http://marijnhaverbeke.nl" } ], "license": "MIT", "bugs": { "url": "https://github.com/marijnh/getdocs/issues" }, "dependencies": { "acorn": "^2.6.0", "glob": "^6.0.1" } } ================================================ FILE: src/doccomments.js ================================================ var acorn = require("acorn/dist/acorn") var walk = require("acorn/dist/walk") var parseType = require("./parsetype") function strip(lines) { for (var head, i = 1; i < lines.length; i++) { var line = lines[i], lineHead = line.match(/^[\s\*]*/)[0] if (lineHead != line) { if (head == null) { head = lineHead } else { var same = 0 while (same < head.length && head.charCodeAt(same) == lineHead.charCodeAt(same)) ++same if (same < head.length) head = head.slice(0, same) } } } if (head != null) { var startIndent = /^\s*/.exec(lines[0])[0] var trailing = /\s*$/.exec(head)[0] var extra = trailing.length - startIndent.length if (extra > 0) head = head.slice(0, head.length - extra) } outer: for (var i = 0; i < lines.length; i++) { var line = lines[i].replace(/\s+$/, "") if (i == 0 && head != null) { for (var j = 0; j < head.length; j++) { var found = line.indexOf(head.slice(j)) if (found == 0) { lines[i] = line.slice(head.length - j) continue outer } } } if (head == null || i == 0) lines[i] = line.replace(/^[\s\*]*/, "") else if (line.length < head.length) lines[i] = "" else lines[i] = line.slice(head.length) } while (lines.length && !lines[lines.length - 1]) lines.pop() while (lines.length && !lines[0]) lines.shift() return lines.join("\n") } exports.stripComment = function(text) { return strip(text.split("\n")) } exports.parse = function(text, options) { var current = null, found = [], filename = options.filename var ast = acorn.parse(text, { ecmaVersion: 6, locations: true, sourceFile: {text: text, name: filename}, sourceType: "module", onComment: function(block, text, start, end, startLoc, endLoc) { if (current && !block && current.endLoc.line == startLoc.line - 1) { current.text.push(text) current.end = end current.endLoc = endLoc } else if (/^\s*[\w\.$]*::/.test(text)) { var obj = {text: text.split("\n"), start: start, end: end, startLoc: startLoc, endLoc: endLoc} found.push(obj) if (!block) current = obj } else { current = null } if (options.onComment) options.onComment(block, text, start, end, startLoc, endLoc) } }) for (var i = 0; i < found.length; i++) { var comment = found[i], loc = comment.startLoc loc.file = filename comment.parsed = parseNestedComments(strip(comment.text), comment.startLoc) } return {ast: ast, comments: found} } function Found() {} exports.findNodeAfter = function(ast, pos, types) { var stack = [] function c(node, _, override) { if (node.end < pos) return if (node.start >= pos && types[node.type]) { stack.push(node) throw new Found } if (!override) stack.push(node) walk.base[override || node.type](node, null, c) if (!override) stack.pop() } try { c(ast) } catch (e) { if (e instanceof Found) return stack throw e } } exports.findNodeAround = function(ast, pos, types) { var stack = [], found function c(node, _, override) { if (node.end <= pos || node.start >= pos) return if (!override) stack.push(node) walk.base[override || node.type](node, null, c) if (types[node.type] && !found) found = stack.slice() if (!override) stack.pop() } c(ast) return found || stack } function parseComment(text, loc) { var match = /^\s*([\w\.$]+)?::\s*(-\s*)?/.exec(text), data, end = match[0].length, name = match[1] if (match[2]) { data = Object.create(null) data.loc = loc } else { var parsed = parseType(text, match[0].length, loc) data = parsed.type end = parsed.end } text = text.slice(end) while (match = /^\s*#([\w$]+)(?:=([^"]\S*|"(?:[^"\\]|\\.)*"))?\s*/.exec(text)) { text = text.slice(match[0].length) var value = match[2] || "true" if (value.charAt(0) == '"') value = JSON.parse(value) data["$" + match[1]] = value } if (/\S/.test(text)) data.description = text return {data: data, name: name, subcomments: []} } function dropIndent(text) { var lines = text.split("\n"), indent = 1e7 for (var i = 0; i < lines.length; i++) if (/\S/.test(lines[i])) indent = Math.min(indent, /^\s*/.exec(lines[i])[0].length) if (indent == 0) return text for (var i = 0; i < lines.length; i++) lines[i] = lines[i].slice(indent) return lines.join("\n") } function parseNestedComments(text, loc) { var line = 0, context = [], top, nextIndent = /^\s*/.exec(text)[0].length for (;;) { var next = /\n( *)[\w\.$]*::/.exec(text) var current = next ? text.slice(0, next.index) : text var parsed = parseComment(dropIndent(current), line ? {line: loc.line + line, column: loc.column, file: loc.file} : loc) if (!top) { top = parsed } else { if (!parsed.name) throw new SyntaxError("Sub-comment without name at " + loc.file + ":" + (loc.line + line)) while (context[context.length - 1].indent >= nextIndent) { context.pop() if (!context.length) throw new SyntaxError("Invalid indentation for sub-field at " + loc.file + ":" + (loc.line + line)) } context[context.length - 1].comment.subcomments.push(parsed) } context.push({indent: nextIndent, comment: parsed}) if (!next) break line += current.split("\n").length + 1 text = text.slice(current.length + 1) nextIndent = next[1].length } return top } ================================================ FILE: src/index.js ================================================ var docComments = require("./doccomments") exports.stripComment = docComments.stripComment var parseType = exports.parseType = require("./parsetype") exports.gather = function(text, options) { var items = options.items || Object.create(null) var top = {properties: items} var found = docComments.parse(text, options) found.comments.forEach(function(comment) { var data = comment.parsed.data if (comment.parsed.name) { var stack = docComments.findNodeAround(found.ast, comment.end, findPath) path = addNameToPath(comment.parsed.name, getPath(stack), data.$static) } else { var stack = docComments.findNodeAfter(found.ast, comment.end, findPath) var node = stack && stack[stack.length - 1] if (!node || !/^(?:[;{},\s]|\/\/.*|\/\*.*?\*\/)*$/.test(text.slice(node.end, comment.start))) throw new SyntaxError("Misplaced documentation block at " + options.filename + ":" + comment.startLoc.line) if (inferForNode.hasOwnProperty(node.type)) data = inferForNode[node.type](node, data, stack) var path = getPath(stack) } var stored = addData(top, path, data) comment.parsed.subcomments.forEach(function(sub) { applySubComment(stored, sub) }) }) // Mark locals exported with `export {a, b, c}` statements as exported for (var i = 0; i < found.ast.body.length; i++) { var node = found.ast.body[i] if (node.type == "ExportNamedDeclaration" && !node.source) { for (var j = 0; j < node.specifiers.length; j++) { var spec = node.specifiers[j] var known = items[spec.local.name] if (known) known.exported = true } } } assignIds(top) return items } function applySubComment(parent, sub) { var target if (parent.type == "Function") { if (sub.name == "return") target = parent.returns else if (parent.params) for (var i = 0; i < parent.params.length; i++) if (parent.params[i].name == sub.name) target = parent.params[i] if (!target) raise("Unknown parameter " + sub.name, sub.data.loc) } else if (parent.type == "class" || parent.type == "interface" || parent.type == "Object") { var path = splitPath(sub.name), target = parent for (var i = 0; i < path.length; i++) { var isStatic = i == path.length - 1 && sub.data.$static target = deref(deref(target, isStatic ? "staticProperties" : "properties"), path[i]) } } else { raise("Can not add sub-fields to named type " + parent.type, sub.data.loc) } var stored = extend(sub.data, target, [sub.name], true) sub.subcomments.forEach(function(sub) { applySubComment(stored, sub) }) } function getPath(ancestors) { var top = ancestors[ancestors.length - 1] return top ? findPath[top.type](top, ancestors) : [] } var findPath = { // FIXME destructuring VariableDeclaration: function(node) { return [node.declarations[0].id.name] }, VariableDeclarator: function(node) { return [node.id.name] }, FunctionDeclaration: function(node) { return [node.id.name] }, ClassDeclaration: function(node) { return [node.id.name] }, AssignmentExpression: function(node, ancestors) { return lvalPath(node.left, ancestors) }, Property: function(node, ancestors) { var path = parentPath(ancestors) path.push(propName(node, true)) return path }, MethodDefinition: function(node, ancestors) { var path = parentPath(ancestors) if (node.kind == "constructor") { path.push("#constructor") } else { if (!node.static) path.push("prototype") path.push(propName(node, true)) } return path }, ExportNamedDeclaration: function(node, ancestors) { return this[node.declaration.type](node.declaration, ancestors) }, ExportDefaultDeclaration: function() { return ["default"] } } function addNameToPath(name, path, isStatic) { var parts = splitPath(name) for (var i = 0; i < parts.length; i++) { if (path.length && ctorName(path[path.length - 1]) && (!isStatic || i < parts.length - 1)) path.push("prototype") path.push(parts[i]) } return path } function addData(top, path, data) { var target = top, isCtor = false for (var i = 0; i < path.length; i++) { var cur = path[i], descend = "properties" if (cur == "#constructor") { target = deref(target, "constructor") break } if (isCtor) { if (cur == "prototype") { if (i == path.length - 1) raise("Can not annotate constructor prototype", data.loc) cur = path[++i] } else { descend = "staticProperties" } } target = deref(deref(target, descend), cur) isCtor = target.type ? target.type == "class" : ctorName(cur) } return extend(data, target, path) } var inferForNode = { VariableDeclaration: function(node, data) { var decl0 = node.declarations[0] return inferExpr(decl0.init, data, decl0.id.name) }, VariableDeclarator: function(node, data) { return inferExpr(node.init, data, node.id.name) }, FunctionDeclaration: function(node, data) { return inferFn(node, data, node.id.name) }, ClassDeclaration: inferClass, ClassExpression: inferClass, AssignmentExpression: function(node, data) { return inferExpr(node.right, data, propName(node.left)) }, Property: function(node, data) { return inferExpr(node.value, data, propName(node, true)) }, MethodDefinition: function(node, data) { return inferFn(node.value, data) }, ExportNamedDeclaration: function(node, data, ancestors) { var inner = this[node.declaration.type](node.declaration, data, ancestors) inner.exported = true return inner }, ExportDefaultDeclaration: function(node, data, ancestors) { var decl = node.declaration if (this[decl.type]) data = this[decl.type](decl, data, ancestors) else data = inferExpr(decl, data) data.exported = true return data } } function raise(msg, loc) { throw new SyntaxError(msg + " at " + (loc.file || loc.source.name) + ":" + (loc.start ? loc.start.line : loc.line)) } function propName(node, force) { var key = node.key || node.property if (!node.computed && key.type == "Identifier") return key.name if (key.type == "Literal") { if (typeof key.value == "string") return key.value if (typeof key.value == "number") return String(key.value) } if (node.computed && key.type == "MemberExpression" && !key.computed && key.object.name == "Symbol") return "[Symbol." + key.property.name + "]" if (force) raise("Expected static property", node.loc) } function inferParam(n) { var param = Object.create(null) param.type = "any" param.loc = n.loc.start param.loc.file = n.loc.source.name if (n.type == "RestElement") { param.rest = true n = n.argument } if (n.type == "AssignmentPattern") { if (n.right.end - n.right.start < 20) param.default = n.loc.source.text.slice(n.right.start, n.right.end) n = n.left param.optional = true } if (n.type == "Identifier") param.name = n.name return param } function ctorName(name) { return name && /^[A-Z]/.test(name) } function inferFn(node, data, name) { var inferredParams = node.params.map(inferParam) if (!data.type) { data.type = "Function" data.params = inferredParams } else if (data.type == "Function") { for (var i = 0, e = Math.min(data.params.length, node.params.length); i < e; i++) { var from = inferredParams[i], to = data.params[i] for (var prop in from) if (!(prop in to)) to[prop] = from[prop] } } if (node.generator) data.generator = true if (ctorName(name)) { return {constructor: data, type: "class", loc: data.loc} } else { return data } } function inferClass(node, data) { if (node.superClass && node.superClass.type == "Identifier") { var loc = node.superClass.loc loc.start.file = loc.source.name data.extends = parseType(node.superClass.name, 0, loc.start).type } if (!data.type) data.type = "class" return data } function inferExpr(node, data, name) { if (!node) return data if (node.type == "ClassExpression") { inferClass(node, data) } else if (node.type == "FunctionExpression" || node.type == "ArrowFunctionExpression") { inferFn(node, data, name) } else if (node.type == "Literal" && !data.type) { if (typeof node.value == "number") data.type = "number" else if (typeof node.value == "boolean") data.type = "bool" else if (typeof node.value == "string") data.type = "string" else if (node.value instanceof RegExp) data.type = "RegExp" } else if (node.type == "NewExpression" && !data.type) { if (node.callee.type == "Identifier" && ctorName(node.callee.name)) data.type = node.callee.name } return data } // Deriving context from ancestor nodes function extend(from, to, path, overrideLoc) { for (var prop in from) { if (!(prop in to) || (prop == "loc" && overrideLoc)) { to[prop] = from[prop] } else if (prop == "properties" || prop == "staticProperties") { extend(from[prop], to[prop], path.concat(prop)) } else { var msg = "Conflicting information for " + path.join(".") + "." + prop if (to.loc) msg += " at " + to.loc.file + ":" + to.loc.line if (from.loc) msg += (to.loc ? " and " : " at ") + from.loc.file + ":" + from.loc.line throw new SyntaxError(msg) } } return to } function deref(obj, name) { return obj[name] || (obj[name] = Object.create(null)) } function assignIds(obj, path) { if (path) obj.id = path if (Object.prototype.hasOwnProperty.call(obj, "constructor")) assignIds(obj.constructor, path + ".constructor") if (obj.properties) for (var prop in obj.properties) assignIds(obj.properties[prop], (path ? path + "." : "") + prop) if (obj.staticProperties) for (var prop in obj.staticProperties) assignIds(obj.staticProperties[prop], path + "^" + prop) if (obj.params) for (var i = 0; i < obj.params.length; i++) if (obj.params[i].name) assignIds(obj.params[i], path + "^" + obj.params[i].name) if (obj.returns) assignIds(obj.returns, path + "^returns") } function lvalPath(lval, ancestors) { var path = [] while (lval.type == "MemberExpression") { path.unshift(propName(lval)) lval = lval.object } if (lval.type == "Identifier") { path.unshift(lval.name) } else if (lval.type == "ThisExpression") { path = selfPath(ancestors.slice(0, ancestors.length - 1)).concat(path) } else { raise("Could not derive a target for this assignment", lval.loc) } return path } function assignedPath(ancestors) { var top = ancestors[ancestors.length - 1] if (top.type == "VariableDeclarator" && top.id.type == "Identifier") return [top.id.name] else if (top.type == "AssignmentExpression") return lvalPath(top.left, ancestors) else raise("Could not derive a name", top.loc) } function findPrototype(ancestors) { var assign = ancestors[ancestors.length - 1] if (assign.type != "AssignmentExpression") return null var lval = assign.left if (lval.type == "MemberExpression" && !lval.computed && lval.object.type == "MemberExpression" && !lval.object.computed && lval.object.property.name == "prototype") return lvalPath(lval.object, ancestors) } function assignedName(node) { if (node.type == "VariableDeclarator" && node.id.type == "Identifier") return node.id.name else if (node.type == "AssignmentExpression") return propName(node.left) } function selfPath(ancestors) { for (var i = ancestors.length - 1; i >= 0; i--) { var ancestor = ancestors[i], found if (ancestor.type == "ClassDeclaration" || (ancestor.type == "FunctionDeclaration" && ctorName(ancestor.id.name))) return [ancestor.id.name, "prototype"] else if (i && (ancestor.type == "ClassExpression" || ancestor.type == "FunctionExpression" && ctorName(assignedName(ancestors[i - 1])))) return assignedPath(ancestors.slice(0, i)).concat("prototype") else if (i && /Function/.test(ancestor.type) && (found = findPrototype(ancestors.slice(0, i)))) return found } raise("No context found for 'this'", ancestors[ancestors.length - 1].loc) } function parentPath(ancestors) { for (var i = ancestors.length - 1; i >= 0; i--) { var ancestor = ancestors[i] if (ancestor.type == "ClassDeclaration") return [ancestor.id.name] else if (i && (ancestor.type == "ClassExpression" || ancestor.type == "ObjectExpression")) return assignedPath(ancestors.slice(0, i)) } } function splitPath(path) { var m, parts = [], rest = path while (rest && (m = /^(\[.*?\]|[^\s\.#]+)(\.)?/.exec(rest))) { parts.push(m[1]) rest = rest.slice(m[0].length) if (!m[2]) break } if (rest) throw new Error("Invalid path: " + path) return parts } ================================================ FILE: src/parsetype.js ================================================ module.exports = function(string, start, loc) { var input = new Input(string, start, loc) var result = parse(input) input.skip() return {type: result, end: input.pos} } function isSpace(ch) { return (ch < 14 && ch > 8) || ch === 32 || ch === 160; } function Input(string, start, loc) { this.str = string this.pos = start this.loc = loc this.skip() } Input.prototype = { skip: function() { while (this.pos < this.str.length && isSpace(this.str.charCodeAt(this.pos))) ++this.pos }, atEnd: function() { return this.pos == this.str.length }, eat: function(ch) { if (this.str.charCodeAt(this.pos) == ch.charCodeAt(0)) { this.pos++ this.skip() return true } }, match: function(re) { var match = re.exec(this.str.slice(this.pos)) if (match) { this.pos += match[0].length this.skip() return match } }, error: function(message) { throw new SyntaxError(message + " for " + this.loc.file + ":" + this.loc.line) } } function parse(input) { var base = parseBase(input) if (input.match(/^extends\b/)) base.extends = parse(input) return base } function parseBase(input) { if (input.eat("?")) { var inner = parse(input) inner.optional = true return inner } var type = Object.create(null) type.loc = input.loc if (input.eat("*")) { type.type = "any" } else if (input.eat("(")) { type.type = "Function" type.params = [] while (!input.eat(")")) { if (type.params.length && !input.eat(",")) input.error("Missing comma or closing paren") var rest = input.match(/^\.\.\./) var name = input.match(/^([\w$]+)(\??)\s*:/) var param = parse(input) if (rest) param.rest = true if (name) param.name = name[1] if (name && name[2]) param.optional = true type.params.push(param) } if (input.match(/^(?:→|->)/)) type.returns = parse(input) } else if (input.eat("[")) { type.type = "Array" type.typeParams = [parse(input)] while (!input.eat("]")) { if (!input.eat(",")) input.error("Missing comma or closing square bracket") type.typeParams.push(parse(input)) } } else if (input.eat("{")) { type.type = "Object" type.properties = Object.create(null) for (var first = true; !input.eat("}"); first = false) { if (!first && !input.eat(",")) input.error("Missing comma or closing brace") var name = input.match(/^([\w$]+)\s*:/) if (!name) input.error("Malformed object type") type.properties[name[1]] = parse(input) } } else { var name = input.match(/^(?:[\w$]+(?:\.[\w$]+)*|"(?:[^"]|\.)*")/) if (!name) input.error("Unexpected syntax: " + input.str.slice(input.pos, input.pos + 5)) type.type = name[0] if (input.eat("<")) { type.typeParams = [] while (!input.eat(">")) { if (type.typeParams.length && !input.eat(",")) input.error("Missing comma or closing angle bracket") type.typeParams.push(parse(input)) } } } return type } ================================================ FILE: test/class_addmethod.js ================================================ // ::- The Foo class class Foo {} // :: (number, string) → bool // A method Foo.prototype.bar = (a, b) => true ================================================ FILE: test/class_addmethod.json ================================================ { "Foo": { "id": "Foo", "description": "The Foo class", "type": "class", "loc": { "file": "test/class_addmethod.js", "line": 1, "column": 0 }, "properties": { "bar": { "id": "Foo.bar", "type": "Function", "params": [ { "type": "number", "name": "a", "id": "Foo.bar^a", "loc": { "file": "test/class_addmethod.js", "line": 4, "column": 0 } }, { "type": "string", "name": "b", "id": "Foo.bar^b", "loc": { "file": "test/class_addmethod.js", "line": 4, "column": 0 } } ], "returns": { "type": "bool", "id": "Foo.bar^returns", "loc": { "file": "test/class_addmethod.js", "line": 4, "column": 0 } }, "description": "A method", "loc": { "file": "test/class_addmethod.js", "line": 4, "column": 0 } } } } } ================================================ FILE: test/class_ctor.js ================================================ // :: (number, number) A vector type function Point(x, y) { // :: number The x coordinate this.x = x // :: number The y coordinate this.y = y } ================================================ FILE: test/class_ctor.json ================================================ { "Point": { "id": "Point", "constructor": { "id": "Point.constructor", "type": "Function", "params": [ { "type": "number", "name": "x", "id": "Point.constructor^x", "loc": { "file": "test/class_ctor.js", "line": 1, "column": 0 } }, { "type": "number", "name": "y", "id": "Point.constructor^y", "loc": { "file": "test/class_ctor.js", "line": 1, "column": 0 } } ], "loc": { "file": "test/class_ctor.js", "line": 1, "column": 0 }, "description": "A vector type" }, "type": "class", "loc": { "file": "test/class_ctor.js", "line": 1, "column": 0 }, "properties": { "x": { "id": "Point.x", "type": "number", "loc": { "file": "test/class_ctor.js", "line": 3, "column": 2 }, "description": "The x coordinate" }, "y": { "id": "Point.y", "type": "number", "loc": { "file": "test/class_ctor.js", "line": 5, "column": 2 }, "description": "The y coordinate" } } } } ================================================ FILE: test/class_expr.js ================================================ // ::- The Foo class const Foo = class extends Bar { // :: (number, string) -> bool m(a, b) { return true } } ================================================ FILE: test/class_expr.json ================================================ { "Foo": { "id": "Foo", "description": "The Foo class", "type": "class", "loc": { "file": "test/class_expr.js", "line": 1, "column": 0 }, "properties": { "m": { "id": "Foo.m", "type": "Function", "params": [ { "type": "number", "name": "a", "id": "Foo.m^a", "loc": { "file": "test/class_expr.js", "line": 3, "column": 2 } }, { "type": "string", "name": "b", "id": "Foo.m^b", "loc": { "file": "test/class_expr.js", "line": 3, "column": 2 } } ], "returns": { "type": "bool", "id": "Foo.m^returns", "loc": { "file": "test/class_expr.js", "line": 3, "column": 2 } }, "loc": { "file": "test/class_expr.js", "line": 3, "column": 2 } } }, "extends": { "type": "Bar", "loc": { "line": 2, "column": 26, "file": "test/class_expr.js" } } } } ================================================ FILE: test/class_simple.js ================================================ // ::- The Foo class class Foo extends Bar { // :: (number, number) constructor(a, b) { this.a = a this.b = b } // :: (number, string) -> bool m(a, b) { return true } } ================================================ FILE: test/class_simple.json ================================================ { "Foo": { "id": "Foo", "description": "The Foo class", "loc": { "file": "test/class_simple.js", "line": 1, "column": 0 }, "type": "class", "properties": { "m": { "id": "Foo.m", "type": "Function", "params": [ { "type": "number", "name": "a", "id": "Foo.m^a", "loc": { "file": "test/class_simple.js", "line": 9, "column": 2 } }, { "type": "string", "name": "b", "id": "Foo.m^b", "loc": { "file": "test/class_simple.js", "line": 9, "column": 2 } } ], "returns": { "type": "bool", "id": "Foo.m^returns", "loc": { "file": "test/class_simple.js", "line": 9, "column": 2 } }, "loc": { "file": "test/class_simple.js", "line": 9, "column": 2 } } }, "extends": { "type": "Bar", "loc": { "line": 2, "column": 18, "file": "test/class_simple.js" } }, "constructor": { "id": "Foo.constructor", "loc": { "file": "test/class_simple.js", "line": 3, "column": 2 }, "type": "Function", "params": [ { "type": "number", "name": "a", "id": "Foo.constructor^a", "loc": { "file": "test/class_simple.js", "line": 3, "column": 2 } }, { "type": "number", "name": "b", "id": "Foo.constructor^b", "loc": { "file": "test/class_simple.js", "line": 3, "column": 2 } } ] } } } ================================================ FILE: test/class_static.js ================================================ // ::- The Foo class class Foo extends Bar { // :: (number, string) -> bool static m(a, b) { return true } } ================================================ FILE: test/class_static.json ================================================ { "Foo": { "id": "Foo", "description": "The Foo class", "type": "class", "loc": { "file": "test/class_static.js", "line": 1, "column": 0 }, "extends": { "type": "Bar", "loc": { "line": 2, "column": 18, "file": "test/class_static.js" } }, "staticProperties": { "m": { "id": "Foo^m", "type": "Function", "params": [ { "type": "number", "name": "a", "id": "Foo^m^a", "loc": { "file": "test/class_static.js", "line": 3, "column": 2 } }, { "type": "string", "name": "b", "id": "Foo^m^b", "loc": { "file": "test/class_static.js", "line": 3, "column": 2 } } ], "returns": { "type": "bool", "id": "Foo^m^returns", "loc": { "file": "test/class_static.js", "line": 3, "column": 2 } }, "loc": { "file": "test/class_static.js", "line": 3, "column": 2 } } } } } ================================================ FILE: test/class_this.js ================================================ // ::- A Foo class Foo { // :: (number) constructor(a) { // :: number // The a property this.a = a } } // :: () Foo.prototype.doStuff = function() { // :: bool // The b property this.b = true } ================================================ FILE: test/class_this.json ================================================ { "Foo": { "id": "Foo", "loc": { "file": "test/class_this.js", "line": 1, "column": 0 }, "description": "A Foo", "type": "class", "constructor": { "id": "Foo.constructor", "type": "Function", "params": [ { "type": "number", "name": "a", "id": "Foo.constructor^a", "loc": { "file": "test/class_this.js", "line": 3, "column": 2 } } ], "loc": { "file": "test/class_this.js", "line": 3, "column": 2 } }, "properties": { "a": { "id": "Foo.a", "type": "number", "loc": { "file": "test/class_this.js", "line": 5, "column": 4 }, "description": "The a property" }, "b": { "id": "Foo.b", "type": "bool", "loc": { "file": "test/class_this.js", "line": 13, "column": 2 }, "description": "The b property" }, "doStuff": { "id": "Foo.doStuff", "type": "Function", "params": [], "loc": { "file": "test/class_this.js", "line": 11, "column": 0 } } } } } ================================================ FILE: test/defaultarg.js ================================================ // :: (bool) function foo(arg = false) {} ================================================ FILE: test/defaultarg.json ================================================ { "foo": { "id": "foo", "type": "Function", "params": [ { "type": "bool", "default": "false", "optional": true, "name": "arg", "id": "foo^arg", "loc": { "file": "test/defaultarg.js", "line": 1, "column": 0 } } ], "loc": { "file": "test/defaultarg.js", "line": 1, "column": 0 } } } ================================================ FILE: test/diffargname.js ================================================ // :: (foo: number, bar: string) function x(a, b) {} ================================================ FILE: test/diffargname.json ================================================ { "x": { "id": "x", "type": "Function", "params": [ { "name": "foo", "id": "x^foo", "type": "number", "loc": { "file": "test/diffargname.js", "line": 1, "column": 0 } }, { "name": "bar", "id": "x^bar", "type": "string", "loc": { "file": "test/diffargname.js", "line": 1, "column": 0 } } ], "loc": { "file": "test/diffargname.js", "line": 1, "column": 0 } } } ================================================ FILE: test/exported.js ================================================ // :: () → string export function hello() { return "hello" } // :: number export var x = 10 // :: string export default "hi" ================================================ FILE: test/exported.json ================================================ { "hello": { "id": "hello", "type": "Function", "params": [], "returns": { "type": "string", "id": "hello^returns", "loc": { "file": "test/exported.js", "line": 1, "column": 0 } }, "loc": { "file": "test/exported.js", "line": 1, "column": 0 }, "exported": true }, "x": { "id": "x", "type": "number", "loc": { "file": "test/exported.js", "line": 4, "column": 0 }, "exported": true }, "default": { "id": "default", "type": "string", "loc": { "file": "test/exported.js", "line": 7, "column": 0 }, "exported": true } } ================================================ FILE: test/function.js ================================================ // :: (number, number) → number // Adds two numbers function add(a, b) { return a + b } ================================================ FILE: test/function.json ================================================ { "add": { "id": "add", "type": "Function", "params": [ { "type": "number", "name": "a", "id": "add^a", "loc": { "file": "test/function.js", "line": 1, "column": 0 } }, { "type": "number", "name": "b", "id": "add^b", "loc": { "file": "test/function.js", "line": 1, "column": 0 } } ], "returns": { "type": "number", "id": "add^returns", "loc": { "file": "test/function.js", "line": 1, "column": 0 } }, "description": "Adds two numbers", "loc": { "file": "test/function.js", "line": 1, "column": 0 } } } ================================================ FILE: test/functionsub.js ================================================ // :: (number, number) → number // A function // // a::- Parameter a // return::- The return value is the sum function foo(a, b) { return a + b } ================================================ FILE: test/functionsub.json ================================================ { "foo": { "id": "foo", "loc": { "line": 1, "column": 0, "file": "test/functionsub.js" }, "type": "Function", "params": [ { "loc": { "line": 5, "column": 0, "file": "test/functionsub.js" }, "type": "number", "name": "a", "id": "foo^a", "description": "Parameter a" }, { "loc": { "line": 1, "column": 0, "file": "test/functionsub.js" }, "type": "number", "name": "b", "id": "foo^b" } ], "returns": { "loc": { "line": 7, "column": 0, "file": "test/functionsub.js" }, "type": "number", "id": "foo^returns", "description": "The return value is the sum" }, "description": "A function\n" } } ================================================ FILE: test/infer.js ================================================ // ::- It's x var x = 10 // ::- It's y var y = true // ::- It's z var z = /foo/ // ::- It's obj var obj = new Something ================================================ FILE: test/infer.json ================================================ { "x": { "id": "x", "loc": { "line": 1, "column": 0, "file": "test/infer.js" }, "description": "It's x", "type": "number" }, "y": { "id": "y", "loc": { "line": 4, "column": 0, "file": "test/infer.js" }, "description": "It's y", "type": "bool" }, "z": { "id": "z", "loc": { "line": 7, "column": 0, "file": "test/infer.js" }, "description": "It's z", "type": "RegExp" }, "obj": { "id": "obj", "loc": { "line": 10, "column": 0, "file": "test/infer.js" }, "description": "It's obj", "type": "Something" } } ================================================ FILE: test/literal.js ================================================ // :: (union<"a","b">, number, number) → number // Returns a or b function add(which, a, b) { return which === "a" ? a : b } ================================================ FILE: test/literal.json ================================================ { "add": { "id": "add", "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "Function", "params": [ { "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "union", "typeParams": [ { "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "\"a\"" }, { "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "\"b\"" } ], "name": "which", "id": "add^which" }, { "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "number", "name": "a", "id": "add^a" }, { "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "number", "name": "b", "id": "add^b" } ], "returns": { "loc": { "line": 1, "column": 0, "file": "test/literal.js" }, "type": "number", "id": "add^returns" }, "description": "Returns a or b" } } ================================================ FILE: test/namedprop.js ================================================ // ::- Boo the class class Boo { // someProp:: (number) Some prop } ================================================ FILE: test/namedprop.json ================================================ { "Boo": { "id": "Boo", "loc": { "line": 1, "column": 0, "file": "test/namedprop.js" }, "type": "class", "description": "Boo the class", "properties": { "someProp": { "id": "Boo.someProp", "loc": { "line": 3, "column": 2, "file": "test/namedprop.js" }, "type": "Function", "params": [ { "loc": { "line": 3, "column": 2, "file": "test/namedprop.js" }, "type": "number" } ], "description": "Some prop" } } } } ================================================ FILE: test/obj.js ================================================ // :: {extra: number} let obj = { // :: number A property foo: 10, bar: 20 } // ::- An added property obj.baz = 30 ================================================ FILE: test/obj.json ================================================ { "obj": { "id": "obj", "type": "Object", "properties": { "extra": { "id": "obj.extra", "type": "number", "loc": { "file": "test/obj.js", "line": 1, "column": 0 } }, "foo": { "id": "obj.foo", "description": "A property", "type": "number", "loc": { "file": "test/obj.js", "line": 3, "column": 2 } }, "baz": { "id": "obj.baz", "description": "An added property", "type": "number", "loc": { "file": "test/obj.js", "line": 8, "column": 0 } } }, "loc": { "file": "test/obj.js", "line": 1, "column": 0 } } } ================================================ FILE: test/phantom.js ================================================ // Abc:: class // The Abc class // Abc.m1:: (a: number) → number // Method #1 // Abc.m2:: () #static // Method #2 // myString:: string // My string ================================================ FILE: test/phantom.json ================================================ { "Abc": { "id": "Abc", "loc": { "line": 1, "column": 0, "file": "test/phantom.js" }, "description": "The Abc class", "type": "class", "properties": { "m1": { "id": "Abc.m1", "loc": { "line": 4, "column": 0, "file": "test/phantom.js" }, "type": "Function", "params": [ { "loc": { "line": 4, "column": 0, "file": "test/phantom.js" }, "type": "number", "name": "a", "id": "Abc.m1^a" } ], "returns": { "loc": { "line": 4, "column": 0, "file": "test/phantom.js" }, "type": "number", "id": "Abc.m1^returns" }, "description": "Method #1" } }, "staticProperties": { "m2": { "id": "Abc^m2", "loc": { "line": 7, "column": 0, "file": "test/phantom.js" }, "type": "Function", "params": [], "$static": "true", "description": "Method #2" } } }, "myString": { "id": "myString", "loc": { "line": 10, "column": 0, "file": "test/phantom.js" }, "type": "string", "description": "My string" } } ================================================ FILE: test/restarg.js ================================================ // :: ([string]) -> bool function bar(...stuff) {} ================================================ FILE: test/restarg.json ================================================ { "bar": { "id": "bar", "type": "Function", "params": [ { "type": "Array", "typeParams": [{ "type": "string", "loc": { "file": "test/restarg.js", "line": 1, "column": 0 } }], "rest": true, "name": "stuff", "id": "bar^stuff", "loc": { "file": "test/restarg.js", "line": 1, "column": 0 } } ], "returns": { "type": "bool", "id": "bar^returns", "loc": { "file": "test/restarg.js", "line": 1, "column": 0 } }, "loc": { "file": "test/restarg.js", "line": 1, "column": 0 } } } ================================================ FILE: test/run.js ================================================ var fs = require("fs") var getdocs = require("../src") var filter = process.argv[2] fs.readdirSync(__dirname).forEach(function(filename) { var isJSON = /^([^\.]+)\.json$/.exec(filename) if (!isJSON || (filter && isJSON[1].indexOf(filter) != 0)) return var expected = JSON.parse(fs.readFileSync(__dirname + "/" + filename, "utf8")) var jsfile = "/" + isJSON[1] + ".js" var returned = getdocs.gather(fs.readFileSync(__dirname + jsfile, "utf8"), {filename: "test" + jsfile}) try { compare(returned, expected, "") } catch(e) { console.error(isJSON[1] + ": " + e.message) console.error("in " + JSON.stringify(returned, null, 2)) } }) function hop(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop) } function compare(a, b, path) { if (typeof a != "object" || typeof b != "object") { if (a !== b) throw new Error("Mismatch at " + path + ": " + a + " vs " + b) } else { for (var prop in a) if (hop(a, prop)) { if (!(prop in b)) throw new Error("Unexpected property " + path + "." + prop) else compare(a[prop], b[prop], path + "." + prop) } for (var prop in b) if (hop(b, prop)) { if (!(prop in a)) throw new Error("Missing property " + path + "." + prop) } } } ================================================ FILE: test/subcomment.js ================================================ // :: Object An object // // x:: number The x coordinate // // y:: Object The y object // // inner:: bool A property of `y` var x = {} ================================================ FILE: test/subcomment.json ================================================ { "x": { "id": "x", "loc": { "line": 1, "column": 0, "file": "test/subcomment.js" }, "type": "Object", "description": "An object\n", "properties": { "x": { "id": "x.x", "loc": { "line": 4, "column": 0, "file": "test/subcomment.js" }, "type": "number", "description": "The x coordinate\n" }, "y": { "id": "x.y", "loc": { "line": 7, "column": 0, "file": "test/subcomment.js" }, "type": "Object", "description": "The y object\n", "properties": { "inner": { "id": "x.y.inner", "loc": { "line": 10, "column": 0, "file": "test/subcomment.js" }, "type": "bool", "description": "A property of `y`" } } } } } } ================================================ FILE: test/tags.js ================================================ // :: () #deprecated #exported=false #context="a \"string\"" // Hello! function foo() {} ================================================ FILE: test/tags.json ================================================ { "foo": { "id": "foo", "loc": { "file": "test/tags.js", "line": 1, "column": 0 }, "$deprecated": "true", "$exported": "false", "$context": "a \"string\"", "description": "Hello!", "type": "Function", "params": [] } } ================================================ FILE: test/union.js ================================================ // :: (union, ?union) → union // That's one complex signature function blah(x, y) {} ================================================ FILE: test/union.json ================================================ { "blah": { "id": "blah", "type": "Function", "params": [ { "type": "union", "typeParams": [ { "type": "number", "loc": { "file": "test/union.js", "line": 1, "column": 0 } }, { "type": "string", "loc": { "file": "test/union.js", "line": 1, "column": 0 } } ], "name": "x", "id": "blah^x", "loc": { "file": "test/union.js", "line": 1, "column": 0 } }, { "type": "union", "typeParams": [ { "type": "bool", "loc": { "file": "test/union.js", "line": 1, "column": 0 } }, { "type": "RegExp", "loc": { "file": "test/union.js", "line": 1, "column": 0 } }, { "type": "Array", "loc": { "file": "test/union.js", "line": 1, "column": 0 }, "typeParams": [{ "type": "Error", "loc": { "file": "test/union.js", "line": 1, "column": 0 } }] } ], "optional": true, "name": "y", "id": "blah^y", "loc": { "file": "test/union.js", "line": 1, "column": 0 } } ], "returns": { "type": "union", "id": "blah^returns", "typeParams": [ { "type": "number", "loc": { "file": "test/union.js", "line": 1, "column": 0 } }, { "type": "bool", "loc": { "file": "test/union.js", "line": 1, "column": 0 } } ], "loc": { "file": "test/union.js", "line": 1, "column": 0 } }, "loc": { "file": "test/union.js", "line": 1, "column": 0 }, "description": "That's one complex signature" } }