├── .gitignore ├── .travis.yml ├── index.js ├── test ├── async-function.js ├── generator-function.js ├── rest-parameter.js ├── traditional-function.js ├── default-parameter.js ├── arrow-function.js └── destructuring.js ├── package.json ├── LICENSE ├── function-parser.js ├── README.md └── parameter-parser.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "9" 6 | script: 7 | - v=$(node -v);if [ ${v:1:1} = 6 ]; then npm run test:skip-async; else npm run test; fi 8 | branches: 9 | only: 10 | - master 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const parseFunction = require('./function-parser') 2 | const ParameterParser = require('./parameter-parser'); 3 | 4 | function reflector(fn, scope = {}) { 5 | const fnString = Function.prototype.toString.call(fn); 6 | const parsed = parseFunction(fnString); 7 | 8 | const paramParser = new ParameterParser(scope); 9 | const params = paramParser.parse(parsed._rawParameter); 10 | 11 | return Object.assign(parsed, {params}) 12 | }; 13 | 14 | module.exports = reflector; 15 | -------------------------------------------------------------------------------- /test/async-function.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Async Function', () => { 5 | before(function () { 6 | if (process.version.startsWith('v6.')) { 7 | this.skip() 8 | } 9 | }) 10 | 11 | it('should have async to be true', () => { 12 | var fn = async function(a, b) { 13 | return a; 14 | } 15 | const actual = functionReflector(fn).async 16 | 17 | expect(actual).to.be(true) 18 | }) 19 | 20 | it('should have async to be false for non-async function', () => { 21 | const actual = functionReflector(() => {}).async 22 | 23 | expect(actual).to.be(false) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/generator-function.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Generator Function', () => { 5 | const fn = function * (list) { 6 | for (var item of list) { 7 | yield item 8 | } 9 | } 10 | 11 | it('should have generator type', () => { 12 | const actual = functionReflector(fn).type 13 | 14 | expect(actual).to.be('GENERATOR') 15 | }) 16 | 17 | it('should parse parameters', () => { 18 | const actual = functionReflector(fn).params 19 | const expected = [ 20 | { 21 | type: 'SIMPLE', 22 | name: 'list' 23 | } 24 | ] 25 | 26 | expect(actual).to.eql(expected) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/rest-parameter.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Rest Parameter', () => { 5 | const fn = (a, b = true, ...c) => { 6 | return a 7 | } 8 | 9 | it('should return javascript object', () => { 10 | const actual = functionReflector(fn) 11 | 12 | expect(actual).to.be.an('object') 13 | }) 14 | 15 | it('should return array of parameters', () => { 16 | const actual = functionReflector(fn).params 17 | const expected = [ 18 | { 19 | type: 'SIMPLE', 20 | name: 'a', 21 | }, 22 | { 23 | type: 'DEFAULT', 24 | name: 'b', 25 | value: true, 26 | }, 27 | { 28 | type: 'REST', 29 | name: 'c', 30 | }, 31 | ] 32 | 33 | expect(actual).to.eql(expected) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-function-reflector", 3 | "version": "2.0.3", 4 | "description": "Function Reflection in Javascript With Support for ES2015+ Syntax", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/*.js", 8 | "test:skip-async": "mocha $(find test/ -type f -not -name 'async-*.js')" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/arrizalamin/js-function-reflector" 13 | }, 14 | "keywords": [ 15 | "function", 16 | "reflection", 17 | "parameter", 18 | "parser" 19 | ], 20 | "author": "Arrizal Amin", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/arrizalamin/js-function-reflector/issues" 24 | }, 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "expect.js": "^0.3.1", 28 | "mocha": "^5.0.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Arrizal Amin 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 | -------------------------------------------------------------------------------- /test/traditional-function.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Traditional Function', () => { 5 | var fn = function(a, b) { 6 | return a; 7 | } 8 | 9 | it('should return javascript object', () => { 10 | const actual = functionReflector(fn) 11 | 12 | expect(actual).to.be.an('object') 13 | }) 14 | 15 | it('should have traditional type', () => { 16 | const actual = functionReflector(fn).type 17 | 18 | expect(actual).to.be('TRADITIONAL') 19 | }) 20 | 21 | it("should return empty array if it has empty parameter", () => { 22 | const actual = functionReflector(function(){ }).params 23 | const expected = [] 24 | 25 | expect(actual).to.eql(expected) 26 | }) 27 | 28 | it('should return array of parameters', () => { 29 | const actual = functionReflector(fn).params 30 | const expected = [ 31 | { 32 | type: 'SIMPLE', 33 | name: 'a', 34 | }, 35 | { 36 | type: 'SIMPLE', 37 | name: 'b', 38 | }, 39 | ] 40 | 41 | expect(actual).to.eql(expected) 42 | }) 43 | 44 | it('should return function name', () => { 45 | function foo() {} 46 | const actual = functionReflector(foo).name 47 | const expected = 'foo' 48 | 49 | expect(actual).to.eql(expected) 50 | }) 51 | 52 | it('should have null name', () => { 53 | const actual = functionReflector(fn).name 54 | 55 | expect(actual).to.be(null) 56 | }) 57 | 58 | it('should return function body in string', () => { 59 | const actual = functionReflector(fn).body 60 | const expected = 'return a;' 61 | 62 | expect(actual).to.eql(expected) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /test/default-parameter.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Default Parameter', () => { 5 | const fn = (a, b = true, c = 'string', d = 5) => { 6 | return a 7 | } 8 | 9 | it('should return javascript object', () => { 10 | const actual = functionReflector(fn) 11 | 12 | expect(actual).to.be.an('object') 13 | }) 14 | 15 | it('should return array of parameters', () => { 16 | const actual = functionReflector(fn).params 17 | const expected = [ 18 | { 19 | type: 'SIMPLE', 20 | name: 'a', 21 | }, 22 | { 23 | type: 'DEFAULT', 24 | name: 'b', 25 | value: true, 26 | }, 27 | { 28 | type: 'DEFAULT', 29 | name: 'c', 30 | value: 'string', 31 | }, 32 | { 33 | type: 'DEFAULT', 34 | name: 'd', 35 | value: 5, 36 | }, 37 | ] 38 | 39 | expect(actual).to.eql(expected) 40 | }) 41 | 42 | it('should parse single parameter with parenthesis', () => { 43 | const singleParamFn = (x = 'a') => {} 44 | const actual = functionReflector(singleParamFn).params 45 | const expected = [ 46 | { 47 | type: 'DEFAULT', 48 | name: 'x', 49 | value: 'a', 50 | }, 51 | ] 52 | 53 | expect(actual).to.eql(expected) 54 | }) 55 | 56 | it('should parse parameter value outside scope', function() { 57 | const OUTSIDE = { 58 | A: 1 59 | } 60 | 61 | const singleParamFn = function(x = OUTSIDE.A) {} 62 | const actual = functionReflector(singleParamFn, { OUTSIDE }).params 63 | const expected = [{ 64 | type: 'DEFAULT', 65 | name: 'x', 66 | value: 1, 67 | }] 68 | 69 | expect(actual).to.eql(expected) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/arrow-function.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Arrow Function', () => { 5 | const fn = (a, b) => { 6 | return a 7 | } 8 | 9 | it('should return javascript object', () => { 10 | const actual = functionReflector(fn) 11 | 12 | expect(actual).to.be.an('object') 13 | }) 14 | 15 | it('should have arrow type', () => { 16 | const actual = functionReflector(fn).type 17 | 18 | expect(actual).to.be('ARROW') 19 | }) 20 | 21 | it('should return a string parameter', () => { 22 | const actual = functionReflector(a => {a}).params 23 | const expected = [ 24 | { 25 | type: 'SIMPLE', 26 | name: 'a', 27 | } 28 | ] 29 | 30 | expect(actual).to.eql(expected) 31 | }) 32 | 33 | it('should return function body', () => { 34 | const actual = functionReflector(() => 'ok').body 35 | const expected = "return 'ok'" 36 | 37 | expect(actual).to.eql(expected) 38 | }) 39 | 40 | it("should return empty array if function doesn't have parameter", () => { 41 | const actual = functionReflector(() => {}).params 42 | const expected = [] 43 | 44 | expect(actual).to.eql(expected) 45 | }) 46 | 47 | it('should return array of parameters', () => { 48 | const actual = functionReflector(fn).params 49 | const expected = [ 50 | { 51 | type: 'SIMPLE', 52 | name: 'a', 53 | }, 54 | { 55 | type: 'SIMPLE', 56 | name: 'b', 57 | }, 58 | ] 59 | 60 | expect(actual).to.eql(expected) 61 | }) 62 | 63 | it('should return null name', () => { 64 | const actual = functionReflector(fn).name 65 | 66 | expect(actual).to.be(null) 67 | }) 68 | 69 | it('should return function body in string', () => { 70 | const actual = functionReflector(fn).body 71 | const expected = 'return a' 72 | 73 | expect(actual).to.eql(expected) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /function-parser.js: -------------------------------------------------------------------------------- 1 | const parseTraditionalFunction = fnString => { 2 | fnString = fnString.slice('function'.length).trim() 3 | 4 | isGenerator = fnString[0] === '*' 5 | if (isGenerator) { 6 | fnString = fnString.slice(1).trim() 7 | } 8 | 9 | const parameterStartIdx = fnString.indexOf('(') 10 | let parameterEndIdx 11 | let counter = 0 12 | for (var i = parameterStartIdx+1; i < fnString.length; i++) { 13 | const token = fnString[i] 14 | if (token == ')' && counter == 0) { 15 | parameterEndIdx = i 16 | break 17 | } 18 | if (token == '(') { 19 | counter += 1 20 | } else if (token == ')') { 21 | counter -= 1 22 | } 23 | } 24 | const isAnonymous = parameterStartIdx == 0 25 | const _rawParameter = fnString.slice(parameterStartIdx + 1, parameterEndIdx) 26 | 27 | const bodyStartIndex = fnString.indexOf('{', parameterEndIdx) 28 | const body = fnString.slice(bodyStartIndex + 1, fnString.length - 1).trim() 29 | fnString = fnString.slice(0, bodyStartIndex).trim() 30 | 31 | 32 | const name = isAnonymous ? null : fnString.slice(0, parameterStartIdx) 33 | 34 | return { 35 | type: isGenerator ? 'GENERATOR' : 'TRADITIONAL', 36 | name, 37 | _rawParameter, 38 | body, 39 | } 40 | } 41 | 42 | const parseArrowFunction = fnString => { 43 | const arrowIndex = fnString.indexOf('=>') 44 | 45 | let body = fnString.slice(arrowIndex+2).trim() 46 | const hasCurlyBrace = body[0] == '{' 47 | if (hasCurlyBrace) { 48 | body = body.slice(1,-1).trim() 49 | } else { 50 | body = 'return ' + body 51 | } 52 | 53 | const parameterWithParentheses = fnString.slice(0, arrowIndex).trim() 54 | const hasParentheses = fnString[0] == '(' 55 | let _rawParameter 56 | if (hasParentheses) { 57 | _rawParameter = parameterWithParentheses.slice(1,-1) 58 | } else { 59 | _rawParameter = parameterWithParentheses 60 | } 61 | 62 | return { 63 | type: 'ARROW', 64 | name: null, 65 | _rawParameter, 66 | body, 67 | } 68 | } 69 | 70 | const parseFunction = fnString => { 71 | const isAsync = fnString.startsWith('async') 72 | if (isAsync) { 73 | fnString = fnString.slice('async'.length).trim() 74 | } 75 | 76 | const isTraditionalFunction = fnString.startsWith('function') 77 | 78 | let parsed 79 | if (isTraditionalFunction) { 80 | parsed = parseTraditionalFunction(fnString) 81 | } else { 82 | parsed = parseArrowFunction(fnString) 83 | } 84 | parsed['async'] = isAsync 85 | 86 | return parsed 87 | } 88 | 89 | module.exports = parseFunction 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-function-reflector 2 | Function Reflection in Javascript With Support for ES2015+ Syntax 3 | 4 | [![Build Status](https://travis-ci.org/arrizalamin/js-function-reflector.svg?branch=master)](https://travis-ci.org/arrizalamin/js-function-reflector) 5 | 6 | 7 | 8 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 9 | 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Examples](#examples) 13 | - [Function with simple parameter](#function-with-simple-parameter) 14 | - [Arrow function](#arrow-function) 15 | - [Async function](#async-function) 16 | - [Generator function](#generator-function) 17 | - [Function with default value parameter](#function-with-default-value-parameter) 18 | - [Function with rest parameter](#function-with-rest-parameter) 19 | - [Function with destructuring parameter](#function-with-destructuring-parameter) 20 | - [Function with variable as default value](#function-with-variable-as-default-value) 21 | 22 | 23 | 24 | 25 | ## Installation 26 | 27 | ```shell 28 | npm install js-function-reflector 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```javascript 34 | const functionReflector = require('js-function-reflector'); 35 | const parsedFunction = functionReflector(yourFunction, scope); 36 | ``` 37 | 38 | ## Examples 39 | 40 | ### Function with simple parameter 41 | 42 | ```javascript 43 | function add(a, b) { 44 | return a + b; 45 | }; 46 | const output = functionReflector(add); 47 | /* output = { 48 | type: "TRADITIONAL", 49 | name: "add", 50 | _rawParameter: "a, b", 51 | body: "return a + b;", 52 | async: false, 53 | params: [ 54 | { 55 | type: "SIMPLE", 56 | name: "a" 57 | }, 58 | { 59 | type: "SIMPLE", 60 | name: "b" 61 | } 62 | ] 63 | } */ 64 | ``` 65 | 66 | ### Arrow function 67 | 68 | ```javascript 69 | const arrowFn = (a, b) => { 70 | return a + b; 71 | } 72 | let output = functionReflector(arrowFn); 73 | /* output = { 74 | type: "ARROW", 75 | name: null, 76 | _rawParameter: "a, b", 77 | body: "return a + b;", 78 | async: false, 79 | params: [ 80 | { 81 | type: "SIMPLE", 82 | name: "a" 83 | }, 84 | { 85 | type: "SIMPLE", 86 | name: "b" 87 | } 88 | ] 89 | } */ 90 | 91 | // inline arrow function automatically added return statement 92 | const arrowWithoutParenthesisAndCurlyBrace = name => 'hello ' + name 93 | output = functionReflector(arrowWithoutParenthesisAndCurlyBrace); 94 | /* output = { 95 | type: "ARROW", 96 | name: null, 97 | _rawParameter: "name", 98 | body: "return 'hello ' + name", 99 | async: false, 100 | params: [ 101 | { 102 | type: "SIMPLE", 103 | name: "name" 104 | } 105 | ] 106 | } */ 107 | ``` 108 | 109 | ### Async function 110 | 111 | ```javascript 112 | const sleep = async function(time) { 113 | return new Promise(resolve, setTimeout(resolve, time)) 114 | } 115 | const output = functionReflector(sleep)) 116 | /* output = { 117 | type: "TRADITIONAL", 118 | name: null, 119 | _rawParameter: "time", 120 | body: "return new Promise(resolve, setTimeout(resolve, time))", 121 | async: true, 122 | params: [ 123 | { 124 | type: "SIMPLE", 125 | name: "time" 126 | } 127 | ] 128 | } 129 | ``` 130 | 131 | ### Generator function 132 | 133 | ```javascript 134 | const generatorFn = function* (list) { 135 | for (var item of list) { 136 | yield item 137 | } 138 | } 139 | const output = functionReflector(generatorFn)) 140 | /* output = { 141 | type: "GENERATOR", 142 | name: null, 143 | _rawParameter: "list", 144 | body: "for (var item of list) {\r\n yield item\r\n }", 145 | async: false, 146 | params: [ 147 | { 148 | type: "SIMPLE", 149 | name: "list" 150 | } 151 | ] 152 | } */ 153 | ``` 154 | 155 | ### Function with default value parameter 156 | 157 | ```javascript 158 | const pow = function(n, power = 2) { 159 | return Math.pow(n, power); 160 | } 161 | const output = functionReflector(pow).params 162 | /* output = [ 163 | { 164 | type: "SIMPLE", 165 | name: "n" 166 | }, 167 | { 168 | type: "DEFAULT", 169 | name: "power", 170 | value: 2 171 | } 172 | ] */ 173 | ``` 174 | 175 | ### Function with rest parameter 176 | 177 | ```javascript 178 | const dummyFn = (a, b = 5, ...c) => c 179 | const output = functionReflector(dummyFn).params 180 | /* output = [ 181 | { 182 | type: "SIMPLE", 183 | name: "a" 184 | }, 185 | { 186 | type: "DEFAULT", 187 | name: "b", 188 | value: 5 189 | }, 190 | { 191 | type: "REST", 192 | name: "c" 193 | } 194 | ] */ 195 | ``` 196 | 197 | ### Function with destructuring parameter 198 | 199 | ```javascript 200 | const destructuringFn = (a, {names: {firstNames, lastNames}, locations: [[country, city], ...restLocations], ...rest}) => {} 201 | const output = functionReflector(destructuringFn).params 202 | /* output = [ 203 | { 204 | type: "SIMPLE", 205 | name: "a" 206 | }, 207 | { 208 | type: "DESTRUCTURING", 209 | value: { 210 | type: "object", 211 | keys: [ 212 | { 213 | type: "DESTRUCTURING", 214 | name: "names", 215 | value: { 216 | type: "object", 217 | keys: [ 218 | { 219 | type: "KEY", 220 | name: "firstNames" 221 | }, 222 | { 223 | type: "KEY", 224 | name: "lastNames" 225 | } 226 | ] 227 | } 228 | }, 229 | { 230 | type: "DESTRUCTURING", 231 | name: "locations", 232 | value: { 233 | type: "array", 234 | keys: [ 235 | { 236 | type: "DESTRUCTURING", 237 | value: { 238 | type: "array", 239 | keys: [ 240 | { 241 | type: "KEY", 242 | name: "country" 243 | }, 244 | { 245 | type: "KEY", 246 | name: "city" 247 | } 248 | ] 249 | } 250 | }, 251 | { 252 | type: "REST", 253 | name: "restLocations" 254 | } 255 | ] 256 | } 257 | }, 258 | { 259 | type: "REST", 260 | name: "rest" 261 | } 262 | ] 263 | } 264 | } 265 | ] */ 266 | ``` 267 | 268 | ### Function with variable as default value 269 | 270 | When parameter's default value contains a variable, we need to pass that variable on the second parameter 271 | 272 | ```javascript 273 | const a = { 274 | number: 1 275 | } 276 | const b = 2 277 | 278 | const dummyFn = function(x = a.number, y = b) {} 279 | const output = functionReflector(dummyFn, { a, b }).params 280 | /* output = [ 281 | { 282 | type: "DEFAULT", 283 | name: "x", 284 | value: 1 285 | }, 286 | { 287 | type: "DEFAULT", 288 | name: "y", 289 | value: 2 290 | } 291 | ] */ 292 | ``` 293 | -------------------------------------------------------------------------------- /parameter-parser.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | VARIABLE: Symbol('VARIABLE'), 3 | DEFAULT: Symbol('DEFAULT'), 4 | REST: Symbol('REST'), 5 | } 6 | 7 | if (!Array.prototype.includes) { 8 | Array.prototype.includes = function(searchElement) { 9 | for (var i in this) { 10 | if (this[i] == searchElement) { 11 | return true; 12 | } 13 | } 14 | return false; 15 | } 16 | } 17 | 18 | const isIn = tokens => token => tokens.includes(token) 19 | const isWhitespace = isIn([' ', '\t', '\n', '\r']) 20 | const isOpening = isIn(['[', '{']) 21 | const isClosing = isIn([']', '}']) 22 | const isStrWrap = isIn(["'", '"', '`']) 23 | 24 | function betterEval(obj, scope = {}){ 25 | try { 26 | return new Function(` 27 | "use strict"; 28 | return (${obj}); 29 | `).call(scope) 30 | } catch (e) { 31 | return new Function(` 32 | "use strict"; 33 | return (this.${obj}); 34 | `).call(scope) 35 | } 36 | } 37 | 38 | class ParameterParser { 39 | constructor(scope = null) { 40 | this.scope = scope 41 | this.state = state.VARIABLE 42 | this.counter = 0 43 | 44 | this.parsed = [] 45 | this.buffer = '' 46 | this.destructuringType = null 47 | this.destructuringKeys = [] 48 | this.destructuringStack = [] 49 | } 50 | 51 | parse(fn) { 52 | let i = -1 53 | while (i < fn.length) { 54 | i += 1 55 | const token = fn[i] 56 | switch (this.state) { 57 | case state.VARIABLE: 58 | if (isWhitespace(token)) continue 59 | if ((token == ':' && this.destructuringType == 'object')) { 60 | this.pushBuffer() 61 | this.destructuringStack.push([this.destructuringType, this.destructuringKeys]) 62 | this.destructuringKeys = [] 63 | continue 64 | } 65 | if (this.destructuringType == 'array' && (token == '[' || token == '{')) { 66 | this.pushBuffer() 67 | this.destructuringStack.push([this.destructuringType, this.destructuringKeys]) 68 | this.destructuringKeys = [] 69 | if (token == '[') { 70 | this.destructuringType = 'array' 71 | } else if (token == '{') { 72 | this.destructuringType = 'object' 73 | } 74 | continue 75 | } 76 | switch (token) { 77 | case '=': 78 | this.pushBuffer() 79 | this.state = state.DEFAULT 80 | continue 81 | case '{': 82 | this.destructuringType = 'object' 83 | continue 84 | case '[': 85 | this.destructuringType = 'array' 86 | continue 87 | case ',': 88 | this.pushBuffer() 89 | 90 | continue 91 | case '.': 92 | if (this.buffer === '') { 93 | i += 2 94 | this.state = state.REST 95 | continue 96 | } 97 | } 98 | if (isClosing(token) && this.destructuringType != null) { 99 | this.pushBuffer() 100 | this.pushDestructuringKeys() 101 | continue 102 | } 103 | this.buffer += token 104 | if (i === fn.length - 1) { 105 | this.pushBuffer() 106 | if (this.destructuringType != null) { 107 | this.pushDestructuringKeys() 108 | } 109 | } 110 | break 111 | 112 | case state.DEFAULT: 113 | if (this.counter === 0 && isWhitespace(token)) continue 114 | if (isStrWrap(token)) { 115 | const closingIdx = fn.indexOf(token, i+1) 116 | this.buffer += fn.slice(i, closingIdx + 1) 117 | if (this.destructuringType == null) { 118 | this.pushBuffer() 119 | } 120 | i = closingIdx 121 | continue 122 | } 123 | if (isClosing(token) && this.counter == 0 && this.destructuringType != null) { 124 | this.pushBuffer() 125 | this.pushDestructuringKeys() 126 | continue 127 | } 128 | if (isOpening(token)) { 129 | this.counter += 1 130 | } else if (isClosing(token)) { 131 | this.counter -= 1 132 | } 133 | if (this.counter === 0 && token === ',') { 134 | this.pushBuffer() 135 | this.state = state.VARIABLE 136 | continue 137 | } 138 | this.buffer += token 139 | if (i === fn.length - 1) { 140 | this.pushBuffer() 141 | if (this.destructuringType != null) { 142 | this.pushDestructuringKeys() 143 | } 144 | } 145 | break 146 | 147 | case state.REST: 148 | if (isClosing(token) && this.counter == 0 && this.destructuringType != null) { 149 | this.pushBuffer() 150 | this.pushDestructuringKeys() 151 | this.state = state.VARIABLE 152 | continue 153 | } 154 | this.buffer += token 155 | if (i === fn.length - 1) { 156 | this.pushBuffer() 157 | } 158 | break 159 | } 160 | } 161 | return this.parsed 162 | } 163 | 164 | pushBuffer() { 165 | if (this.buffer === '') return 166 | 167 | switch (this.state) { 168 | case state.VARIABLE: 169 | if (this.destructuringType == null) { 170 | this.parsed.push({ 171 | type: 'SIMPLE', 172 | name: this.buffer, 173 | }) 174 | } else { 175 | this.destructuringKeys.push({ 176 | type: 'KEY', 177 | name: this.buffer, 178 | }) 179 | } 180 | break 181 | 182 | case state.DEFAULT: 183 | let topStack 184 | if (this.destructuringType == null) { 185 | topStack = this.parsed.pop() 186 | } else { 187 | topStack = this.destructuringKeys.pop() 188 | } 189 | 190 | const defaultParam = betterEval(this.buffer, this.scope) 191 | 192 | if (this.destructuringType == null) { 193 | this.parsed.push({ 194 | type: 'DEFAULT', 195 | name: topStack.name, 196 | value: defaultParam, 197 | }) 198 | } else { 199 | this.destructuringKeys.push({ 200 | type: 'KEY_WITH_DEFAULT', 201 | name: topStack.name, 202 | value: defaultParam, 203 | }) 204 | } 205 | break 206 | 207 | case state.REST: 208 | if (this.destructuringType == null) { 209 | this.parsed.push({ 210 | type: 'REST', 211 | name: this.buffer, 212 | }) 213 | } else { 214 | this.destructuringKeys.push({ 215 | type: 'REST', 216 | name: this.buffer, 217 | }) 218 | } 219 | break 220 | } 221 | this.buffer = '' 222 | } 223 | 224 | pushDestructuringKeys() { 225 | const parsed = { 226 | type: this.destructuringType, 227 | keys: this.destructuringKeys, 228 | } 229 | if (this.destructuringStack.length > 0) { 230 | let [topType, topStack] = this.destructuringStack.pop() 231 | if (topType == 'object') { 232 | let lastStack = topStack[topStack.length - 1] 233 | topStack[topStack.length - 1] = { 234 | type: 'DESTRUCTURING', 235 | name: lastStack.name, 236 | value: parsed, 237 | } 238 | } else { 239 | topStack.push({ 240 | type: 'DESTRUCTURING', 241 | value: parsed, 242 | }) 243 | } 244 | 245 | this.destructuringKeys = topStack 246 | this.destructuringType = topType 247 | } else { 248 | this.parsed.push({ 249 | type: 'DESTRUCTURING', 250 | value: parsed, 251 | }) 252 | this.destructuringKeys = [] 253 | this.destructuringType = null 254 | } 255 | } 256 | } 257 | 258 | module.exports = ParameterParser 259 | -------------------------------------------------------------------------------- /test/destructuring.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js') 2 | const functionReflector = require('../index') 3 | 4 | describe('Destructuring Parameter', () => { 5 | const objDestructuringFn = (a, b = true, {name,age}, d) => { 6 | return age 7 | } 8 | const arrDestructuringFn = (a, b = true, [name, age, ...rest]) => { 9 | return name 10 | } 11 | 12 | it('should parse object destructuring parameter', () => { 13 | const actual = functionReflector(objDestructuringFn).params 14 | const expected = [ 15 | { 16 | type: 'SIMPLE', 17 | name: 'a', 18 | }, 19 | { 20 | type: 'DEFAULT', 21 | name: 'b', 22 | value: true, 23 | }, 24 | { 25 | type: 'DESTRUCTURING', 26 | value: { 27 | type: 'object', 28 | keys: [ 29 | { 30 | type: 'KEY', 31 | name: 'name', 32 | }, 33 | { 34 | type: 'KEY', 35 | name: 'age' 36 | }, 37 | ], 38 | }, 39 | }, 40 | { 41 | type: 'SIMPLE', 42 | name: 'd', 43 | }, 44 | ] 45 | 46 | expect(actual).to.eql(expected) 47 | }) 48 | 49 | it('should parse array destructuring parameter', () => { 50 | const actual = functionReflector(arrDestructuringFn).params 51 | const expected = [ 52 | { 53 | type: 'SIMPLE', 54 | name: 'a', 55 | }, 56 | { 57 | type: 'DEFAULT', 58 | name: 'b', 59 | value: true, 60 | }, 61 | { 62 | type: 'DESTRUCTURING', 63 | value: { 64 | type: 'array', 65 | keys: [ 66 | { 67 | type: 'KEY', 68 | name: 'name', 69 | }, 70 | { 71 | type: 'KEY', 72 | name: 'age' 73 | }, 74 | { 75 | type: 'REST', 76 | name: 'rest', 77 | }, 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | expect(actual).to.eql(expected) 84 | }) 85 | 86 | it('should parse object destructuring with default value parameter', () => { 87 | const actual = functionReflector(objDestructuringFn).params 88 | const expected = [ 89 | { 90 | type: 'SIMPLE', 91 | name: 'a', 92 | }, 93 | { 94 | type: 'DEFAULT', 95 | name: 'b', 96 | value: true, 97 | }, 98 | { 99 | type: 'DESTRUCTURING', 100 | value: { 101 | type: 'object', 102 | keys: [ 103 | { 104 | type: 'KEY', 105 | name: 'name', 106 | }, 107 | { 108 | type: 'KEY', 109 | name: 'age' 110 | }, 111 | ], 112 | }, 113 | }, 114 | { 115 | type: 'SIMPLE', 116 | name: 'd', 117 | }, 118 | ] 119 | 120 | expect(actual).to.eql(expected) 121 | }) 122 | 123 | it('should parse array destructuring parameter with default value', () => { 124 | const actual = functionReflector(([a = true, b = 'foo', c]) => {}).params 125 | const expected = [ 126 | { 127 | type: 'DESTRUCTURING', 128 | value: { 129 | type: 'array', 130 | keys: [ 131 | { 132 | type: 'KEY_WITH_DEFAULT', 133 | name: 'a', 134 | value: true, 135 | }, 136 | { 137 | type: 'KEY_WITH_DEFAULT', 138 | name: 'b', 139 | value: 'foo', 140 | }, 141 | { 142 | type: 'KEY', 143 | name: 'c', 144 | }, 145 | ], 146 | }, 147 | }, 148 | ] 149 | 150 | expect(actual).to.eql(expected) 151 | }) 152 | 153 | it('should parse array destructuring parameter with deep default value', () => { 154 | const actual = functionReflector(([a = [1,2], b = {name: 'ABC', age: 20}, 155 | c = [{country: 'Indonesia', city: 'Jakarta'}]]) => {}).params 156 | const expected = [ 157 | { 158 | type: 'DESTRUCTURING', 159 | value: { 160 | type: 'array', 161 | keys: [ 162 | { 163 | type: 'KEY_WITH_DEFAULT', 164 | name: 'a', 165 | value: [1,2], 166 | }, 167 | { 168 | type: 'KEY_WITH_DEFAULT', 169 | name: 'b', 170 | value: {name: 'ABC', age: 20}, 171 | }, 172 | { 173 | type: 'KEY_WITH_DEFAULT', 174 | name: 'c', 175 | value: [{country: 'Indonesia', city: 'Jakarta'}], 176 | } 177 | ], 178 | }, 179 | }, 180 | ] 181 | 182 | expect(actual).to.eql(expected) 183 | }) 184 | 185 | it('should parse object destructuring parameter with deep default value', () => { 186 | const actual = functionReflector(({a = [1,2], b = {name: 'ABC', age: 20}, 187 | c = [{country: 'Indonesia', city: 'Jakarta'}]}) => {}).params 188 | const expected = [ 189 | { 190 | type: 'DESTRUCTURING', 191 | value: { 192 | type: 'object', 193 | keys: [ 194 | { 195 | type: 'KEY_WITH_DEFAULT', 196 | name: 'a', 197 | value: [1,2], 198 | }, 199 | { 200 | type: 'KEY_WITH_DEFAULT', 201 | name: 'b', 202 | value: {name: 'ABC', age: 20}, 203 | }, 204 | { 205 | type: 'KEY_WITH_DEFAULT', 206 | name: 'c', 207 | value: [{country: 'Indonesia', city: 'Jakarta'}], 208 | }, 209 | ], 210 | }, 211 | }, 212 | ] 213 | 214 | expect(actual).to.eql(expected) 215 | }) 216 | 217 | it('should parse deep object destructuring parameter', () => { 218 | const deepDestructuringFn = ({ids: [firstId, ...restIds], names: {lastNames}}) => {} 219 | const actual = functionReflector(deepDestructuringFn).params 220 | const expected = [ 221 | { 222 | type: 'DESTRUCTURING', 223 | value: { 224 | type: 'object', 225 | keys: [ 226 | { 227 | type: 'DESTRUCTURING', 228 | name: 'ids', 229 | value: { 230 | type: 'array', 231 | keys: [ 232 | { 233 | type: 'KEY', 234 | name: 'firstId', 235 | }, 236 | { 237 | type: 'REST', 238 | name: 'restIds', 239 | }, 240 | ], 241 | }, 242 | }, 243 | { 244 | type: 'DESTRUCTURING', 245 | name: 'names', 246 | value: { 247 | type: 'object', 248 | keys: [ 249 | { 250 | type: 'KEY', 251 | name: 'lastNames', 252 | }, 253 | ], 254 | }, 255 | }, 256 | ], 257 | }, 258 | }, 259 | ] 260 | 261 | expect(actual).to.eql(expected) 262 | }) 263 | 264 | it('should parse deep array destructuring parameter', () => { 265 | const deepDestructuringFn = ([[firstId, ...restIds], {lastNames}]) => {} 266 | const actual = functionReflector(deepDestructuringFn).params 267 | const expected = [ 268 | { 269 | type: 'DESTRUCTURING', 270 | value: { 271 | type: 'array', 272 | keys: [ 273 | { 274 | type: 'DESTRUCTURING', 275 | value: { 276 | type: 'array', 277 | keys: [ 278 | { 279 | type: 'KEY', 280 | name: 'firstId', 281 | }, 282 | { 283 | type: 'REST', 284 | name: 'restIds', 285 | }, 286 | ], 287 | }, 288 | }, 289 | { 290 | type: 'DESTRUCTURING', 291 | value: { 292 | type: 'object', 293 | keys: [ 294 | { 295 | type: 'KEY', 296 | name: 'lastNames', 297 | }, 298 | ], 299 | }, 300 | }, 301 | ], 302 | }, 303 | }, 304 | ] 305 | 306 | expect(actual).to.eql(expected) 307 | }) 308 | 309 | 310 | it('should parse multiline destructuring parameter', () => { 311 | const multilineFn = function ({ 312 | param1, 313 | param2 314 | }) {} 315 | const actual = functionReflector(multilineFn).params 316 | const expected = [ 317 | { 318 | type: 'DESTRUCTURING', 319 | value: { 320 | type: 'object', 321 | keys: [ 322 | { 323 | type: 'KEY', 324 | name: 'param1', 325 | }, 326 | { 327 | type: 'KEY', 328 | name: 'param2', 329 | }, 330 | ], 331 | }, 332 | }, 333 | ] 334 | 335 | expect(actual).to.eql(expected) 336 | }) 337 | }) 338 | --------------------------------------------------------------------------------