├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── bin ├── comment-value.js └── comment-value.sh ├── images ├── comment-value-compose.webm ├── comment-value-watch-mode.webm └── comment-value.png ├── package.json ├── src ├── beautify.js ├── comment-parser.js ├── comment-value-spec.js ├── comments-spec.js ├── comments.js ├── index.js ├── instrument-source.js ├── instrument.js └── update.js └── test ├── array ├── expected.js ├── index.js └── manual.js ├── async-example.js ├── combined ├── expected.js └── index.js ├── comma ├── expected.js └── index.js ├── compose ├── curried-compose.js ├── expected.js ├── index.js ├── manual.js ├── package.json ├── ramda-compose.js └── spec.js ├── console-log ├── expected.js ├── index.js └── spec.js ├── example.js ├── identifier-example.js ├── lodash-chain ├── composed.js ├── expected.js ├── index.js └── package.json ├── nested ├── add.js └── index.js ├── next-line ├── expected.js ├── index.js └── spec.js ├── promise-example.js ├── property ├── expected.js ├── index.js └── spec.js ├── query-example └── index.js ├── spec.js ├── symbols ├── expected.js ├── index.js └── spec.js ├── types ├── expected.js ├── index.js └── spec.js ├── types2 ├── expected.js ├── index.js └── spec.js └── variable ├── expected.js ├── index.js └── spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | instrumented.js 5 | results.json 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | progress=false 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: true 7 | node_js: 8 | - '6' 9 | before_script: 10 | - npm prune 11 | script: 12 | - npm test 13 | - npm test 14 | - npm run test-variable 15 | - npm run test-types 16 | - npm run test-types2 17 | - npm run test-comma 18 | - npm run hook 19 | - npm run test-console 20 | - npm run test-nested 21 | - npm run test-symbols 22 | - npm run test-array 23 | - npm run test-chain 24 | - npm run test-compose 25 | - npm run test-property 26 | after_success: 27 | - npm run semantic-release 28 | branches: 29 | except: 30 | - /^v\d+\.\d+\.\d+$/ 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # comment-value 2 | 3 | > Instruments a Node program and updates its comments with computed expression values 4 | 5 | [![NPM][npm-icon] ][npm-url] 6 | 7 | [![Build status][ci-image] ][ci-url] 8 | [![semantic-release][semantic-image] ][semantic-url] 9 | [![js-standard-style][standard-image]][standard-url] 10 | 11 | ## Why? 12 | 13 | Writing and maintaining code examples is hard. Often the values shown in 14 | the comments (think blog posts) are out of date and incorrect. 15 | 16 | Read more about the problem and this tool solves it in blog post 17 | [Accurate values in comments](https://glebbahmutov.com/blog/accurate-values-in-comments/). 18 | 19 | ## What does comment-value do? 20 | 21 | `comment-value` (or available aliases `values`, `comment`, `comments`) 22 | executes your Node program, instrumenting it on the fly. 23 | Every time it sees a special comment that starts with `//>`, 24 | it will set it value from whatever the expression immediately to its left is. 25 | 26 | When Node finishes, the file is saved back with updated comments. 27 | 28 | [![comment-value in action](images/comment-value.png)](https://comment-values-awgxclhogn.now.sh/comment-values.webm) 29 | 30 | Click on the above screen shot to see 15 second demo clip. 31 | 32 | ## Watch mode 33 | 34 | Automatically (well, as long as `chokidar` works) reruns and updates comments 35 | in the source file. See the 36 | [video](https://glebbahmutov.com/comment-value/comment-value-watch-mode.webm), 37 | it is awesome! 38 | 39 | ## Install and use 40 | 41 | Install `comment-value` either as a global or as a local package. 42 | 43 | ``` 44 | npm install -g comment-value 45 | ``` 46 | 47 | Use either using `node -r comment-value index.js` or via CLI alias: 48 | `comment-value`, `values` or `cv` like this `values index.js`. 49 | 50 | Alias `values` is the preferred way. It allows 51 | 52 | * Watch files for changes and rerun with `-w, --watch` option 53 | * Print instrumented file with `-i, --instrumented` option 54 | 55 | ## Example 56 | 57 | Add a few comments that start with `//>` to `index.js`. You can put anything 58 | after that into the comment. 59 | 60 | ```js 61 | // index.js 62 | const add = (a, b) => a + b 63 | add(2, 3) //> 64 | add(2, -3) //> ? anything here 65 | // you can also print variables 66 | const t = typeof add 67 | // t: 68 | // or variable types directly 69 | // add:: 70 | ``` 71 | 72 | Run the `comment-value` script which runs your Node 73 | 74 | ```sh 75 | $ comment-value index.js 76 | ``` 77 | 78 | The `index.js` will now contain 79 | 80 | ```js 81 | // index.js 82 | const add = (a, b) => a + b 83 | add(2, 3) //> 5 84 | add(2, -3) //> -1 85 | // you can also print variables! 86 | const t = typeof add 87 | // t: "function" 88 | // add:: "function" 89 | ``` 90 | 91 | ## Comment format 92 | 93 | You can start special value comments to be updated with strings 94 | `//>`, `//=>`, `//~>`, `// >`, `// =>` and even `// ~>`. 95 | 96 | For variables, use line comment with just variable name followed by `:`, 97 | for example 98 | 99 | ```js 100 | function add(a, b) { 101 | // a: 102 | // b: 103 | } 104 | add(10, 2) 105 | ``` 106 | 107 | which will produce 108 | 109 | ```js 110 | function add(a, b) { 111 | // a: 10 112 | // b: 2 113 | } 114 | add(10, 2) 115 | ``` 116 | 117 | To print the type of a variable, use variable name followed by `::`, for 118 | example 119 | 120 | ```js 121 | function add(a, b) { 122 | // a:: "number" 123 | // b:: "string" 124 | } 125 | add(10, 'foo') 126 | ``` 127 | 128 | ## Composed functions 129 | 130 | You can even get results from composed functions, for example, the values 131 | below were all computed automatically 132 | 133 | ```js 134 | var R = require('ramda') 135 | R.compose( 136 | Math.abs, //=> 7 137 | R.add(1), //=> -7 138 | R.multiply(2) //=> -8 139 | )(-4) //=> 7 140 | ``` 141 | 142 | ## Mixing values and types 143 | 144 | You can record either values or types of values 145 | 146 | ```js 147 | var R = require('ramda') 148 | R.compose( 149 | Math.abs, //:: number 150 | R.add(1), //=> -7 151 | R.multiply(2) //=> -8 152 | )(-4) 153 | // :: number 154 | ``` 155 | 156 | ## Console log statements 157 | 158 | If the value comment is on the left of `console.log(value)` expression, 159 | then it will be updated with the `value`. 160 | 161 | ```js 162 | // index.js 163 | console.log(2 + 40) //> ? 164 | ``` 165 | 166 | ```sh 167 | $ comment-value index.js 168 | 42 169 | ``` 170 | 171 | ```js 172 | // index.js 173 | console.log(2 + 40) //> 42 174 | ``` 175 | 176 | ## Debug 177 | 178 | To see verbose messages while this module runs, set the environment 179 | variable `DEBUG` before running 180 | 181 | ``` 182 | DEBUG=comment-value node ... 183 | ``` 184 | 185 | The instrumenting function has a global emitter, you can receive messages 186 | when special comments are found and when an expression is wrapped. 187 | For example this code will produce the following events 188 | 189 | ```js 190 | // index.js 191 | console.log(2 + 40) //> ?? 192 | // spec.js 193 | let emitter 194 | beforeEach(() => { 195 | emitter = global.instrument 196 | }) 197 | it('finds the comment', () => { 198 | const comments = [] 199 | const wrapped = [] 200 | emitter.on('comment', c => comments.push(c)) 201 | emitter.on('wrap', w => wrapped.push(w)) 202 | instrument(source) 203 | // comments will be ["> ??"] 204 | // wrapped will be ["2 + 40"] 205 | }) 206 | ``` 207 | 208 | This is an internal feature and is used during unit tests. 209 | 210 | ### Small print 211 | 212 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2017 213 | 214 | * [@bahmutov](https://twitter.com/bahmutov) 215 | * [glebbahmutov.com](http://glebbahmutov.com) 216 | * [blog](http://glebbahmutov.com/blog) 217 | 218 | License: MIT - do anything with the code, but don't blame me if it does not work. 219 | 220 | Support: if you find any problems with this module, email / tweet / 221 | [open issue](https://github.com/bahmutov/comment-value/issues) on Github 222 | 223 | ## MIT License 224 | 225 | Copyright (c) 2017 Gleb Bahmutov <gleb.bahmutov@gmail.com> 226 | 227 | Permission is hereby granted, free of charge, to any person 228 | obtaining a copy of this software and associated documentation 229 | files (the "Software"), to deal in the Software without 230 | restriction, including without limitation the rights to use, 231 | copy, modify, merge, publish, distribute, sublicense, and/or sell 232 | copies of the Software, and to permit persons to whom the 233 | Software is furnished to do so, subject to the following 234 | conditions: 235 | 236 | The above copyright notice and this permission notice shall be 237 | included in all copies or substantial portions of the Software. 238 | 239 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 240 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 241 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 242 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 243 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 244 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 245 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 246 | OTHER DEALINGS IN THE SOFTWARE. 247 | 248 | [npm-icon]: https://nodei.co/npm/comment-value.svg?downloads=true 249 | [npm-url]: https://npmjs.org/package/comment-value 250 | [ci-image]: https://travis-ci.org/bahmutov/comment-value.svg?branch=master 251 | [ci-url]: https://travis-ci.org/bahmutov/comment-value 252 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 253 | [semantic-url]: https://github.com/semantic-release/semantic-release 254 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg 255 | [standard-url]: http://standardjs.com/ 256 | -------------------------------------------------------------------------------- /bin/comment-value.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const debug = require('debug')('comment-value') 4 | const program = require('commander') 5 | const spawn = require('cross-spawn') 6 | const chokidar = require('chokidar') 7 | const path = require('path') 8 | const firstExisting = require('first-existing') 9 | const la = require('lazy-ass') 10 | const is = require('check-more-types') 11 | 12 | program 13 | .option('-w, --watch', 'Keep watching input files') 14 | .option('-i, --instrumented', 'Print instrumented files') 15 | .parse(process.argv) 16 | 17 | if (!program.args.length) { 18 | console.error('values ') 19 | process.exit(1) 20 | } 21 | 22 | const filename = path.resolve(program.args[0]) 23 | 24 | function findChangedValuesModule() { 25 | const choices = [ 26 | '../src/index.js', 27 | '../lib/node_modules/comment-value/src/index.js' 28 | ] 29 | return firstExisting(__dirname, choices) 30 | } 31 | const modulePath = findChangedValuesModule() 32 | la(is.unemptyString(modulePath), 'could not find comment-value module') 33 | 34 | function runNode(inputFilename) { 35 | debug('running program %s', inputFilename) 36 | return new Promise((resolve, reject) => { 37 | const args = ['-r', modulePath, inputFilename] 38 | const env = Object.assign({}, process.env) 39 | if (program.instrumented) { 40 | env.instrumented = 1 41 | } 42 | const opts = { 43 | stdio: 'inherit', 44 | env 45 | } 46 | const child = spawn('node', args, opts) 47 | child.on('error', err => { 48 | console.error(err) 49 | reject(err) 50 | }) 51 | child.on('close', code => { 52 | debug('node is done, exit code %d', code) 53 | if (code) { 54 | debug('Error when running %s', inputFilename) 55 | return reject(new Error('Node exit code ' + code)) 56 | } 57 | resolve() 58 | }) 59 | }) 60 | } 61 | 62 | function onError(err) { 63 | console.error('An error') 64 | console.error(err) 65 | process.exit(1) 66 | } 67 | 68 | function watchFile() { 69 | // TODO determine files to watch from 70 | // collected files 71 | console.log('watching file', filename) 72 | const watchOptions = { 73 | persistent: true, 74 | ignoreInitial: true 75 | } 76 | const watcher = chokidar.watch(filename, watchOptions) 77 | const updateComments = path => { 78 | debug('updating comments in %s on watch', path) 79 | return runNode(filename) 80 | .catch(onError) 81 | } 82 | 83 | watcher.on('change', path => { 84 | // while the update is running, remove the watcher 85 | // to avoid infinite loop 86 | debug('file %s has changed', path) 87 | watcher.unwatch(filename) 88 | updateComments(path) 89 | .then(() => { 90 | watcher.add(filename) 91 | }) 92 | }) 93 | } 94 | 95 | if (program.watch) { 96 | debug('watch options is true') 97 | runNode(filename) 98 | .catch(onError) 99 | .then(watchFile) 100 | .catch(console.error) 101 | } else { 102 | debug('updating values once') 103 | runNode(filename) 104 | .catch(onError) 105 | } 106 | 107 | -------------------------------------------------------------------------------- /bin/comment-value.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # when in NPM global bin folder 4 | # find the path to the actual file with require hook 5 | BASEDIR=$(dirname "$0") 6 | module=$BASEDIR/../lib/node_modules/comment-value 7 | node -r $module $@ 8 | -------------------------------------------------------------------------------- /images/comment-value-compose.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/comment-value/dc329f3a77e68ae0aa21c4fece323a0a94be7e83/images/comment-value-compose.webm -------------------------------------------------------------------------------- /images/comment-value-watch-mode.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/comment-value/dc329f3a77e68ae0aa21c4fece323a0a94be7e83/images/comment-value-watch-mode.webm -------------------------------------------------------------------------------- /images/comment-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/comment-value/dc329f3a77e68ae0aa21c4fece323a0a94be7e83/images/comment-value.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comment-value", 3 | "description": "Instruments a Node program and updates its comments with computed expression values", 4 | "version": "0.0.0-development", 5 | "author": "Gleb Bahmutov ", 6 | "bugs": "https://github.com/bahmutov/comment-value/issues", 7 | "config": { 8 | "pre-git": { 9 | "commit-msg": "simple", 10 | "pre-commit": [ 11 | "npm prune", 12 | "npm run deps", 13 | "npm test", 14 | "npm run test-variable", 15 | "npm run hook", 16 | "npm run test-console", 17 | "npm run test-nested", 18 | "npm run test-symbols", 19 | "npm run test-array", 20 | "npm run test-chain", 21 | "npm run test-compose", 22 | "npm run test-comma", 23 | "npm run test-property", 24 | "npm run ban" 25 | ], 26 | "pre-push": [ 27 | "npm run secure", 28 | "npm run license", 29 | "npm run ban -- --all", 30 | "npm run size" 31 | ], 32 | "post-commit": [], 33 | "post-merge": [] 34 | } 35 | }, 36 | "engines": { 37 | "node": ">=6" 38 | }, 39 | "files": [ 40 | "src/*.js", 41 | "!src/*-spec.js", 42 | "bin", 43 | "images/*.png" 44 | ], 45 | "homepage": "https://github.com/bahmutov/comment-value#readme", 46 | "keywords": [ 47 | "comment", 48 | "comments", 49 | "cover", 50 | "coverage", 51 | "data", 52 | "demo", 53 | "demos", 54 | "instrument", 55 | "value" 56 | ], 57 | "license": "MIT", 58 | "main": "src/", 59 | "bin": { 60 | "cv": "bin/comment-value.sh", 61 | "comment-value": "bin/comment-value.sh", 62 | "values": "bin/comment-value.js", 63 | "comment": "bin/comment-value.js", 64 | "comments": "bin/comment-value.js" 65 | }, 66 | "publishConfig": { 67 | "registry": "http://registry.npmjs.org/" 68 | }, 69 | "preferGlobal": true, 70 | "repository": { 71 | "type": "git", 72 | "url": "https://github.com/bahmutov/comment-value.git" 73 | }, 74 | "scripts": { 75 | "ban": "ban", 76 | "deps": "deps-ok", 77 | "issues": "git-issues", 78 | "license": "license-checker --production --onlyunknown --csv", 79 | "lint": "standard --verbose --fix src/*.js test/property/*.js", 80 | "pretest": "npm run lint", 81 | "secure": "nsp check", 82 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 83 | "unit": "mocha src/*-spec.js test/spec.js test/**/spec.js", 84 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 85 | "test": "DEBUG=comment-value instrumented=1 npm run instrument && npm start && cat results.json && npm run update", 86 | "posttest": "npm run unit", 87 | "instrument": "instrumented=1 node src/instrument.js test/example.js", 88 | "update": "node src/update.js test/example.js", 89 | "start": "node instrumented.js", 90 | "hook": "node -r ./src/index.js test/example.js", 91 | "test-nested": "DEBUG=comment-value instrumented=1 node -r ./src/index.js test/nested/index.js", 92 | "deploy": "gh-pages -d images", 93 | "test-symbols": "DEBUG=comment-value instrumented=1 node -r . test/symbols/index.js", 94 | "posttest-symbols": "diff test/symbols/index.js test/symbols/expected.js", 95 | "test-types": "DEBUG=comment-value instrumented=1 node -r . test/types/index.js", 96 | "posttest-types": "diff test/types/index.js test/types/expected.js", 97 | "test-types2": "DEBUG=comment-value instrumented=1 node -r . test/types2/index.js", 98 | "posttest-types2": "diff test/types2/index.js test/types2/expected.js", 99 | "test-array": "DEBUG=comment-value instrumented=1 node -r . test/array/index.js", 100 | "posttest-array": "diff test/array/index.js test/array/expected.js", 101 | "test-chain": "DEBUG=comment-value instrumented=1 node -r . test/lodash-chain/index.js", 102 | "posttest-chain": "diff test/lodash-chain/index.js test/lodash-chain/expected.js", 103 | "test-compose": "DEBUG=comment-value instrumented=1 node -r . test/compose/index.js", 104 | "posttest-compose": "diff test/compose/index.js test/compose/expected.js", 105 | "test-console": "DEBUG=comment-value instrumented=1 node -r . test/console-log/index.js", 106 | "posttest-console": "diff test/console-log/index.js test/console-log/expected.js", 107 | "e2e": ".git/hooks/pre-commit", 108 | "test-comma": "DEBUG=comment-value instrumented=1 node -r . test/comma/index.js", 109 | "posttest-comma": "diff test/comma/index.js test/comma/expected.js", 110 | "test-property": "DEBUG=comment-value instrumented=1 node -r . test/property/index.js", 111 | "posttest-property": "diff test/property/index.js test/property/expected.js", 112 | "test-variable": "DEBUG=comment-value instrumented=1 node -r . test/variable/index.js", 113 | "posttest-variable": "diff test/variable/index.js test/variable/expected.js" 114 | }, 115 | "devDependencies": { 116 | "ban-sensitive-files": "1.9.0", 117 | "dependency-check": "2.7.0", 118 | "deps-ok": "1.2.0", 119 | "dont-crack": "1.1.0", 120 | "gh-pages": "0.12.0", 121 | "git-issues": "1.3.1", 122 | "license-checker": "8.0.3", 123 | "mocha": "3.2.0", 124 | "nsp": "2.6.2", 125 | "pre-git": "3.12.0", 126 | "semantic-release": "^6.3.2", 127 | "standard": "8.6.0" 128 | }, 129 | "dependencies": { 130 | "check-more-types": "2.23.0", 131 | "chokidar": "1.6.0", 132 | "commander": "2.9.0", 133 | "cross-spawn": "5.0.1", 134 | "debug": "2.6.0", 135 | "falafel": "2.0.0", 136 | "first-existing": "1.2.0", 137 | "js-beautify": "1.6.4", 138 | "lazy-ass": "1.5.0", 139 | "node-hook": "0.4.0", 140 | "ramda": "0.23.0" 141 | }, 142 | "release": { 143 | "verifyRelease": { 144 | "path": "dont-crack", 145 | "test-against": [ 146 | "https://github.com/bahmutov/comment-value-test" 147 | ] 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/beautify.js: -------------------------------------------------------------------------------- 1 | function beautifyJavaScript (source) { 2 | const beautify = require('js-beautify').js_beautify 3 | return beautify(source, {indent_size: 2}) 4 | } 5 | 6 | module.exports = beautifyJavaScript 7 | -------------------------------------------------------------------------------- /src/comment-parser.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const comments = require('./comments') 4 | const { 5 | findCommentValue, 6 | findCommentType, 7 | findCommentVariable, 8 | findCommentVariableType, 9 | isLineComment, 10 | parseLineComment 11 | } = comments 12 | 13 | function initVariableParser (filename, comments, emitter) { 14 | la(is.array(comments), 'missing list for output comments') 15 | 16 | const parseAsCommentVariable = (text, start, end, from, to) => { 17 | const variable = findCommentVariable(text) 18 | if (!variable) { 19 | return 20 | } 21 | 22 | const comment = { 23 | value: undefined, 24 | start, 25 | text, 26 | from, 27 | to, 28 | filename, 29 | variable 30 | } 31 | return comment 32 | } 33 | 34 | const parserOptions = { 35 | locations: true, 36 | onComment (block, text, start, end, from, to) { 37 | if (block) { 38 | return 39 | } 40 | const comment = parseAsCommentVariable(text, start, end, from, to) 41 | if (comment) { 42 | comment.index = comments.length 43 | comments.push(comment) 44 | emitter.emit('comment', comment) 45 | } 46 | } 47 | } 48 | return parserOptions 49 | } 50 | 51 | function findVariables (lines) { 52 | const output = [] 53 | lines.forEach((line, lineIndex) => { 54 | if (!isLineComment(line)) { 55 | return 56 | } 57 | const parsed = parseLineComment(line) 58 | la(parsed, 'could not parse line comment', line) 59 | 60 | const variable = findCommentVariable(parsed.comment) 61 | if (variable) { 62 | la(is.unemptyString(variable), 'expected variable name', variable, 63 | 'from', parsed.comment) 64 | const comment = { 65 | find: 'value', 66 | line, 67 | lineIndex, 68 | variable 69 | } 70 | output.push(comment) 71 | return 72 | } 73 | 74 | const variableType = findCommentVariableType(parsed.comment) 75 | if (variableType) { 76 | la(is.unemptyString(variableType), 77 | 'expected variable name for type', variableType, 78 | 'from', parsed.comment) 79 | const comment = { 80 | find: 'type', 81 | line, 82 | lineIndex, 83 | variable: variableType 84 | } 85 | output.push(comment) 86 | } 87 | }) 88 | return output 89 | } 90 | 91 | function parseCommentVariables (source, filename, list, emitter) { 92 | la(is.array(list), 'missing output list for variables') 93 | 94 | const lines = source.split('\n') 95 | const output = findVariables(lines) 96 | const initialLength = list.length 97 | 98 | output.forEach((c, k) => { 99 | c.filename = filename 100 | c.index = initialLength + k 101 | list.push(c) 102 | emitter.emit('comment', c) 103 | }) 104 | 105 | output.forEach((c, k) => { 106 | la(is.number(c.lineIndex), 'missing line index', c) 107 | // account for previous insertions 108 | const newLineIndex = c.lineIndex + k 109 | const reference = `global.__instrumenter.variables[${c.index}].value` 110 | let what 111 | if (c.find === 'value') { 112 | what = `${c.variable}` 113 | } else if (c.find === 'type') { 114 | what = `typeof ${c.variable}` 115 | } else { 116 | throw new Error(`Unknown info to find ${c.find} for variable ${c.variable}`) 117 | } 118 | const store = `${reference} = ${what}` 119 | lines.splice(newLineIndex + 1, 0, store) 120 | }) 121 | 122 | return lines.join('\n') 123 | } 124 | 125 | function initExpressionParser (filename, comments, emitter) { 126 | la(is.array(comments), 'missing list for output comments') 127 | 128 | const parseAsCommentValue = (text, start, end, from, to) => { 129 | let commentStart 130 | commentStart = findCommentValue(text) 131 | if (commentStart) { 132 | const comment = { 133 | find: 'value', 134 | value: undefined, 135 | start, 136 | text, 137 | from, 138 | to, 139 | filename, 140 | commentStart 141 | } 142 | return comment 143 | } 144 | 145 | commentStart = findCommentType(text) 146 | if (commentStart) { 147 | const comment = { 148 | find: 'type', 149 | value: undefined, 150 | start, 151 | text, 152 | from, 153 | to, 154 | filename, 155 | commentStart 156 | } 157 | return comment 158 | } 159 | } 160 | 161 | const parserOptions = { 162 | locations: true, 163 | onComment (block, text, start, end, from, to) { 164 | if (block) { 165 | return 166 | } 167 | const comment = parseAsCommentValue(text, start, end, from, to) 168 | if (comment) { 169 | comment.index = comments.length 170 | comments.push(comment) 171 | emitter.emit('comment', comment) 172 | } 173 | } 174 | } 175 | return parserOptions 176 | } 177 | 178 | module.exports = { 179 | initVariableParser, 180 | parseCommentVariables, 181 | initExpressionParser 182 | } 183 | -------------------------------------------------------------------------------- /src/comment-value-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it */ 4 | describe('comment-value', () => { 5 | it('dummy test', () => { 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/comments-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const la = require('lazy-ass') 4 | const is = require('check-more-types') 5 | 6 | /* global describe, it */ 7 | describe('comments white space', () => { 8 | const {isWhiteSpace} = require('./comments') 9 | 10 | it('matches 1 space', () => { 11 | la(isWhiteSpace(' ')) 12 | }) 13 | 14 | it('matches 2 spaces', () => { 15 | la(isWhiteSpace(' ')) 16 | }) 17 | 18 | it('allows opening comma', () => { 19 | la(isWhiteSpace(', ')) 20 | }) 21 | 22 | it('passes newlines', () => { 23 | la(isWhiteSpace('\n ')) 24 | }) 25 | 26 | it('does not match braces', () => { 27 | la(!isWhiteSpace('()')) 28 | la(!isWhiteSpace('( )')) 29 | }) 30 | }) 31 | 32 | describe('line comment', () => { 33 | const {isLineComment} = require('./comments') 34 | it('finds line comment from the start of the line', () => { 35 | la(isLineComment('//something')) 36 | la(isLineComment('// something')) 37 | }) 38 | 39 | it('finds line comment', () => { 40 | la(isLineComment(' //something')) 41 | la(isLineComment(' // something')) 42 | }) 43 | 44 | it('does not find lines with something else', () => { 45 | la(!isLineComment(' foo //something')) 46 | }) 47 | 48 | describe('parsing line comment', () => { 49 | const {parseLineComment} = require('./comments') 50 | it('parses //something', () => { 51 | const line = ' // something' 52 | const p = parseLineComment(line) 53 | la(is.object(p), 'expected object', p, 'from', line) 54 | la(p.line === line, p) 55 | la(p.comment === ' something', 'wrong comment', p, 'from', line) 56 | }) 57 | }) 58 | }) 59 | 60 | describe('comment variable type parser', () => { 61 | const {findCommentVariableType} = require('./comments') 62 | it('finds foo', () => { 63 | const s = ' foo:: whatever here' 64 | const variable = findCommentVariableType(s) 65 | la(variable === 'foo', variable, 'from', s) 66 | }) 67 | }) 68 | 69 | describe('findCommentValue', () => { 70 | const {findCommentValue} = require('./comments') 71 | 72 | it('finds a comment', () => { 73 | const comment = '> foo' 74 | const c = findCommentValue(comment) 75 | la(c === '>', c, 'from', comment) 76 | }) 77 | 78 | it('ignores type comments', () => { 79 | const comment = ' ::' 80 | const c = findCommentValue(comment) 81 | la(!c, 'found wrong type', c, 'from', comment) 82 | }) 83 | }) 84 | 85 | describe('findCommentType', () => { 86 | const {findCommentType} = require('./comments') 87 | 88 | it('finds a type comment', () => { 89 | const comment = ' ::' 90 | const c = findCommentType(comment) 91 | la(c === ' ::', c, 'from', comment) 92 | }) 93 | }) 94 | 95 | describe('comment variable parser', () => { 96 | const {findCommentVariable} = require('./comments') 97 | it('finds foo', () => { 98 | const s = ' foo: whatever here' 99 | const variable = findCommentVariable(s) 100 | la(variable === 'foo', variable, 'from', s) 101 | }) 102 | 103 | it('finds bare foo', () => { 104 | const s = ' foo:' 105 | const variable = findCommentVariable(s) 106 | la(variable === 'foo', variable, 'from', s) 107 | }) 108 | 109 | it('finds variable with space after', () => { 110 | const s = ' foo: ' 111 | const variable = findCommentVariable(s) 112 | la(variable === 'foo', variable, 'from', s) 113 | }) 114 | 115 | it('finds fooBar', () => { 116 | const s = ' fooBar: whatever here' 117 | const variable = findCommentVariable(s) 118 | la(variable === 'fooBar', variable, 'from', s) 119 | }) 120 | 121 | it('ignores http links', () => { 122 | const s = 'http://foo.com' 123 | const variable = findCommentVariable(s) 124 | la(!variable, 'found variable name', variable, 'in', s) 125 | }) 126 | 127 | it('ignores names with text after', () => { 128 | const s = 'foo:no' 129 | const variable = findCommentVariable(s) 130 | la(!variable, 'found variable name', variable, 'in', s) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /src/comments.js: -------------------------------------------------------------------------------- 1 | const R = require('ramda') 2 | const la = require('lazy-ass') 3 | const is = require('check-more-types') 4 | 5 | // look for comments that start with these symbols 6 | const comments = ['//>', '//=>', '// >', '// >>', '// =>', '//~>', '// ~>'] 7 | // remove leading '//' 8 | const starts = comments.map(c => c.substr(2)) 9 | // checks if source is separated by white space from a "special" comment 10 | function isWhiteSpace (text) { 11 | const maybe = /^,?\s+$/.test(text) 12 | return maybe 13 | } 14 | 15 | // finds which special comment start (if any) is present in the 16 | // given comment 17 | // for example //> 18 | function findCommentValue (s) { 19 | return R.find(c => s.startsWith(c), starts) 20 | } 21 | 22 | // finds if we want a type of the preceding expression 23 | // for example 24 | // add(2, 3) // :: 25 | // will be 26 | // add(2, 3) // :: number 27 | function findCommentType (s) { 28 | const typeStarts = ['::', ' ::'] 29 | return R.find(c => s.startsWith(c), typeStarts) 30 | } 31 | 32 | // tries to find the variable name (if any) used in special 33 | // comment, for example " fooBar: anything here" returns match "fooBar" 34 | function findCommentVariable (s) { 35 | const r = /^ (\w+):(?:\s+|\n|$)/ 36 | const matches = r.exec(s) 37 | return matches && matches[1] 38 | } 39 | 40 | // finds variable names where we should output type 41 | // like "foo::" 42 | function findCommentVariableType (s) { 43 | const r = /^ (\w+)::(?:\s+|\n|$)/ 44 | const matches = r.exec(s) 45 | return matches && matches[1] 46 | } 47 | 48 | function isLineComment (line) { 49 | const r = /^\s*\/\// 50 | return r.test(line) 51 | } 52 | 53 | function parseLineComment (line) { 54 | la(isLineComment(line), 'not a line comment', line) 55 | const commentStarts = line.indexOf('//') 56 | la(is.found(commentStarts), 'could not find //', line) 57 | const comment = line.substr(commentStarts + 2) 58 | 59 | return { 60 | line, 61 | commentStarts, 62 | comment 63 | } 64 | } 65 | 66 | module.exports = { 67 | comments, 68 | starts, 69 | isWhiteSpace, 70 | findCommentValue, 71 | findCommentVariable, 72 | findCommentVariableType, 73 | isLineComment, 74 | parseLineComment, 75 | findCommentType 76 | } 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const hook = require('node-hook') 2 | const instrumentSource = require('./instrument-source') 3 | const updateSourceFile = require('./update') 4 | const debug = require('debug')('comment-value') 5 | 6 | const instrumentedFiles = [] 7 | 8 | const isNodeModules = filename => filename.includes('node_modules') 9 | 10 | const printInstrumented = Boolean(process.env.instrumented) 11 | debug('print instrumented code?', printInstrumented) 12 | 13 | function instrumentLoadedFiles (source, filename) { 14 | if (isNodeModules(filename)) { 15 | return source 16 | } 17 | debug('instrumenting file %s', filename) 18 | 19 | const instrumented = instrumentSource(source, filename) 20 | if (instrumented !== source) { 21 | instrumentedFiles.push(filename) 22 | if (printInstrumented) { 23 | console.log('instrumented file', filename) 24 | console.log(instrumented) 25 | } 26 | } 27 | 28 | return instrumented 29 | } 30 | hook.hook('.js', instrumentLoadedFiles) 31 | 32 | process.on('exit', function writeResults () { 33 | if (global.__instrumenter) { 34 | if (instrumentedFiles.length) { 35 | debug('%d instrumented file(s) to update', instrumentedFiles.length) 36 | instrumentedFiles.forEach(name => 37 | updateSourceFile(name, global.__instrumenter)) 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/instrument-source.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const falafel = require('falafel') 4 | const debug = require('debug')('comment-value') 5 | const commentStarts = require('./comments').starts 6 | la(is.strings(commentStarts), 'invalid comment starts', commentStarts) 7 | const {parseCommentVariables, initExpressionParser} = require('./comment-parser') 8 | 9 | const {isWhiteSpace} = require('./comments') 10 | const R = require('ramda') 11 | const beautifySource = require('./beautify') 12 | // emit events when finding comment / instrumenting 13 | // allows quick testing 14 | if (!global.instrument) { 15 | const EventEmitter = require('events') 16 | global.instrument = new EventEmitter() 17 | } 18 | const emitter = global.instrument 19 | 20 | function storeInIIFE (reference, value, typeReference) { 21 | return `(function () { 22 | if (typeof ${value} === 'function') { 23 | return function () { 24 | ${reference} = ${value}.apply(null, arguments); 25 | ${typeReference} = typeof ${reference}; 26 | return ${reference} 27 | } 28 | } else { 29 | ${reference} = ${value}; 30 | ${typeReference} = typeof ${reference}; 31 | return ${reference} 32 | } 33 | }())` 34 | } 35 | function storeInBlock (reference, value, typeReference) { 36 | const store = `${reference} = ${value}` 37 | const storeType = `${typeReference} = typeof ${reference}` 38 | return `;{ ${store}; ${storeType}; ${reference} }` 39 | } 40 | 41 | function instrumentSource (source, filename) { 42 | la(is.string(source), 'missing source', source) 43 | 44 | // TODO handle multiple files by making this object global 45 | // and avoiding overwriting it 46 | const __instrumenter = global.__instrumenter || { 47 | comments: [], 48 | variables: [] 49 | } 50 | 51 | function isWhiteSpaceBefore (from, comment) { 52 | const region = source.substr(from, comment.start - from) 53 | if (is.empty(region)) { 54 | return true 55 | } 56 | // console.log(`region "${region}" from ${from} comment starts ${comment.start}`) 57 | const maybe = isWhiteSpace(region) 58 | // console.log(`region "${region}" test ${maybe}`) 59 | return maybe 60 | } 61 | 62 | const isCloseComment = R.curry((line, c) => { 63 | return c.from.line === line || c.from.line === line + 1 64 | }) 65 | 66 | const findComment = node => { 67 | // console.log('looking for comment for node', 68 | // node.source(), node.end, 'line', node.loc.end.line) 69 | la(node, 'missing node', node) 70 | // console.log(node) 71 | if (!node.loc) { 72 | // console.log('node is missing loc') 73 | return 74 | } 75 | la(node.loc, 'missing node location', node) 76 | return __instrumenter.comments 77 | .filter(isCloseComment(node.loc.end.line)) 78 | .find(c => isWhiteSpaceBefore(node.end, c)) 79 | } 80 | 81 | const endsBeforeInstrumentedComment = R.compose(Boolean, findComment) 82 | 83 | const hasNotSeen = node => { 84 | const c = findComment(node) 85 | return !c.instrumented 86 | } 87 | 88 | const isConsoleLog = node => 89 | node && 90 | node.type === 'CallExpression' && 91 | node.callee.type === 'MemberExpression' && 92 | node.callee.object.name === 'console' && 93 | node.callee.property.name === 'log' 94 | 95 | const isConsoleLogExpression = node => 96 | node.expression && 97 | node.expression.type === 'CallExpression' && 98 | node.expression.callee.type === 'MemberExpression' && 99 | node.expression.callee.object.name === 'console' && 100 | node.expression.callee.property.name === 'log' 101 | 102 | function instrument (node) { 103 | // console.log(node.type, node.end, node.source()) 104 | // TODO can we handle individual value? 105 | if (node.type === 'ExpressionStatement' || 106 | node.type === 'Identifier' || 107 | node.type === 'CallExpression') { 108 | if (node.type === 'CallExpression') { 109 | // console.log(node.source(), node) 110 | // ignore top level "console.log(...)", 111 | // we only care about the arguments 112 | if (isConsoleLog(node)) { 113 | return 114 | } 115 | } 116 | 117 | if (endsBeforeInstrumentedComment(node) && hasNotSeen(node)) { 118 | debug('need to instrument', node.type, node.source()) 119 | const comment = findComment(node) 120 | debug('will instrument "%s" for comment "%s"', node.source(), comment.text) 121 | comment.instrumented = true 122 | const reference = 'global.__instrumenter.comments[' + comment.index + '].value' 123 | const typeReference = 'global.__instrumenter.comments[' + comment.index + '].type' 124 | if (isConsoleLogExpression(node)) { 125 | debug('instrumenting console.log', node.source()) 126 | // instrument inside the console.log (the first argument) 127 | const value = node.expression.arguments[0].source() 128 | emitter.emit('wrap', value) 129 | const store = reference + ' = ' + value 130 | const storeAndReturn = '(function () {' + store + '; return ' + reference + '}())' 131 | const printStored = 'console.log(' + storeAndReturn + ')' 132 | node.update(printStored) 133 | } else { 134 | // console.log(node) 135 | const value = node.source() 136 | debug(`instrumenting ${node.type} value ${value}`) 137 | debug('parent node type %s source %s', 138 | node.parent.type, node.parent.source()) 139 | // debug('grandparent node type %s source %s', 140 | // node.parent.parent.type, node.parent.parent.source()) 141 | 142 | let storeAndReturn 143 | if (node.parent.type === 'CallExpression') { 144 | storeAndReturn = storeInIIFE(reference, value, typeReference) 145 | emitter.emit('wrap', value) 146 | emitter.emit('wrap-node', {type: 'CallExpression', value}) 147 | } else if (node.parent.type === 'MemberExpression') { 148 | // update the entire parent node 149 | const value = node.parent.source() 150 | emitter.emit('wrap', value) 151 | emitter.emit('wrap-node', {type: 'MemberExpression', value}) 152 | let parentStore = storeInIIFE(reference, value, typeReference) 153 | node.parent.update(parentStore) 154 | return 155 | } else { 156 | emitter.emit('wrap', value) 157 | emitter.emit('wrap-node', {type: node.parent.type, value}) 158 | storeAndReturn = storeInBlock(reference, value, typeReference) 159 | } 160 | 161 | if (node.parent.parent && 162 | node.parent.parent.type === 'ExpressionStatement') { 163 | node.update(';' + storeAndReturn) 164 | } else { 165 | node.update(storeAndReturn) 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | // first pass - find and instrument just the variables in the 173 | // comments 174 | const output1 = parseCommentVariables(source, filename, 175 | __instrumenter.variables, emitter) 176 | debug('instrumented for %d variables', 177 | __instrumenter.variables.length) 178 | __instrumenter.variables.forEach(c => 179 | debug('line %d variable %s', c.lineIndex, c.variable)) 180 | 181 | // second pass - instrument all implicit expressions 182 | debug('second pass - finding expressions to instrument') 183 | const expressionParser = initExpressionParser( 184 | filename, __instrumenter.comments, emitter) 185 | const output2 = falafel(output1, expressionParser, instrument) 186 | debug('instrumented for total %d expressions in comments', 187 | __instrumenter.comments.length) 188 | __instrumenter.comments.forEach(c => 189 | debug('line %d expression %s', c.from.line, c.text)) 190 | 191 | // console.log(__instrumenter.comments) 192 | const preamble = 'if (!global.__instrumenter) {global.__instrumenter=' + 193 | JSON.stringify(__instrumenter, null, 2) + '}\n' 194 | 195 | const sep = ';\n' 196 | const instrumented = preamble + sep + output2 197 | const beautify = true 198 | const beautified = beautify ? beautifySource(instrumented) : instrumented 199 | return beautified 200 | } 201 | 202 | module.exports = instrumentSource 203 | -------------------------------------------------------------------------------- /src/instrument.js: -------------------------------------------------------------------------------- 1 | const instrumentSource = require('./instrument-source') 2 | const fs = require('fs') 3 | const debug = require('debug')('comment-value') 4 | const beautify = require('./beautify') 5 | 6 | function instrumentFile (filename, outputFilename) { 7 | debug('instrumenting file %s', filename) 8 | const source = fs.readFileSync(filename, 'utf8') 9 | const instrumented = instrumentSource(source, filename) 10 | // TODO move outside 11 | function saveResults () { 12 | const fs = require('fs') 13 | process.on('exit', function writeResults () { 14 | fs.writeFileSync('./results.json', 15 | JSON.stringify(global.__instrumenter, null, 2) + '\n', 'utf8') 16 | }) 17 | } 18 | const save = '(' + saveResults.toString() + '())\n' 19 | const sep = ';\n' 20 | const output = beautify(instrumented + sep + save) + '\n' 21 | fs.writeFileSync(outputFilename, output, 'utf8') 22 | } 23 | 24 | module.exports = instrumentFile 25 | 26 | if (!module.parent) { 27 | const path = require('path') 28 | const sourceFilename = process.argv[2] 29 | console.log('updating comment values in', sourceFilename) 30 | const fullFilename = path.join(process.cwd(), sourceFilename) 31 | const outputFilename = './instrumented.js' 32 | instrumentFile(fullFilename, outputFilename) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/update.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const la = require('lazy-ass') 4 | const is = require('check-more-types') 5 | const debug = require('debug')('comment-value') 6 | const R = require('ramda') 7 | 8 | function updateFile (filename, results) { 9 | debug('updating comment values in file %s', filename) 10 | la(is.unemptyString(filename), 'missing filename', filename) 11 | 12 | const source = fs.readFileSync(filename, 'utf8') 13 | if (!results) { 14 | const resultsFilename = path.join(process.cwd(), 'results.json') 15 | debug('loading results from file %s', resultsFilename) 16 | results = require(resultsFilename) 17 | } 18 | 19 | const forThisFile = comment => comment.filename === filename 20 | 21 | // console.log('updating source from file', filename) 22 | // console.log(source) 23 | 24 | function debugPrintComment (c) { 25 | debug('comment line %d value %s', c.from.line, c.value) 26 | } 27 | 28 | function debugPrintVariable (c) { 29 | debug('variable line %d name %s value %s', 30 | c.lineIndex, c.variable, c.value) 31 | } 32 | 33 | const lines = source.split('\n') 34 | results.comments 35 | .filter(forThisFile) 36 | .map(R.tap(debugPrintComment)) 37 | .forEach(updateComment) 38 | 39 | results.variables 40 | .filter(forThisFile) 41 | .map(R.tap(debugPrintVariable)) 42 | .forEach(updateVariableComment) 43 | 44 | function updateVariableComment (c) { 45 | const line = lines[c.lineIndex] 46 | la(line, 'missing line', c.lineIndex, 'for comment', c) 47 | la(is.oneOf(['value', 'type'], c.find), 'invalid variable', c.variable, 48 | 'find', c.find, c) 49 | const variableString = c.find === 'value' 50 | ? ` ${c.variable}:` : ` ${c.variable}::` 51 | const variableIndex = line.indexOf(variableString) 52 | la(is.found(variableIndex), 53 | 'cannot find variable comment', c, 'on line', line) 54 | const start = line.substr(0, variableIndex + variableString.length) 55 | const newComment = start + ' ' + JSON.stringify(c.value) 56 | lines[c.lineIndex] = newComment 57 | } 58 | 59 | function updateComment (c) { 60 | const commentStart = c.commentStart 61 | la(is.unemptyString(commentStart), 'missing comment start', c) 62 | // console.log('updating comment') 63 | // console.log(c) 64 | // line starts with 1 65 | la(c.from.line === c.to.line, 'line mismatch', c) 66 | const line = lines[c.from.line - 1] 67 | la(line, 'missing line', c.from.line - 1, 'for comment', c) 68 | // console.log('updating line', line, 'with value', c.value) 69 | const k = line.indexOf(commentStart) 70 | la(k >= 0, 'line does not have comment', k, 'for comment', c) 71 | 72 | const value = c.find === 'value' ? JSON.stringify(c.value) : c.type 73 | 74 | const newComment = commentStart + ' ' + value 75 | const updatedLine = line.substr(0, k) + newComment 76 | lines[c.from.line - 1] = updatedLine 77 | } 78 | 79 | const updatedSource = lines.join('\n') 80 | 81 | if (updatedSource !== source) { 82 | debug('updated source is different in %s', filename) 83 | fs.writeFileSync(filename, updatedSource, 'utf8') 84 | } else { 85 | debug('file %s is the same as before, not overwriting', filename) 86 | } 87 | } 88 | 89 | module.exports = updateFile 90 | 91 | if (!module.exports) { 92 | const sourceFilename = path.join(process.cwd(), process.argv[2]) 93 | updateFile(sourceFilename) 94 | } 95 | -------------------------------------------------------------------------------- /test/array/expected.js: -------------------------------------------------------------------------------- 1 | // how does it print an array 2 | const a = ['foo', 'bar']; 3 | a //> ["foo","bar"] 4 | -------------------------------------------------------------------------------- /test/array/index.js: -------------------------------------------------------------------------------- 1 | // how does it print an array 2 | const a = ['foo', 'bar']; 3 | a //> ["foo","bar"] 4 | -------------------------------------------------------------------------------- /test/array/manual.js: -------------------------------------------------------------------------------- 1 | if (!global.__instrumenter) { 2 | global.__instrumenter = { 3 | "comments": [{ 4 | "start": 58, 5 | "text": "> undefined", 6 | "index": 0, 7 | "from": { 8 | "line": 3, 9 | "column": 2 10 | }, 11 | "to": { 12 | "line": 3, 13 | "column": 15 14 | }, 15 | "filename": "/Users/gleb/git/comment-value/test/array/index.js", 16 | "commentStart": ">", 17 | "instrumented": true 18 | }] 19 | } 20 | }; 21 | // how does it print an array 22 | const a = ['foo', 'bar']; { 23 | console.log('saving a', a) 24 | global.__instrumenter.comments[0].value = a; global.__instrumenter.comments[0].value 25 | } //> undefined 26 | 27 | console.log(global.__instrumenter) 28 | -------------------------------------------------------------------------------- /test/async-example.js: -------------------------------------------------------------------------------- 1 | function add (a, b) { 2 | return a + b 3 | } 4 | setTimeout(() => { 5 | add(2, 3) //> 5 6 | console.log('all done') 7 | }, 100) 8 | 9 | add(1, -2) //> -1 10 | 11 | -------------------------------------------------------------------------------- /test/combined/expected.js: -------------------------------------------------------------------------------- 1 | // set value of any variable 2 | // by using the "// name:" syntax 3 | const foo = 'f' + 'o' + 'o' 4 | // foo: "foo" 5 | // this is line 5 6 | // the updated variable should keep the leading whitespace 7 | 8 | function add(a, b) { 9 | // a: 10 10 | // b: 2 11 | return a + b 12 | } 13 | add(10, 2) 14 | -------------------------------------------------------------------------------- /test/combined/index.js: -------------------------------------------------------------------------------- 1 | const x = 2 2 | // x: 3 | function add(a, b) { 4 | return a + b 5 | } 6 | add(x, 3) //> 7 | -------------------------------------------------------------------------------- /test/comma/expected.js: -------------------------------------------------------------------------------- 1 | const {pipe, negate, inc} = require('ramda') 2 | const f = pipe( 3 | Math.pow, 4 | negate, //> -81 5 | inc //> -80 6 | ) 7 | console.log(f(3, 4)) //> -80 8 | -------------------------------------------------------------------------------- /test/comma/index.js: -------------------------------------------------------------------------------- 1 | const {pipe, negate, inc} = require('ramda') 2 | const f = pipe( 3 | Math.pow, 4 | negate, //> -81 5 | inc //> -80 6 | ) 7 | console.log(f(3, 4)) //> -80 8 | -------------------------------------------------------------------------------- /test/compose/curried-compose.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda') 2 | R.compose( 3 | Math.abs, 4 | R.add(1), 5 | R.multiply(2) //> ?? 6 | )(-4) 7 | -------------------------------------------------------------------------------- /test/compose/expected.js: -------------------------------------------------------------------------------- 1 | function wrap (fn) { 2 | console.log('running function', fn.name) 3 | return fn() 4 | } 5 | 6 | const foo = () => 'foo' 7 | 8 | const result = wrap( 9 | foo //> "foo" 10 | ) 11 | console.assert(result === 'foo', result) 12 | -------------------------------------------------------------------------------- /test/compose/index.js: -------------------------------------------------------------------------------- 1 | function wrap (fn) { 2 | console.log('running function', fn.name) 3 | return fn() 4 | } 5 | 6 | const foo = () => 'foo' 7 | 8 | const result = wrap( 9 | foo //> "foo" 10 | ) 11 | console.assert(result === 'foo', result) 12 | -------------------------------------------------------------------------------- /test/compose/manual.js: -------------------------------------------------------------------------------- 1 | // manually wrapped function "foo" in the chain 2 | 3 | const values = { 4 | foo: undefined 5 | } 6 | 7 | function wrap (fn) { 8 | return fn() 9 | } 10 | 11 | const foo = () => 'foo' 12 | 13 | const result = wrap( 14 | (function () { 15 | if (typeof foo === 'function') { 16 | const result = foo.apply(null, arguments) 17 | values.foo = result 18 | return result 19 | } else { 20 | values.foo = foo 21 | return foo 22 | } 23 | }) 24 | ) 25 | console.assert(result === 'foo', result) 26 | 27 | console.log('foo result', values.foo) 28 | -------------------------------------------------------------------------------- /test/compose/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lodash-chain", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "../../bin/comment-value.js -i index.js", 8 | "instrument": "DEBUG=comment-value node ../../src/instrument.js index.js", 9 | "postinstrument": "node instrumented.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /test/compose/ramda-compose.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda') 2 | var classyGreeting = (firstName, lastName) => 3 | "The name's " + lastName + ", " + firstName + " " + lastName 4 | var yellGreeting = R.compose( 5 | R.toUpper, //=> "THE NAME'S BOND, JAMES BOND" 6 | classyGreeting //=> "The name's Bond, James Bond" 7 | ); 8 | yellGreeting('James', 'Bond'); //=> "THE NAME'S BOND, JAMES BOND" 9 | 10 | R.compose( 11 | Math.abs, //=> 7 12 | R.add(1), //=> -7 13 | R.multiply(2) //=> -8 14 | )(-4) //=> 7 15 | -------------------------------------------------------------------------------- /test/compose/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('compose with curried functions', () => { 9 | const source = read(inFolder('curried-compose.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('instruments', () => { 17 | const comments = [] 18 | const output = instrument(source) 19 | la(is.unemptyString(output), 'did not get output') 20 | }) 21 | 22 | it('finds the comment', () => { 23 | const comments = [] 24 | emitter.on('comment', comments.push.bind(comments)) 25 | instrument(source) 26 | la(R.equals(['> ??'], R.map(R.prop('text'), comments)), comments) 27 | }) 28 | 29 | it('wraps the entire curried expression', () => { 30 | const wrapped = [] 31 | emitter.on('wrap', wrapped.push.bind(wrapped)) 32 | instrument(source) 33 | la(R.equals(['R.multiply(2)'], wrapped), wrapped) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/console-log/expected.js: -------------------------------------------------------------------------------- 1 | // this should update comment with 2 | // the printed value 3 | console.log(2 + 40) //> 42 4 | -------------------------------------------------------------------------------- /test/console-log/index.js: -------------------------------------------------------------------------------- 1 | // this should update comment with 2 | // the printed value 3 | console.log(2 + 40) //> 42 4 | -------------------------------------------------------------------------------- /test/console-log/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('console.log is special', () => { 9 | const source = read(inFolder('index.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('finds the comment', () => { 17 | const comments = [] 18 | emitter.on('comment', comments.push.bind(comments)) 19 | instrument(source) 20 | la(R.equals(['> 42'], R.map(R.prop('text'), comments)), comments) 21 | }) 22 | 23 | it('wraps the first argument only', () => { 24 | const wrapped = [] 25 | emitter.on('wrap', wrapped.push.bind(wrapped)) 26 | instrument(source) 27 | la(R.equals(['2 + 40'], wrapped), wrapped) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | function add (a, b) { 2 | return a + b 3 | } 4 | // (typeof add) //> undefined 5 | add(2, 3) //> 5 6 | add(1, -2) //> -1 7 | add(10, -2) //> 8 8 | 9 | console.log('all done in', __filename) 10 | -------------------------------------------------------------------------------- /test/identifier-example.js: -------------------------------------------------------------------------------- 1 | // last value wins! 2 | const I = x => 3 | x //> 250 4 | console.log(I(50)) 5 | console.log(I(150)) 6 | console.log(I(250)) 7 | -------------------------------------------------------------------------------- /test/lodash-chain/composed.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda') 2 | var users = [ 3 | { 'user': 'barney', 'age': 36 }, 4 | { 'user': 'fred', 'age': 40 }, 5 | { 'user': 'pebbles', 'age': 1 } 6 | ]; 7 | 8 | var youngest = R.pipe( 9 | R.sortBy(R.prop('age')), //> [{"user":"pebbles","age":1},{"user":"barney","age":36},{"user":"fred","age":40}] 10 | R.head 11 | )(users) 12 | console.log(youngest) //> {"user":"pebbles","age":1} 13 | -------------------------------------------------------------------------------- /test/lodash-chain/expected.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var users = [ 3 | { 'user': 'barney', 'age': 36 }, 4 | { 'user': 'fred', 'age': 40 }, 5 | { 'user': 'pebbles', 'age': 1 } 6 | ]; 7 | 8 | var youngest = _ 9 | .chain(users) 10 | .sortBy('age') 11 | .head() 12 | .value() 13 | console.log(youngest) //> {"user":"pebbles","age":1} 14 | -------------------------------------------------------------------------------- /test/lodash-chain/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var users = [ 3 | { 'user': 'barney', 'age': 36 }, 4 | { 'user': 'fred', 'age': 40 }, 5 | { 'user': 'pebbles', 'age': 1 } 6 | ]; 7 | 8 | var youngest = _ 9 | .chain(users) 10 | .sortBy('age') 11 | .head() 12 | .value() 13 | console.log(youngest) //> {"user":"pebbles","age":1} 14 | -------------------------------------------------------------------------------- /test/lodash-chain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lodash-chain", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "lodash": "4.17.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/nested/add.js: -------------------------------------------------------------------------------- 1 | module.exports = function add (a, b) { 2 | return a + b 3 | } 4 | -------------------------------------------------------------------------------- /test/nested/index.js: -------------------------------------------------------------------------------- 1 | const add = require('./add') 2 | add(2, 3) //> 5 3 | add(2, 2) //> 4 4 | -------------------------------------------------------------------------------- /test/next-line/expected.js: -------------------------------------------------------------------------------- 1 | // different comment symbol 2 | (10 + 20); //=> 30 3 | // more spaces between the expression and comment 4 | (2 + 7); //> 9 5 | // space between // and => symbols 6 | (2 * 8); // => 16 7 | (11 - 1); // ~> 10 8 | -------------------------------------------------------------------------------- /test/next-line/index.js: -------------------------------------------------------------------------------- 1 | // comment on next line 2 | (10 + 20); 3 | //=> 30 4 | 5 | console.log(3 * 3) 6 | //> 9 7 | -------------------------------------------------------------------------------- /test/next-line/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('comment on next line', () => { 9 | const source = read(inFolder('index.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('finds the comments', () => { 17 | const comments = [] 18 | emitter.on('comment', c => comments.push(c.text)) 19 | instrument(source) 20 | la(R.equals(['=> 30', '> 9'], comments), comments) 21 | }) 22 | 23 | it('wraps the right stuff', () => { 24 | const wrap = [] 25 | emitter.on('wrap', w => wrap.push(w)) 26 | instrument(source) 27 | la(R.equals(['(10 + 20);', '3 * 3'], wrap), wrap) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/promise-example.js: -------------------------------------------------------------------------------- 1 | Promise.resolve(42) 2 | // .then(x => 3 | // x + x //> undefined 4 | // ) 5 | .then(x => 6 | x //> 42 7 | ) 8 | .then(console.log) 9 | -------------------------------------------------------------------------------- /test/property/expected.js: -------------------------------------------------------------------------------- 1 | const {pipe, negate, inc} = require('ramda') 2 | const f = pipe( 3 | Math.pow, // > 81 4 | negate, 5 | inc 6 | ) 7 | console.log(f(3, 4)) 8 | -------------------------------------------------------------------------------- /test/property/index.js: -------------------------------------------------------------------------------- 1 | const {pipe, negate, inc} = require('ramda') 2 | const f = pipe( 3 | Math.pow, // > 81 4 | negate, 5 | inc 6 | ) 7 | console.log(f(3, 4)) 8 | -------------------------------------------------------------------------------- /test/property/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | /* global describe, beforeEach, it */ 9 | describe('property functions', () => { 10 | const source = read(inFolder('index.js'), 'utf8') 11 | let emitter 12 | 13 | beforeEach(() => { 14 | emitter = global.instrument 15 | }) 16 | 17 | it('instruments', () => { 18 | const output = instrument(source) 19 | la(is.unemptyString(output), 'did not get output') 20 | }) 21 | 22 | it('finds the comment', () => { 23 | const comments = [] 24 | emitter.on('comment', comments.push.bind(comments)) 25 | instrument(source) 26 | la(R.equals([' > 81'], R.map(R.prop('text'), comments)), comments) 27 | }) 28 | 29 | it('wraps the property expression', () => { 30 | const wrapped = [] 31 | emitter.on('wrap', wrapped.push.bind(wrapped)) 32 | instrument(source) 33 | la(R.equals(['Math.pow'], wrapped), wrapped) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/query-example/index.js: -------------------------------------------------------------------------------- 1 | // code example from 2 | // egghead.io/lessons/javascript-convert-a-querystring-to-an-object-using-function-composition-in-ramda 3 | // using tool https://github.com/bahmutov/comment-value 4 | const R = require('ramda') 5 | const queryString = '?page=2&pageSize=10&total=foo' 6 | 7 | const parseQs = R.pipe( 8 | R.tail, //> "page=2&pageSize=10&total=foo" 9 | R.split('&'), //> ["page=2","pageSize=10","total=foo"] 10 | R.map(R.split('=')), 11 | //> [["page","2"],["pageSize","10"],["total","foo"]] 12 | R.fromPairs 13 | ) 14 | 15 | const result = parseQs(queryString) 16 | console.log(result) 17 | // result: {"page":"2","pageSize":"10","total":"foo"} 18 | // target {"page":"2","pageSize":"10","total":"203"} 19 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../src/instrument-source') 4 | const R = require('ramda') 5 | 6 | describe.only('composed types', () => { 7 | const source = ` 8 | const R = require('ramda') 9 | const pipe = R.pipe( 10 | R.inc, //:: 11 | R.multiply(10) //> 12 | ) 13 | pipe(-4) 14 | ` 15 | let emitter 16 | 17 | beforeEach(() => { 18 | emitter = global.instrument 19 | delete global.__instrumenter 20 | }) 21 | 22 | it('finds the special comments', () => { 23 | const starts = [] 24 | emitter.on('comment', c => starts.push(c.commentStart)) 25 | instrument(source) 26 | la(R.equals(['::', '>'], starts), starts) 27 | }) 28 | 29 | it('evaluates original source', () => { 30 | const result = eval(source) 31 | la(result === -30, result) 32 | }) 33 | 34 | it('evaluates instrumented source', () => { 35 | const s = instrument(source) 36 | const result = eval(s) 37 | la(result === -30, result) 38 | la(global.__instrumenter.comments.length === 2, 39 | global.__instrumenter.comments) 40 | }) 41 | 42 | it('keeps type for first comment', () => { 43 | const s = instrument(source) 44 | const result = eval(s) 45 | const first = global.__instrumenter.comments[0] 46 | la(first.find === 'type', 'should find type', first) 47 | la(first.type === 'number', 'found type', first) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/symbols/expected.js: -------------------------------------------------------------------------------- 1 | // different comment symbol 2 | (10 + 20); //=> 30 3 | // more spaces between the expression and comment 4 | (2 + 7); //> 9 5 | // space between // and => symbols 6 | (2 * 8); // => 16 7 | (11 - 1); // ~> 10 8 | // no space between the expression and comment 9 | (10 + 20);//> 30 10 | -------------------------------------------------------------------------------- /test/symbols/index.js: -------------------------------------------------------------------------------- 1 | // different comment symbol 2 | (10 + 20); //=> 30 3 | // more spaces between the expression and comment 4 | (2 + 7); //> 9 5 | // space between // and => symbols 6 | (2 * 8); // => 16 7 | (11 - 1); // ~> 10 8 | // no space between the expression and comment 9 | (10 + 20);//> 30 10 | -------------------------------------------------------------------------------- /test/symbols/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('comment symbols', () => { 9 | const source = read(inFolder('index.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('finds the comments', () => { 17 | const comments = [] 18 | emitter.on('comment', c => comments.push(c.text)) 19 | instrument(source) 20 | const expected = ['=> 30', '> 9', ' => 16', ' ~> 10', '> 30'] 21 | const compareComments = R.equals(expected) 22 | la(compareComments(comments), comments) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/types/expected.js: -------------------------------------------------------------------------------- 1 | // set type of any variable 2 | // by using the "// name::" syntax 3 | const foo = 'f' + 'o' + 'o' 4 | // foo:: "string" 5 | const life = 42 6 | // life:: "number" 7 | -------------------------------------------------------------------------------- /test/types/index.js: -------------------------------------------------------------------------------- 1 | // set type of any variable 2 | // by using the "// name::" syntax 3 | const foo = 'f' + 'o' + 'o' 4 | // foo:: "string" 5 | const life = 42 6 | // life:: "number" 7 | -------------------------------------------------------------------------------- /test/types/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('variable types', () => { 9 | const source = read(inFolder('index.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('finds the variable type comment', () => { 17 | const variables = [] 18 | emitter.on('comment', c => variables.push(c.variable)) 19 | instrument(source) 20 | la(R.equals(['foo', 'life'], variables), variables) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/types2/expected.js: -------------------------------------------------------------------------------- 1 | // type of preceding expression 2 | const add = (a, b) => a + b 3 | add(2, 3) // :: number 4 | -------------------------------------------------------------------------------- /test/types2/index.js: -------------------------------------------------------------------------------- 1 | // type of preceding expression 2 | const add = (a, b) => a + b 3 | add(2, 3) // :: number 4 | -------------------------------------------------------------------------------- /test/types2/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('expression type', () => { 9 | const source = read(inFolder('index.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('finds the variable type comment', () => { 17 | const starts = [] 18 | emitter.on('comment', c => starts.push(c.commentStart)) 19 | instrument(source) 20 | la(R.equals([' ::'], starts), starts) 21 | }) 22 | 23 | it('wraps the right expressions', () => { 24 | const wraps = [] 25 | emitter.on('wrap', s => wraps.push(s)) 26 | instrument(source) 27 | la(R.equals(['add(2, 3)'], wraps), wraps) 28 | }) 29 | 30 | it('wraps the right AST node type', () => { 31 | const nodeTypes = [] 32 | emitter.on('wrap-node', s => nodeTypes.push(s.type)) 33 | instrument(source) 34 | la(R.equals(['ExpressionStatement'], nodeTypes), nodeTypes) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/variable/expected.js: -------------------------------------------------------------------------------- 1 | // set value of any variable 2 | // by using the "// name:" syntax 3 | const foo = 'f' + 'o' + 'o' 4 | // foo: "foo" 5 | // this is line 5 6 | // the updated variable should keep the leading whitespace 7 | 8 | function add(a, b) { 9 | // a: 10 10 | // b: 2 11 | return a + b 12 | } 13 | add(10, 2) 14 | 15 | // should ignore web links for example 16 | // https://github.com/bahmutov/comment-value/issues/25 17 | -------------------------------------------------------------------------------- /test/variable/index.js: -------------------------------------------------------------------------------- 1 | // set value of any variable 2 | // by using the "// name:" syntax 3 | const foo = 'f' + 'o' + 'o' 4 | // foo: "foo" 5 | // this is line 5 6 | // the updated variable should keep the leading whitespace 7 | 8 | function add(a, b) { 9 | // a: 10 10 | // b: 2 11 | return a + b 12 | } 13 | add(10, 2) 14 | 15 | // should ignore web links for example 16 | // https://github.com/bahmutov/comment-value/issues/25 17 | -------------------------------------------------------------------------------- /test/variable/spec.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const instrument = require('../../src/instrument-source') 4 | const read = require('fs').readFileSync 5 | const inFolder = require('path').join.bind(null, __dirname) 6 | const R = require('ramda') 7 | 8 | describe('variable value', () => { 9 | const source = read(inFolder('index.js'), 'utf8') 10 | let emitter 11 | 12 | beforeEach(() => { 13 | emitter = global.instrument 14 | }) 15 | 16 | it('finds the variable comment', () => { 17 | const variables = [] 18 | emitter.on('comment', c => variables.push(c.variable)) 19 | instrument(source) 20 | la(R.equals(['foo', 'a', 'b'], variables), variables) 21 | }) 22 | }) 23 | --------------------------------------------------------------------------------