├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── app.js ├── index.js ├── lib └── falafel.js ├── package.json └── tests ├── expected_output ├── 01-object-literal.js ├── 02-es6-class.js ├── 03-stand-alone-function.js ├── 04-function-as-var.js ├── 05-prototype-class.js ├── 06-nested-functions.js ├── 07-es6-class-field-decls.js ├── 08-inline-syntax.js ├── 09-jsx.js ├── 10-es6-fat-arrows.js ├── 11-optional-params.js ├── 12-es6-export-function.js └── 13-es6-export-const-fat-arrow.js ├── input ├── 01-object-literal.js ├── 02-es6-class.js ├── 03-stand-alone-function.js ├── 04-function-as-var.js ├── 05-prototype-class.js ├── 06-nested-functions.js ├── 07-es6-class-field-decls.js ├── 08-inline-syntax.js ├── 09-jsx.js ├── 10-es6-fat-arrows.js ├── 11-optional-params.js ├── 12-es6-export-function.js └── 13-es6-export-const-fat-arrow.js └── runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | webhooks: 3 | urls: 4 | - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGtlZ2FuJTNBbWF0cml4Lm9yZy8lMjF6cVdZdHh5UXVvZFlURGVJQ3MlM0FtYXRyaXgub3Jn" 5 | - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGtlZ2FuJTNBbWF0cml4Lm9yZy8lMjFqRXhKa2NOY1BJUHhpQXZ3UEglM0FtYXRyaXgub3Jn" 6 | on_success: always # always|never|change 7 | on_failure: always 8 | on_start: always 9 | 10 | language: node_js 11 | node_js: 12 | - "4.1" 13 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | ----- 3 | 4 | - Add support for default values inside parameters (thanks @wardpeet!) 5 | - Add support for `@returns` in addition to `@return` (thanks @albe!) 6 | - Add support for in-line `export` functions (thanks @jwineman!) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flow-jsdoc 2 | [![Build Status](https://travis-ci.org/Kegsay/flow-jsdoc.svg?branch=master)](https://travis-ci.org/Kegsay/flow-jsdoc) 3 | 4 | This project is looking for maintainers: https://github.com/facebook/flow/issues/5670#issuecomment-436965321 5 | 6 | This is a CLI tool to convert [JSDoc](http://usejsdoc.org/index.html) annotations into standard [Flow](https://flowtype.org/) type annotations. This means: 7 | - You only need to document your types once: in JSDoc. 8 | - You can get the benefits of Flow without having to go through a [transpiler](http://babeljs.io/), and without having to use [ugly looking comment syntax](https://flowtype.org/blog/2015/02/20/Flow-Comments.html). 9 | - You can do tiny in-line type comments for those functions which don't have JSDoc but you still want types. 10 | 11 | ```javascript 12 | // Converts this: 13 | 14 | /** 15 | * @param {Foobar[]} bar A foobar array 16 | * @param {Function} baz 17 | * @return {number} 18 | */ 19 | function foo(bar, baz) { 20 | return 42; 21 | } 22 | 23 | // Into this: 24 | 25 | /** 26 | * @param {Foobar[]} bar A foobar array 27 | * @param {Function} baz 28 | * @return {number} 29 | */ 30 | function foo(bar: Array, baz: Function) : number { 31 | return 42; 32 | } 33 | ``` 34 | 35 | Furthermore, a short in-line style is also supported: 36 | 37 | ```js 38 | // Converts this: 39 | 40 | //: (string, number) : Object 41 | function foo(a, b) { 42 | return {}; 43 | } 44 | 45 | // Into this: 46 | 47 | function foo(a: string, b: number) : Object { 48 | return {}; 49 | } 50 | 51 | // NB: The ":" at the start of the comment is REQUIRED. 52 | // NBB: The in-line comment is REMOVED in the output to avoid Flow re-interpreting it.. 53 | ``` 54 | 55 | The goal of this project is to make type checking as easy as running a linter, so you can take any project and run the following to get type errors: 56 | ``` 57 | $ flow-jsdoc -d ./lib -o ./annotated 58 | $ flow check --all ./annotated 59 | ``` 60 | 61 | # Usage 62 | 63 | *This tool will NOT apply* `/* @flow */` *to the file. You still need to do that!* 64 | 65 | ## CLI 66 | ``` 67 | $ npm install -g flow-jsdoc 68 | $ flow-jsdoc -f path/to/file.js 69 | # annotated file prints to stdout 70 | 71 | $ flow-jsdoc -d path/to/lib -o path/to/output 72 | # every file in path/to/lib is processed and output to path/to/output (directory structure preserved) 73 | ``` 74 | 75 | ## JS 76 | ```javascript 77 | var flowJsdoc = require("flow-jsdoc"); 78 | var fileContents = // extract your file contents e.g. via 'fs' - this should be a string 79 | var opts = { 80 | // no options yet! 81 | }; 82 | var annotatedContents = flowJsdoc(fileContents, opts); 83 | // write out annotated contents to file 84 | ``` 85 | 86 | 87 | 88 | # What this does 89 | Currently, this tool will only work on functions and ES6 classes. It will handle functions represented in the following ways: 90 | * `function foo(bar) {}` 91 | * `var foo = function(bar) {}` 92 | * `var obj = { foo: function(bar) {} }` 93 | * `ObjClass.prototype.foo = function(bar) {}` - ES5 Classes 94 | * `class ObjClass { foo(bar) {} }` - ES6 Classes 95 | * `(foo, bar) => { }` - ES6 "fat arrow" functions 96 | 97 | For each recognised function, the JSDoc tags `@param` and `@return` will be mapped to Flow annotations. This will currently do the following mappings from JSDoc to Flow: 98 | * `{AnyThingHere}` => `: AnyThingHere` (Name expressions) 99 | * `{String[]}` => `: Array` (Type applications) 100 | * `{*}` => `: any` (Any type) 101 | * `{Object|String}` => `: Object | String` (Type unions) 102 | * `{string=}` => `: ?string` (Optional params) 103 | * `{?string}` => `: ?string` (Nullable types) 104 | 105 | ES6 classes will include [field declarations](https://flowtype.org/docs/classes.html#_) via the `@prop` and `@property` tags like so: 106 | 107 | ```javascript 108 | // Converts this ES6 Class: 109 | 110 | class Foo { 111 | /** 112 | * Construct a Foo. 113 | * @property {string} bar 114 | * @prop {number} baz 115 | */ 116 | constructor(bar, baz) { 117 | this.bar = bar; 118 | this.baz = baz; 119 | } 120 | } 121 | 122 | // Into this: 123 | 124 | class Foo { 125 | bar: string; 126 | baz: number; 127 | 128 | /** 129 | * Construct a Foo. 130 | * @property {string} bar 131 | * @prop {number} baz 132 | */ 133 | constructor(bar, baz) { 134 | this.bar = bar; 135 | this.baz = baz; 136 | } 137 | } 138 | ``` 139 | 140 | This tool will then produce the whole file again with flow annotations included (JSDoc preserved). 141 | 142 | # Additions 143 | There are plans for this tool to (roughly in priority order): 144 | * Handle record types `{{a: number, b: string, c}}` 145 | * **Auto-require()ing types you reference in other files if you don't import them yourself.** When you start type-annotating, sometimes you'll declare a type that is defined in another file but you won't need to require() it manually (e.g. because it's just passed as a function argument). Flow *needs* to know where the type is declared, so you need to import it somehow even if it's a no-op in the code. This tool should be able to automatically do this. 146 | * Handle type definitions `@typedef` 147 | * Handle callback type resolution (mapping `@callback` sensibly) 148 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var fs = require("fs"); 5 | var nopt = require("nopt"); 6 | var path = require("path"); 7 | var glob = require("glob"); 8 | var mkdirp = require("mkdirp"); 9 | var jsdocFlow = require("./index"); 10 | 11 | var opts = nopt({ 12 | "file": path, 13 | "directory": path, 14 | "outdir": path, 15 | "copy": Boolean, 16 | "skip": Boolean 17 | }, { 18 | "f": "--file", 19 | "d": "--directory", 20 | "o": "--outdir", 21 | "c": "--copy", 22 | "s": "--skip" 23 | }); 24 | 25 | if (opts.file) { 26 | console.log( 27 | jsdocFlow(fs.readFileSync(opts.file, "utf8")) 28 | ); 29 | } 30 | else if (opts.directory && opts.outdir) { 31 | var absDirectory = path.resolve(opts.directory); 32 | var absOutDirectory = path.resolve(opts.outdir); 33 | // make directory if not exists 34 | try { 35 | fs.mkdirSync(absOutDirectory); 36 | } 37 | catch (e) { 38 | if (e.code !== "EEXIST") { 39 | throw e; 40 | } 41 | } 42 | // loop all files in absDirectory which have either any file ext or .js 43 | var fileExt = opts.copy ? "*" : "js"; 44 | glob(absDirectory + "/**/*." + fileExt, function(err, files) { 45 | if (err) { 46 | console.error(err); 47 | process.exit(1); 48 | return; 49 | } 50 | files.forEach(function(fpath) { 51 | // snip the absolute part to get the directory structure for outdir 52 | // Also make sure we're speaking the same slashes 53 | var relativeDir = fpath.replace(absDirectory.replace(/\\/g, "/"), ""); 54 | var outFilePath = path.join(absOutDirectory, relativeDir); 55 | // make directories after stripping filename 56 | mkdirp.sync(path.dirname(outFilePath)); 57 | 58 | if (opts.copy && path.extname(fpath) !== ".js") { 59 | // copy it over. We don't know how big this is so use streams 60 | var rs = fs.createReadStream(fpath); 61 | rs.on("error", function(err) { 62 | console.error(err); 63 | process.exit(1); 64 | }); 65 | var ws = fs.createWriteStream(outFilePath); 66 | ws.on("error", function(err) { 67 | console.error(err); 68 | process.exit(1); 69 | }); 70 | rs.pipe(ws); 71 | return; 72 | } 73 | console.log(fpath); 74 | var input = fs.readFileSync(fpath, "utf8"); 75 | var output; 76 | try { 77 | output = jsdocFlow(input); 78 | } 79 | catch(err) { 80 | // if we fail to convert and --skip is enabled, then just copy 81 | // the input file as-is. 82 | if (opts.skip) { 83 | console.log(" Failed to parse: " + err); 84 | output = input; 85 | } 86 | else { 87 | throw err; 88 | } 89 | } 90 | fs.writeFileSync(outFilePath, output); 91 | }); 92 | }); 93 | } 94 | else { 95 | console.log("Convert JSDoc comments in a file to Flow annotations"); 96 | console.log("Usage:"); 97 | console.log(" flow-jsdoc -f FILEPATH"); 98 | console.log(" flow-jsdoc -d PATH -o PATH [-c]"); 99 | console.log("Options:"); 100 | console.log(" -f, --file FILEPATH The .js file with JSDoc to convert. Prints to stdout."); 101 | console.log(" -d, --directory PATH The directory with .js files to convert. Will inspect recursively."); 102 | console.log(" -o, --outdir PATH Required if -d is set. The output directory to dump flow-annotated files to."); 103 | console.log(" -c, --copy Set to copy other file extensions across to outdir."); 104 | console.log(" -s, --skip Set to copy over .js files which cannot be parsed correctly (e.g. invalid ES6) to outdir."); 105 | console.log("File Usage:\n flow-jsdoc -f path/to/file.js"); 106 | console.log("Directory Usage:\n flow-jsdoc -d path/to/dir -o out/dir -c"); 107 | process.exit(0); 108 | } 109 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var falafel = require("./lib/falafel"); 4 | var doctrine = require("doctrine"); // JSDoc parsing 5 | var fs = require("fs"); 6 | var util = require("util"); 7 | 8 | function jsdocTagToFlowTag(tag) { 9 | // console.log(util.inspect(tag)); 10 | return { 11 | loc: tag.title, //param|return 12 | name: tag.name, // the parameter name 13 | type: jsdocTypeToFlowType(tag.type) // the parameter type 14 | }; 15 | } 16 | 17 | /** 18 | * Extract formatted JSDoc from a comment. 19 | * @param {String} comment The comment which may have JSDoc in it. 20 | * @return {Object} With 'params' and 'return' arrays which have 'loc', 'name' 21 | * and 'type' elements. 22 | */ 23 | function extractJsdoc(comment) { 24 | var docAst = doctrine.parse(comment, { unwrap: true, sloppy: true }); 25 | if (!docAst.tags || docAst.tags.length === 0) { 26 | return null; 27 | } 28 | // only interested in @param @property, and @return 29 | var paramTags = docAst.tags.filter(function(tag) { 30 | return tag.title === "param"; 31 | }).map(jsdocTagToFlowTag); 32 | 33 | var returnTags = docAst.tags.filter(function(tag) { 34 | return tag.title === "return" || tag.title === "returns"; 35 | }).map(jsdocTagToFlowTag); 36 | 37 | var propTags = docAst.tags.filter(function(tag) { 38 | return tag.title === "property" || tag.title === "prop"; 39 | }).map(jsdocTagToFlowTag); 40 | 41 | return { 42 | params: paramTags, 43 | returns: returnTags, 44 | props: propTags 45 | }; 46 | } 47 | 48 | /** 49 | * Extract "formatted JSDoc" from an in-line comment. This is actually a 50 | * custom syntax specific to flow-jsdoc which looks like: 51 | * //: (string, number): Object 52 | * There needs to be a ":" to start it off (to avoid false positives). 53 | * Since there are no parameter names in this syntax form, the function 54 | * node is also required. 55 | * @return {Object} Exactly the same format as `extractJsdoc(comment)`. 56 | */ 57 | function extractInlineAnnotations(funcNode, comment) { 58 | comment = comment.trim(); 59 | if (comment[0] !== ":") { 60 | return null; // invalid syntax 61 | } 62 | // [ "", " (string, number)", " Object" ] 63 | var segments = comment.split(":"); 64 | var returnType = segments[2] ? segments[2].trim() : null; // optional, may be 'void' 65 | var paramBlock = segments[1].trim(); 66 | if (paramBlock[0] !== "(" || paramBlock[paramBlock.length-1] !== ")") { 67 | panicInline(funcNode, comment, 68 | "Expected inline comment to have ( ) around parameters" 69 | ); 70 | } 71 | paramBlock = paramBlock.slice(1, -1); // "string, number" 72 | var paramTypes = paramBlock.split(",").map(function(p) { 73 | return p.trim(); 74 | }).filter(function(p) { 75 | return p.length > 0; 76 | }); 77 | 78 | // Make sure the number of params in the comment == num in function 79 | if (paramTypes.length !== funcNode.params.length) { 80 | panicInline(funcNode, comment, 81 | "Inline comment has wrong number of parameters: " + paramTypes.length + 82 | ". Expected: " + funcNode.params.length 83 | ); 84 | process.exit(1); 85 | } 86 | 87 | var paramTags = funcNode.params.map(function(p, i) { 88 | return { 89 | loc: "param", 90 | name: p.name, 91 | type: paramTypes[i] 92 | }; 93 | }); 94 | var returnTags = []; 95 | if (returnType) { 96 | returnTags = [{ 97 | loc: "return", 98 | type: returnType 99 | }]; 100 | } 101 | 102 | return { 103 | params: paramTags, 104 | returns: returnTags, 105 | props: [] 106 | }; 107 | } 108 | 109 | function panicInline(funcNode, comment, msg) { 110 | console.error( 111 | msg + "\n" + 112 | " " + comment + "\n" + 113 | " " + funcNode.source().split("\n")[0] // just the function header 114 | ); 115 | process.exit(1); 116 | } 117 | 118 | function jsdocTypeToFlowType(jsdocType) { 119 | if (!jsdocType || !jsdocType.type) { 120 | return; 121 | } 122 | switch(jsdocType.type) { 123 | case "NameExpression": // {string} 124 | return jsdocType.name; 125 | case "TypeApplication": // {Foo} 126 | // e.g. 'Array' in Array 127 | var baseType = jsdocTypeToFlowType(jsdocType.expression); 128 | // Flow only supports single types for generics 129 | var specificType = jsdocTypeToFlowType(jsdocType.applications[0]); 130 | if (baseType && specificType) { 131 | return baseType + "<" + specificType + ">"; 132 | } 133 | break; 134 | case "UnionType": // {(Object|String)} 135 | var types = jsdocType.elements.map(function(t) { 136 | return jsdocTypeToFlowType(t); 137 | }); 138 | return types.join(" | "); 139 | case "AllLiteral": // {*} 140 | return "any"; 141 | case "OptionalType": // {string=} 142 | case "NullableType": // {?string} 143 | return "?" + jsdocTypeToFlowType(jsdocType.expression); 144 | default: 145 | // console.log("Unknown jsdoc type: %s", JSON.stringify(jsdocType)); 146 | break; 147 | } 148 | } 149 | 150 | /** 151 | * Retrieve a function node along with parsed JSDoc comments for it. 152 | * @param {Node} node The node to inspect. 153 | * @return {?Object} An object with "jsdoc" and "node" keys, or null. 154 | */ 155 | function getCommentedFunctionNode(node) { 156 | if (!node.leadingComments) { 157 | // JSDoc comments are always before the function, so if there is 158 | // nothing here, we ain't interested. 159 | return null; 160 | } 161 | // console.log("================="); 162 | // console.log("type: " + node.type); 163 | // console.log(util.inspect(node)); 164 | /* 165 | * We handle the following function representations: 166 | * 167 | * Type Path to Function Example 168 | * ========================================================================================== 169 | * FunctionDeclaration - function foo(bar) {} 170 | * VariableDeclaration .declarations[0].init var foo = function(bar) {} 171 | * ExpressionStatement .expression.right ObjClass.prototype.foo = function(bar) {} 172 | * MethodDefinition .value class ObjClass { foo(bar) {} } 173 | * Property .value var obj = { key: function(bar) {} } 174 | * ReturnStatement .argument return function(foo, bar) {} 175 | * ArrowFunctionExpression - (foo, bar) => {} 176 | * ExportNamedDeclaration .declaration export function foo(bar) {} 177 | * 178 | */ 179 | var nodeTypes = [ 180 | "FunctionDeclaration", "ExpressionStatement", "VariableDeclaration", 181 | "MethodDefinition", "Property", "ReturnStatement", "ArrowFunctionExpression", 182 | "ExportNamedDeclaration" 183 | ]; 184 | if (nodeTypes.indexOf(node.type) === -1) { 185 | return null; 186 | } 187 | var funcNode = null; 188 | switch (node.type) { 189 | case "FunctionDeclaration": 190 | case "ArrowFunctionExpression": 191 | funcNode = node; 192 | break; 193 | case "VariableDeclaration": 194 | funcNode = node.declarations[0].init; 195 | break; 196 | case "ExpressionStatement": 197 | funcNode = node.expression.right; 198 | break; 199 | case "MethodDefinition": 200 | funcNode = node.value; 201 | break; 202 | case "Property": 203 | funcNode = node.value; 204 | break; 205 | case "ReturnStatement": 206 | funcNode = node.argument; 207 | break; 208 | case "ExportNamedDeclaration": 209 | var declaration = node.declaration; 210 | if (declaration.type === 'VariableDeclaration') { 211 | funcNode = declaration.declarations[0].init; 212 | } else { 213 | funcNode = declaration 214 | } 215 | break; 216 | } 217 | var funcNodeTypes = ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]; 218 | if (!funcNode || funcNodeTypes.indexOf(funcNode.type) === -1) { 219 | // We can't find a function here which can map to leadingComments. 220 | return null; 221 | } 222 | var funcDocs = null; 223 | for (var i=0; i 329 | // whereas we actually want it BEFORE the => 330 | // Sadly, there isn't a nice way to access the bit before the =>. We do 331 | // however know that the node source is the entire function, and the node 332 | // body is the stuff between the { }. We can therefore grab the function 333 | // header portion and find/replace on the fat arrow. 334 | var funcHeader = funcNode.node.source().replace(funcNode.node.body.source(), ""); 335 | var arrowIndex = funcHeader.lastIndexOf("=>"); 336 | if (arrowIndex === -1) { 337 | // well this was unexpected... 338 | return; 339 | } 340 | // inject annotation before the arrow 341 | funcHeader = ( 342 | funcHeader.slice(0, arrowIndex) + ": " + returnDoc.type + " " + funcHeader.slice(arrowIndex) 343 | ); 344 | // replace the entire function to replace the header portion 345 | funcNode.node.update( 346 | funcHeader + funcNode.node.body.source() 347 | ); 348 | } 349 | else { 350 | funcNode.node.body.update( 351 | ": " + returnDoc.type + " " + funcNode.node.body.source() 352 | ); 353 | } 354 | } 355 | } 356 | 357 | /** 358 | * Modify ES6 classes by adding 'field declarations' to them if the constructor has JSDoc. 359 | */ 360 | function decorateClasses(node) { 361 | // check for class nodes 362 | var clsNode = getCommentedClassNode(node); 363 | if (!clsNode || !clsNode.jsdoc || clsNode.jsdoc.props.length === 0) { 364 | return; 365 | } 366 | 367 | var clsSrc = clsNode.node.source(); 368 | if (clsSrc[0] !== "{") { 369 | // something isn't right, bail. 370 | return; 371 | } 372 | 373 | // work out line endings (find first \n and see if it has \r before it) 374 | var nl = "\n"; 375 | var newlineIndex = clsSrc.indexOf("\n"); 376 | if (clsSrc[newlineIndex-1] === "\r") { 377 | nl = "\r\n"; 378 | } 379 | 380 | // use the same indent as the next non-blank line 381 | var indent = ""; 382 | var lines = clsSrc.split(nl); 383 | for (var i = 1; i < lines.length; i++) { //i=1 to skip the starting { 384 | if (lines[i].length > 0) { 385 | var whitespaceMatch = /^[ \t]+/.exec(lines[i]); // match spaces or tabs 386 | if (whitespaceMatch) { 387 | indent = whitespaceMatch[0]; 388 | break; 389 | } 390 | } 391 | } 392 | 393 | // work out what to inject into the class definition 394 | var fieldTypeDecls = clsNode.jsdoc.props.map(function(p) { 395 | return indent + p.name + ": " + p.type + ";"; 396 | }).join(nl); 397 | 398 | clsNode.node.update("{" + nl + fieldTypeDecls + clsSrc.substr(1)); 399 | } 400 | 401 | module.exports = function(src, opts) { 402 | opts = opts || {}; 403 | 404 | // Walk the AST. 405 | // Esprima has an undocumented 'attachComment' option which binds comments 406 | // to the nodes in the AST. 407 | var output = falafel(src, {attachComment: true, jsx: true, sourceType: "module"}, function (node) { 408 | decorateFunctions(node); 409 | decorateClasses(node); 410 | }); 411 | 412 | return output; 413 | }; 414 | -------------------------------------------------------------------------------- /lib/falafel.js: -------------------------------------------------------------------------------- 1 | // This is https://github.com/jareware/node-falafel 2 | // Using normal 'falafel' from npm isn't producing node.source() correctly; it is giving the entire file. 3 | var parse = require('esprima').parse; 4 | var objectKeys = Object.keys || function (obj) { 5 | var keys = []; 6 | for (var key in obj) keys.push(key); 7 | return keys; 8 | }; 9 | var forEach = function (xs, fn) { 10 | if (xs.forEach) return xs.forEach(fn); 11 | for (var i = 0; i < xs.length; i++) { 12 | fn.call(xs, xs[i], i, xs); 13 | } 14 | }; 15 | 16 | var isArray = Array.isArray || function (xs) { 17 | return Object.prototype.toString.call(xs) === '[object Array]'; 18 | }; 19 | 20 | module.exports = function (src, opts, fn) { 21 | if (typeof opts === 'function') { 22 | fn = opts; 23 | opts = {}; 24 | } 25 | if (typeof src === 'object') { 26 | opts = src; 27 | src = opts.source; 28 | delete opts.source; 29 | } 30 | src = src === undefined ? opts.source : src; 31 | opts.range = true; 32 | if (typeof src !== 'string') src = String(src); 33 | 34 | var ast = (opts.parse || parse)(src, opts); 35 | 36 | var result = { 37 | chunks : src.split(''), 38 | toString : function () { return result.chunks.join('') }, 39 | inspect : function () { return result.toString() } 40 | }; 41 | var index = 0; 42 | 43 | (function walk (node, parent) { 44 | insertHelpers(node, parent, result.chunks); 45 | 46 | forEach(objectKeys(node), function (key) { 47 | if (key === 'parent') return; 48 | 49 | var child = node[key]; 50 | if (isArray(child)) { 51 | forEach(child, function (c) { 52 | if (c && typeof c.type === 'string') { 53 | walk(c, node); 54 | } 55 | }); 56 | } 57 | else if (child && typeof child.type === 'string') { 58 | insertHelpers(child, node, result.chunks); 59 | walk(child, node); 60 | } 61 | }); 62 | fn(node); 63 | })(ast, undefined); 64 | 65 | return result; 66 | }; 67 | 68 | function insertHelpers (node, parent, chunks) { 69 | if (!node.range) return; 70 | 71 | node.parent = parent; 72 | 73 | node.source = function () { 74 | return chunks.slice( 75 | node.range[0], node.range[1] 76 | ).join(''); 77 | }; 78 | 79 | if (node.update && typeof node.update === 'object') { 80 | var prev = node.update; 81 | forEach(objectKeys(prev), function (key) { 82 | update[key] = prev[key]; 83 | }); 84 | node.update = update; 85 | } 86 | else { 87 | node.update = update; 88 | } 89 | 90 | function update (s) { 91 | chunks[node.range[0]] = s; 92 | for (var i = node.range[0] + 1; i < node.range[1]; i++) { 93 | chunks[i] = ''; 94 | } 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-jsdoc", 3 | "version": "0.3.1", 4 | "description": "Represent flow type annotations with JSDoc syntax", 5 | "main": "index.js", 6 | "dependencies": { 7 | "doctrine": "^0.6.4", 8 | "esprima": "^4.0.0", 9 | "glob": "^5.0.14", 10 | "mkdirp": "^0.5.1", 11 | "nopt": "^3.0.3" 12 | }, 13 | "repository": { 14 | "url": "https://github.com/Kegsay/flow-jsdoc" 15 | }, 16 | "devDependencies": {}, 17 | "scripts": { 18 | "test": "node tests/runner.js" 19 | }, 20 | "bin": { 21 | "flow-jsdoc": "./app.js" 22 | }, 23 | "keywords": [ 24 | "flowtype", 25 | "jsdoc3" 26 | ], 27 | "author": "Kegan Dougal", 28 | "license": "Apache-2.0" 29 | } 30 | -------------------------------------------------------------------------------- /tests/expected_output/01-object-literal.js: -------------------------------------------------------------------------------- 1 | var obj = { 2 | /** 3 | * Some description 4 | * @param {number} key The key! 5 | * @return {Object} The lock! 6 | */ 7 | foo: function(key: number) : Object { 8 | return {}; 9 | }, 10 | 11 | /** 12 | * The answer to life, the universe, and everything. 13 | * @return {number} The answer 14 | */ 15 | bar: function(undocumented) : number { 16 | return 42; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /tests/expected_output/02-es6-class.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | /** 3 | * Construct a Foo. 4 | * @constructor 5 | * @param {string} bar The bar 6 | */ 7 | constructor(bar: string) { 8 | this.bar = bar; 9 | } 10 | 11 | /** 12 | * Do baz 13 | * @param {string=} key Optional key 14 | * @return {?Object} 15 | */ 16 | baz(key: ?string) : ?Object { 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/expected_output/03-stand-alone-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Foobar[]} bar A foobar array 3 | * @param {Function} baz 4 | * @return {number} 5 | */ 6 | function foo(bar: Array, baz: Function) : number { 7 | return 42; 8 | } 9 | 10 | /** 11 | * @returns {number} 12 | */ 13 | function bar() : number { 14 | return 42; 15 | } -------------------------------------------------------------------------------- /tests/expected_output/04-function-as-var.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} bar 3 | * @return {Array} 4 | */ 5 | var foo = function(bar: any, baz) : Array { 6 | return ["hello", "world"]; 7 | }; -------------------------------------------------------------------------------- /tests/expected_output/05-prototype-class.js: -------------------------------------------------------------------------------- 1 | function Foo() { 2 | this.data = {} 3 | } 4 | 5 | /** 6 | * Bar 7 | * @param {boolean} baz 8 | * @return {Object} 9 | */ 10 | Foo.prototype.bar = function(baz: boolean) : Object { 11 | return this.data; 12 | }; 13 | -------------------------------------------------------------------------------- /tests/expected_output/06-nested-functions.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {string} bar 4 | * @param {Function} fn 5 | * @return {Function} 6 | */ 7 | function makeMoreFunctions(bar: string, fn: Function) : Function { 8 | /** 9 | * @param {Object} baz 10 | * @return {string} 11 | */ 12 | return function(baz: Object) : string { 13 | fn(); 14 | /** 15 | * @return {string} 16 | */ 17 | return function() : string { 18 | return bar; 19 | }; 20 | }; 21 | } -------------------------------------------------------------------------------- /tests/expected_output/07-es6-class-field-decls.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | bar: string; 3 | bars: Array; 4 | 5 | 6 | /** 7 | * Oh no a function before the constructor! 8 | * @param {string=} key Optional key 9 | * @prop {number} trap It's a trap! This isn't a constructor! 10 | * @return {?Object} 11 | */ 12 | baz(key: ?string) : ?Object { 13 | return null; 14 | } 15 | 16 | /** 17 | * Construct a Foo. 18 | * @property {string} bar 19 | * @prop {Object[]} bars Support prop tag alias 20 | */ 21 | constructor(bar) { 22 | this.bar = bar; 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/expected_output/08-inline-syntax.js: -------------------------------------------------------------------------------- 1 | 2 | function foo(a: string, b: string, c: Promise) : number { 3 | return 99; 4 | } 5 | 6 | 7 | 8 | var bar = function() : Person { 9 | return {}; 10 | } 11 | 12 | 13 | // (number, string): NoColon 14 | var ignore = function(a, b) { 15 | 16 | } 17 | 18 | 19 | function quuz(a: string) { 20 | 21 | } 22 | 23 | var obj = { 24 | 25 | 26 | bar: function(undocumented: string) : number { 27 | return 42; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/expected_output/09-jsx.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Foo { 4 | 5 | /** 6 | * @return {boolean} Always true 7 | */ 8 | canParseJsx() : boolean { 9 | return true; 10 | } 11 | 12 | render() { 13 | return
14 | Hello world! 15 |
16 | Click here! 17 |
18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tests/expected_output/10-es6-fat-arrows.js: -------------------------------------------------------------------------------- 1 | thingWithCallback( 2 | /** 3 | * @param {Foobar[]} bar A foobar array 4 | * @param {Function} baz 5 | * @return {number} 6 | */ 7 | (bar: Array, baz: Function) : number => { 8 | return 42; 9 | } 10 | ); 11 | 12 | const obj = { 13 | 14 | /** 15 | * @function 16 | * @param {string} a 17 | * @param {string} b 18 | * @return {number} 19 | */ 20 | func: (a: string, b: string) : number => { 21 | return 1; 22 | }, 23 | 24 | }; -------------------------------------------------------------------------------- /tests/expected_output/11-optional-params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} foo 3 | * @param {string} [bar] 4 | * @param {string=} baz some blurb 5 | * @param {string} [quuz=Nope Nope Nope] more blurb 6 | * @return {number} 7 | */ 8 | function allTheOptionalForms(foo: string, bar: ?string, baz: ?string, quuz: ?string) : number { 9 | return 4; 10 | }; 11 | 12 | /** 13 | * @param {string} foo 14 | * @param {string} [bar] 15 | * @param {string=} baz some blurb 16 | * @param {string} [quuz=Nope Nope Nope] more blurb 17 | * @return {number} 18 | */ 19 | function allTheOptionalFormsWithDefaults(foo: string, bar: ?string, baz: string = 'Nope', quuz: string = 'Nope Nope Nope') : number { 20 | return 4; 21 | }; 22 | 23 | /** 24 | * @param {number} foo 25 | * @return {number} 26 | */ 27 | function optionalParamWithWrongJSdoc(foo: number = '4') : number { 28 | return Number(foo); 29 | }; -------------------------------------------------------------------------------- /tests/expected_output/12-es6-export-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Foobar[]} bar A foobar array 3 | * @param {Function} baz 4 | * @return {number} 5 | */ 6 | export function foo(bar: Array, baz: Function) : number { 7 | return 42; 8 | } 9 | -------------------------------------------------------------------------------- /tests/expected_output/13-es6-export-const-fat-arrow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Foobar[]} bar A foobar array 3 | * @param {Function} baz 4 | * @return {number} 5 | */ 6 | export const foo = (bar: Array, baz: Function) : number => { 7 | return 42; 8 | } 9 | -------------------------------------------------------------------------------- /tests/input/01-object-literal.js: -------------------------------------------------------------------------------- 1 | var obj = { 2 | /** 3 | * Some description 4 | * @param {number} key The key! 5 | * @return {Object} The lock! 6 | */ 7 | foo: function(key) { 8 | return {}; 9 | }, 10 | 11 | /** 12 | * The answer to life, the universe, and everything. 13 | * @return {number} The answer 14 | */ 15 | bar: function(undocumented) { 16 | return 42; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /tests/input/02-es6-class.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | /** 3 | * Construct a Foo. 4 | * @constructor 5 | * @param {string} bar The bar 6 | */ 7 | constructor(bar) { 8 | this.bar = bar; 9 | } 10 | 11 | /** 12 | * Do baz 13 | * @param {string=} key Optional key 14 | * @return {?Object} 15 | */ 16 | baz(key) { 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/input/03-stand-alone-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Foobar[]} bar A foobar array 3 | * @param {Function} baz 4 | * @return {number} 5 | */ 6 | function foo(bar, baz) { 7 | return 42; 8 | } 9 | 10 | /** 11 | * @returns {number} 12 | */ 13 | function bar() { 14 | return 42; 15 | } -------------------------------------------------------------------------------- /tests/input/04-function-as-var.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} bar 3 | * @return {Array} 4 | */ 5 | var foo = function(bar, baz) { 6 | return ["hello", "world"]; 7 | }; -------------------------------------------------------------------------------- /tests/input/05-prototype-class.js: -------------------------------------------------------------------------------- 1 | function Foo() { 2 | this.data = {} 3 | } 4 | 5 | /** 6 | * Bar 7 | * @param {boolean} baz 8 | * @return {Object} 9 | */ 10 | Foo.prototype.bar = function(baz) { 11 | return this.data; 12 | }; 13 | -------------------------------------------------------------------------------- /tests/input/06-nested-functions.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {string} bar 4 | * @param {Function} fn 5 | * @return {Function} 6 | */ 7 | function makeMoreFunctions(bar, fn) { 8 | /** 9 | * @param {Object} baz 10 | * @return {string} 11 | */ 12 | return function(baz) { 13 | fn(); 14 | /** 15 | * @return {string} 16 | */ 17 | return function() { 18 | return bar; 19 | }; 20 | }; 21 | } -------------------------------------------------------------------------------- /tests/input/07-es6-class-field-decls.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | 3 | 4 | /** 5 | * Oh no a function before the constructor! 6 | * @param {string=} key Optional key 7 | * @prop {number} trap It's a trap! This isn't a constructor! 8 | * @return {?Object} 9 | */ 10 | baz(key) { 11 | return null; 12 | } 13 | 14 | /** 15 | * Construct a Foo. 16 | * @property {string} bar 17 | * @prop {Object[]} bars Support prop tag alias 18 | */ 19 | constructor(bar) { 20 | this.bar = bar; 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/input/08-inline-syntax.js: -------------------------------------------------------------------------------- 1 | //: (string, string, Promise) : number 2 | function foo(a, b, c) { 3 | return 99; 4 | } 5 | 6 | 7 | //: () : Person 8 | var bar = function() { 9 | return {}; 10 | } 11 | 12 | 13 | // (number, string): NoColon 14 | var ignore = function(a, b) { 15 | 16 | } 17 | 18 | /*:(string)*/ 19 | function quuz(a) { 20 | 21 | } 22 | 23 | var obj = { 24 | 25 | //: (string): number 26 | bar: function(undocumented) { 27 | return 42; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/input/09-jsx.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Foo { 4 | 5 | /** 6 | * @return {boolean} Always true 7 | */ 8 | canParseJsx() { 9 | return true; 10 | } 11 | 12 | render() { 13 | return
14 | Hello world! 15 |
16 | Click here! 17 |
18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tests/input/10-es6-fat-arrows.js: -------------------------------------------------------------------------------- 1 | thingWithCallback( 2 | /** 3 | * @param {Foobar[]} bar A foobar array 4 | * @param {Function} baz 5 | * @return {number} 6 | */ 7 | (bar, baz) => { 8 | return 42; 9 | } 10 | ); 11 | 12 | const obj = { 13 | 14 | /** 15 | * @function 16 | * @param {string} a 17 | * @param {string} b 18 | * @return {number} 19 | */ 20 | func: (a, b) => { 21 | return 1; 22 | }, 23 | 24 | }; -------------------------------------------------------------------------------- /tests/input/11-optional-params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} foo 3 | * @param {string} [bar] 4 | * @param {string=} baz some blurb 5 | * @param {string} [quuz=Nope Nope Nope] more blurb 6 | * @return {number} 7 | */ 8 | function allTheOptionalForms(foo, bar, baz, quuz) { 9 | return 4; 10 | }; 11 | 12 | /** 13 | * @param {string} foo 14 | * @param {string} [bar] 15 | * @param {string=} baz some blurb 16 | * @param {string} [quuz=Nope Nope Nope] more blurb 17 | * @return {number} 18 | */ 19 | function allTheOptionalFormsWithDefaults(foo, bar, baz = 'Nope', quuz = 'Nope Nope Nope') { 20 | return 4; 21 | }; 22 | 23 | /** 24 | * @param {number} foo 25 | * @return {number} 26 | */ 27 | function optionalParamWithWrongJSdoc(foo = '4') { 28 | return Number(foo); 29 | }; -------------------------------------------------------------------------------- /tests/input/12-es6-export-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Foobar[]} bar A foobar array 3 | * @param {Function} baz 4 | * @return {number} 5 | */ 6 | export function foo(bar, baz) { 7 | return 42; 8 | } 9 | -------------------------------------------------------------------------------- /tests/input/13-es6-export-const-fat-arrow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Foobar[]} bar A foobar array 3 | * @param {Function} baz 4 | * @return {number} 5 | */ 6 | export const foo = (bar, baz) => { 7 | return 42; 8 | } 9 | -------------------------------------------------------------------------------- /tests/runner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var fs = require("fs"); 3 | var glob = require("glob"); 4 | var path = require("path"); 5 | var jsdocFlow = require("../index"); 6 | 7 | var inputDir = path.join(__dirname, "/input"); 8 | var outputDir = path.join(__dirname, "/expected_output"); 9 | 10 | var RED = "\x1b[41m"; 11 | var RESET = "\x1b[49m"; 12 | 13 | glob(inputDir + "/*.js", function(err, files) { 14 | if (err) { 15 | console.error(err); 16 | process.exit(1); 17 | return; 18 | } 19 | var exitCode = 0; 20 | var numFails = 0; 21 | files.forEach(function(f) { 22 | f = path.resolve(f); // convert \ to / so we can replace with out dir 23 | var outFile = f.replace(inputDir, outputDir); 24 | var expectedOutput = fs.readFileSync(outFile, "utf8"); 25 | var actualOutput = new String(jsdocFlow(fs.readFileSync(f, "utf8"))); 26 | // console.log("EXPECT: " + expectedOutput); 27 | // console.log("ACTUAL: " + actualOutput); 28 | var actualOutLines = actualOutput.split("\n"); 29 | var expectedOutLines = expectedOutput.split("\n"); 30 | if (actualOutLines.length === expectedOutLines.length) { 31 | for (var lineNum = 0; lineNum < actualOutLines.length; lineNum++) { 32 | if (actualOutLines[lineNum] === expectedOutLines[lineNum]) { 33 | continue; 34 | } 35 | var lineLength = Math.max(actualOutLines[lineNum].length, expectedOutLines[lineNum].length); 36 | for (var charNum = 0; charNum < lineLength; charNum++) { 37 | if (actualOutLines[lineNum][charNum] === expectedOutLines[lineNum][charNum]) { 38 | continue; 39 | } 40 | var badChar = escape(actualOutLines[lineNum][charNum]); 41 | var actualSnippet = snippet(actualOutLines[lineNum], charNum); 42 | var expectedSnippet = snippet(expectedOutLines[lineNum], charNum); 43 | 44 | console.error( 45 | "%s:%s:%s %s", outFile, lineNum + 1, charNum + 1, 46 | "Unexpected char: '" + badChar + "'. " + 47 | "Got \"" + actualSnippet + "\", expected \"" + expectedSnippet + "\"." 48 | ); 49 | numFails += 1; 50 | break; 51 | } 52 | } 53 | } 54 | else { 55 | console.error( 56 | "%s:%s %s", outFile, 0, 57 | "Expected " + expectedOutLines.length + " lines, got " + actualOutLines.length 58 | ); 59 | numFails += 1; 60 | } 61 | }); 62 | console.log("%s test failures out of %s tests.", numFails, files.length); 63 | process.exit(numFails > 0 ? 1 : 0); 64 | }); 65 | 66 | function snippet(line, charNum) { 67 | var leftMinSnip = Math.max(charNum - 15, 0); 68 | var leftMaxSnip = Math.max(charNum, 0); 69 | var rightMinSnip = Math.min(charNum + 1, line.length); 70 | var rightMaxSnip = Math.min(charNum + 15, line.length); 71 | return ( 72 | escape(line.substring(leftMinSnip, leftMaxSnip)) + 73 | RED + escape(line[charNum]) + RESET + 74 | escape(line.substring(rightMinSnip, rightMaxSnip)) 75 | ); 76 | } 77 | 78 | function escape(thing) { 79 | if (thing === undefined) { 80 | return ""; 81 | } 82 | var escapedThing = JSON.stringify(thing); 83 | return escapedThing.substring(1, escapedThing.length-1); 84 | } 85 | --------------------------------------------------------------------------------