├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.js ├── module-reader.js ├── tsmorph-utils.js └── type-definition-parser.js └── tests ├── __snapshots__ └── index.test.js.snap ├── example ├── human.js ├── index.js ├── mutant.js └── power.js └── index.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .vscode 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "12" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## 0.0.10 - 2021-06-11 [YANKED] 10 | 11 | ## 0.0.9 - 2021-02-24 [YANKED] 12 | 13 | ## 0.0.8 - 2021-02-24 [YANKED] 14 | 15 | ## 0.0.7 - 2021-02-01 [YANKED] 16 | 17 | ## 0.0.6 - 2021-02-01 [YANKED] 18 | 19 | ## 0.0.5 - 2021-02-01 [YANKED] 20 | 21 | ## 0.0.4 - 2021-02-01 [YANKED] 22 | 23 | ## 0.0.3 - 2021-02-01 [YANKED] 24 | 25 | ## 0.0.2 - 2021-01-27 [YANKED] 26 | [Unreleased]: https://github.com/geut/jsdast/compare/v0.0.10...HEAD 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to jsdast 2 | 3 | ## Issue Contributions 4 | 5 | When opening new issues or commenting on existing issues on this repository 6 | please make sure discussions are related to concrete technical issues. 7 | 8 | Try to be *friendly* (we are not animals :monkey: or bad people :rage4:) and explain correctly how we can reproduce your issue. 9 | 10 | ## Code Contributions 11 | 12 | This document will guide you through the contribution process. 13 | 14 | ### Step 1: Fork 15 | 16 | Fork the project [on GitHub](https://github.com/geut/jsdast) and check out your copy locally. 17 | 18 | ```bash 19 | $ git clone git@github.com:username/jsdast.git 20 | $ cd jsdast 21 | $ npm install 22 | $ git remote add upstream git://github.com/geut/jsdast.git 23 | ``` 24 | 25 | ### Step 2: Branch 26 | 27 | Create a feature branch and start hacking: 28 | 29 | ```bash 30 | $ git checkout -b my-feature-branch -t origin/main 31 | ``` 32 | 33 | ### Step 3: Test 34 | 35 | Bug fixes and features **should come with tests**. We use [jest](https://jestjs.io/) to do that. 36 | 37 | ```bash 38 | $ npm test 39 | ``` 40 | 41 | ### Step 4: Lint 42 | 43 | Make sure the linter is happy and that all tests pass. Please, do not submit 44 | patches that fail either check. 45 | 46 | We use [standard](https://standardjs.com/) 47 | 48 | ### Step 5: Commit 49 | 50 | Make sure git knows your name and email address: 51 | 52 | ```bash 53 | $ git config --global user.name "Bruce Wayne" 54 | $ git config --global user.email "bruce@batman.com" 55 | ``` 56 | 57 | Writing good commit logs is important. A commit log should describe what 58 | changed and why. 59 | 60 | ### Step 6: Changelog 61 | 62 | If your changes are really important for the project probably the users want to know about it. 63 | 64 | We use [chan](https://github.com/geut/chan/) to maintain a well readable changelog for our users. 65 | 66 | ### Step 7: Push 67 | 68 | ```bash 69 | $ git push origin my-feature-branch 70 | ``` 71 | 72 | ### Step 8: Make a pull request ;) 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GEUT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsdast 2 | Syntax tree JSDoc based on Unist spec 3 | 4 | [![Build Status](https://travis-ci.com/geut/jsdast.svg?token=yjuaa2ubUupz6ACajDgF&branch=main)](https://travis-ci.com/geut/jsdast) 5 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 7 | 8 | [![Made by GEUT][geut-badge]][geut-url] 9 | 10 | ## Install 11 | 12 | ``` 13 | $ npm install @geut/jsdast 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```javascript 19 | const unified = require('unified') 20 | const { parser } = require('@geut/jsdast') 21 | 22 | const tree = unified().use(parser).parse(` 23 | /** 24 | * @param {number} a 25 | * @param {number} b 26 | * @returns {number} 27 | */ 28 | function sum(a, b) { 29 | return a + b 30 | } 31 | `) 32 | 33 | console.log(JSON.stringify(tree, null, 2)) 34 | 35 | /* 36 | { 37 | "type": "Root", 38 | "children": [ 39 | { 40 | "type": "Module", 41 | "name": "Index", 42 | "doc": { 43 | "description": "", 44 | "tags": [] 45 | }, 46 | "children": [ 47 | { 48 | "type": "FunctionDeclaration", 49 | "name": "sum", 50 | "doc": { 51 | "tags": [ 52 | { 53 | "tagName": "returns", 54 | "fullText": "@returns {number}", 55 | "typeExpression": "number" 56 | } 57 | ] 58 | }, 59 | "isExported": false, 60 | "isDefaultExport": false, 61 | "valueType": "number", 62 | "isGenerator": false, 63 | "isAsync": false, 64 | "children": [ 65 | { 66 | "type": "Parameter", 67 | "name": "a", 68 | "doc": { 69 | "tags": [] 70 | }, 71 | "isRestParameter": false, 72 | "valueType": "number", 73 | "isOptional": false 74 | }, 75 | { 76 | "type": "Parameter", 77 | "name": "b", 78 | "doc": { 79 | "tags": [] 80 | }, 81 | "isRestParameter": false, 82 | "valueType": "number", 83 | "isOptional": false 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | */ 92 | ``` 93 | 94 | 95 | ## Issues 96 | 97 | :bug: If you found an issue we encourage you to report it on [github](https://github.com/geut/jsdast/issues). Please specify your OS and the actions to reproduce it. 98 | 99 | ## Contributing 100 | 101 | :busts_in_silhouette: Ideas and contributions to the project are welcome. You must follow this [guideline](https://github.com/geut/jsdast/blob/main/CONTRIBUTING.md). 102 | 103 | ## License 104 | 105 | MIT © A [**GEUT**](http://geutstudio.com/) project 106 | 107 | [geut-url]: https://geutstudio.com 108 | [geut-badge]: https://img.shields.io/badge/Made%20By-GEUT-4f5186?style=for-the-badge&link=https://geutstudio.com&labelColor=white&logo=data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCABAAEADASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAABAYDBQACBwH/xAA0EAACAQMBBAcGBgMAAAAAAAABAgMABBEFBhIhQRMiMVFhgcEUIzJxkbFCUmKh0fAkcuH/xAAYAQADAQEAAAAAAAAAAAAAAAABAwQCAP/EACARAAMAAwACAgMAAAAAAAAAAAABAgMRIRIxBEEiM1H/2gAMAwEAAhEDEQA/AOgVlau6xoXdgqqMkk8AKV9U2oYs0WngBRw6VhxPyFamXXoDeiz1PUbmzuujQIUKgjIqGLXnz72FSO9TikfVbi6uXWSSaWRuzixNBx3VzCepNIvgTw+hpjwv+iGr3tM6xa30F2PdP1uangRRNc70fUbi4JLIVaPskXgM/wA076Ze+2W+WwJF4MPWlNaemajI2/GvYbWVlZQHCptZqLNKLGJsKoDSY5nkKorKzlvrlYIRlm5nsA7zWX8pnv55SfikJ/emPZGDcs7m6CguTuL5DPrVf64Me2F2mzNhAg6ZTO/MsSB9BW15s1pt1GVEPRHvQ+hqbTNT9sZ0kCpIOIA5ij5ZEijaSRgqqMkmpVkb7sMuWtoV73S49L3I4B7kjq57c881BZ6vFpuoKjq7dIvYBw8PtUOqX1xcSxoJXw8mQuewVW3vX1eFR+Fcn96OLVvpFzz8kM020kp4QwIvixzVpot5Je2bSTEFw5HAY7qUKadnIymm7x/G5I+3pTskzM8G4rqq6JGpI8E1wi8HR2H0NT7P6rcRKUEzYR9/czgEf0VabV2JgvhdKPdzdvg399aVG37K4Esfw/3hTU1S2NpNrSHqax9q/wAzTm3lY5KA4ZTQl2mo9CWljncL+cnA+tVVhqeSGt5mik5qDg/9o+XVb6aFonuDusMHqjP2qavjbfGTPX3xgTstrm4uGDSEYVV+woWPMKy3dzwd+JHcOQrdkgtyZpXJb87nJ8qqr68a7cKgIjB4DmadGNQjohs9i1C66Xqtvbx+EjIp10jaOMLBaPasDwRTGc5PyNJ1rb9EN5/jP7U17KaaZJvbpV6icI88z3+VG0vH8ipJJ8Ga8tIr22eCYZVh5g94pC1TTJtPmMU67yH4XxwYV0So54IriIxzRrIh7QwzSIyOTbWzlElkCcxtjwNedHeKMCVseDmnq72UgkJa1maL9LDeH81XvspfA9WSBh/sR6U9XD+zDQp+yTSNmR/MnJomG3SLiBlu80zQ7JXTH31xEg/Tlj6Vb2OzljaEO6meQc5OweVc8koOmUGjaFLfuss4MdsOOewv8v5p0ijSGNY41CoowAOQrbsr2p7t0zSWj//Z 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@geut/jsdast", 3 | "version": "0.0.10", 4 | "description": "Syntax tree JSDoc based on Unist spec", 5 | "main": "src/index.js", 6 | "files": [ 7 | "lib", 8 | "src", 9 | "bin", 10 | "index.js" 11 | ], 12 | "scripts": { 13 | "start": "node index.js", 14 | "test": "jest --passWithNoTests", 15 | "posttest": "npm run lint", 16 | "lint": "standard", 17 | "version": "chan release --allow-yanked ${npm_package_version} && git add .", 18 | "prepublishOnly": "npm test" 19 | }, 20 | "dependencies": { 21 | "fast-glob": "^3.2.5", 22 | "lodash.trim": "^4.5.1", 23 | "pascalcase": "^1.0.0", 24 | "ts-morph": "^9.1.0", 25 | "typescript": "^4.1.2", 26 | "unist-builder": "^2.0.3", 27 | "unist-util-parents": "^1.0.3" 28 | }, 29 | "devDependencies": { 30 | "@geut/chan": "^2.0.0", 31 | "jest": "^24.8.0", 32 | "standard": "^16.0.3", 33 | "to-vfile": "^6.1.0", 34 | "unified": "^9.2.0" 35 | }, 36 | "jest": { 37 | "testMatch": [ 38 | "**/tests/**/*.test.js" 39 | ] 40 | }, 41 | "standard": { 42 | "env": [ 43 | "jest", 44 | "node", 45 | "browser" 46 | ] 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/geut/jsdast.git" 51 | }, 52 | "keywords": [ 53 | "jsdoc", 54 | "tsdoc", 55 | "unist", 56 | "ast" 57 | ], 58 | "author": { 59 | "name": "GEUT", 60 | "email": "contact@geutstudio.com" 61 | }, 62 | "license": "MIT", 63 | "bugs": { 64 | "url": "https://github.com/geut/jsdast/issues" 65 | }, 66 | "homepage": "https://github.com/geut/jsdast#readme", 67 | "publishConfig": { 68 | "access": "public" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const TypeDefinitionParser = require('./type-definition-parser') 4 | 5 | function jsdastParser (options = {}) { 6 | const { type = 'js', ...parserOptions } = options 7 | 8 | const parser = new TypeDefinitionParser(parserOptions) 9 | 10 | this.Parser = function parse (_, vfile) { 11 | if (!vfile.path) { 12 | vfile.path = path.join(vfile.cwd, 'index.' + type) 13 | } 14 | 15 | return parser.run(vfile) 16 | } 17 | } 18 | 19 | module.exports = { parser: jsdastParser, TypeDefinitionParser } 20 | -------------------------------------------------------------------------------- /src/module-reader.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('ts-morph').SourceFile} SourceFile */ 2 | /** @typedef {import('ts-morph').JSDoc} JSDoc */ 3 | 4 | const assert = require('assert') 5 | const { Project, InMemoryFileSystemHost, Node } = require('ts-morph') 6 | const pascalcase = require('pascalcase') 7 | const crypto = require('crypto') 8 | const fg = require('fast-glob') 9 | const fs = require('fs') 10 | const { resolve: resolvePath } = require('path') 11 | 12 | const { parseTextTag, moduleTags, getJsDocStructure } = require('./tsmorph-utils') 13 | 14 | class ModuleFile { 15 | constructor (sourceFile, declarationFile) { 16 | /** @type {SourceFile} */ 17 | this.sourceFile = sourceFile 18 | /** @type {SourceFile} */ 19 | this.declarationFile = declarationFile 20 | /** @type {JSDoc} */ 21 | this.doc = null 22 | 23 | const statements = this.sourceFile.getStatementsWithComments() 24 | const documentation = statements.length > 0 && statements[0].getJsDocs && statements[0].getJsDocs()[0] 25 | if (documentation && documentation.getTags().find(t => moduleTags.includes(t.getTagName()))) { 26 | // module documentation 27 | this.doc = documentation 28 | } 29 | 30 | this.events = [] 31 | statements.forEach(statement => { 32 | if (!statement.getJsDocs) return 33 | const docs = statement.getJsDocs() 34 | docs.forEach(doc => { 35 | if (!doc.getTags().find(t => t.getTagName() === 'event')) { 36 | return 37 | } 38 | 39 | doc = getJsDocStructure(doc) 40 | const eventTag = doc.tags.find(t => t.tagName === 'event') 41 | doc.tags = doc.tags.filter(t => t.tagName !== 'event') 42 | const [target, name] = eventTag.text.split('#') 43 | doc.eventTarget = target 44 | doc.eventName = name 45 | this.events.push(doc) 46 | }) 47 | }) 48 | } 49 | 50 | get path () { 51 | return this.sourceFile.getFilePath() 52 | } 53 | 54 | get name () { 55 | const tagModule = this.doc && this.doc.getTags().find(t => t.getTagName() === 'module') 56 | return tagModule ? parseTextTag(tagModule) : pascalcase(this.sourceFile.getBaseName().split('.').slice(0, -1).join('.')) 57 | } 58 | 59 | get description () { 60 | return this.doc ? this.doc.getDescription() : '' 61 | } 62 | 63 | get tags () { 64 | return this.doc ? this.doc.getTags() : [] 65 | } 66 | } 67 | 68 | class ModuleReader { 69 | constructor (opts = {}) { 70 | this._opts = opts 71 | this._project = new Project({ 72 | ...this._opts, 73 | compilerOptions: { 74 | exclude: ['node_modules'], 75 | ...(this._opts.compilerOptions || {}), 76 | allowJs: true, 77 | declaration: true, 78 | esModuleInterop: true, 79 | resolveJsonModule: true 80 | }, 81 | fileSystem: new InMemoryFileSystemHost({ skipLoadingLibFiles: true }) 82 | }) 83 | this._fromProject = new Project({ 84 | ...this._opts, 85 | compilerOptions: { 86 | exclude: ['node_modules'], 87 | ...(this._opts.compilerOptions || {}), 88 | allowJs: true, 89 | declaration: true, 90 | esModuleInterop: true, 91 | resolveJsonModule: true 92 | } 93 | }) 94 | this._modules = new Map() 95 | } 96 | 97 | /** 98 | * @returns {Array} 99 | */ 100 | read (src) { 101 | const { path, contents } = src 102 | 103 | assert(path, 'path is required') 104 | 105 | fg.sync([path]).forEach(entry => { 106 | const filePath = resolvePath(entry) 107 | let text = fs.readFileSync(filePath, 'utf-8') 108 | // we have to add a last statement for bottom JSDoc documentations 109 | text += '\nfunction __JSDAST_END_OF_FILE__ () {}' 110 | this._fromProject.createSourceFile(path, text, { overwrite: true }) 111 | }) 112 | 113 | if (contents) { 114 | this._fromProject.createSourceFile(path, contents, { overwrite: true }) 115 | } 116 | 117 | this._fromProject.resolveSourceFileDependencies() 118 | 119 | const result = this._fromProject.emitToMemory({ emitOnlyDtsFiles: true }) 120 | for (const diagnostic of result.getDiagnostics()) { 121 | console.warning(diagnostic.getMessageText()) 122 | } 123 | 124 | for (let declarationFile of result.getFiles()) { 125 | const { filePath } = declarationFile 126 | const pattern = `${filePath.replace('.d.ts', '')}{.ts,.js,.jsx,.tsx}` 127 | const sourceFile = this._fromProject.getSourceFiles(pattern)[0] 128 | declarationFile = this._project.createSourceFile(filePath, declarationFile.text, { overwrite: true }) 129 | this._modules.set(sourceFile.getFilePath(), new ModuleFile(sourceFile, declarationFile)) 130 | } 131 | 132 | return Array.from(this._modules.values()) 133 | } 134 | 135 | getStatementFromText (text) { 136 | const sourceFile = this._fromProject.createSourceFile(`${crypto.randomBytes(32).toString('hex')}.js`, text, { overwrite: true }) 137 | 138 | const result = sourceFile.getEmitOutput({ emitOnlyDtsFiles: true }) 139 | let declarationFile = result.getOutputFiles()[0] 140 | declarationFile = this._project.createSourceFile(declarationFile.getFilePath(), declarationFile.getText(), { overwrite: true }) 141 | 142 | return { 143 | sourceFile, 144 | declarationFile, 145 | sourceStatement: sourceFile.forEachChild(child => child), 146 | declarationStatement: declarationFile.forEachChild(child => child) 147 | } 148 | } 149 | 150 | getJsDocFromText (text) { 151 | const info = this.getStatementFromText(text) 152 | return { doc: info.declarationFile.getFirstDescendantOrThrow(Node.isJSDocTag), ...info } 153 | } 154 | } 155 | 156 | module.exports = ModuleReader 157 | -------------------------------------------------------------------------------- /src/tsmorph-utils.js: -------------------------------------------------------------------------------- 1 | const { SyntaxKind } = require('ts-morph') 2 | const trim = require('lodash.trim') 3 | 4 | const moduleTags = ['module', 'remarks', 'privateRemarks', 'packageDocumentation'] 5 | 6 | const parseTextTag = (tag) => tag.getMessageText ? tag.getMessageText() : tag.getComment() 7 | 8 | const typeRegex = /\{(.*)\}/ 9 | 10 | const parseTags = (tag) => { 11 | const structure = tag.getStructure() 12 | 13 | const name = tag.getName && tag.getName() 14 | 15 | let typeExpression = tag.getTypeExpression && tag.getTypeExpression() 16 | typeExpression = typeExpression && tag.getTypeExpression().getTypeNode().getText() 17 | 18 | let text = tag.getMessageText ? tag.getMessageText() : tag.getComment() 19 | text = text && text.length > 0 ? text : structure.text 20 | 21 | if (text) { 22 | // example and remarks allows to define rich text inside, we ignore those tags 23 | if (!moduleTags.includes(structure.tagName) && structure.tagName !== 'example') { 24 | const matches = text.match(typeRegex) 25 | if (matches) { 26 | text = text.replace(matches[0], '').trim() 27 | if (!typeExpression) { 28 | typeExpression = matches[1] 29 | } 30 | } 31 | } 32 | 33 | const texts = text.split(' ') 34 | 35 | if (texts[0] === name || texts[0].startsWith(name) || texts[0].startsWith(`[${name}`)) { 36 | text = texts.slice(1).join(' ') 37 | } 38 | } 39 | 40 | return { 41 | tagName: tag.getTagName(), 42 | name, 43 | text: text && text.length > 0 ? trim(text, '\n ') : undefined, 44 | fullText: tag.getFullText(), 45 | typeExpression 46 | } 47 | } 48 | 49 | const getJsDoc = (node) => { 50 | if (!node.getJsDocs) return 51 | 52 | const docs = node.getJsDocs() 53 | if (docs.length === 0) return 54 | return docs[docs.length - 1] 55 | } 56 | 57 | const getJsDocStructure = (node, filter = () => true) => { 58 | const doc = node.getKind() === SyntaxKind.JSDocComment ? node : getJsDoc(node) 59 | if (!doc) return 60 | const description = trim(doc.getDescription(), '\n ') 61 | return { 62 | description: description.length > 0 ? description : undefined, 63 | tags: doc.getTags().map(parseTags).filter(filter) 64 | } 65 | } 66 | 67 | const getName = node => { 68 | if (node.getName) return node.getName() 69 | 70 | if (node.getKind() === SyntaxKind.VariableStatement) { 71 | return node.getStructure().declarations[0].name 72 | } 73 | } 74 | 75 | const getType = (node, asArray = false) => { 76 | let type 77 | if (typeof node === 'string') { 78 | type = node 79 | } else { 80 | const st = node.getStructure() 81 | const stType = node.getReturnType ? st.returnType : st.type 82 | const { compilerType } = node.getReturnType ? node.getReturnType() : node.getType() 83 | type = compilerType.thisType ? compilerType.thisType.symbol.escapedName : stType 84 | } 85 | if (!type) return 86 | const arr = type.split('|').map(t => t.trim()) 87 | if (asArray) return arr 88 | return arr.join(' | ').replace('=', '') 89 | } 90 | 91 | const getIsOptional = (node, type) => { 92 | if (type[type.length - 1].trim() === 'null') return true 93 | if (typeof node !== 'string' && node.isOptional) return node.isOptional() 94 | return false 95 | } 96 | 97 | const parseParameterType = (node, doc) => { 98 | let type = getType(node, true) 99 | let isOptional = getIsOptional(node, type) 100 | 101 | type = type.filter(t => t !== 'null').join(' | ') 102 | 103 | if (!doc) return { valueType: type, isOptional } 104 | 105 | const defaultValue = doc.fullText.match(new RegExp(`\\}\\s\\[${doc.name}=(\\s*.*)\\]`, 'i')) 106 | if (defaultValue && defaultValue.length === 2) { 107 | return { valueType: type, isOptional: true, defaultValue: defaultValue[1] } 108 | } 109 | 110 | if (doc.fullText.includes(`} [${doc.name}]`) || (typeof node === 'string' && node.endsWith('='))) { 111 | isOptional = true 112 | } 113 | 114 | return { valueType: type, isOptional } 115 | } 116 | 117 | const removeDocParams = tag => !['param'].includes(tag.tagName) 118 | 119 | const getParameterNameFromText = text => { 120 | const name = text.split(' ')[2] 121 | if (!name) return '' 122 | return trim(name, '[]').split('=')[0].trim() 123 | } 124 | 125 | const getParameterTypeFromText = text => { 126 | let type = text.split(' ')[1] || 'any' 127 | type = trim(type, '{} ') 128 | 129 | return parseParameterType(type, { 130 | name: getParameterNameFromText(text), 131 | fullText: text 132 | }) 133 | } 134 | 135 | const getParameterDescriptionFromText = text => { 136 | const desc = text.split(' ').slice(3) 137 | if (desc.length === 0) return '' 138 | return desc.join(' ').trim(desc, '- \n') 139 | } 140 | 141 | module.exports = { 142 | parseTextTag, 143 | parseTags, 144 | getJsDoc, 145 | getJsDocStructure, 146 | getName, 147 | moduleTags, 148 | getType, 149 | parseParameterType, 150 | removeDocParams, 151 | getParameterNameFromText, 152 | getParameterTypeFromText, 153 | getParameterDescriptionFromText 154 | } 155 | -------------------------------------------------------------------------------- /src/type-definition-parser.js: -------------------------------------------------------------------------------- 1 | const { SyntaxKind } = require('ts-morph') 2 | const u = require('unist-builder') 3 | const trim = require('lodash.trim') 4 | const parents = require('unist-util-parents') 5 | 6 | const ModuleReader = require('./module-reader') 7 | const { parseTags, getJsDocStructure, getName, getType, parseParameterType, removeDocParams, getParameterNameFromText, getParameterTypeFromText, getParameterDescriptionFromText } = require('./tsmorph-utils') 8 | 9 | class TypeDefinitionParser { 10 | constructor (opts = {}) { 11 | this._modules = new ModuleReader(opts) 12 | } 13 | 14 | run (src) { 15 | const modules = this._modules.read(src) 16 | return parents(u('Root', this._parseModules(modules))) 17 | } 18 | 19 | _parseModules (modules) { 20 | return modules.map(mod => { 21 | this._currentModule = mod 22 | 23 | return u('Module', { 24 | name: mod.name, 25 | path: mod.path, 26 | doc: { 27 | description: mod.description, 28 | tags: mod.tags.map(parseTags) 29 | } 30 | }, this._parseStatements()) 31 | }) 32 | } 33 | 34 | _parseStatements () { 35 | return this._currentModule.declarationFile.getStatements() 36 | .filter(statement => { 37 | if ([SyntaxKind.ExportDeclaration, SyntaxKind.ImportDeclaration].includes(statement.getKind())) return false 38 | if (getName(statement) === '__JSDAST_END_OF_FILE__') return false 39 | return true 40 | }) 41 | .map(statement => { 42 | const structure = statement.getStructure() 43 | 44 | const props = { 45 | name: getName(statement), 46 | doc: getJsDocStructure(statement, removeDocParams), 47 | isExported: structure.isExported, 48 | isDefaultExport: structure.isDefaultExport 49 | } 50 | 51 | switch (statement.getKind()) { 52 | case SyntaxKind.TypeAliasDeclaration: 53 | return this._parseTypeAliasDeclaration(statement, props) 54 | case SyntaxKind.VariableStatement: 55 | return this._parseDeclaration(statement, props) 56 | case SyntaxKind.FunctionDeclaration: 57 | return this._parseFunctionDeclaration(statement, props) 58 | case SyntaxKind.ClassDeclaration: 59 | return this._parseClassDeclaration(statement, props) 60 | default: 61 | return null 62 | } 63 | }).filter(Boolean) 64 | } 65 | 66 | _parseTypeAliasDeclaration (node, props) { 67 | node = node.getTypeNode() 68 | 69 | let children 70 | 71 | switch (node.getKind()) { 72 | case SyntaxKind.FunctionType: 73 | props.valueType = getType(node) 74 | children = node.getParameters().map((param, index) => this._parseParameter(param, index)) 75 | break 76 | case SyntaxKind.TypeLiteral: 77 | children = [ 78 | ...node.getMethods().map(method => this._parseMethod(method)), 79 | ...node.getProperties().map(prop => this._parseProperty(prop)), 80 | ...this._parseEvents(props) 81 | ] 82 | break 83 | default: 84 | return null 85 | } 86 | 87 | return u(node.getKindName(), props, children) 88 | } 89 | 90 | _parseFunctionDeclaration (node, props) { 91 | const st = node.getStructure() 92 | const source = this._currentModule.sourceFile.forEachDescendant(n => { 93 | if (getName(n) === st.name && getName(n.getParent()) === getName(node.getParent())) { 94 | if ([SyntaxKind.FunctionDeclaration, SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction].includes(n.getKind())) { 95 | return n 96 | } 97 | // it's a VariableStatement, we need to keep looking inside for the FunctionExpression 98 | return n.forEachDescendant(subN => { 99 | if ([SyntaxKind.FunctionDeclaration, SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction].includes(subN.getKind())) { 100 | return subN 101 | } 102 | }) 103 | } 104 | }) 105 | 106 | const sourceStructure = source.getStructure() 107 | props.valueType = getType(node) 108 | props.isGenerator = sourceStructure.isGenerator 109 | props.isAsync = sourceStructure.isAsync 110 | const children = node.getParameters().map((param, index) => this._parseParameter(param, index, sourceStructure.parameters)) 111 | return u(node.getKindName(), props, children) 112 | } 113 | 114 | _parseClassDeclaration (node, props) { 115 | const st = node.getStructure() 116 | props.extends = st.extends 117 | return u(node.getKindName(), props, [ 118 | ...node.getConstructors().map(ctr => this._parseConstructor(ctr)), 119 | ...node.getProperties().map(prop => this._parseProperty(prop)), 120 | ...node.getGetAccessors().map(accessor => this._parseAccessor(accessor)), 121 | ...node.getSetAccessors().map(accessor => this._parseAccessor(accessor)), 122 | ...node.getMethods().map(method => this._parseMethod(method)), 123 | ...this._parseEvents(props) 124 | ]) 125 | } 126 | 127 | _parseDeclaration (node, props) { 128 | props.kind = node.getDeclarationKind() 129 | const dec = node.getDeclarations()[0] 130 | const st = dec.getStructure() 131 | props.name = st.name 132 | props.valueType = getType(dec) 133 | return u(dec.getKindName(), props) 134 | } 135 | 136 | _parseProperty (node) { 137 | const type = node.getKindName() 138 | const st = node.getStructure() 139 | const doc = getJsDocStructure(node, removeDocParams) 140 | 141 | if (doc && !doc.description) { 142 | const tag = doc.tags && doc.tags.find(t => t.tagName === 'type') 143 | doc.tags = doc.tags.filter(t => t !== tag) 144 | doc.description = tag && tag.text 145 | } 146 | 147 | return u(type, { 148 | name: st.name, 149 | valueType: getType(node), 150 | isReadonly: st.isReadonly, 151 | doc 152 | }) 153 | } 154 | 155 | _parseConstructor (node) { 156 | const source = this._currentModule.sourceFile.forEachDescendant(n => { 157 | if (n.getKindName() === node.getKindName() && getName(n.getParent()) === getName(node.getParent())) { 158 | return n 159 | } 160 | }) 161 | 162 | const children = node.getParameters().map((param, index) => this._parseParameter(param, index, source && source.getStructure().parameters)) 163 | return u(node.getKindName(), { 164 | valueType: getType(node), 165 | doc: getJsDocStructure(node, removeDocParams) 166 | }, children) 167 | } 168 | 169 | _parseAccessor (node) { 170 | const st = node.getStructure() 171 | return u(node.getKindName(), { 172 | name: st.name, 173 | isStatic: st.isStatic, 174 | isReadonly: st.isReadonly, 175 | valueType: getType(node), 176 | doc: getJsDocStructure(node, removeDocParams) 177 | }) 178 | } 179 | 180 | _parseMethod (node) { 181 | const st = node.getStructure() 182 | 183 | const source = this._currentModule.sourceFile.forEachDescendant(n => { 184 | if (getName(n) === st.name && getName(n.getParent()) === getName(node.getParent())) { 185 | return n 186 | } 187 | }) 188 | 189 | const children = node.getParameters().map((param, index) => this._parseParameter(param, index, source.getStructure().parameters)) 190 | return u(node.getKindName(), { 191 | name: st.name, 192 | valueType: getType(node), 193 | doc: getJsDocStructure(node, removeDocParams), 194 | isGenerator: source.isGenerator(), 195 | isAsync: source.isAsync(), 196 | isStatic: st.isStatic 197 | }, children) 198 | } 199 | 200 | _parseParameter (node, index, sourceParameters = []) { 201 | const st = node.getStructure() 202 | const parentDoc = getJsDocStructure(node.getParent()) 203 | let doc = parentDoc && parentDoc.tags.filter(t => t.tagName === 'param')[index] 204 | let children = null 205 | let typeInfo = null 206 | 207 | if (doc && st.type.startsWith('{') && st.type.endsWith('}')) { 208 | const multipleObjectParameter = this._parseMultipleObjectParameter( 209 | doc.fullText 210 | .split('@param') 211 | .map(t => trim(t, '\n *')) 212 | .filter(Boolean) 213 | .map(t => `@param ${t}`) 214 | ) 215 | typeInfo = multipleObjectParameter.typeInfo 216 | children = multipleObjectParameter.parameters 217 | doc = multipleObjectParameter.doc 218 | } else { 219 | typeInfo = parseParameterType(node, doc) 220 | } 221 | 222 | let name = st.name 223 | if (doc && (name.startsWith('{') || name.startsWith('['))) { 224 | name = doc.name 225 | } 226 | 227 | const props = { 228 | name, 229 | doc: { 230 | description: doc && doc.text, 231 | tags: [] 232 | }, 233 | isRestParameter: st.isRestParameter, 234 | ...typeInfo 235 | } 236 | 237 | const sourceParam = sourceParameters[index] 238 | if (sourceParam && sourceParam.initializer) { 239 | props.defaultValue = sourceParam.initializer 240 | props.isOptional = true 241 | } 242 | 243 | return u(node.getKindName(), props, children) 244 | } 245 | 246 | _parseEvents (props) { 247 | return this._currentModule.events 248 | .filter(ev => ev.eventTarget === props.name) 249 | .map(ev => { 250 | let parameterTags = [] 251 | const returnTag = [] 252 | const tags = ev.tags.filter(t => { 253 | if (['param', 'type', 'prop', 'property'].includes(t.tagName)) { 254 | parameterTags.push(t) 255 | return false 256 | } 257 | 258 | if (['return', 'returns'].includes(t.tagName)) { 259 | returnTag.push(`${trim(t.fullText, '\n* ')}`) 260 | return false 261 | } 262 | 263 | return true 264 | }) 265 | 266 | const type = ev.tags.find(t => t.tagName === 'type') 267 | 268 | if (type) { 269 | parameterTags = [ 270 | ...parameterTags 271 | .filter(t => t.tagName !== 'param') 272 | .map(t => { 273 | if (t.tagName === 'type') { 274 | return `@param {${t.typeExpression}} arg` 275 | } 276 | 277 | const [name, ...text] = t.text.split(' ') 278 | return `@param {${t.typeExpression}} arg.${name}${text.length > 0 ? ' ' + text.join(' ') : ''}` 279 | }) 280 | ] 281 | } else { 282 | parameterTags = [ 283 | ...parameterTags 284 | .filter(t => !['prop', 'property'].includes(t.tagName)) 285 | .map(t => `${trim(t.fullText, '\n* ')}`) 286 | ] 287 | } 288 | 289 | const { valueType = 'void' } = (returnTag[0] && getParameterTypeFromText(returnTag[0])) || {} 290 | 291 | parameterTags = parameterTags.map(t => u('Parameter', { 292 | name: getParameterNameFromText(t), 293 | doc: { 294 | description: getParameterDescriptionFromText(t), 295 | tags: [] 296 | }, 297 | ...getParameterTypeFromText(t) 298 | })) 299 | 300 | return u('Event', { 301 | name: ev.eventName, 302 | doc: { 303 | description: ev.description, 304 | tags 305 | }, 306 | valueType 307 | }, parameterTags) 308 | }) 309 | } 310 | 311 | _parseMultipleObjectParameter (template) { 312 | const scopeName = getParameterNameFromText(template[0]) 313 | 314 | return { 315 | doc: { 316 | description: getParameterDescriptionFromText(template[0]), 317 | tags: [] 318 | }, 319 | typeInfo: getParameterTypeFromText(template[0]), 320 | parameters: template 321 | .slice(1) 322 | .map(t => { 323 | return u('Parameter', { 324 | name: getParameterNameFromText(t).replace(`${scopeName}.`, ''), 325 | doc: { 326 | description: getParameterDescriptionFromText(t), 327 | tags: [] 328 | }, 329 | ...getParameterTypeFromText(t) 330 | }) 331 | }) 332 | } 333 | } 334 | } 335 | 336 | module.exports = TypeDefinitionParser 337 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MultipleObjectParameter 1`] = ` 4 | Object { 5 | "children": Array [ 6 | Object { 7 | "children": Array [ 8 | Object { 9 | "children": Array [ 10 | Object { 11 | "doc": Object { 12 | "description": undefined, 13 | "tags": Array [], 14 | }, 15 | "isOptional": true, 16 | "isRestParameter": false, 17 | "name": "a", 18 | "type": "Parameter", 19 | "valueType": "string", 20 | }, 21 | Object { 22 | "children": Array [ 23 | Object { 24 | "defaultValue": "'test'", 25 | "doc": Object { 26 | "description": "name description", 27 | "tags": Array [], 28 | }, 29 | "isOptional": true, 30 | "name": "name", 31 | "type": "Parameter", 32 | "valueType": "string", 33 | }, 34 | Object { 35 | "doc": Object { 36 | "description": "age description", 37 | "tags": Array [], 38 | }, 39 | "isOptional": false, 40 | "name": "age", 41 | "type": "Parameter", 42 | "valueType": "number", 43 | }, 44 | ], 45 | "doc": Object { 46 | "description": undefined, 47 | "tags": Array [], 48 | }, 49 | "isOptional": false, 50 | "isRestParameter": false, 51 | "name": "opts", 52 | "type": "Parameter", 53 | "valueType": "object", 54 | }, 55 | Object { 56 | "doc": Object { 57 | "description": undefined, 58 | "tags": Array [], 59 | }, 60 | "isOptional": false, 61 | "isRestParameter": false, 62 | "name": "b", 63 | "type": "Parameter", 64 | "valueType": "number", 65 | }, 66 | ], 67 | "doc": Object { 68 | "description": undefined, 69 | "tags": Array [], 70 | }, 71 | "isAsync": false, 72 | "isDefaultExport": false, 73 | "isExported": false, 74 | "isGenerator": false, 75 | "name": "test", 76 | "type": "FunctionDeclaration", 77 | "valueType": "string", 78 | }, 79 | Object { 80 | "children": Array [ 81 | Object { 82 | "children": Array [ 83 | Object { 84 | "defaultValue": "'test'", 85 | "doc": Object { 86 | "description": "name description", 87 | "tags": Array [], 88 | }, 89 | "isOptional": true, 90 | "name": "name", 91 | "type": "Parameter", 92 | "valueType": "string", 93 | }, 94 | Object { 95 | "doc": Object { 96 | "description": "age description", 97 | "tags": Array [], 98 | }, 99 | "isOptional": false, 100 | "name": "age", 101 | "type": "Parameter", 102 | "valueType": "number", 103 | }, 104 | ], 105 | "defaultValue": "{}", 106 | "doc": Object { 107 | "description": undefined, 108 | "tags": Array [], 109 | }, 110 | "isOptional": true, 111 | "isRestParameter": false, 112 | "name": "opts", 113 | "type": "Parameter", 114 | "valueType": "object", 115 | }, 116 | ], 117 | "doc": Object { 118 | "description": undefined, 119 | "tags": Array [], 120 | }, 121 | "isAsync": false, 122 | "isDefaultExport": false, 123 | "isExported": false, 124 | "isGenerator": false, 125 | "name": "test2", 126 | "type": "FunctionDeclaration", 127 | "valueType": "string", 128 | }, 129 | Object { 130 | "children": Array [ 131 | Object { 132 | "children": Array [ 133 | Object { 134 | "doc": Object { 135 | "description": undefined, 136 | "tags": Array [], 137 | }, 138 | "isOptional": true, 139 | "isRestParameter": false, 140 | "name": "a", 141 | "type": "Parameter", 142 | "valueType": "string", 143 | }, 144 | Object { 145 | "children": Array [ 146 | Object { 147 | "defaultValue": "'test'", 148 | "doc": Object { 149 | "description": "name description", 150 | "tags": Array [], 151 | }, 152 | "isOptional": true, 153 | "name": "name", 154 | "type": "Parameter", 155 | "valueType": "string", 156 | }, 157 | Object { 158 | "doc": Object { 159 | "description": "age description", 160 | "tags": Array [], 161 | }, 162 | "isOptional": false, 163 | "name": "age", 164 | "type": "Parameter", 165 | "valueType": "number", 166 | }, 167 | ], 168 | "doc": Object { 169 | "description": undefined, 170 | "tags": Array [], 171 | }, 172 | "isOptional": false, 173 | "isRestParameter": false, 174 | "name": "opts", 175 | "type": "Parameter", 176 | "valueType": "object", 177 | }, 178 | Object { 179 | "doc": Object { 180 | "description": undefined, 181 | "tags": Array [], 182 | }, 183 | "isOptional": false, 184 | "isRestParameter": false, 185 | "name": "b", 186 | "type": "Parameter", 187 | "valueType": "number", 188 | }, 189 | ], 190 | "doc": Object { 191 | "description": undefined, 192 | "tags": Array [], 193 | }, 194 | "isAsync": false, 195 | "isGenerator": false, 196 | "isStatic": false, 197 | "name": "test", 198 | "type": "MethodDeclaration", 199 | "valueType": "void", 200 | }, 201 | ], 202 | "doc": undefined, 203 | "extends": undefined, 204 | "isDefaultExport": false, 205 | "isExported": false, 206 | "name": "Test", 207 | "type": "ClassDeclaration", 208 | }, 209 | ], 210 | "doc": Object { 211 | "description": "", 212 | "tags": Array [], 213 | }, 214 | "name": "Index", 215 | "type": "Module", 216 | }, 217 | ], 218 | "type": "Root", 219 | } 220 | `; 221 | 222 | exports[`basic from plain content 1`] = ` 223 | Object { 224 | "children": Array [ 225 | Object { 226 | "children": Array [ 227 | Object { 228 | "children": Array [ 229 | Object { 230 | "doc": Object { 231 | "description": undefined, 232 | "tags": Array [], 233 | }, 234 | "isOptional": false, 235 | "isRestParameter": false, 236 | "name": "a", 237 | "type": "Parameter", 238 | "valueType": "number", 239 | }, 240 | Object { 241 | "doc": Object { 242 | "description": undefined, 243 | "tags": Array [], 244 | }, 245 | "isOptional": false, 246 | "isRestParameter": false, 247 | "name": "b", 248 | "type": "Parameter", 249 | "valueType": "number", 250 | }, 251 | ], 252 | "doc": Object { 253 | "description": undefined, 254 | "tags": Array [ 255 | Object { 256 | "fullText": "@returns {number}", 257 | "name": undefined, 258 | "tagName": "returns", 259 | "text": undefined, 260 | "typeExpression": "number", 261 | }, 262 | ], 263 | }, 264 | "isAsync": false, 265 | "isDefaultExport": false, 266 | "isExported": false, 267 | "isGenerator": false, 268 | "name": "sum", 269 | "type": "FunctionDeclaration", 270 | "valueType": "number", 271 | }, 272 | ], 273 | "doc": Object { 274 | "description": "", 275 | "tags": Array [], 276 | }, 277 | "name": "Index", 278 | "type": "Module", 279 | }, 280 | ], 281 | "type": "Root", 282 | } 283 | `; 284 | 285 | exports[`basic from vfile 1`] = ` 286 | Object { 287 | "children": Array [ 288 | Object { 289 | "children": Array [ 290 | Object { 291 | "children": Array [ 292 | Object { 293 | "doc": Object { 294 | "description": undefined, 295 | "tags": Array [], 296 | }, 297 | "isOptional": false, 298 | "isRestParameter": false, 299 | "name": "age", 300 | "type": "Parameter", 301 | "valueType": "number", 302 | }, 303 | ], 304 | "doc": Object { 305 | "description": "This is a callback example", 306 | "tags": Array [], 307 | }, 308 | "isDefaultExport": false, 309 | "isExported": true, 310 | "name": "createHumanCallback", 311 | "type": "FunctionType", 312 | "valueType": "Human", 313 | }, 314 | Object { 315 | "children": Array [ 316 | Object { 317 | "doc": Object { 318 | "description": "some a description", 319 | "tags": Array [], 320 | }, 321 | "isReadonly": false, 322 | "name": "a", 323 | "type": "PropertySignature", 324 | "valueType": "string", 325 | }, 326 | Object { 327 | "doc": undefined, 328 | "isReadonly": false, 329 | "name": "b", 330 | "type": "PropertySignature", 331 | "valueType": "number", 332 | }, 333 | ], 334 | "doc": Object { 335 | "description": "This is a typedef example", 336 | "tags": Array [], 337 | }, 338 | "isDefaultExport": false, 339 | "isExported": true, 340 | "name": "options", 341 | "type": "TypeLiteral", 342 | }, 343 | Object { 344 | "children": Array [ 345 | Object { 346 | "children": Array [ 347 | Object { 348 | "doc": Object { 349 | "description": "Set a name", 350 | "tags": Array [], 351 | }, 352 | "isOptional": false, 353 | "isRestParameter": false, 354 | "name": "name", 355 | "type": "Parameter", 356 | "valueType": "string", 357 | }, 358 | Object { 359 | "doc": Object { 360 | "description": "Set the age", 361 | "tags": Array [], 362 | }, 363 | "isOptional": false, 364 | "isRestParameter": false, 365 | "name": "age", 366 | "type": "Parameter", 367 | "valueType": "number", 368 | }, 369 | ], 370 | "doc": Object { 371 | "description": undefined, 372 | "tags": Array [ 373 | Object { 374 | "fullText": "@constructor 375 | * ", 376 | "name": undefined, 377 | "tagName": "constructor", 378 | "text": undefined, 379 | "typeExpression": undefined, 380 | }, 381 | ], 382 | }, 383 | "type": "Constructor", 384 | "valueType": "Human", 385 | }, 386 | Object { 387 | "doc": undefined, 388 | "isReadonly": false, 389 | "name": "_name", 390 | "type": "PropertyDeclaration", 391 | "valueType": "string", 392 | }, 393 | Object { 394 | "doc": undefined, 395 | "isReadonly": false, 396 | "name": "_age", 397 | "type": "PropertyDeclaration", 398 | "valueType": "number", 399 | }, 400 | Object { 401 | "doc": Object { 402 | "description": undefined, 403 | "tags": Array [ 404 | Object { 405 | "fullText": "@private", 406 | "name": undefined, 407 | "tagName": "private", 408 | "text": undefined, 409 | "typeExpression": undefined, 410 | }, 411 | ], 412 | }, 413 | "isReadonly": false, 414 | "name": "_aPrivateMethod", 415 | "type": "PropertyDeclaration", 416 | "valueType": undefined, 417 | }, 418 | Object { 419 | "doc": Object { 420 | "description": undefined, 421 | "tags": Array [ 422 | Object { 423 | "fullText": "@prop ", 424 | "name": undefined, 425 | "tagName": "prop", 426 | "text": undefined, 427 | "typeExpression": "string", 428 | }, 429 | ], 430 | }, 431 | "isReadonly": undefined, 432 | "isStatic": false, 433 | "name": "name", 434 | "type": "GetAccessor", 435 | "valueType": "string", 436 | }, 437 | Object { 438 | "doc": undefined, 439 | "isReadonly": undefined, 440 | "isStatic": false, 441 | "name": "age", 442 | "type": "GetAccessor", 443 | "valueType": "number", 444 | }, 445 | Object { 446 | "doc": undefined, 447 | "isReadonly": undefined, 448 | "isStatic": false, 449 | "name": "age", 450 | "type": "SetAccessor", 451 | "valueType": undefined, 452 | }, 453 | Object { 454 | "children": Array [ 455 | Object { 456 | "doc": Object { 457 | "description": "Set a name", 458 | "tags": Array [], 459 | }, 460 | "isOptional": false, 461 | "isRestParameter": false, 462 | "name": "name", 463 | "type": "Parameter", 464 | "valueType": "string", 465 | }, 466 | Object { 467 | "doc": Object { 468 | "description": undefined, 469 | "tags": Array [], 470 | }, 471 | "isOptional": false, 472 | "isRestParameter": false, 473 | "name": "age", 474 | "type": "Parameter", 475 | "valueType": "number", 476 | }, 477 | ], 478 | "doc": Object { 479 | "description": "Static factory constructor", 480 | "tags": Array [ 481 | Object { 482 | "fullText": "@returns {Human}", 483 | "name": undefined, 484 | "tagName": "returns", 485 | "text": undefined, 486 | "typeExpression": "Human", 487 | }, 488 | ], 489 | }, 490 | "isAsync": false, 491 | "isGenerator": false, 492 | "isStatic": true, 493 | "name": "create", 494 | "type": "MethodDeclaration", 495 | "valueType": "Human", 496 | }, 497 | Object { 498 | "children": Array [], 499 | "doc": undefined, 500 | "isAsync": false, 501 | "isGenerator": false, 502 | "isStatic": false, 503 | "name": "sayHello", 504 | "type": "MethodDeclaration", 505 | "valueType": "string", 506 | }, 507 | ], 508 | "doc": Object { 509 | "description": undefined, 510 | "tags": Array [ 511 | Object { 512 | "fullText": "@example 513 | * ", 514 | "name": undefined, 515 | "tagName": "example", 516 | "text": "const human = new Human('pepe', 20) 517 | console.log(human.age)", 518 | "typeExpression": undefined, 519 | }, 520 | Object { 521 | "fullText": "@description ", 522 | "name": undefined, 523 | "tagName": "description", 524 | "text": "eso es", 525 | "typeExpression": undefined, 526 | }, 527 | ], 528 | }, 529 | "extends": undefined, 530 | "isDefaultExport": false, 531 | "isExported": true, 532 | "name": "Human", 533 | "type": "ClassDeclaration", 534 | }, 535 | Object { 536 | "children": Array [ 537 | Object { 538 | "doc": Object { 539 | "description": "Set a name", 540 | "tags": Array [], 541 | }, 542 | "isOptional": false, 543 | "isRestParameter": false, 544 | "name": "name", 545 | "type": "Parameter", 546 | "valueType": "string", 547 | }, 548 | ], 549 | "doc": Object { 550 | "description": "It's a human factory", 551 | "tags": Array [ 552 | Object { 553 | "fullText": "@returns {createHumanCallback}", 554 | "name": undefined, 555 | "tagName": "returns", 556 | "text": "a function", 557 | "typeExpression": "createHumanCallback", 558 | }, 559 | ], 560 | }, 561 | "isAsync": false, 562 | "isDefaultExport": false, 563 | "isExported": true, 564 | "isGenerator": false, 565 | "name": "humanFactory", 566 | "type": "FunctionDeclaration", 567 | "valueType": "createHumanCallback", 568 | }, 569 | Object { 570 | "doc": undefined, 571 | "isDefaultExport": false, 572 | "isExported": true, 573 | "kind": "const", 574 | "name": "CONSTANT", 575 | "type": "VariableDeclaration", 576 | "valueType": "\\"test\\"", 577 | }, 578 | ], 579 | "doc": Object { 580 | "description": " 581 | A library for building widgets. 582 | ", 583 | "tags": Array [ 584 | Object { 585 | "fullText": "@remarks 586 | * ", 587 | "name": undefined, 588 | "tagName": "remarks", 589 | "text": "The \`widget-lib\` defines the {@link IWidget} interface and {@link Widget} class, 590 | which are used to build widgets.", 591 | "typeExpression": undefined, 592 | }, 593 | Object { 594 | "fullText": "@packageDocumentation", 595 | "name": undefined, 596 | "tagName": "packageDocumentation", 597 | "text": undefined, 598 | "typeExpression": undefined, 599 | }, 600 | ], 601 | }, 602 | "name": "Human", 603 | "type": "Module", 604 | }, 605 | Object { 606 | "children": Array [ 607 | Object { 608 | "children": Array [ 609 | Object { 610 | "children": Array [ 611 | Object { 612 | "doc": Object { 613 | "description": undefined, 614 | "tags": Array [], 615 | }, 616 | "isOptional": false, 617 | "isRestParameter": false, 618 | "name": "name", 619 | "type": "Parameter", 620 | "valueType": "string", 621 | }, 622 | ], 623 | "doc": Object { 624 | "description": undefined, 625 | "tags": Array [ 626 | Object { 627 | "fullText": "@constructor 628 | * ", 629 | "name": undefined, 630 | "tagName": "constructor", 631 | "text": undefined, 632 | "typeExpression": undefined, 633 | }, 634 | ], 635 | }, 636 | "type": "Constructor", 637 | "valueType": "Power", 638 | }, 639 | Object { 640 | "doc": undefined, 641 | "isReadonly": false, 642 | "name": "_name", 643 | "type": "PropertyDeclaration", 644 | "valueType": "string", 645 | }, 646 | Object { 647 | "doc": Object { 648 | "description": undefined, 649 | "tags": Array [ 650 | Object { 651 | "fullText": "@prop ", 652 | "name": undefined, 653 | "tagName": "prop", 654 | "text": undefined, 655 | "typeExpression": "string", 656 | }, 657 | ], 658 | }, 659 | "isReadonly": undefined, 660 | "isStatic": false, 661 | "name": "name", 662 | "type": "GetAccessor", 663 | "valueType": "string", 664 | }, 665 | ], 666 | "doc": undefined, 667 | "extends": undefined, 668 | "isDefaultExport": false, 669 | "isExported": true, 670 | "name": "Power", 671 | "type": "ClassDeclaration", 672 | }, 673 | ], 674 | "doc": Object { 675 | "description": "", 676 | "tags": Array [], 677 | }, 678 | "name": "Power", 679 | "type": "Module", 680 | }, 681 | Object { 682 | "children": Array [ 683 | Object { 684 | "children": Array [ 685 | Object { 686 | "children": Array [ 687 | Object { 688 | "doc": Object { 689 | "description": undefined, 690 | "tags": Array [], 691 | }, 692 | "isOptional": false, 693 | "isRestParameter": false, 694 | "name": "name", 695 | "type": "Parameter", 696 | "valueType": "string", 697 | }, 698 | Object { 699 | "doc": Object { 700 | "description": undefined, 701 | "tags": Array [], 702 | }, 703 | "isOptional": false, 704 | "isRestParameter": false, 705 | "name": "age", 706 | "type": "Parameter", 707 | "valueType": "number", 708 | }, 709 | ], 710 | "doc": undefined, 711 | "type": "Constructor", 712 | "valueType": "Mutant", 713 | }, 714 | Object { 715 | "doc": undefined, 716 | "isReadonly": false, 717 | "name": "_power", 718 | "type": "PropertyDeclaration", 719 | "valueType": "Power", 720 | }, 721 | Object { 722 | "children": Array [ 723 | Object { 724 | "doc": Object { 725 | "description": undefined, 726 | "tags": Array [], 727 | }, 728 | "isOptional": false, 729 | "isRestParameter": false, 730 | "name": "power", 731 | "type": "Parameter", 732 | "valueType": "Power", 733 | }, 734 | ], 735 | "doc": Object { 736 | "description": "Set a power", 737 | "tags": Array [ 738 | Object { 739 | "fullText": "@returns {Mutan}", 740 | "name": undefined, 741 | "tagName": "returns", 742 | "text": undefined, 743 | "typeExpression": "Mutan", 744 | }, 745 | ], 746 | }, 747 | "isAsync": false, 748 | "isGenerator": false, 749 | "isStatic": false, 750 | "name": "setPower", 751 | "type": "MethodDeclaration", 752 | "valueType": "any", 753 | }, 754 | Object { 755 | "children": Array [], 756 | "doc": Object { 757 | "description": undefined, 758 | "tags": Array [ 759 | Object { 760 | "fullText": "@returns {Power}", 761 | "name": undefined, 762 | "tagName": "returns", 763 | "text": undefined, 764 | "typeExpression": "Power", 765 | }, 766 | ], 767 | }, 768 | "isAsync": false, 769 | "isGenerator": false, 770 | "isStatic": false, 771 | "name": "getPower", 772 | "type": "MethodDeclaration", 773 | "valueType": "Power", 774 | }, 775 | ], 776 | "doc": undefined, 777 | "extends": "Human", 778 | "isDefaultExport": false, 779 | "isExported": true, 780 | "name": "Mutant", 781 | "type": "ClassDeclaration", 782 | }, 783 | ], 784 | "doc": Object { 785 | "description": "", 786 | "tags": Array [], 787 | }, 788 | "name": "Mutant", 789 | "type": "Module", 790 | }, 791 | Object { 792 | "children": Array [], 793 | "doc": Object { 794 | "description": "", 795 | "tags": Array [], 796 | }, 797 | "name": "Index", 798 | "type": "Module", 799 | }, 800 | ], 801 | "type": "Root", 802 | } 803 | `; 804 | 805 | exports[`destructuring 1`] = ` 806 | Object { 807 | "children": Array [ 808 | Object { 809 | "children": Array [ 810 | Object { 811 | "children": Array [ 812 | Object { 813 | "doc": Object { 814 | "description": undefined, 815 | "tags": Array [], 816 | }, 817 | "isOptional": false, 818 | "isRestParameter": false, 819 | "name": "opts", 820 | "type": "Parameter", 821 | "valueType": "object", 822 | }, 823 | ], 824 | "doc": Object { 825 | "description": undefined, 826 | "tags": Array [], 827 | }, 828 | "isAsync": false, 829 | "isDefaultExport": false, 830 | "isExported": false, 831 | "isGenerator": false, 832 | "name": "test", 833 | "type": "FunctionDeclaration", 834 | "valueType": "void", 835 | }, 836 | ], 837 | "doc": Object { 838 | "description": "", 839 | "tags": Array [], 840 | }, 841 | "name": "Index", 842 | "type": "Module", 843 | }, 844 | ], 845 | "type": "Root", 846 | } 847 | `; 848 | 849 | exports[`events 1`] = ` 850 | Object { 851 | "children": Array [ 852 | Object { 853 | "children": Array [ 854 | Object { 855 | "children": Array [], 856 | "doc": undefined, 857 | "isAsync": false, 858 | "isDefaultExport": false, 859 | "isExported": false, 860 | "isGenerator": false, 861 | "name": "test", 862 | "type": "FunctionDeclaration", 863 | "valueType": "void", 864 | }, 865 | Object { 866 | "children": Array [], 867 | "doc": undefined, 868 | "extends": undefined, 869 | "isDefaultExport": false, 870 | "isExported": false, 871 | "name": "Robot", 872 | "type": "ClassDeclaration", 873 | }, 874 | ], 875 | "doc": Object { 876 | "description": "", 877 | "tags": Array [], 878 | }, 879 | "name": "Index", 880 | "type": "Module", 881 | }, 882 | ], 883 | "type": "Root", 884 | } 885 | `; 886 | -------------------------------------------------------------------------------- /tests/example/human.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A library for building widgets. 3 | * 4 | * @remarks 5 | * The `widget-lib` defines the {@link IWidget} interface and {@link Widget} class, 6 | * which are used to build widgets. 7 | * 8 | * @packageDocumentation 9 | */ 10 | 11 | /** 12 | * This is a callback example 13 | * @callback createHumanCallback 14 | * @param {number} age Set the age 15 | * @returns {Human} 16 | */ 17 | 18 | /** 19 | * This is a typedef example 20 | * @typedef options 21 | * @type {Object} 22 | * @prop {string} options.a some a description 23 | * @prop {number} options.b 24 | */ 25 | 26 | /** 27 | * It's a human factory 28 | * 29 | * @param {string} name Set a name 30 | * @returns {createHumanCallback} a function 31 | */ 32 | function humanFactory (name) { 33 | return function createHuman (age) { 34 | return new Human(name, age) 35 | } 36 | } 37 | 38 | /** 39 | * @example 40 | * const human = new Human('pepe', 20) 41 | * console.log(human.age) 42 | * @description eso es 43 | */ 44 | class Human { 45 | /** 46 | * Static factory constructor 47 | * 48 | * @param {string} name Set a name 49 | * @param {number} age 50 | * @returns {Human} 51 | */ 52 | static create (name, age) { 53 | return new Human(name, age) 54 | } 55 | 56 | /** 57 | * @constructor 58 | * @param {string} name Set a name 59 | * @param {number} age Set the age 60 | */ 61 | constructor (name, age) { 62 | this._name = name 63 | this._age = age 64 | } 65 | 66 | /** 67 | * @prop {string} 68 | */ 69 | get name () { 70 | return this._name 71 | } 72 | 73 | get age () { 74 | return this._age 75 | } 76 | 77 | set age (val) { 78 | this._age = val 79 | } 80 | 81 | sayHello () { 82 | return this._name 83 | } 84 | 85 | /** 86 | * @private 87 | */ 88 | _aPrivateMethod () {} 89 | } 90 | 91 | const CONSTANT = 'test' 92 | 93 | module.exports = { Human, humanFactory, CONSTANT } 94 | -------------------------------------------------------------------------------- /tests/example/index.js: -------------------------------------------------------------------------------- 1 | const { Human } = require('./human') 2 | const { Mutant } = require('./mutant') 3 | 4 | module.exports = { Human, Mutant } 5 | -------------------------------------------------------------------------------- /tests/example/mutant.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('./power').Power} Power */ 2 | 3 | const { Human } = require('./human') 4 | 5 | class Mutant extends Human { 6 | /** 7 | * Set a power 8 | * 9 | * @param {Power} power 10 | * @returns {Mutan} 11 | */ 12 | setPower (power) { 13 | this._power = power 14 | return this 15 | } 16 | 17 | /** 18 | * @returns {Power} 19 | */ 20 | getPower () { 21 | return this._power 22 | } 23 | } 24 | 25 | module.exports = { Mutant } 26 | -------------------------------------------------------------------------------- /tests/example/power.js: -------------------------------------------------------------------------------- 1 | class Power { 2 | /** 3 | * @constructor 4 | * @param {string} name 5 | */ 6 | constructor (name) { 7 | this._name = name 8 | } 9 | 10 | /** 11 | * @prop {string} 12 | */ 13 | get name () { 14 | return this._name 15 | } 16 | } 17 | 18 | module.exports = { Power } 19 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | const unified = require('unified') 2 | const toVFile = require('to-vfile') 3 | const path = require('path') 4 | 5 | const { parser } = require('..') 6 | 7 | test('basic from vfile', () => { 8 | const tree = unified().use(parser).parse(toVFile(path.join(__dirname, './example/index.js'))) 9 | 10 | for (const node of tree.children) { 11 | delete node.path 12 | } 13 | 14 | expect(tree).toMatchSnapshot() 15 | }) 16 | 17 | test('basic from plain content', () => { 18 | const tree = unified().use(parser).parse(` 19 | /** 20 | * @param {number} a 21 | * @param {number} b 22 | * @returns {number} 23 | */ 24 | function sum(a, b) { 25 | return a + b 26 | } 27 | `) 28 | 29 | for (const node of tree.children) { 30 | delete node.path 31 | } 32 | 33 | expect(tree).toMatchSnapshot() 34 | }) 35 | 36 | test('MultipleObjectParameter', () => { 37 | const tree = unified().use(parser).parse(` 38 | /** 39 | * @param {string} [a] 40 | * @param {object} opts a text description 41 | * @param {string} [opts.name='test'] name description 42 | * @param {number} opts.age age description 43 | * @param {number} b 44 | */ 45 | function test(a, opts, b) { 46 | return opts.name 47 | } 48 | 49 | class Test { 50 | /** 51 | * @param {string} [a] 52 | * @param {object} opts a text description 53 | * @param {string} [opts.name='test'] name description 54 | * @param {number} opts.age age description 55 | * @param {number} b 56 | */ 57 | test(a, opts, b) {} 58 | } 59 | 60 | /** 61 | * @param {object} opts a text description 62 | * @param {string} [opts.name='test'] name description 63 | * @param {number} opts.age age description 64 | */ 65 | function test2(opts = {}) { 66 | return opts.name 67 | } 68 | `) 69 | 70 | for (const node of tree.children) { 71 | delete node.path 72 | } 73 | 74 | expect(tree).toMatchSnapshot() 75 | }) 76 | 77 | test('destructuring', () => { 78 | const tree = unified().use(parser).parse(` 79 | /** 80 | * @param {object} opts 81 | */ 82 | function test({ name, age }) {} 83 | `) 84 | 85 | for (const node of tree.children) { 86 | delete node.path 87 | } 88 | 89 | expect(tree).toMatchSnapshot() 90 | }) 91 | 92 | test('events', () => { 93 | const tree = unified().use(parser).parse(` 94 | function test() {} 95 | 96 | class Robot {} 97 | 98 | /** 99 | * @event Robot#pong 100 | * @param {string} a some a text 101 | * @param {string} [b] some b text 102 | * @return {Promise} 103 | */ 104 | 105 | /** 106 | * @event Robot#ping 107 | * @param {Object} opts 108 | * @param {string} opts.a some a text 109 | * @returns {Promise} 110 | */ 111 | `) 112 | 113 | for (const node of tree.children) { 114 | delete node.path 115 | } 116 | 117 | expect(tree).toMatchSnapshot() 118 | }) 119 | --------------------------------------------------------------------------------