├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _cli.js ├── cfg-04.png ├── cli.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── ast.js ├── block.js ├── cfg.js ├── defs.js ├── dot.js ├── dump.js ├── edges.js ├── leader.js ├── manager.js ├── plugins.js ├── types.js ├── utils.js └── visitors.js ├── test-data ├── cfg-test-01.js ├── cfg-test-02.js ├── cfg-test-03.js ├── cfg-test-04.js ├── cfg-test-05.js └── cfg-test-06.js └── test ├── ast-test.js ├── dot3.txt ├── index-test.js ├── table6.txt └── text6.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [{Makefile,**.mk}] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true 6 | }, 7 | "sourceType": "module" 8 | }, 9 | "root": true, 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "mocha": true 14 | }, 15 | "plugins": [ 16 | ], 17 | "globals": { 18 | "document": false, 19 | "navigator": false, 20 | "window": false, 21 | "global": true, 22 | "describe": true, 23 | "it": true, 24 | "before": true, 25 | "JSON": true 26 | }, 27 | "rules": { 28 | "func-call-spacing": [ 2, "never" ], 29 | "prefer-promise-reject-errors": 2, 30 | "max-len": [ "error", 240 ], 31 | "max-depth": [ "error", 5 ], 32 | "max-lines": [ "error", 1500 ], 33 | "max-params": [ "error", 5 ], 34 | "max-statements": [ "error", 30 ], 35 | "accessor-pairs": "off", 36 | "arrow-spacing": [ 37 | 2, 38 | { 39 | "before": true, 40 | "after": true 41 | } 42 | ], 43 | "brace-style": "off", 44 | "camelcase": "off", 45 | "comma-dangle": [ 46 | 2, 47 | "never" 48 | ], 49 | "comma-spacing": [ 50 | 2, 51 | { 52 | "before": false, 53 | "after": true 54 | } 55 | ], 56 | "comma-style": [ 57 | 2, 58 | "last" 59 | ], 60 | "constructor-super": 2, 61 | "curly": [ 1, "multi-or-nest" ], 62 | "dot-location": [ 63 | 2, 64 | "property" 65 | ], 66 | "eol-last": 2, 67 | "eqeqeq": [ 68 | 2, 69 | "allow-null" 70 | ], 71 | "generator-star-spacing": [ 72 | 2, 73 | { 74 | "before": true, 75 | "after": false 76 | } 77 | ], 78 | "handle-callback-err": [ 79 | 2, 80 | "^(err|error)$" 81 | ], 82 | "indent": "off", 83 | "key-spacing": "off", 84 | "keyword-spacing": [ 85 | 2, 86 | { 87 | "before": true, 88 | "after": true 89 | } 90 | ], 91 | "new-cap": [ 92 | 2, 93 | { 94 | "newIsCap": true, 95 | "capIsNew": false 96 | } 97 | ], 98 | "no-alert": 2, 99 | "no-caller": 2, 100 | "new-parens": 2, 101 | "no-array-constructor": 2, 102 | "no-class-assign": 2, 103 | "no-cond-assign": 2, 104 | "no-const-assign": 2, 105 | "no-control-regex": 2, 106 | "no-debugger": 2, 107 | "no-delete-var": 2, 108 | "no-dupe-args": 2, 109 | "no-dupe-class-members": 2, 110 | "no-dupe-keys": 2, 111 | "no-duplicate-case": 2, 112 | "no-duplicate-imports": 2, 113 | "no-empty-character-class": 2, 114 | "no-empty-pattern": 2, 115 | "no-eval": 2, 116 | "no-ex-assign": 2, 117 | "no-extend-native": 2, 118 | "no-extra-bind": 2, 119 | "no-extra-boolean-cast": 2, 120 | "no-extra-parens": "off", 121 | "no-fallthrough": 1, 122 | "no-floating-decimal": 2, 123 | "no-func-assign": 2, 124 | "no-implied-eval": 2, 125 | "no-inner-declarations": [ 126 | 2, 127 | "functions" 128 | ], 129 | "guard-for-in": 1, 130 | "no-trailing-spaces": "off", 131 | "no-invalid-regexp": 2, 132 | "no-irregular-whitespace": "off", 133 | "no-iterator": 2, 134 | "no-label-var": 2, 135 | "no-labels": [ 136 | 2, 137 | { 138 | "allowLoop": false, 139 | "allowSwitch": false 140 | } 141 | ], 142 | "no-lone-blocks": 2, 143 | "no-mixed-spaces-and-tabs": 2, 144 | "no-multi-spaces": "off", 145 | "no-multi-str": 2, 146 | "no-multiple-empty-lines": [ 147 | 2, 148 | { 149 | "max": 4 150 | } 151 | ], 152 | "no-native-reassign": 2, 153 | "no-negated-in-lhs": 2, 154 | "no-new": 2, 155 | "no-new-func": 1, 156 | "no-new-object": 2, 157 | "no-new-require": 2, 158 | "no-new-symbol": 2, 159 | "no-new-wrappers": 2, 160 | "no-obj-calls": 2, 161 | "no-octal": 2, 162 | "no-octal-escape": 2, 163 | "no-path-concat": 2, 164 | "no-proto": 2, 165 | "no-redeclare": 2, 166 | "no-regex-spaces": 2, 167 | "no-return-assign": "off", 168 | "no-self-assign": 2, 169 | "no-self-compare": 1, 170 | "no-sequences": "off", 171 | "no-shadow-restricted-names": 2, 172 | "no-spaced-func": 2, 173 | "no-sparse-arrays": 2, 174 | "no-this-before-super": 2, 175 | "no-throw-literal": 2, 176 | "no-undef": 2, 177 | "no-undef-init": 2, 178 | "no-unexpected-multiline": 2, 179 | "no-unmodified-loop-condition": 2, 180 | "no-unneeded-ternary": [ 181 | 2, 182 | { 183 | "defaultAssignment": false 184 | } 185 | ], 186 | "no-unreachable": 2, 187 | "no-unsafe-finally": 2, 188 | "no-unused-vars": [ 189 | 2, 190 | { 191 | "vars": "local", 192 | "args": "after-used" 193 | } 194 | ], 195 | "no-useless-call": 2, 196 | "no-useless-return": 2, 197 | "no-useless-computed-key": 2, 198 | "no-useless-constructor": 2, 199 | "no-useless-escape": 2, 200 | "no-whitespace-before-property": 2, 201 | "no-with": 2, 202 | "one-var": [ 203 | 1, { 204 | "var": "always" 205 | } 206 | ], 207 | "operator-linebreak": [ 208 | 2, 209 | "after", 210 | { 211 | "overrides": { 212 | "?": "before", 213 | ":": "before" 214 | } 215 | } 216 | ], 217 | "padded-blocks": "off", 218 | "quotes": "off", 219 | "require-jsdoc": [ 220 | "warn", 221 | { 222 | "require": { 223 | "FunctionDeclaration": true, 224 | "MethodDefinition": true, 225 | "ClassDeclaration": true 226 | } 227 | } 228 | ], 229 | "semi": [ 230 | 2, 231 | "always" 232 | ], 233 | "semi-spacing": [ 234 | 2, 235 | { 236 | "before": false, 237 | "after": true 238 | } 239 | ], 240 | "space-before-blocks": [ 241 | 2, 242 | "always" 243 | ], 244 | "space-before-function-paren": [ 245 | "error", 246 | { 247 | "anonymous": "never", 248 | "named": "never" 249 | } 250 | ], 251 | "space-in-parens": [ 252 | 2, 253 | "always" 254 | ], 255 | "space-infix-ops": 2, 256 | "space-unary-ops": [ 257 | 2, 258 | { 259 | "words": true, 260 | "nonwords": false 261 | } 262 | ], 263 | "spaced-comment": [ 264 | 2, 265 | "always", 266 | { 267 | "markers": [ 268 | "global", 269 | "globals", 270 | "eslint", 271 | "eslint-disable", 272 | "*package", 273 | "!", 274 | "," 275 | ], 276 | "exceptions": [ 277 | "*" 278 | ] 279 | } 280 | ], 281 | "template-curly-spacing": [ 282 | 2, 283 | "never" 284 | ], 285 | "use-isnan": 2, 286 | "valid-typeof": 2, 287 | "wrap-iife": [ 288 | 2, 289 | "any" 290 | ], 291 | "yield-star-spacing": [ 292 | 2, 293 | "before" 294 | ], 295 | "yoda": [ 296 | 2, 297 | "never" 298 | ], 299 | "array-bracket-newline": [ 1, { "multiline": true } ], 300 | "array-bracket-spacing": [ 2, "always" ], 301 | "object-curly-spacing": [ 2, "always" ], 302 | "block-spacing": [ 2, "always" ], 303 | "computed-property-spacing": [ 1, "always" ] 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # code coverage 2 | coverage 3 | lib-cov 4 | test-results.xml 5 | clover.xml 6 | 7 | # editor and that .tmp file 8 | .idea 9 | .tmp 10 | 11 | # Node logs 12 | npm-debug.log* 13 | logs 14 | *.log 15 | pids 16 | *.pid 17 | *.seed 18 | tmp.* 19 | .tmp* 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Yeoman/bow/&c. 44 | bower_components/ 45 | build/ 46 | dist/ 47 | .yo-rc.json 48 | docs/ 49 | 50 | # Linux ignores 51 | *~ 52 | .fuse_hidden 53 | .directory 54 | .Trash-* 55 | 56 | ## JetBrains file-based project format: 57 | *.iws 58 | 59 | # JetBrains Rider 60 | *.sln.iml 61 | 62 | # SASS 63 | .sass-cache/ 64 | *.css.map 65 | 66 | ### OSX ### 67 | .DS_Store 68 | .AppleDouble 69 | .LSOverride 70 | 71 | # Windows image file caches 72 | Thumbs.db 73 | ehthumbs.db 74 | 75 | # Folder config file 76 | Desktop.ini 77 | 78 | # Recycle Bin used on file shares 79 | $RECYCLE.BIN/ 80 | 81 | # Windows shortcuts 82 | *.lnk 83 | 84 | # Misc. detritus 85 | .svn/ 86 | /.tgitconfig 87 | .vscode 88 | 89 | dots/ 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | before_script: 5 | - npm run gkupdate 6 | after_script: 7 | - npm run coveralls 8 | - npm run gkupload 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [v1.0.18](https://github.com/julianjensen/ast-flow-graph/compare/v1.0.7...v1.0.18) 8 | 9 | > 12 July 2019 10 | 11 | - chore: migrate to cli-table3 [`#5`](https://github.com/julianjensen/ast-flow-graph/pull/5) 12 | - Update dependencies to enable Greenkeeper 🌴 [`#1`](https://github.com/julianjensen/ast-flow-graph/pull/1) 13 | - fixed bug relating to labels and updated deps [`f364742`](https://github.com/julianjensen/ast-flow-graph/commit/f3647423a16133bb40c4266b364de09de40038bb) 14 | - Build issues, unknown cause, shifting to npm from yarn [`8e1d784`](https://github.com/julianjensen/ast-flow-graph/commit/8e1d7849e644478aab36ffc6e751c7dccdd65312) 15 | - nothing important [`ad36ff9`](https://github.com/julianjensen/ast-flow-graph/commit/ad36ff9bd44da83d9b6a791f9cdc1ccb1d316e3f) 16 | - greenkeeper added [`00cebcf`](https://github.com/julianjensen/ast-flow-graph/commit/00cebcf2a3be7a99ec6a5feb5567c7feb969ed10) 17 | - Updated esm to new module [`cd26e46`](https://github.com/julianjensen/ast-flow-graph/commit/cd26e4641d63a0944bb07d55c91cdf6de10b821b) 18 | - Updated the annotations for doc purposes [`e37507d`](https://github.com/julianjensen/ast-flow-graph/commit/e37507d3bae8a7dbaa48db045090bfb912dfe9d1) 19 | - Sync package-lock file [`d75f667`](https://github.com/julianjensen/ast-flow-graph/commit/d75f667aa61bf1651c0f269b8e54f7742d543fae) 20 | - Documentation was wrong, decided to update the code, instead. [`ad5fadf`](https://github.com/julianjensen/ast-flow-graph/commit/ad5fadfcd08429adea754436a098b7a21d75a793) 21 | - Security alert for old versions of hoek, moved coveralls to npx [`5202cf0`](https://github.com/julianjensen/ast-flow-graph/commit/5202cf0bae9abd0b90380fe6eac0ea06cc17551a) 22 | - -a [`10e8c1d`](https://github.com/julianjensen/ast-flow-graph/commit/10e8c1d1d4ae14af1aa2198306ca8895501ad7cd) 23 | 24 | #### [v1.0.7](https://github.com/julianjensen/ast-flow-graph/compare/v1.0.6...v1.0.7) 25 | 26 | > 19 January 2018 27 | 28 | - Added a simple plugin mechanism [`15e3cb3`](https://github.com/julianjensen/ast-flow-graph/commit/15e3cb337fc945d45e42f428550e1d8ff4317357) 29 | - Wrong title, dangit! [`dc6a41d`](https://github.com/julianjensen/ast-flow-graph/commit/dc6a41d499990f4890bcdb0cb9c5c06a0b328f30) 30 | 31 | #### [v1.0.6](https://github.com/julianjensen/ast-flow-graph/compare/v1.0.5...v1.0.6) 32 | 33 | > 18 January 2018 34 | 35 | - Small but important documentation error [`5bf0c96`](https://github.com/julianjensen/ast-flow-graph/commit/5bf0c96b03bd7cb86ac1b1719ea82bc9f42a1f33) 36 | 37 | #### [v1.0.5](https://github.com/julianjensen/ast-flow-graph/compare/v1.0.4...v1.0.5) 38 | 39 | > 18 January 2018 40 | 41 | - Removed inclusion of , relying on instead [`8613a66`](https://github.com/julianjensen/ast-flow-graph/commit/8613a66791e27f000f9f9813dff90c2ddd729f6d) 42 | 43 | #### [v1.0.4](https://github.com/julianjensen/ast-flow-graph/compare/v1.0.3...v1.0.4) 44 | 45 | > 18 January 2018 46 | 47 | - Much better docs [`7867afd`](https://github.com/julianjensen/ast-flow-graph/commit/7867afd14734fa079fc5c14edda7cb248baf4db5) 48 | - Updated badge names [`21d9d40`](https://github.com/julianjensen/ast-flow-graph/commit/21d9d4005217a2734cdd33ed0dd42df9517d0d39) 49 | 50 | #### [v1.0.3](https://github.com/julianjensen/ast-flow-graph/compare/v1.0.1...v1.0.3) 51 | 52 | > 18 January 2018 53 | 54 | - Trouble with the rename [`08b5594`](https://github.com/julianjensen/ast-flow-graph/commit/08b55948fa81332804aa1d17a24ae9ffdc4ec610) 55 | - Renamed the repo [`9491e68`](https://github.com/julianjensen/ast-flow-graph/commit/9491e68c026ed0750e7679836a9d3d1056f332ba) 56 | 57 | #### v1.0.1 58 | 59 | > 18 January 2018 60 | 61 | - Adding type inference, need to split into modules. Also, adding ESM. [`b9c825e`](https://github.com/julianjensen/ast-flow-graph/commit/b9c825e0091596b14ee620b4126dd1e285fbb307) 62 | - Just the CFG code now [`5fb6451`](https://github.com/julianjensen/ast-flow-graph/commit/5fb6451fa90056be8806a2c465bb1882643df37e) 63 | - Copy for work [`e400634`](https://github.com/julianjensen/ast-flow-graph/commit/e400634eecb838c8a6a00767a81b60861679f4bb) 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017 Julian Jensen jjdanois@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /_cli.js: -------------------------------------------------------------------------------- 1 | /** **************************************************************************** 2 | * DESCRIPTION 3 | * @author julian.jensen 4 | * @since 0.0.1 5 | *******************************************************************************/ 6 | "use strict"; 7 | 8 | // eslint-disable-next-line no-native-reassign 9 | require = require( 'esm' )( module, { mode: 'auto' } ); 10 | require( './cli' ); 11 | 12 | -------------------------------------------------------------------------------- /cfg-04.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c003aea214773dbe19b906f422f75771dd0c733f07d01eb91b070d4d6f32b626 3 | size 89301 4 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Command line starter 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 11-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import CFG from './src/cfg'; 10 | import path from 'path'; 11 | import fs from 'fs'; 12 | import cli from 'command-line-args'; 13 | import usage from 'command-line-usage'; 14 | import { load_plugins } from "./src/utils"; 15 | 16 | const 17 | version = require( path.join( __dirname, 'package.json' ) ).version, 18 | { stat, writeFile } = fs.promises, 19 | stdin = process.stdin, 20 | argumentos = [ 21 | { 22 | alias: 'd', 23 | name: 'debug', 24 | description: "Turn on debugging mode. Warning: This will generate a lot of output.", 25 | type: Boolean, 26 | defaultValue: false 27 | }, 28 | { 29 | alias: 'g', 30 | name: 'graph', 31 | description: "Create a .dot file for graph-viz", 32 | defaultValue: false, 33 | type: Boolean 34 | }, 35 | { 36 | alias: 'o', 37 | name: 'output', 38 | description: "If this option is present, save the .dot files to this directory.", 39 | type: String 40 | }, 41 | { 42 | alias: 's', 43 | name: 'source', 44 | description: "Input source file. Can also be specified at the end of the command line.", 45 | type: String, 46 | defaultOption: true, 47 | multiple: true 48 | }, 49 | { 50 | alias: 't', 51 | name: 'table', 52 | description: "Output a table showing all CFG blocks", 53 | defaultValue: false, 54 | type: Boolean 55 | }, 56 | { 57 | alias: 'l', 58 | name: 'lines', 59 | description: "Output CFG blocks as text", 60 | defaultValue: false, 61 | type: Boolean 62 | }, 63 | { 64 | alias: 'n', 65 | name: 'name', 66 | description: "Specify a function name to only display information for that function.", 67 | type: String, 68 | defaultValue: [], 69 | multiple: true 70 | }, 71 | { 72 | alias: 'v', 73 | name: 'verbose', 74 | description: "Output additional information", 75 | type: Boolean, 76 | defaultValue: false 77 | }, 78 | { 79 | alias: 'h', 80 | name: 'help', 81 | description: "Display this help message", 82 | type: Boolean, 83 | defaultValue: false 84 | } 85 | ], 86 | sections = [ 87 | { content: `cfg version ${version}` }, 88 | { header: 'Usage', content: `cfg [-d] [-g] [-s ...sources] [-t] [-n ...names] [-r] [-v] [-h] [...files]` }, 89 | { header: 'Options', optionList: argumentos }, 90 | { 91 | header: 'Description', content: "Creates a CFG from one or more source files." 92 | } 93 | ], 94 | args = cli( argumentos ); 95 | 96 | if ( args.help ) 97 | { 98 | console.log( usage( sections ) ); 99 | process.exit(); 100 | } 101 | 102 | if ( !args.source && args.name && args.name.some( n => n.endsWith( '.js' ) ) ) 103 | { 104 | args.source = args.name.filter( n => n.endsWith( '.js' ) ); 105 | args.name = args.name.filter( n => !n.endsWith( '.js' ) ); 106 | } 107 | 108 | process_all(); 109 | 110 | /** */ 111 | function process_all() 112 | { 113 | let str = ''; 114 | 115 | load_plugins(); 116 | 117 | if ( args.source && args.source.length ) 118 | args.source.forEach( async name => await process_file( fs.readFileSync( name, 'utf8' ) ) ); 119 | else 120 | { 121 | stdin.setEncoding( 'utf8' ); 122 | 123 | stdin.on( 'readable', () => { 124 | str += stdin.read() || ''; 125 | } ); 126 | 127 | stdin.on( 'end', async () => await process_file( str ) ); 128 | } 129 | 130 | } 131 | 132 | /** 133 | * @param {string} source 134 | */ 135 | function process_file( source ) 136 | { 137 | const 138 | cfg = new CFG( source, { ssaSource: args.rewrite } ); 139 | 140 | if ( args.name && args.name.length ) 141 | args.name.forEach( async name => await single_function( cfg, name, true ) ); 142 | else 143 | { 144 | cfg.generate(); 145 | cfg.forEach( async c => await single_function( cfg, c.name, false ) ); 146 | } 147 | } 148 | 149 | /** 150 | * @param {CFG} cfg 151 | * @param {string} name 152 | * @param {boolean} generate 153 | * @return {Promise} 154 | */ 155 | async function single_function( cfg, name, generate ) 156 | { 157 | // eslint-disable-next-line max-len 158 | let hdr = `------------------------------------------------------------------------------------------------------------\nNEW FUNCTION: __FN__\n------------------------------------------------------------------------------------------------------------`; 159 | 160 | const c = generate ? cfg.generate( name ) : cfg.by_name( name ); 161 | 162 | if ( args.verbose ) console.log( hdr.replace( '__FN__', c.name ) ); 163 | if ( args.table ) console.log( c.toTable() ); 164 | if ( args.lines ) console.log( '' + cfg ); 165 | 166 | if ( args.graph ) 167 | { 168 | const dot = cfg.create_dot( c ); 169 | 170 | if ( args.output ) 171 | { 172 | try 173 | { 174 | const s = await stat( args.output ); 175 | if ( !s.isDirectory() ) throw new Error(); 176 | await writeFile( `./dots/${c.name}.dot`, dot ); 177 | } 178 | catch ( err ) 179 | { 180 | console.error( `Unable to stat "${args.output}"` ); 181 | process.exit( 1 ); 182 | } 183 | } 184 | else 185 | console.log( `${dot}\n\n` ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Description of file here. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date Sat Dec 16 2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | require = require( 'esm' )( module, { mode: 'auto' } ); // eslint-disable-line no-native-reassign 10 | const CFG = require( './src/cfg' ).default; 11 | const { Block, Edge } = require( './src/types' ); 12 | const { 13 | load_plugins, 14 | plugin, 15 | current 16 | } = require( './src/utils' ); 17 | 18 | let loaded = false; 19 | 20 | /** 21 | * @param {string} s 22 | * @param {object} [o] 23 | * @return {CFG} 24 | */ 25 | function load( s, o ) 26 | { 27 | if ( !loaded && o && o.plugins ) 28 | { 29 | loaded = true; 30 | load_plugins( o.plugins ); 31 | plugin( 'general', 'postload', current ); 32 | } 33 | 34 | return new CFG( s, o ); 35 | } 36 | 37 | load.Block = Block; 38 | load.Edge = Edge; 39 | 40 | module.exports = load; 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ast-flow-graph", 3 | "version": "1.0.18", 4 | "description": "Constructs a CFG for JavaScript source code.", 5 | "main": "index.js", 6 | "bin": "_cli.js", 7 | "scripts": { 8 | "lint": "eslint index.js cli.js _cli.js src/**/*.js", 9 | "deps": "npx npm-check", 10 | "check": "npm run lint -s && npm run deps", 11 | "test": "rm -rf node_modules/.cache coverage/ && nyc --reporter=lcov --reporter=text --produce-source-map --require=esm mocha", 12 | "coveralls": "npm test && nyc report --reporter=text-lcov | tee coverage.lcov | npx coveralls", 13 | "postcoveralls": "codecov && rimraf ./coverage coverage.lcov", 14 | "prepublishOnly": "npm test", 15 | "patch": "npm version patch && npm publish", 16 | "minor": "npm version minor && npm publish", 17 | "major": "npm version major && npm publish", 18 | "postpublish": "git push origin master --follow-tags", 19 | "changelog": "npx auto-changelog -p -l 10 && git add CHANGELOG.md", 20 | "save": "git commit -a --amend -C HEAD", 21 | "marktoc": "npx replace -s -q '### Changelog' '\\\n' CHANGELOG.md", 22 | "toc": "npx doctoc --github --title \"### Changelog\" CHANGELOG.md", 23 | "api": "node -e \"const x='README',{readFileSync:_r,writeFileSync:w}=require('fs'),r=f=>_r(f+'.md','utf8'),s=r(x),t='\\n'+r('tmp')+'/,t))\"", 24 | "gendocs": "npx jsdoc-to-markdown src/*.js > tmp.md", 25 | "rmdocs": "rimraf tmp.md", 26 | "docs": "run-s -s gendocs api rmdocs", 27 | "docsite": "npx documentation build index.js --infer-private='^_.*' -f html -o docs", 28 | "version": "run-s -s changelog marktoc toc docs", 29 | "gkupdate": "greenkeeper-lockfile-update", 30 | "gkupload": "greenkeeper-lockfile-upload" 31 | }, 32 | "repository": { 33 | "types": "git", 34 | "url": "https://github.com/julianjensen/ast-flow-graph.git" 35 | }, 36 | "keywords": [ 37 | "cfg", 38 | "flowgraph", 39 | "flow graph", 40 | "control flow graph" 41 | ], 42 | "author": "Julian Jensen ", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/julianjensen/ast-flow-graph/issues" 46 | }, 47 | "homepage": "https://github.com/julianjensen/ast-flow-graph#readme", 48 | "devDependencies": { 49 | "@types/esprima": "^4.0.2", 50 | "@types/estree": "^0.0.39", 51 | "chai": "^4.2.0", 52 | "codecov": "^3.5.0", 53 | "eslint": "^6.0.1", 54 | "greenkeeper-lockfile": "^1.15.1", 55 | "mocha": "^6.1.4", 56 | "npm-run-all": "^4.1.5", 57 | "nyc": "^14.1.1" 58 | }, 59 | "dependencies": { 60 | "chalk": "^2.4.2", 61 | "cli-table3": "^0.5.1", 62 | "command-line-args": "^5.1.1", 63 | "command-line-usage": "^6.0.2", 64 | "dominators": "^1.1.2", 65 | "escope": "^3.6.0", 66 | "esm": "^3.2.25", 67 | "espree": "^6.0.0", 68 | "estraverse": "^4.2.0", 69 | "traversals": "^1.0.15", 70 | "yallist": "^3.0.3" 71 | }, 72 | "esm": { 73 | "mode": "auto" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ast.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Manages the AST, does some scans, and provides walkers. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 26-Nov-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import assert from 'assert'; 10 | import { traverse, Syntax } from 'estraverse'; 11 | import { analyze } from 'escope'; 12 | import { VisitorKeys } from 'espree'; 13 | import { plugin, current } from './utils'; 14 | 15 | const 16 | { isArray: array } = Array, 17 | /** 18 | * @param {string} type 19 | * @return {boolean} 20 | * @private 21 | */ 22 | isBaseFunction = ( { type } ) => type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression || type === Syntax.ArrowFunctionExpression, 23 | /** 24 | * @return {string} 25 | * @private 26 | */ 27 | nodeString = function() { 28 | let keys = VisitorKeys[ this.type ].map( key => `${key}${array( this[ key ] ) ? '(' + this[ key ].length + ')' : ''}` ).join( ', ' ); 29 | 30 | if ( keys ) keys = ': [' + keys + ']'; 31 | 32 | return `${this.type}, lvl: ${this.level}, line ${this.loc.start.line}${keys}`; 33 | }; 34 | 35 | let stableSort = 0; 36 | 37 | /** 38 | * @class 39 | * @param {string} source 40 | * @param {object} options 41 | */ 42 | export default class AST 43 | { 44 | /** 45 | * @param {string} source 46 | * @param {object} options 47 | */ 48 | constructor( source, options ) 49 | { 50 | const stepsUp = es => { 51 | let s = -1; 52 | while ( es ) 53 | { 54 | ++s; 55 | es = es.upper; 56 | } 57 | 58 | return s; 59 | }; 60 | 61 | this.addedLines = []; 62 | this.blankLines = []; 63 | this.lines = source.split( /\r?\n/ ); 64 | this.lines.forEach( ( line, num ) => /^\s*$/.test( line ) && this.blankLines.push( num ) ); 65 | 66 | this.source = source; 67 | this.renameOffsets = []; 68 | 69 | current.ast = this; 70 | plugin( 'ast', 'init', this ); 71 | 72 | // this.root = this.ast = espree.parse( source, options ); 73 | this.root = this.ast = plugin( 'parse', null, source, options ); 74 | 75 | this.escope = analyze( this.ast, { 76 | ecmaVersion: 6, 77 | sourceType: options.sourceType, 78 | directive: true 79 | } ); 80 | 81 | this.nodesByIndex = []; 82 | this.functions = [ this.get_from_function( this.ast ) ]; 83 | 84 | let index = 0, 85 | labeled = []; 86 | 87 | current.ast = this; 88 | plugin( 'ast', 'postinit', this ); 89 | 90 | // this.traverse( ( node, parent ) => { 91 | this.walker( ( node, parent, _, field, findex ) => { 92 | this.nodesByIndex[ index ] = node; 93 | node.index = index++; 94 | node.parent = parent; 95 | node.cfg = null; 96 | node.toString = nodeString; 97 | node.level = 0; 98 | node.field = field; 99 | node.fieldIndex = findex; 100 | 101 | if ( node.type === Syntax.LabeledStatement ) labeled.push( node ); 102 | 103 | if ( isBaseFunction( node ) ) 104 | this.functions.push( this.get_from_function( node ) ); 105 | 106 | if ( node.type === Syntax.BlockStatement && node.body.length === 0 ) 107 | { 108 | node.body.push( { 109 | type: Syntax.EmptyStatement, 110 | loc: node.loc, 111 | range: node.range 112 | } ); 113 | } 114 | } ); 115 | 116 | current.ast = this; 117 | plugin( 'ast', 'finish', this ); 118 | 119 | // this.escope = escope.analyze( this.ast, { 120 | this.traverse( node => { 121 | const s = this.node_to_scope( node ); 122 | node.level = stepsUp( s ); 123 | node.scope = s; 124 | } ); 125 | 126 | this.associate = new Map(); 127 | 128 | labeled.forEach( node => { 129 | let escope = this.node_to_scope( node ), 130 | assoc = this.associate.get( escope ); 131 | 132 | if ( !assoc ) this.associate.set( escope, assoc = { labels: [] } ); 133 | assoc.labels.push( { 134 | label: node.label.name, 135 | node: node 136 | } ); 137 | } ); 138 | 139 | current.ast = this; 140 | plugin( 'ast', 'postfinish', this ); 141 | } 142 | 143 | /** 144 | * @param {AnnotatedNode} node 145 | * @return {*} 146 | */ 147 | node_to_scope( node ) 148 | { 149 | let scope = this.escope.acquire( node ); 150 | 151 | if ( scope ) return scope; 152 | 153 | while ( node && !scope ) 154 | { 155 | node = node.parent; 156 | if ( node ) scope = this.escope.acquire( node ); 157 | } 158 | 159 | return scope; 160 | } 161 | 162 | /** 163 | * @type {Iterable} 164 | */ 165 | *forFunctions() 166 | { 167 | yield *this.functions; 168 | } 169 | 170 | /** 171 | * @param {AnnotatedNode} start 172 | * @param {string} label 173 | * @return {?CFGBlock} 174 | * @private 175 | */ 176 | find_label( start, label ) 177 | { 178 | let scope = this.node_to_scope( start ); 179 | 180 | while ( scope ) 181 | { 182 | const assoc = this.associate.get( scope ); 183 | 184 | if ( assoc && assoc.labels ) 185 | { 186 | const la = assoc.labels.find( la => la.label === label ); 187 | if ( la ) return la.node.cfg; 188 | } 189 | 190 | scope = scope.upper; 191 | } 192 | 193 | return null; 194 | } 195 | 196 | /** 197 | * @param {Node|function} ast 198 | * @param {?function} [enter] 199 | * @param {?function} [leave] 200 | */ 201 | traverse( ast, enter, leave ) 202 | { 203 | if ( typeof ast === 'function' ) 204 | { 205 | leave = enter; 206 | enter = ast; 207 | ast = this.root; 208 | } 209 | 210 | const funcs = { 211 | enter 212 | }; 213 | 214 | if ( typeof leave === 'function' ) 215 | funcs.leave = leave; 216 | 217 | traverse( ast, funcs ); 218 | } 219 | 220 | /** 221 | * @param {AnnotatedNode|BaseNode|Node} node 222 | * @param {function} [enter] 223 | * @param {function} [leave] 224 | */ 225 | walker( node, enter = () => {}, leave = () => {} ) 226 | { 227 | if ( typeof node === 'function' ) 228 | { 229 | leave = enter; 230 | enter = node; 231 | node = this.root; 232 | } 233 | 234 | /** 235 | * @param {BaseNode|Array} node 236 | * @param {?(BaseNode|Node)} parent 237 | * @param {?Node} previous 238 | * @param {?string} [field] 239 | * @param {number} [index] 240 | * @param {?Node} next 241 | * @private 242 | */ 243 | function _walker( node, parent, previous, field, index, next ) // eslint-disable-line max-params 244 | { 245 | if ( !node ) return; 246 | 247 | const 248 | isa = array( node ), 249 | er = !isa ? enter( node, parent, previous, field, index, next ) : true; 250 | 251 | if ( er !== false ) 252 | { 253 | VisitorKeys[ node.type ].forEach( key => { 254 | if ( array( node[ key ] ) ) 255 | { 256 | const arr = node[ key ]; 257 | 258 | arr.forEach( ( n, i ) => _walker( arr[ i ], node, i ? arr[ i - 1 ] : null, key, i, i === arr.length - 1 ? null : arr[ i + 1 ] ) ); 259 | } 260 | else 261 | _walker( node[ key ], node, null, key, next ); 262 | } ); 263 | } 264 | 265 | if ( !isa ) leave( node, parent, previous, field, index, next ); 266 | } 267 | 268 | _walker( node || this.root, null, null, null, 0, null ); 269 | } 270 | 271 | /** 272 | * Iterate over all nodes in a block without recursing into sub-nodes. 273 | * 274 | * @param {Array|AnnotatedNode} nodes 275 | * @param {function(AnnotatedNode,function(AnnotatedNode):boolean):boolean} cb 276 | */ 277 | flat_walker( nodes, cb ) 278 | { 279 | if ( !array( nodes ) ) 280 | { 281 | if ( nodes.body && /Function/.test( nodes.type ) ) 282 | nodes = nodes.body; 283 | 284 | if ( !array( nodes ) ) nodes = [ nodes ]; 285 | } 286 | 287 | let i = 0; 288 | 289 | while ( i < nodes.length && cb( nodes[ i ], n => this.flat_walker( n, cb ) ) !== false ) 290 | ++i; 291 | } 292 | 293 | /** 294 | * Callback for each visitor key for a given node. 295 | * 296 | * @param {AnnotatedNode} node 297 | * @param {function((Array|AnnotatedNode))} cb 298 | */ 299 | call_visitors( node, cb ) 300 | { 301 | if ( !node || !VisitorKeys[ node.type ] ) return; 302 | 303 | VisitorKeys[ node.type ].forEach( key => node[ key ] && cb( node[ key ] ) ); 304 | } 305 | 306 | /** 307 | * Add a new line to the source code. 308 | * 309 | * @param {number} lineNumber - 0-based line number 310 | * @param {string} sourceLine - The source line to add 311 | */ 312 | add_line( lineNumber, sourceLine ) 313 | { 314 | const renumIndex = this.blankLines.findIndex( ln => ln <= lineNumber ); 315 | 316 | if ( renumIndex !== -1 ) 317 | { 318 | for ( let i = renumIndex; i < this.blankLines.length; i++ ) 319 | this.blankLines[ i ]++; 320 | } 321 | 322 | let i = lineNumber; 323 | 324 | while ( i < this.lines.length && !this.lines[ i ] ) i++; 325 | 326 | if ( this.lines[ i ] ) 327 | sourceLine = this.lines[ i ].replace( /^(\s*).*$/, '$1' ) + sourceLine.replace( /^\s*(.*)$/, '$1' ); 328 | 329 | this.addedLines.push( { lineNumber: lineNumber * 10000 + stableSort++, sourceLine } ); 330 | } 331 | 332 | /** 333 | * @param {Identifier|AnnotatedNode} inode - A node of type Syntax.Identifier 334 | * @param {string} newName - The new name of the identifier 335 | */ 336 | rename( inode, newName ) 337 | { 338 | assert( inode.type === Syntax.Identifier || inode.type === Syntax.MemberExpression, "Not an Identifier in rename, found: " + inode.type ); 339 | 340 | if ( !~this.renameOffsets.findIndex( ro => ro.start === inode.range[ 0 ] ) ) 341 | this.renameOffsets.push( { start: inode.range[ 0 ], end: inode.range[ 1 ], newName } ); 342 | } 343 | 344 | /** 345 | * Return the AST nodes as source code, including any modifications made. 346 | * @return {string} - The lossy source code 347 | */ 348 | as_source() 349 | { 350 | if ( this.renameOffsets.length === 0 ) return this.source; 351 | 352 | const offsets = this.renameOffsets.sort( ( a, b ) => b.start - a.start ); 353 | 354 | let source = this.source; 355 | 356 | for ( const { start, end, newName } of offsets ) 357 | source = source.substr( 0, start ) + newName + source.substr( end ); 358 | 359 | const lines = source.split( /\r?\n/ ); 360 | 361 | this.addedLines.sort( ( a, b ) => b.lineNumber - a.lineNumber ).forEach( ( { lineNumber, sourceLine } ) => { 362 | lines.splice( Math.floor( lineNumber / 10000 ), 0, sourceLine ); 363 | } ); 364 | 365 | return lines.map( ( l, i ) => `${i.toString().padStart( 3 )}. ${l}` ).join( '\n' ); 366 | } 367 | 368 | /** 369 | * @param {FunctionDeclaration|FunctionExpression|MethodDefinition|ArrowFunctionExpression|Property|Node} node 370 | * @param {string} [whatToGet='all'] 371 | * @return {Array|string|CFGInfo} 372 | * @protected 373 | */ 374 | get_from_function( node, whatToGet = 'all' ) 375 | { 376 | if ( node.type === Syntax.Program ) 377 | { 378 | const pg = { 379 | name: 'main', 380 | params: [], 381 | body: grab_body( node ), 382 | lines: [ node.loc.start.line, node.loc.end.line ], 383 | node 384 | }; 385 | 386 | return whatToGet && whatToGet !== 'all' ? pg[ whatToGet ] : pg; 387 | } 388 | 389 | const 390 | /** 391 | * @param {AnnotatedNode} n 392 | * @return {?string} 393 | * @private 394 | */ 395 | hopeForName = n => { 396 | if ( n.type === Syntax.Identifier ) 397 | return n.name; 398 | else if ( n.type === Syntax.MemberExpression ) 399 | { 400 | if ( !n.computed && n.object.type === Syntax.Identifier && n.property.type === Syntax.Identifier ) return n.object.name + '.' + n.property.name; 401 | if ( !n.computed || n.object.type !== Syntax.Identifier || n.property.type !== Syntax.Identifier ) return null; 402 | // if ( n.object.name !== 'Symbol' && n.object.name !== 'super' ) return null; 403 | 404 | return n.object + '.' + n.property; 405 | 406 | } 407 | else if ( n.type === Syntax.MethodDefinition ) 408 | { 409 | if ( n.kind === 'constructor' ) 410 | return 'constructor'; 411 | else if ( n.kind === 'method' ) 412 | return hopeForName( n.key ); 413 | else 414 | { 415 | const _name = hopeForName( n.key ); 416 | 417 | return typeof _name === 'string' ? _name + '.' + n.kind : _name; 418 | } 419 | } 420 | else if ( n.id ) 421 | return hopeForName( n.id ); 422 | else if ( n.parent.type === Syntax.Property || n.parent.type === Syntax.MethodDefinition ) 423 | return hopeForName( n.parent.key ); 424 | else if ( n.parent.type === Syntax.VariableDeclarator ) 425 | return hopeForName( n.parent.id ); 426 | else if ( n.parent.type === Syntax.AssignmentExpression ) 427 | return hopeForName( n.parent.left ); 428 | 429 | return 'anonymous'; 430 | }; 431 | 432 | if ( node.type === Syntax.Property || node.type === Syntax.MethodDefinition ) 433 | return this.get_from_function( node.value, whatToGet ); 434 | else if ( !isBaseFunction( node ) ) 435 | throw new SyntaxError( `No function found near ${node.type}, unable to find ${whatToGet}` ); 436 | 437 | return grab_info(); 438 | 439 | /** 440 | * @param {AnnotatedNode|BaseFunction|MethodDefinition|Program} node 441 | * @returns {?(AnnotatedNode|Array)} 442 | * @private 443 | */ 444 | function grab_body( node ) 445 | { 446 | switch ( node.type ) 447 | { 448 | case Syntax.Program: 449 | case Syntax.FunctionDeclaration: 450 | case Syntax.FunctionExpression: 451 | case Syntax.ArrowFunctionExpression: 452 | return node.body.type === Syntax.BlockStatement ? node.body.body : node.body; 453 | 454 | case Syntax.MethodDefinition: 455 | return grab_body( node.value ); 456 | } 457 | } 458 | 459 | /** 460 | * @returns {*} 461 | * @private 462 | */ 463 | function grab_info() 464 | { 465 | switch ( whatToGet ) 466 | { 467 | case 'name': 468 | return hopeForName( node ); 469 | 470 | case 'params': 471 | return node.params; 472 | 473 | case 'body': 474 | return grab_body( node ); 475 | 476 | case 'lines': 477 | return [ node.loc.start.line, node.loc.end.line ]; 478 | 479 | default: 480 | return { 481 | name: hopeForName( node ), 482 | params: node.params, 483 | body: node.body.type === Syntax.BlockStatement ? node.body.body : node.body, 484 | lines: [ node.loc.start.line, node.loc.end.line ], 485 | node 486 | }; 487 | } 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/block.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file CFG block definition, equivalent to a basic block in compiler parlance. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 18-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import assert from 'assert'; 10 | import { Block, Edge, enum_to_string } from './types'; 11 | import { display_options, array, plugin, current } from './utils'; 12 | 13 | const 14 | { 15 | SPACE_PER_EDGE, 16 | MAX_EDGES, 17 | LEFT_EDGES, 18 | RIGHT_EDGES, 19 | TRUE_EDGE, 20 | FALSE_EDGE, 21 | START_NODE, 22 | EXIT_NODE 23 | } = display_options( true ), 24 | padLeft = ( n, target, pre, post ) => [ ' '.repeat( target - `${n}`.length ), pre, `${n}`, post ], 25 | digits = ( n, d = 2, pre = '', post = '' ) => padLeft( n, d - 1, pre, post ).join( '' ); 26 | 27 | /** 28 | * @param {number} id 29 | * @param {Edges} edges 30 | */ 31 | export default class CFGBlock 32 | { 33 | /** 34 | * @param {number} id 35 | * @param {Edges} edges 36 | */ 37 | constructor( id, edges ) 38 | { 39 | assert( edges ); 40 | // To prevent edges from showing up when doing `console.log` on these blocks 41 | Object.defineProperty( this, 'edges', { value: edges, enumerable: false } ); 42 | 43 | this.id = id; 44 | /** @type {Array} */ 45 | this.nodes = []; 46 | 47 | this.lastEdge = null; 48 | this.types = Block.NORMAL; 49 | 50 | this.createdBy = ''; 51 | this.scope = null; 52 | 53 | current.block = this; 54 | plugin( 'cfgblock', 'init', this ); 55 | } 56 | 57 | /** 58 | * @return {number[]} 59 | */ 60 | get succesors_as_indices() 61 | { 62 | return this.succs.map( s => s.id ); 63 | } 64 | 65 | /** 66 | * @return {Array} 67 | */ 68 | get successors() 69 | { 70 | return this.succs; 71 | } 72 | 73 | /** 74 | * @return {Array} 75 | */ 76 | get succs() 77 | { 78 | return this.edges.get_succs( this ); 79 | } 80 | 81 | /** 82 | * @return {Array} 83 | */ 84 | get preds() 85 | { 86 | return this.edges.get_preds( this ); 87 | } 88 | 89 | /** 90 | * @return {boolean} 91 | */ 92 | isEmpty() 93 | { 94 | return this.nodes.length === 0; 95 | } 96 | 97 | /** 98 | * @param {number|CFGBlock} to 99 | * @param {string} type 100 | * @return {CFGBlock} 101 | */ 102 | classify( to, type ) 103 | { 104 | this.edges.classify( this, to, type ); 105 | return this; 106 | } 107 | 108 | /** 109 | * @param {CFGBlock|CFGBlock[]} cb 110 | * @return {CFGBlock} 111 | */ 112 | follows( cb ) 113 | { 114 | if ( !cb ) return this; 115 | 116 | if ( !array( cb ) ) 117 | cb.to( this ); 118 | else 119 | cb.forEach( block => block.to( this ) ); 120 | 121 | return this; 122 | } 123 | 124 | /** 125 | * @param {CFGBlock|CFGBlock[]} cb 126 | * @return {CFGBlock} 127 | */ 128 | from( cb ) 129 | { 130 | return this.follows( cb ); 131 | } 132 | 133 | /** 134 | * @param {CFGBlock|CFGBlock[]} cb 135 | * @return {CFGBlock} 136 | */ 137 | to( cb ) 138 | { 139 | if ( !cb ) return this; 140 | 141 | if ( array( cb ) && cb.length === 1 ) cb = cb[ 0 ]; 142 | 143 | if ( !array( cb ) ) 144 | cb.lastEdge = this.lastEdge = this.edges.add( this, cb ).lastEdge; 145 | else 146 | cb.forEach( block => block.lastEdge = this.lastEdge = this.edges.add( this, block ).lastEdge ); 147 | 148 | return this; 149 | } 150 | 151 | /** 152 | * @return {CFGBlock} 153 | */ 154 | remove_succs() 155 | { 156 | this.edges.get_succs( this ).forEach( s => this.edges.remove_succ( this, s ) ); 157 | return this; 158 | } 159 | 160 | /** 161 | * @param {number|CFGBlock} kill 162 | * @return {CFGBlock} 163 | */ 164 | remove_succ( kill ) 165 | { 166 | this.edges.remove_succ( this, kill ); 167 | return this; 168 | } 169 | 170 | /** 171 | * @param {number} nodeType 172 | * @return {CFGBlock} 173 | */ 174 | as( nodeType ) 175 | { 176 | if ( nodeType & Block.EXCLUSIVE ) 177 | this.types = ( this.types & ~Block.EXCLUSIVE ) | ( nodeType & Block.EXCLUSIVE ); 178 | 179 | this.types |= ( nodeType & ~Block.EXCLUSIVE ); 180 | 181 | if ( this.types & ~Block.NORMAL ) this.types &= ~Block.NORMAL; 182 | 183 | return this; 184 | } 185 | 186 | /** 187 | * Sets the last edge to type. 188 | * 189 | * @param {Edge} edgeType 190 | * @param {number|CFGBlock} [to] 191 | * @return {CFGBlock} 192 | */ 193 | edge_as( edgeType, to ) 194 | { 195 | to = to || to === 0 ? to : this.lastEdge.to; 196 | 197 | this.edges.classify( this, to || this.lastEdge.to, edgeType ); 198 | return this; 199 | } 200 | 201 | /** 202 | * Removes a type from this block. 203 | * 204 | * @param {Edge} nodeType 205 | * @return {CFGBlock} 206 | */ 207 | not( nodeType ) 208 | { 209 | this.types &= ~nodeType; 210 | return this; 211 | } 212 | 213 | /** 214 | * For test nodes, this adds the edge taken when the condition is true. 215 | * 216 | * @param {CFGBlock} block 217 | * @return {CFGBlock} 218 | */ 219 | whenTrue( block ) 220 | { 221 | if ( !block ) return this; 222 | 223 | this.to( block ).as( Block.TEST ); 224 | this.edge_as( Edge.TRUE, block ); 225 | return this; 226 | } 227 | 228 | /** 229 | * For test nodes, this adds the edge taken when the condition is false. 230 | * 231 | * @param {CFGBlock} block 232 | * @return {CFGBlock} 233 | */ 234 | whenFalse( block ) 235 | { 236 | if ( !block ) return this; 237 | 238 | this.to( block ).as( Block.TEST ); 239 | this.edge_as( Edge.FALSE, block ); 240 | return this; 241 | } 242 | 243 | /** 244 | * Add a current-level AST node to this block. 245 | * 246 | * @param {AnnotatedNode|BaseNode|Node} node 247 | * @return {CFGBlock} 248 | */ 249 | add( node ) 250 | { 251 | node.cfg = this; 252 | this.nodes.push( node ); 253 | 254 | return this; 255 | } 256 | 257 | /** 258 | * Returns the first AST node (if any) of this block. 259 | * 260 | * @return {?(AnnotatedNode|BaseNode|Node)} 261 | */ 262 | first() 263 | { 264 | return this.nodes.length ? this.nodes[ 0 ] : null; 265 | } 266 | 267 | /** 268 | * Returns the last AST node (if any) of this block. 269 | * 270 | * @return {?(AnnotatedNode|BaseNode|Node)} 271 | */ 272 | last() 273 | { 274 | return this.nodes.length ? this.nodes[ this.nodes.length - 1 ] : null; 275 | } 276 | 277 | /** 278 | * Free-text field indicating the manner of of creation of this node. For information in graphs and printouts only. 279 | * 280 | * @param {string} txt 281 | * @return {CFGBlock} 282 | */ 283 | by( txt ) 284 | { 285 | this.createdBy = typeof txt === 'string' ? txt : 'label'; 286 | return this; 287 | } 288 | 289 | /** 290 | * Check if this block has a particular type. 291 | * 292 | * @param {number} typeName 293 | * @returns {boolean} 294 | */ 295 | isa( typeName ) 296 | { 297 | return !!( this.types & typeName ); 298 | } 299 | 300 | /** 301 | * Remove itself if it's an empty node and isn't the start or exit node. 302 | * 303 | * @return {boolean} - true if it was deleted 304 | */ 305 | eliminate() 306 | { 307 | if ( this.nodes.length || this.isa( Block.START ) || this.isa( Block.EXIT ) || this.succs.some( s => s === this ) ) return false; 308 | 309 | this.edges.retarget_multiple( this ); 310 | 311 | this.as( Block.DELETED ); 312 | return true; 313 | } 314 | 315 | /** 316 | * @param {Edge} type 317 | */ 318 | defer_edge_type( type ) 319 | { 320 | this.deferredEdgeType = type; 321 | } 322 | 323 | /***************************************************************************************************************** 324 | * 325 | * PRINT AND OUTPUT HELPERS 326 | * 327 | *****************************************************************************************************************/ 328 | 329 | /** 330 | * For the vertices. 331 | * 332 | * @return {string} 333 | */ 334 | graph_label() 335 | { 336 | let 337 | txt = enum_to_string( Block, this.types ).join( '|' ), 338 | lns = this.nodes.length && this.nodes[ 0 ].loc && this.nodes[ 0 ].loc.start.line, 339 | lne = this.nodes.length && this.nodes[ this.nodes.length - 1 ].loc && this.nodes[ this.nodes.length - 1 ].loc.end.line, 340 | ln = lns === lne ? lns : lns + '-' + lne; 341 | 342 | if ( this.isa( Block.START ) || this.isa( Block.EXIT ) ) txt += ':' + this.id; 343 | return txt ? `${txt}:${this.id}@${ln}` : `NORMAL:${this.id}@${ln || ''}`; 344 | } 345 | 346 | /** 347 | * Stringified line numbers for this block. 348 | * 349 | * @return {string} 350 | */ 351 | lines() 352 | { 353 | if ( this.nodes.length === 0 ) return ''; 354 | 355 | const 356 | f = this.first() || {}, 357 | l = this.last() || {}, 358 | { 359 | start: { 360 | line: start = 0 361 | } 362 | } = f.loc, 363 | { 364 | end: { 365 | line: end = 0 366 | } 367 | } = l.loc; 368 | 369 | return start === end ? `:${start}` : `:${start}-${end}`; 370 | } 371 | 372 | /** 373 | * @return {Array} 374 | */ 375 | pred_edge_types() 376 | { 377 | return this.edges.pred_edges( this ).map( e => e.type.isa( Edge.TRUE ) ? TRUE_EDGE : e.type.isa( Edge.FALSE ) ? FALSE_EDGE : e.type.isa( ~Edge.CLEAR ) ? '*' : '' ); 378 | } 379 | 380 | /** 381 | * @return {Array} 382 | */ 383 | succ_edge_types() 384 | { 385 | return this.edges.edges( this ).map( e => e.type.isa( Edge.TRUE ) ? TRUE_EDGE : e.type.isa( Edge.FALSE ) ? FALSE_EDGE : e.type.isa( ~Edge.CLEAR ) ? '*' : '' ); 386 | } 387 | 388 | /** 389 | * @param {Array<*>} arr 390 | * @param {number} chunkSize 391 | * @return {Array} 392 | */ 393 | split_by( arr, chunkSize ) 394 | { 395 | let offset = 0, 396 | lng = arr.length, 397 | out = []; 398 | 399 | while ( offset < lng ) 400 | { 401 | out.push( arr.slice( offset, offset + chunkSize ) ); 402 | offset += chunkSize; 403 | } 404 | 405 | return out; 406 | } 407 | 408 | /** 409 | * Headers are 410 | * TYPE / LINES / LEFT EDGES / NODE / RIGHT EDGES / CREATED BY / AST 411 | * @return {Array} 412 | */ 413 | toRow() 414 | { 415 | let preds = this.preds, 416 | succs = this.succs, 417 | leftEdges = this.pred_edge_types().map( ( c, i ) => digits( preds[ i ].id, SPACE_PER_EDGE, c, '' ) ).join( '' ), 418 | rightEdges = this.succ_edge_types().map( ( c, i ) => digits( succs[ i ].id, SPACE_PER_EDGE, '', c ) ).join( '' ); 419 | 420 | return [ 421 | enum_to_string( Block, this.types ).join( '\n' ), 422 | this.lines().substr( 1 ), 423 | leftEdges, 424 | digits( this.id, SPACE_PER_EDGE, this.isa( Block.START ) ? START_NODE : '', this.isa( Block.EXIT ) ? EXIT_NODE : '' ), 425 | rightEdges, 426 | this.createdBy || '', 427 | this.nodes.length ? this.split_by( this.nodes.map( n => n.type + '(' + n.index + ')' ), 1 ).map( sect => sect.join( ' ' ) ).join( '\n' ) : '' 428 | ]; 429 | } 430 | 431 | /** 432 | * @return {Array} 433 | */ 434 | toString() 435 | { 436 | let preds = this.preds, 437 | succs = this.succs, 438 | leftEdges = this.pred_edge_types().map( ( c, i ) => digits( preds[ i ].id, SPACE_PER_EDGE, c, '' ) ).join( '' ), 439 | rightEdges = this.succ_edge_types().map( ( c, i ) => digits( succs[ i ].id, SPACE_PER_EDGE, '', c ) ).join( '' ); 440 | 441 | if ( !/^\s*$/.test( leftEdges ) ) leftEdges = leftEdges + ' ' + LEFT_EDGES; 442 | if ( !/^\s*$/.test( rightEdges ) ) rightEdges = RIGHT_EDGES + ' ' + rightEdges; 443 | 444 | leftEdges = leftEdges.padStart( MAX_EDGES * SPACE_PER_EDGE + 8 ); 445 | rightEdges = rightEdges.padEnd( MAX_EDGES * SPACE_PER_EDGE + 8 ); 446 | 447 | return [ 448 | enum_to_string( Block, this.types ).join( '|' ).padStart( 10 ), 449 | this.lines().substr( 1 ).padStart( 9 ), 450 | leftEdges, 451 | digits( this.id, SPACE_PER_EDGE, this.isa( Block.START ) ? START_NODE : '', this.isa( Block.EXIT ) ? EXIT_NODE : '' ), 452 | rightEdges, 453 | ( this.createdBy || '' ).padStart( 26 ), 454 | this.nodes.length ? this.split_by( this.nodes.map( n => n.type + '(' + n.index + ')' ), 1 ).map( sect => sect.join( ' ' ) ).join( ', ' ) : '' 455 | ].join( ' ' ); 456 | } 457 | 458 | } 459 | -------------------------------------------------------------------------------- /src/cfg.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file The main CFG definition class. Generally, you start here. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 26-Nov-2017 6 | *********************************************************************************************************************/ 7 | 8 | "use strict"; 9 | 10 | import AST from './ast'; 11 | import create_new_cfg from './leader'; 12 | import { plugin, current } from './utils'; 13 | 14 | const 15 | defaultOptions = { 16 | plugins: [], 17 | ssaSource: false, 18 | parser: { 19 | loc: true, 20 | range: true, 21 | comment: true, 22 | tokens: true, 23 | ecmaVersion: 9, 24 | sourceType: 'module', 25 | ecmaFeatures: { 26 | impliedStrict: true, 27 | experimentalObjectRestSpread: true 28 | } 29 | } 30 | }; 31 | 32 | /** 33 | * @param {string} source 34 | * @param {object} [options] 35 | */ 36 | export default class CFG 37 | { 38 | /** 39 | * @param {string} source 40 | * @param {object} [options] 41 | */ 42 | constructor( source, options = defaultOptions ) 43 | { 44 | const 45 | ecma = Object.assign( {}, defaultOptions.parser.ecmaFeatures, options.parser && options.parser.ecmaFeatures || {} ), 46 | p = Object.assign( {}, defaultOptions.parser, options.parser || {} ); 47 | 48 | this.options = Object.assign( {}, defaultOptions, options ); 49 | this.options.parser = p; 50 | this.options.parser.ecmaFeatures = ecma; 51 | 52 | current.cfg = this; 53 | plugin( 'cfg', 'init', this ); 54 | this.ast = new AST( source, this.options.parser ); 55 | this.cfgs = []; 56 | plugin( 'cfg', 'postinit', this ); 57 | this.preGen = false; 58 | this.preGenName = new Set(); 59 | } 60 | 61 | /** 62 | * @return {string} 63 | */ 64 | toString() 65 | { 66 | return this.cfgs.map( b => `${b}` ).join( '\n\n' ); 67 | } 68 | 69 | /** 70 | * @return {string} 71 | */ 72 | toTable() 73 | { 74 | return this.cfgs.map( b => b.toTable() ).join( '\n\n' ); 75 | } 76 | 77 | /** 78 | * @param {string} [name] 79 | * @return {Array|CFG} 80 | */ 81 | generate( name ) 82 | { 83 | current.cfg = this; 84 | 85 | if ( !name ) 86 | { 87 | for ( const func of this.ast.forFunctions() ) 88 | this.cfgs.push( create_new_cfg( func, this.ast, this.options ) ); 89 | 90 | if ( !this.preGen ) 91 | { 92 | this.preGen = true; 93 | plugin( 'cfg', 'finish', this ); 94 | } 95 | 96 | return this; 97 | } 98 | else 99 | { 100 | const func = [ ...this.ast.forFunctions() ].find( cfgInfo => cfgInfo.name === name ); 101 | 102 | if ( !func ) 103 | return null; 104 | 105 | const cfgInfo = create_new_cfg( func, this.ast, this.options ); 106 | 107 | if ( !this.preGenName.has( name ) ) 108 | { 109 | this.preGenName.add( name ); 110 | plugin( 'cfg', 'finish', this ); 111 | } 112 | 113 | return cfgInfo; 114 | } 115 | 116 | } 117 | 118 | /** 119 | * @param {string} name 120 | * @return {CFGInfo} 121 | */ 122 | by_name( name ) 123 | { 124 | return this.cfgs.find( cfg => cfg.name === name ); 125 | } 126 | 127 | /** 128 | * @type {Iterable} 129 | */ 130 | *[ Symbol.iterator ]() 131 | { 132 | yield *this.cfgs; 133 | } 134 | 135 | /** 136 | * @param {function(CFGInfo, number)} fn 137 | */ 138 | forEach( fn ) 139 | { 140 | this.cfgs.forEach( fn ); 141 | } 142 | 143 | /** 144 | * @param {CFGInfo} cfg 145 | * @param {string} [title] 146 | * @return {string} 147 | */ 148 | create_dot( cfg, title = cfg.name + ':' + cfg.lines.join( '-' ) ) 149 | { 150 | return cfg.bm.create_dot( title ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/defs.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file A place to keep all the typedefs. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 31-Dec-2017 6 | *********************************************************************************************************************/ 7 | 8 | /** 9 | * @typedef {object} CFGInfo 10 | * @property {string} name 11 | * @property {Array} params 12 | * @property {AnnotatedNode|Array} body 13 | * @property {Array} lines 14 | * @property {BlockManager} [bm] 15 | * @property {CFGBlock} [trailing] 16 | * @property {AnnotatedNode|Node|BaseNode} node, 17 | * @property {AST} ast 18 | * @property {Scope} topScope 19 | * @property {function():string} toString 20 | * @property {function():string} toTable 21 | */ 22 | 23 | /** 24 | * @typedef {object} VisitorHelper 25 | * @property {BlockManager} BlockManager 26 | * @property {BlockManager} bm 27 | * @property {AST} ast 28 | * @property {?CFGBlock} prev 29 | * @property {?CFGBlock} block 30 | * @property {function():CFGBlock} newBlock 31 | * @property {Array} toExit 32 | * @property {function(CFGBlock,AnnotatedNode|Array,VisitorHelper):CFGBlock} [flatWalk] 33 | * @property {function(CFGBlock,AnnotatedNode,VisitorHelper):*} [scanWalk] 34 | * @property {Array} breakTargets 35 | * @property {function(CFGBlock):*} addBreakTarget 36 | * @property {function(CFGBlock, CFGBlock):*} addLoopTarget 37 | * @property {function():*} popTarget 38 | * @property {function():?CFGBlock} getBreakTarget 39 | * @property {function():?CFGBlock} getLoopTarget 40 | */ 41 | 42 | /** 43 | * It's damn near impossible to make WebStorm understand a class hierarchy. 44 | * 45 | * @typedef {Statement|Function|Expression|Pattern|Declaration|Node|BaseNode|Esprima.Node} AnnotatedNode 46 | * @extends BaseNode 47 | * @extends Node 48 | * @extends VariableDeclarator 49 | * @extends Statement 50 | * @extends Declaration 51 | * @extends Pattern 52 | * @extends Expression 53 | * @extends Function 54 | * @extends BlockStatement 55 | * @extends espree.Node 56 | * @property {number} [index] 57 | * @property {AnnotatedNode} [parent] 58 | * @property {?CFGBlock} [cfg] 59 | * @property {function():string} [toString] 60 | * @property {Scope} scope 61 | * @property {number} level 62 | * @property {?string} field 63 | * @property {number} fieldIndex 64 | */ 65 | 66 | /** 67 | * @typedef {object} CFGOptions 68 | * @property {boolean} ssaSource 69 | * @property {object} parser 70 | */ 71 | 72 | /** 73 | * @typedef {object} DotOptions 74 | * @property {string} title 75 | * @property {Array} nodeLabels 76 | * @property {Array} edgeLabels // was graph_label 77 | * @property {number} [start] 78 | * @property {number} [end] 79 | * @property {Array>} conditional - Actually an array of a tuple length 2: [ number, number ] 80 | * @property {Array>} unconditional - Actually an array of a tuple length 2: [ number, number ] 81 | * @property {object} [dotOptions={}] 82 | * @property {Array} blocks 83 | */ 84 | 85 | /** 86 | * @typedef {object} FunctionInfo 87 | * @property {string} name 88 | * @property {Array|AnnotatedNode} body 89 | * @property {Array} [params] 90 | * @property {AnnotatedNode} node 91 | * @property {Array} lines - A tuple with length 2 92 | */ 93 | 94 | /** 95 | * @typedef {object} EdgeInfo 96 | * @property {function(number):EdgeInfo} as 97 | * @property {function(number):EdgeInfo} not 98 | * @property {function(number):boolean} isa 99 | * @property {number} index 100 | * @private 101 | */ 102 | 103 | /** 104 | * @typedef {object} CaseInfo 105 | * @property {?CFGBlock} [test] 106 | * @property {?CFGBlock} [body] 107 | * @property {?AnnotatedNode} switchTest 108 | * @property {AnnotatedNode} consequent 109 | * @private 110 | */ 111 | 112 | /** 113 | * @typedef {object} Connection 114 | * @property {number} from 115 | * @property {number} to 116 | * @property {EdgeInfo} type 117 | */ 118 | 119 | -------------------------------------------------------------------------------- /src/dot.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Create a DOT file for graphviz. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 16-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | const 10 | defaultDotOptions = { 11 | defaults: { 12 | default: '#0D3B66', 13 | bgcolor: 'white', 14 | color: '#0D3B66', 15 | fontcolor: '#0D3B66', 16 | fontname: 'arial', 17 | shape: 'ellipse', 18 | nodesep: 1.5, 19 | margin: [ 0.5, 0.2 ] 20 | }, 21 | node: { 22 | style: 'rounded', 23 | color: '#0D3B66', 24 | fontcolor: '#0D3B66' 25 | }, 26 | test: { 27 | style: 'rounded', 28 | color: '#F95738', 29 | fontcolor: '#F95738', 30 | shape: 'diamond' 31 | }, 32 | entry: { 33 | style: 'rounded', 34 | shape: 'box', 35 | color: '#C6AC4D' 36 | }, 37 | exit: { 38 | style: 'rounded', 39 | shape: 'box', 40 | color: '#C6AC4D' 41 | }, 42 | unconditional: { 43 | color: '#0D3B65' 44 | }, 45 | conditional: { 46 | color: '#F95738', 47 | fontname: 'arial italic', 48 | style: 'dashed', 49 | fontcolor: '#F95738' 50 | } 51 | }; 52 | 53 | /** 54 | * @param {DotOptions} opts 55 | * @return {string} 56 | * @private 57 | */ 58 | export default function dot( opts ) 59 | { 60 | const 61 | { 62 | title, 63 | labels, 64 | start = 0, 65 | end = labels.length - 1, 66 | dotOptions = {}, 67 | conditional: condEdges, 68 | unconditional: uncondEdges, 69 | nodeLabels 70 | } = opts, 71 | /** 72 | * @param {Edge} edge 73 | * @return {string} 74 | * @private 75 | */ 76 | formatEdge = ( { from, to, type } ) => { 77 | const 78 | label = type.toString(), // type === 'normal' ? '' : type, 79 | escapedLabel = label && label.replace( /"/g, '\\"' ), 80 | attributes = label ? ` [label = "${escapedLabel}"]` : ""; 81 | 82 | return `${from} -> ${to}${attributes}`; 83 | }, 84 | 85 | neat = a => Array.isArray( a ) ? `"${a.join( ', ' )}"` : `"${a}"`, 86 | toStr = ( o, eol = '' ) => { 87 | if ( !o ) return []; 88 | const strs = Object.entries( o ).map( ( [ name, value ] ) => `${name} = ${neat( value )}${eol}` ); 89 | 90 | if ( !eol ) return strs.join( ', ' ); 91 | 92 | return strs; 93 | }, 94 | 95 | diffs = o => { 96 | if ( !o ) return null; 97 | 98 | const d = { 99 | color: defaultDotOptions.defaults.color, 100 | fontcolor: defaultDotOptions.defaults.fontcolor, 101 | fontname: defaultDotOptions.defaults.fontname 102 | }; 103 | 104 | Object.entries( o ).forEach( ( [ key, value ] ) => defaultDotOptions.defaults[ key ] !== value && ( d[ key ] = value ) ); 105 | 106 | return Object.keys( d ).length ? d : null; 107 | }, 108 | merge = key => Object.assign( {}, defaultDotOptions[ key ], dotOptions[ key ] || {} ), 109 | 110 | defaults = toStr( defaultDotOptions.defaults, ';' ), 111 | node = toStr( diffs( merge( 'node' ) ) ), 112 | test = toStr( diffs( merge( 'test' ) ) ), 113 | entry = toStr( diffs( merge( 'entry' ) ) ), 114 | exit = toStr( diffs( merge( 'exit' ) ) ), 115 | unconditional = toStr( diffs( merge( 'unconditional' ) ) ), 116 | conditional = toStr( diffs( merge( 'conditional' ) ) ), 117 | 118 | innerLines = [ ...defaults ].concat( `labelloc="t";`, `label="${title}";`, `fontsize=30` ); 119 | 120 | if ( node ) innerLines.push( `node [${node}];` ); 121 | 122 | innerLines.push( `${start} [label = "entry:${start}"${entry ? ', ' + entry : ''}];` ); 123 | innerLines.push( `${end} [label = "exit:${end}"${exit ? ', ' + exit : ''}];` ); 124 | innerLines.push( ...opts.blocks 125 | .filter( b => !!b ) 126 | .map( b => b.id !== start && b.id !== end && !!nodeLabels[ b.id ] && `${b.id} [label = "${nodeLabels[ b.id ]}"${condEdges.includes( b.id ) && test ? ', ' + test : ''}];` || null ) 127 | .filter( s => !!s ) ); 128 | 129 | if ( condEdges.length ) 130 | { 131 | innerLines.push( "", "// Unconditional edges" ); 132 | if ( unconditional ) innerLines.push( `edge [${unconditional}];` ); 133 | innerLines.push( ...uncondEdges.map( formatEdge ) ); 134 | } 135 | 136 | if ( uncondEdges.length ) 137 | { 138 | innerLines.push( "", "// Conditional edges" ); 139 | if ( conditional ) innerLines.push( `edge [${conditional}];` ); 140 | innerLines.push( ...condEdges.map( formatEdge ) ); 141 | } 142 | 143 | let graphLines = [ `digraph "${title}" {`, ...innerLines.map( l => ' ' + l ), "}" ]; 144 | 145 | if ( title ) graphLines.unshift( `// ${title}` ); 146 | 147 | return graphLines.join( '\n' ); 148 | } 149 | -------------------------------------------------------------------------------- /src/dump.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Functions to dump out information as pretty CLI tables. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 03-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | const 10 | Table = require( 'cli-table3' ), 11 | chalk = require( 'chalk' ), 12 | string = s => typeof s === 'string', 13 | { isArray: array } = Array, 14 | headline = s => chalk.yellowBright( s ), 15 | header = s => chalk.cyanBright( s ), 16 | row = r => r.map( s => chalk.whiteBright( s ) ); 17 | 18 | export const log = ( ...args ) => console.log( ...args ); 19 | 20 | /** 21 | * @param {string|string[]|string[][]} hdr 22 | * @param {string[]|string[][]} [headers] 23 | * @param {string[][]} [rows] 24 | */ 25 | function _as_table( hdr, headers, rows ) 26 | { 27 | let output, 28 | isRows = a => array( a ) && array( a[ 0 ] ), 29 | isHeader = a => array( a ) && string( a[ 0 ] ), 30 | index = a => isRows( a ) ? 2 : isHeader( a ) ? 1 : 0, 31 | args = []; 32 | 33 | if ( hdr ) args[ index( hdr ) ] = hdr; 34 | if ( headers ) args[ index( headers ) ] = headers; 35 | if ( rows ) args[ index( rows ) ] = rows; 36 | 37 | [ hdr, headers, rows ] = args; 38 | 39 | if ( hdr ) hdr = headline( hdr ); 40 | if ( headers ) headers = headers.map( header ); 41 | if ( rows ) rows = rows.map( row ); 42 | 43 | if ( hdr && headers && rows ) 44 | { 45 | output = new Table( { 46 | head: [ { colSpan: rows[ 0 ].length, hAlign: 'center', content: hdr } ] 47 | } ); 48 | output.push( headers ); 49 | } 50 | else if ( !hdr && headers ) 51 | { 52 | output = new Table( { 53 | head: headers 54 | } ); 55 | } 56 | 57 | if ( rows ) 58 | output.push( ...rows.map( row ) ); 59 | 60 | return output; 61 | } 62 | 63 | /** 64 | * @param {string|string[]|string[][]} hdr 65 | * @param {string[]|string[][]} [headers] 66 | * @param {string[][]} [rows] 67 | * @return {string} 68 | */ 69 | export function str_table( hdr, headers, rows ) 70 | { 71 | return _as_table( hdr, headers, rows ).toString(); 72 | } 73 | -------------------------------------------------------------------------------- /src/edges.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Handles all edges. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 10-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import { Edge, enum_to_string } from './types'; 10 | import { reverse_graph } from 'dominators'; 11 | 12 | const 13 | code = ( from, to ) => `${from}->${to}`, 14 | index = block => typeof block === 'object' && !Array.isArray( block ) && block !== null ? block.id : block; 15 | 16 | 17 | /** 18 | * @param {number} index 19 | * @param {number} _type 20 | * @return {EdgeInfo} 21 | * @private 22 | */ 23 | function blockEdge( index, _type = Edge.NONE ) 24 | { 25 | let type = _type, 26 | self = { 27 | as: t => type |= t, 28 | not: t => type &= ~t, 29 | isa: t => type & t, 30 | type: () => type, 31 | get index() { return index; }, 32 | set index( i ) { 33 | index = i; 34 | return self; 35 | }, 36 | toString() 37 | { 38 | return enum_to_string( Edge, type ).join( '|' ); 39 | } 40 | }; 41 | 42 | return self; 43 | } 44 | 45 | /** 46 | * Keeps track of edges apart from the successors listed in the {@link CFGBlock} so we can 47 | * attach types and classifications on edges separate from their blocks. 48 | * 49 | * @param {BlockManager} bm 50 | */ 51 | export default class Edges 52 | { 53 | /** 54 | * @param {BlockManager} bm 55 | */ 56 | constructor( bm ) 57 | { 58 | this.blockManager = bm; 59 | this.succs = []; 60 | this.edgeInfo = new Map(); 61 | this.lastEdge = null; 62 | this._preds = null; 63 | } 64 | 65 | /** 66 | * @param {number} from 67 | * @param {number} to 68 | * @param {number} i 69 | * @private 70 | */ 71 | _reindex_one( from, to, i ) 72 | { 73 | const be = this.edgeInfo.get( code( from, to ) ); 74 | 75 | if ( be ) be.index = i; 76 | } 77 | 78 | /** 79 | * @param {CFGBlock|number} from 80 | * @return {Edges} 81 | */ 82 | reindex( from ) 83 | { 84 | if ( from !== void 0 ) 85 | { 86 | from = index( from ); 87 | const succs = this.succs[ from ] || []; 88 | 89 | succs.forEach( ( id, i ) => this._reindex_one( from, id, i ) ); 90 | 91 | return this; 92 | } 93 | 94 | this.succs.forEach( ( ss, from ) => ss.forEach( ( id, i ) => this._reindex_one( from, id, i ) ) ); 95 | return this; 96 | } 97 | 98 | /** 99 | * Add an edge between to CFGBlocks. 100 | * 101 | * @param {CFGBlock|number} from 102 | * @param {CFGBlock|number} to 103 | * @param {Edge} type 104 | * @return {Edges} 105 | */ 106 | add( from, to, type = Edge.NONE ) 107 | { 108 | from = index( from ); 109 | to = index( to ); 110 | 111 | this.__add( from, to ); 112 | 113 | const 114 | succs = this.succs[ from ], 115 | be = blockEdge( succs.length - 1, type ); 116 | 117 | this.edgeInfo.set( code( from, to ), be ); 118 | this.lastEdge = { from, to, edgeInfo: be }; 119 | 120 | return this; 121 | } 122 | 123 | /** 124 | * Set a type on an arbitrary edge. 125 | * 126 | * @param {CFGBlock|number} from 127 | * @param {CFGBlock|number} to 128 | * @param {Edge} ctype 129 | * @return {Edges} 130 | */ 131 | classify( from, to, ctype ) 132 | { 133 | from = index( from ); 134 | to = index( to ); 135 | 136 | const be = this.edgeInfo.get( code( from, to ) ); 137 | if ( be ) be.as( ctype ); 138 | 139 | return this; 140 | } 141 | 142 | /** 143 | * Remove a type from an arbitrary edge. 144 | * 145 | * @param {CFGBlock|number} from 146 | * @param {CFGBlock|number} to 147 | * @param {Edge} type 148 | * @return {Edges} 149 | */ 150 | not( from, to, type ) 151 | { 152 | from = index( from ); 153 | to = index( to ); 154 | 155 | const be = this.edgeInfo.get( code( from, to ) ); 156 | if ( be ) be.not( type ); 157 | return this; 158 | } 159 | 160 | /** 161 | * @param {number} from 162 | * @param {number} to 163 | * @private 164 | */ 165 | __add( from, to ) 166 | { 167 | if ( !this.succs[ from ] ) 168 | this.succs[ from ] = [ to ]; 169 | else if ( !this.succs[ from ].includes( to ) ) 170 | this.succs[ from ].push( to ); 171 | 172 | if ( !this.succs[ to ] ) this.succs[ to ] = []; 173 | } 174 | 175 | /** 176 | * @param {CFGBlock|number} from 177 | * @param {CFGBlock|number} to 178 | * @param {Array} newTargets 179 | * @return {Edges} 180 | * @private 181 | */ 182 | _retarget( from, to, ...newTargets ) 183 | { 184 | from = index( from ); 185 | to = index( to ); 186 | 187 | newTargets = newTargets.map( index ); 188 | 189 | const 190 | _code = code( from, to ), 191 | be = this.edgeInfo.get( _code ), 192 | edgeType = be.type(); 193 | 194 | newTargets.forEach( target => this.add( from, index( target ), edgeType ) ); 195 | 196 | if ( be ) 197 | this.edgeInfo.delete( _code ); 198 | 199 | const 200 | succs = this.succs[ from ] || [], 201 | i = succs.indexOf( to ); 202 | 203 | if ( !this.succs[ from ] ) this.succs[ from ] = succs; 204 | 205 | if ( i !== -1 ) 206 | succs.splice( i, 1 ); 207 | 208 | this.reindex(); 209 | return this; 210 | } 211 | 212 | /** 213 | * Point one or more edges to a new {@link CFGBlock}, used in block removal. 214 | * 215 | * @param {CFGBlock|number} node 216 | * @return {Edges} 217 | */ 218 | retarget_multiple( node ) 219 | { 220 | node = index( node ); 221 | 222 | const 223 | preds = this.preds[ node ], 224 | succs = this.succs[ node ] || []; 225 | 226 | preds.forEach( p => this._retarget( p, node, ...succs ) ); 227 | 228 | this.succs[ node ] = []; 229 | 230 | return this; 231 | } 232 | 233 | /** 234 | * Remove a successor {@link CFGBlock} from a {@link CFGBlock} 235 | * 236 | * @param {CFGBlock|number} from 237 | * @param {CFGBlock|number} to 238 | * @return {Edges} 239 | */ 240 | remove_succ( from, to ) 241 | { 242 | from = index( from ); 243 | to = index( to ); 244 | 245 | this.edgeInfo.delete( code( from, to ) ); 246 | 247 | const succs = this.succs[ from ]; 248 | 249 | if ( !succs ) return this; 250 | 251 | const i = succs.indexOf( to ); 252 | 253 | if ( i === -1 ) return this; 254 | 255 | succs.splice( i, 1 ); 256 | this.reindex( from ); 257 | return this; 258 | } 259 | 260 | /** 261 | * Get all successors for a given {@link CFGBlock}. 262 | * 263 | * @param {CFGBlock|number} from 264 | * @return {Array} 265 | */ 266 | get_succs( from ) 267 | { 268 | from = index( from ); 269 | 270 | const succs = this.succs[ from ] || []; 271 | 272 | return succs.map( id => this.blockManager.get( id ) ); 273 | } 274 | 275 | /** 276 | * Get all predecessors for a given {@link CFGBlock} 277 | * 278 | * @param {CFGBlock|number} from 279 | * @return {Array} 280 | */ 281 | get_preds( from ) 282 | { 283 | from = index( from ); 284 | 285 | const preds = this.preds[ from ] || []; 286 | 287 | return preds.map( id => this.blockManager.get( id ) ); 288 | } 289 | 290 | /** 291 | * Renumber all indices (`id` field) because of removed {@link CFGBlock}s. 292 | * 293 | * @param {Array} newOffsets 294 | */ 295 | renumber( newOffsets ) 296 | { 297 | if ( newOffsets[ newOffsets.length - 1 ] === 0 ) return; 298 | 299 | let from = 0; 300 | 301 | const _succs = []; 302 | 303 | for ( const succs of this.successors() ) 304 | { 305 | let newFrom = from + newOffsets[ from ]; 306 | 307 | for ( const to of succs ) 308 | { 309 | if ( from !== newFrom || to !== to + newOffsets[ to ] ) 310 | this._relabel( from, to, newFrom, to + newOffsets[ to ] ); 311 | } 312 | 313 | _succs[ newFrom ] = succs.map( s => s + newOffsets[ s ] ); 314 | from++; 315 | } 316 | 317 | this.succs = _succs; 318 | } 319 | 320 | /** 321 | * Keep edge information up-to-date. 322 | * 323 | * @param {CFGBlock|number} from 324 | * @param {CFGBlock|number} to 325 | * @param {CFGBlock|number} from1 326 | * @param {CFGBlock|number} to1 327 | * @return {?Edges} 328 | * @private 329 | */ 330 | _relabel( from, to, from1, to1 ) 331 | { 332 | const 333 | _code = code( from, to ), 334 | newCode = code( from1, to1 ), 335 | be = this.edgeInfo.get( _code ); 336 | 337 | if ( !be ) return; 338 | 339 | this.edgeInfo.delete( _code ); 340 | this.edgeInfo.set( newCode, be ); 341 | return this; 342 | } 343 | 344 | /** 345 | * @type {Iterable} 346 | */ 347 | *successors() 348 | { 349 | yield *this.succs; 350 | } 351 | 352 | /** 353 | * Is there an edge of the gievn type? 354 | * 355 | * @param {CFGBlock|number} from 356 | * @param {Edge} type 357 | * @return {boolean} 358 | */ 359 | has( from, type ) 360 | { 361 | from = index( from ); 362 | 363 | return this.succs.some( s => { 364 | const be = this.edgeInfo.get( code( from, s ) ); 365 | return be && be.isa( type ); 366 | } ); 367 | } 368 | 369 | /** 370 | * Get edge information for a given {@link CFGBlock}, i.e. successors. 371 | * 372 | * @param {CFGBlock|number} from 373 | * @return {Array} 374 | */ 375 | edges( from ) 376 | { 377 | from = index( from ); 378 | 379 | return this.succs[ from ].map( to => ( { from, to, type: this.edgeInfo.get( code( from, to ) ) } ) ); 380 | } 381 | 382 | /** 383 | * Get all predecessors for a given {@link CFGBlock} 384 | * 385 | * @return {Array} 386 | */ 387 | get preds() 388 | { 389 | // if ( !this._preds ) 390 | this._preds = reverse_graph( this.succs ); 391 | 392 | return this._preds; 393 | } 394 | 395 | /** 396 | * Get all predecessor edge information for a given {@link CFGBlock}. 397 | * 398 | * @param {CFGBlock|number} _from 399 | * @return {Array} 400 | */ 401 | pred_edges( _from ) 402 | { 403 | const 404 | self = index( _from ), 405 | preds = this.preds; 406 | 407 | return preds[ self ].map( p => ( { from: p, to: self, type: this.edgeInfo.get( code( p, self ) ) } ) ); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/leader.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what cfg does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 19-Nov-2017 6 | *********************************************************************************************************************/ 7 | 8 | "use strict"; 9 | 10 | import { str_table } from './dump'; 11 | import { Block, Edge } from './types'; 12 | import BlockManager from './manager'; 13 | import * as visitors from './visitors'; 14 | import { plugin, current } from './utils'; 15 | 16 | const 17 | { isArray: array } = Array; 18 | 19 | /** 20 | * @param {CFGInfo} CFGInfo 21 | * @param {AST} ast 22 | * @param {CFGOptions} options 23 | * @private 24 | */ 25 | export default function create_new_cfg( cfgInfo, ast, options ) { 26 | 27 | ast.root = cfgInfo.node; 28 | cfgInfo.ast = ast; 29 | cfgInfo.topScope = ast.node_to_scope( ast.root ); 30 | 31 | /** 32 | * @type {CFGInfo} 33 | * @private 34 | */ 35 | let cfg = cfgInfo; 36 | 37 | cfg.bm = new BlockManager( ast, options ); 38 | 39 | const 40 | visitorHelper = { 41 | BlockManager, 42 | bm: cfg.bm, 43 | ast, 44 | prev: null, 45 | block: cfg.bm.startNode, 46 | toExit: [], 47 | newBlock: () => cfg.bm.block(), 48 | flatWalk: ( b, n, vh ) => flat_walker( b, n, vh ), 49 | breakTargets: [], 50 | addBreakTarget( block ) 51 | { 52 | this.breakTargets.push( { 53 | type: Edge.BREAK, 54 | breakBlock: block 55 | } ); 56 | }, 57 | addLoopTarget( lblock, bblock ) 58 | { 59 | cfg.bm.in_loop( lblock.id ); 60 | this.breakTargets.push( { 61 | type: Edge.LOOP, 62 | breakBlock: bblock, 63 | loopBlock: lblock 64 | } ); 65 | }, 66 | popTarget() 67 | { 68 | const popped = this.breakTargets.pop(); 69 | if ( popped.type === Edge.LOOP ) cfg.bm.out_loop( popped.loopBlock.id ); 70 | }, 71 | getBreakTarget() 72 | { 73 | return this.breakTargets.length ? this.breakTargets[ this.breakTargets.length - 1 ].breakBlock : null; 74 | }, 75 | getLoopTarget() 76 | { 77 | let i = this.breakTargets.length; 78 | 79 | if ( i === 0 ) return null; 80 | 81 | while ( i-- ) 82 | if ( this.breakTargets[ i ].type === Edge.LOOP ) return this.breakTargets[ i ].loopBlock; 83 | 84 | return null; 85 | } 86 | }; 87 | 88 | Object.entries( visitorHelper ).forEach( ( [ name, fn ] ) => { 89 | if ( typeof fn !== 'function' ) return; 90 | if ( !name.includes( 'Target' ) ) return; 91 | visitorHelper[ name ] = fn.bind( visitorHelper ); 92 | } ); 93 | 94 | let final = flat_walker( visitorHelper.block, ast.root, visitorHelper ); 95 | 96 | visitorHelper.toExit.forEach( xn => cfg.bm.toExitNode( xn ) ); 97 | 98 | if ( array( final ) ) 99 | final = final.filter( b => !!b ); 100 | else if ( final ) 101 | final = [ final ]; 102 | 103 | cfg.toString = () => `${cfg.name}:${cfg.lines[ 0 ]}-${cfg.lines[ 1 ]}\n${cfg.bm}`; 104 | cfg.toTable = () => str_table( `${cfg.name}:${cfg.lines[ 0 ]}-${cfg.lines[ 1 ]}`, [ "TYPE", "LINES", "LEFT EDGES", "NODE", "RIGHT EDGES", "CREATED BY", "AST" ], cfg.bm.toTable() ); 105 | cfg.bm.finish( final ); 106 | return cfg; 107 | } 108 | 109 | /** 110 | * @param {CFGBlock} block 111 | * @param {AnnotatedNode|Node|BaseNode|BlockStatement|Array} nodes 112 | * @param {VisitorHelper} visitorHelper 113 | * @return {CFGBlock} 114 | * @private 115 | */ 116 | function flat_walker( block, nodes, visitorHelper ) 117 | { 118 | visitorHelper.block = block; 119 | 120 | if ( !nodes ) return visitorHelper.block = block; 121 | 122 | /** 123 | * @param {AnnotatedNode} node 124 | * @return {boolean} 125 | * @private 126 | */ 127 | function add_cfg( node ) 128 | { 129 | let cbBlock; 130 | 131 | if ( visitors[ node.type ] ) 132 | { 133 | cbBlock = visitorHelper.block; 134 | let outputs = visitors[ node.type ]( node, visitorHelper ); 135 | 136 | current.block = cbBlock; 137 | plugin( 'cfgblock', 'finish', cbBlock ); 138 | 139 | if ( !outputs ) 140 | { 141 | visitorHelper.block = null; 142 | return false; 143 | } 144 | else if ( !array( outputs ) ) 145 | { 146 | if ( !outputs.createdBy ) outputs.createdBy = 'CFG: ' + node.type; 147 | 148 | if ( outputs.isa( Block.CONVERGE ) ) 149 | visitorHelper.block = outputs.not( Block.CONVERGE ); 150 | else 151 | visitorHelper.block = visitorHelper.newBlock().from( outputs ); 152 | } 153 | else 154 | { 155 | outputs = outputs.filter( b => !!b ); 156 | if ( !outputs.length ) 157 | { 158 | visitorHelper.block = null; 159 | return false; 160 | } 161 | 162 | outputs.forEach( b => { 163 | if ( !b.createdBy ) b.createdBy = 'AST: ' + ( b.first() ? b.first().type : 'none' ); 164 | if ( b.isa( Block.CONVERGE ) ) 165 | visitorHelper.block = b.not( Block.CONVERGE ); 166 | } ); 167 | visitorHelper.block = visitorHelper.newBlock().from( outputs ); 168 | } 169 | 170 | block = visitorHelper.block; 171 | } 172 | else if ( visitorHelper.block ) 173 | visitorHelper.block.add( node ); 174 | 175 | current.block = cbBlock; 176 | plugin( 'cfgblock', 'postFinish', cbBlock ); 177 | } 178 | 179 | visitorHelper.ast.flat_walker( nodes, add_cfg ); 180 | 181 | return visitorHelper.block; 182 | } 183 | -------------------------------------------------------------------------------- /src/manager.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file The class that manages the individual blocks. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 02-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import assert from 'assert'; 10 | import { error, warn, plugin, current } from './utils'; 11 | import { postOrder } from 'traversals'; 12 | import dot from './dot'; 13 | import CFGBlock from './block'; 14 | import { Block, Edge } from './types'; 15 | import Edges from './edges'; 16 | 17 | /** 18 | * @param {AST} ast 19 | * @param {CFGOptions} options 20 | */ 21 | export default class BlockManager 22 | { 23 | /** 24 | * @param {AST} ast 25 | * @param {CFGOptions} options 26 | */ 27 | constructor( ast, options ) 28 | { 29 | 30 | BlockManager.blockId = 0; 31 | this.edges = new Edges( this ); 32 | /** @type {CFGBlock[]} */ 33 | this.blocks = []; 34 | this.loops = []; 35 | this.startNode = this.block().as( Block.START ); 36 | this.toExit = []; 37 | this.ast = ast; 38 | this.options = options; 39 | current.blockManager = this; 40 | plugin( 'blockmanager', 'init', this ); 41 | } 42 | 43 | /** 44 | * @param {CFGBlock} block 45 | * @private 46 | */ 47 | toExitNode( block ) 48 | { 49 | this.toExit.push( block ); 50 | } 51 | 52 | /** 53 | * @param {Array} final 54 | * @private 55 | */ 56 | finish( final ) 57 | { 58 | if ( final ) 59 | final.forEach( f => this.toExitNode( f ) ); 60 | 61 | this.exitNode = this.block().as( Block.EXIT ); 62 | this.toExit.forEach( b => { 63 | b.to( this.exitNode ); 64 | if ( b.deferredEdgeType ) 65 | b.classify( this.exitNode, b.deferredEdgeType ); 66 | } ); 67 | 68 | current.blockManager = this; 69 | plugin( 'blockmanager', 'finish', this ); 70 | this.clean(); 71 | 72 | BlockManager.blockId = this.size = this.blocks.length; 73 | 74 | current.blockManager = this; 75 | plugin( 'blockmanager', 'postfinish', this ); 76 | } 77 | 78 | /** 79 | * @param {function(CFGBlock,number,Array):*} fn 80 | */ 81 | forEach( fn ) 82 | { 83 | this.blocks.forEach( ( b, i, bl ) => b && fn( b, i, bl ) ); 84 | } 85 | 86 | /** 87 | * @param {function(CFGBlock,number,Array):*} fn 88 | */ 89 | map( fn ) 90 | { 91 | return this.blocks.map( fn ); 92 | } 93 | 94 | /** 95 | * @param {number} index 96 | * @return {CFGBlock} 97 | */ 98 | get( index ) 99 | { 100 | return this.blocks[ index ]; 101 | } 102 | 103 | /** 104 | * Allocates a new block. 105 | * 106 | * @returns {CFGBlock} 107 | * @private 108 | */ 109 | block() 110 | { 111 | assert( this.edges ); 112 | const block = new CFGBlock( BlockManager.blockId++, this.edges ); 113 | 114 | this.blocks[ block.id ] = block; 115 | if ( this.loops.length ) 116 | block.as( Block.LOOP ); 117 | 118 | return block; 119 | } 120 | 121 | /** 122 | * @return {string} 123 | */ 124 | toString() 125 | { 126 | return this.blocks.map( b => `${b}` ).join( '\n' ); 127 | } 128 | 129 | /** 130 | * @return {Array} 131 | */ 132 | toTable() 133 | { 134 | return this.blocks.map( b => b.toRow() ); 135 | } 136 | 137 | /** 138 | * @type {Iterable} 139 | */ 140 | *[ Symbol.iterator ]() 141 | { 142 | for ( const block of this.blocks ) 143 | { 144 | if ( !block ) continue; 145 | yield block; 146 | } 147 | } 148 | 149 | /** 150 | * @param {string} title 151 | * @return {string} 152 | */ 153 | create_dot( title ) 154 | { 155 | const 156 | cond = [], 157 | uncond = []; 158 | 159 | this.blocks.forEach( b => { 160 | for ( const edge of this.edges.edges( b ) ) 161 | { 162 | if ( edge.type.isa( Edge.TRUE | Edge.FALSE | Edge.EXCEPTION ) ) 163 | cond.push( edge ); 164 | else 165 | uncond.push( edge ); 166 | } 167 | } ); 168 | 169 | return dot( { 170 | title, 171 | nodeLabels: [ ...this ].map( b => b.graph_label() ), 172 | start: this.startNode.id, 173 | end: this.exitNode.id, 174 | conditional: cond, 175 | unconditional: uncond, 176 | blocks: this.blocks 177 | } ); 178 | } 179 | 180 | /** 181 | * @param blocks 182 | * @return {Array} 183 | * @private 184 | */ 185 | pack( blocks ) 186 | { 187 | const 188 | offsets = [], 189 | packed = []; 190 | 191 | let cur = 0; 192 | 193 | for ( let i = 0; i < BlockManager.blockId; i++ ) 194 | { 195 | if ( blocks[ i ] && !blocks[ i ].isa( Block.DELETED ) ) 196 | { 197 | blocks[ i ].oldId = blocks[ i ].id; 198 | blocks[ i ].id = packed.length; 199 | packed.push( blocks[ i ] ); 200 | cur = blocks[ i ].id - blocks[ i ].oldId; 201 | } 202 | 203 | offsets.push( cur ); 204 | } 205 | 206 | this.edges.renumber( offsets ); 207 | return packed; 208 | } 209 | 210 | /** 211 | * @private 212 | */ 213 | clean() 214 | { 215 | let changed = true, 216 | blocks = this.blocks; 217 | 218 | /** 219 | * @param {number} blockIndex 220 | * @private 221 | */ 222 | function pass( blockIndex ) 223 | { 224 | const block = blocks[ blockIndex ]; 225 | 226 | if ( !block || block.isa( Block.DELETED ) || block.isa( Block.START ) || block.isa( Block.EXIT ) ) return; 227 | 228 | if ( block.isa( Block.TEST ) ) 229 | { 230 | if ( block.succs.length === 2 && block.succs[ 0 ] === block.succs[ 1 ] ) 231 | { 232 | const succ = block.succs[ 0 ]; 233 | block.remove_succs(); 234 | block.as( Block.NORMAL ).to( succ ); 235 | changed = true; 236 | } 237 | } 238 | 239 | if ( block.succs.length === 1 ) 240 | { 241 | const 242 | succ = block.succs[ 0 ]; 243 | 244 | if ( !succ || succ.isa( Block.START ) || succ.isa( Block.EXIT ) ) return; 245 | 246 | if ( block.isEmpty() && block.eliminate() ) changed = true; 247 | 248 | if ( !block.isa( Block.DELETED ) && !succ.isa( Block.DELETED ) && succ.preds.length === 1 ) 249 | { 250 | if ( !succ.isEmpty() && succ.scope === block.scope ) 251 | { 252 | const 253 | on = succ.nodes.slice(); 254 | 255 | succ.nodes.length = 0; 256 | if ( succ.eliminate() ) 257 | { 258 | block.nodes = block.nodes.concat( on ); 259 | changed = true; 260 | block.types |= ( succ.types & ~Block.DELETED ); 261 | } 262 | else 263 | succ.nodes = on; 264 | } 265 | 266 | if ( succ.isEmpty() && succ.isa( Block.TEST ) ) 267 | { 268 | block.as( Block.TEST ); 269 | block.remove_succ( succ ); 270 | succ.eliminate(); 271 | changed = true; 272 | } 273 | } 274 | } 275 | } 276 | 277 | blocks.forEach( b => !b.succs.length && !b.preds.length && !b.isa( Block.START ) && !b.isa( Block.EXIT ) && b.as( Block.DELETED ) ); 278 | blocks = this.pack( blocks ); 279 | 280 | while ( changed ) 281 | { 282 | changed = false; 283 | 284 | postOrder( blocks.map( b => b.succs.map( s => s.id ) ), pass ); 285 | 286 | if ( changed ) 287 | blocks = this.pack( blocks ); 288 | } 289 | 290 | this.blocks = blocks; 291 | } 292 | 293 | /** 294 | * @param {number} id 295 | * @private 296 | */ 297 | in_loop( id ) 298 | { 299 | this.loops.push( id ); 300 | } 301 | 302 | /** 303 | * @param {number} id 304 | * @private 305 | */ 306 | out_loop( id ) 307 | { 308 | let skipped = []; 309 | 310 | if ( this.loops[ this.loops.length - 1 ] === id ) 311 | this.loops.pop(); 312 | else 313 | { 314 | while ( this.loops.length ) 315 | { 316 | const top = this.loops.pop(); 317 | if ( top === id ) break; 318 | skipped.push( top ); 319 | } 320 | 321 | if ( !this.loops.length ) 322 | console.error( error( `Skipped all loops with id ${id}` ) ); 323 | else 324 | console.log( warn( `Skipping loop nesting [ ${skipped.join( ', ' )} ] with ${id}` ) ); 325 | } 326 | } 327 | } 328 | 329 | BlockManager.blockId = 0; 330 | -------------------------------------------------------------------------------- /src/plugins.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Manage all the plugins. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 18-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import { parse } from 'espree'; 10 | import { format } from 'util'; 11 | 12 | let debugTrack = false; 13 | 14 | const 15 | func = fn => typeof fn === 'function', 16 | obj = o => typeof o === 'object' && !Array.isArray( o ) && o !== null, 17 | { isArray: array } = Array, 18 | isTop = topKey => [ 'parse', 'cfg', 'ast', 'cfgblock', 'manager', 'general' ].includes( topKey.toLowerCase() ), 19 | forKeys = ( o, cb ) => obj( o ) && Object.keys( o ).forEach( k => cb( o[ k ], k ) ), 20 | dout = ( ...args ) => debugTrack && process.stdout.write( format( ...args ) ), 21 | dlog = ( ...args ) => debugTrack && console.log( ...args ); 22 | 23 | /** */ 24 | export default class PluginManager 25 | { 26 | /** 27 | * @param {Array} pluginList 28 | */ 29 | constructor( pluginList ) 30 | { 31 | this.pluginList = pluginList; 32 | this.callMap = { 33 | cfg: { 34 | fn: [], 35 | init: [], 36 | postInit: [], 37 | finish: [] 38 | }, 39 | ast: { 40 | fn: [], 41 | init: [], 42 | postInit: [], 43 | finish: [] 44 | }, 45 | cfgblock: { 46 | fn: [], 47 | init: [], 48 | finish: [], 49 | postFinish: [] 50 | }, 51 | blockmanager: { 52 | fn: [], 53 | init: [], 54 | postInit: [], 55 | finish: [], 56 | postFinish: [] 57 | }, 58 | output: { 59 | tableheaders: [], 60 | tablerows: [], 61 | asstring: [], 62 | json: [] 63 | }, 64 | general: { 65 | postload: [], 66 | preexit: [] 67 | }, 68 | parse: ( tk, sk, source, options ) => parse( source, options ) 69 | }; 70 | this.allFunctions = []; 71 | } 72 | 73 | /** 74 | * @param {string} moduleName 75 | * @static 76 | */ 77 | static load_module( moduleName ) 78 | { 79 | try 80 | { 81 | dout( `Attempting to load "${moduleName}"...` ); 82 | const m = require( moduleName ); 83 | dlog( 'ok' ); 84 | return m; 85 | } 86 | catch ( err ) 87 | { 88 | dlog( 'fail:', err ); 89 | return null; 90 | } 91 | } 92 | 93 | /** */ 94 | load_plugins() 95 | { 96 | const 97 | add_function = fn => this.allFunctions.includes( fn ) || this.allFunctions.push( fn ); 98 | 99 | this.plugins = this.pluginList.map( pluginModule => PluginManager.load_module( pluginModule ) ).filter( m => !!m ); 100 | 101 | this.plugins.forEach( plugin => { 102 | const 103 | cbs = func( plugin ) ? plugin() : plugin, 104 | all = {}, 105 | group = key => all[ key ] || ( all[ key ] = {} ), 106 | _add = grp => key => grp[ key ] || ( grp[ key ] = [] ); 107 | 108 | if ( typeof cbs !== 'object' || Array.isArray( cbs ) || cbs === null ) return; 109 | 110 | forKeys( cbs, ( value, topKey ) => { 111 | 112 | if ( !isTop( topKey ) ) return; 113 | 114 | let add = _add( group( topKey ) ), 115 | add_fn = ( fn, subKey ) => { 116 | add( subKey || 'fn' ).push( fn ); 117 | add_function( fn ); 118 | }; 119 | 120 | if ( func( value ) ) 121 | { 122 | value = value(); 123 | 124 | if ( func( value ) ) 125 | return add_fn( value ); 126 | } 127 | 128 | if ( obj( value ) ) 129 | forKeys( value, add_fn ); 130 | } ); 131 | } ); 132 | } 133 | 134 | /** 135 | * @param {string} topKey 136 | * @param {?string} subKey 137 | * @param { ...*} args 138 | * @return {*} 139 | */ 140 | callback( topKey, subKey, ...args ) 141 | { 142 | dout( `Plugin callback for ${topKey}.${subKey || '*'} = ` ); 143 | const 144 | top = this.callMap[ topKey ], 145 | sub = subKey && top[ subKey ]; 146 | 147 | dout( `top func? ${func( top )}, top array? ${array( top.fn )} [${array( top.fn ) && top.fn.length}]` ); 148 | if ( func( top ) ) 149 | { 150 | dlog( ' done' ); 151 | return top( topKey, subKey, ...args ); 152 | } 153 | else if ( array( top ) && top.fn.length ) 154 | top.fn.length.forEach( cb => cb( topKey, subKey, ...args ) ); 155 | 156 | dout( `, sub func? ${func( top )}, top array? ${array( top.fn )} [${array( top.fn ) && top.fn.length}]` ); 157 | if ( func( sub ) ) 158 | { 159 | dlog( ' done' ); 160 | return sub( topKey, subKey, ...args ); 161 | } 162 | else if ( array( sub ) && sub.length ) 163 | sub.length.forEach( cb => cb( topKey, subKey, ...args ) ); 164 | 165 | dlog( ' done' ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Think types.h 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 03-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | /** 10 | * @param {object} names 11 | * @return {Block|Edge|SymbolFlags|ModifierFlags} 12 | * @private 13 | */ 14 | function make_enum_from_object( names ) // , start = 0, bitWise = false ) 15 | { 16 | let __enum = {}; 17 | 18 | Object.entries( names ).forEach( ( [ name, val ] ) => __enum[ __enum[ name ] = val ] = name ); 19 | 20 | return __enum; 21 | } 22 | 23 | 24 | /** 25 | * @enum {number} 26 | * @name Block 27 | */ 28 | export let Block = { 29 | NONE: 0, 30 | START: 1, 31 | EXIT: 2, 32 | NORMAL: 4, 33 | TEST: 8, 34 | LOOP: 16, 35 | CONVERGE: 32, 36 | TEMPORARY: 64, 37 | DELETED: 128, 38 | CATCH: 512, 39 | THROW: 1024, 40 | CLEAR: 3, 41 | EXCLUSIVE: 15 42 | }; 43 | /** 44 | * @type {Block} 45 | * @private 46 | */ 47 | Block = make_enum_from_object( Block ); 48 | /** 49 | * @enum {number} 50 | * @name Edge 51 | */ 52 | export let Edge = { 53 | NONE: 0, 54 | TREE: 1, 55 | FORWARD: 2, 56 | BACK: 4, 57 | CROSS: 8, 58 | JUMP: 256, 59 | EXCEPTION: 512, 60 | RETURN: 1024, 61 | BREAK: 2048, 62 | CONTINUE: 4096, 63 | TRUE: 8192, 64 | FALSE: 16384, 65 | LOOP: 32768, 66 | CLEAR: 255 67 | 68 | }; 69 | /** 70 | * @type {Edge} 71 | * @private 72 | */ 73 | Edge = make_enum_from_object( Edge ); 74 | /** 75 | * The default display options for table and string output. 76 | */ 77 | export let defaultOutputOptions = { 78 | MAX_EDGES_TO_PRINT: 7, 79 | SPACE_PER_EDGE: 4, 80 | LEFT_EDGES: ' <-- ', // ' ←── ', 81 | RIGHT_EDGES: ' --> ', // ' ──→ ', 82 | AST_NODES: ' => ', 83 | TRUE_EDGE: '+', // '✔', 84 | FALSE_EDGE: '-', // '✖', 85 | START_NODE: '+', // '→', 86 | EXIT_NODE: '$' // '⛔', 87 | }; 88 | 89 | export const outputOptions = defaultOutputOptions; 90 | 91 | /** 92 | * Override display options. 93 | * 94 | * @param options 95 | */ 96 | export function output( options = {} ) 97 | { 98 | Object.assign( outputOptions, defaultOutputOptions, options ); 99 | } 100 | 101 | /** 102 | * Turns an `enum` into an array of strings. 103 | * 104 | * @param {enum} enumType 105 | * @param {number} val 106 | * @return {Array} 107 | * @private 108 | */ 109 | export function enum_to_string( enumType, val ) 110 | { 111 | let vals = []; 112 | 113 | for ( let i = 1; i < 1 << 30; i = i << 1 ) 114 | { 115 | if ( !( val & ~( i - 1 ) ) ) break; 116 | if ( val & i ) vals.push( enumType[ val & i ] ); 117 | } 118 | 119 | return vals; 120 | } 121 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | ` * @file Describe what utils does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 01-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import chalk from 'chalk'; 10 | import PluginManager from './plugins'; 11 | 12 | /** 13 | * @type {PluginManager} 14 | */ 15 | let pluginManager; 16 | 17 | export const 18 | { isArray: array } = Array, 19 | 20 | warn = s => chalk.hex( '#ffd700' )( s ), // xterm( 220 ), 21 | error = s => chalk.hex( '#ff0000' )( s ), // xterm( 196 ), 22 | info = s => chalk.hex( '#87d7ff' )( s ), // xterm( 117 ), 23 | log = console.log.bind( console ), 24 | 25 | colors = { 26 | dark: { 27 | green: s => chalk.hex( '#00af00' )( s ), // 34 28 | blue: s => chalk.hex( '#005fd7' )( s ), // 26 29 | cyan: s => chalk.hex( '#00d7ff' )( s ), // 45 30 | purple: s => chalk.hex( '#5f00ff' )( s ), // 57 31 | red: s => chalk.hex( '#d7005f' )( s ), // 161 32 | orange: s => chalk.hex( '#d75f5f' )( s ), // 167 33 | yellow: s => chalk.hex( '#d7d700' )( s ), // 184 34 | pink: s => chalk.hex( '#d75fd7' )( s ), // 170 35 | gray: s => chalk.hex( '#a8a8a8' )( s ) // 248 36 | }, 37 | light: { 38 | green: s => chalk.hex( '#87ff00' )( s ), // 118 39 | blue: s => chalk.hex( '#00afff' )( s ), // 39 40 | cyan: s => chalk.hex( '#87ffff' )( s ), // 123 41 | purple: s => chalk.hex( '#afafff' )( s ), // 147 42 | red: s => chalk.hex( '#ff0000' )( s ), // 196 43 | orange: s => chalk.hex( '#ff8700' )( s ), // 208 44 | yellow: s => chalk.hex( '#ffff00' )( s ), // 226 45 | pink: s => chalk.hex( '#ff87ff' )( s ), // 213 46 | gray: s => chalk.hex( '#d0d0d0' )( s ) // 252 47 | }, 48 | white: s => chalk.hex( '#ffffff' )( s ) 49 | }, 50 | 51 | dull = { 52 | LEFT_EDGES: ' <-- ', // ' ←── ', 53 | RIGHT_EDGES: ' --> ', // ' ──→ ', 54 | AST_NODES: ' => ', 55 | TRUE_EDGE: '+', // '✔', 56 | FALSE_EDGE: '-', // '✖', 57 | START_NODE: '+', // '→', 58 | EXIT_NODE: '$' // '⛔', 59 | }, 60 | nice = { 61 | LEFT_EDGES: ' ←── ', 62 | RIGHT_EDGES: ' ──→ ', 63 | AST_NODES: ' => ', 64 | TRUE_EDGE: colors.dark.green( '✔' ), 65 | FALSE_EDGE: colors.light.red( '✖' ), 66 | START_NODE: colors.dark.orange( '→' ), 67 | EXIT_NODE: '⛔' 68 | }, 69 | display_options = function( fancy ) { 70 | return Object.assign( { SPACE_PER_EDGE: 4, MAX_EDGES: 7 }, fancy ? nice : dull ); 71 | }, 72 | current = { 73 | cfg: null, 74 | blockManager: null, 75 | ast: null, 76 | block: null 77 | }, 78 | /** 79 | * @param {Array} list 80 | * @private 81 | */ 82 | load_plugins = function( list ) { 83 | pluginManager = new PluginManager( list || [] ); 84 | pluginManager.load_plugins(); 85 | }, 86 | /** 87 | * @param {string} topKey 88 | * @param {?string} subKey 89 | * @param {...*} args 90 | * @return {*} 91 | * @private 92 | */ 93 | plugin = function( topKey, subKey, ...args ) { 94 | if ( pluginManager ) return pluginManager.callback( topKey, subKey, ...args ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/visitors.js: -------------------------------------------------------------------------------- 1 | /** **************************************************************************************************** 2 | * @description 3 | * 4 | * ### The `for` Statement 5 | * 6 | * The `ForStatement` works like shown in the following. Note that the init, test, and update 7 | * may be absent, leaving only a body, in which case it is an infirnite loop, barring a `break` 8 | * or `continue`. 9 | * 10 | * PREV -> 11 | * ┌───────────────┐ 12 | * v │ 13 | * INIT -> TEST -> BODY -> UPDATE <───── `ContinueStatement` target 14 | * │ 15 | * └────> REST OF CODE PAST LOOP <───── `BreakStatement` target 16 | * 17 | * [INIT] -> [TEST] 18 | * 19 | * BODY -> [UPDATE] 20 | * 21 | * REST 22 | * 23 | * ### The `if` Statement 24 | * 25 | * │ 26 | * │ 27 | * V 28 | * ┌────────────┐ ┌─────────────┐ 29 | * │ test │ ──> │ (alternate) │ 30 | * └────────────┘ └─────────────┘ 31 | * │ │ 32 | * │ │ 33 | * V │ 34 | * ┌────────────┐ │ 35 | * │ consequent │ │ 36 | * └────────────┘ │ 37 | * │ │ 38 | * │ │ 39 | * V │ 40 | * ┌────────────┐ │ 41 | * │ block │<───────────┘ 42 | * └────────────┘ 43 | * 44 | * ### The `switch` Statement 45 | * 46 | * Originally, I treated the Switc/SwitchCase structure like shown in the 47 | * first diagram but ended up changing it to something more leaborate but 48 | * closer to how it would actually be treated by a compiler (sub-dividing 49 | * test cases and so on). 50 | * 51 | * ┌──────────────┐ 52 | * │ │ 53 | * │ switch │ 54 | * │ │ 55 | * └──┬─┬─┬─┬─┬─┬─┘ 56 | * │ │ │ │ │ │ 57 | * │ │ │ │ │ │ ┌─────────────┐ 58 | * │ │ │ │ │ │ │ │ 59 | * │ │ │ │ │ └────────>│ case1 │ 60 | * │ │ │ │ │ │ │ 61 | * │ │ │ │ │ └─────────────┘ 62 | * │ │ │ │ │ 63 | * │ │ │ │ │ ┌─────────────┐ 64 | * │ │ │ │ │ │ │ 65 | * │ │ │ │ └──────────>│ case2 │ 66 | * │ │ │ │ │ │ 67 | * │ │ │ │ └─────────────┘ 68 | * │ │ │ │ 69 | * │ │ │ │ ┌─────────────┐ 70 | * │ │ │ │ │ │ [ fall through succ. is next case ] 71 | * │ │ │ └────────────>│ case3 │ 72 | * │ │ │ │ │ 73 | * │ │ │ └──────┬──────┘ 74 | * │ │ │ │ 75 | * │ │ │ Falls through 76 | * │ │ │ │ 77 | * │ │ │ ┌──────┴──────┐ 78 | * │ │ │ │ │ [ previous falls through, preds are switch and previous case ] 79 | * │ │ └──────────────>│ case4 │ 80 | * │ │ │ │ 81 | * │ │ └─────────────┘ 82 | * │ │ 83 | * │ │ ┌─────────────┐ 84 | * │ │ │ │ 85 | * │ └────────────────>│ default │ 86 | * │ │ │ 87 | * Pred if no default └──────┬──────┘ 88 | * │ │ 89 | * v Pred if default 90 | * ┌─────────────┐ │ 91 | * │ │ │ 92 | * │ next │<───────────┘ 93 | * │ │ 94 | * └─────────────┘ 95 | * 96 | * Now, I deal with it as shown below. We go from test to test, based on the discriminant 97 | * in the `SwitchStatement` itself. A `false` result moves on to the next test while a 98 | * true result transfers control to the body of the `SwitchCase`. Some caveats are: 99 | * 100 | * 1. Without a `BreakStatement` at the end that post-dmoniates the block, it will fall 101 | * through to the next `SwitchCase` body. 102 | * 2. There is no requirement that a `SwitchCase` will have a body block, in which case, we 103 | * keep falling to the next one, until we reach a body or exit the `SwitchStatement` entirely. 104 | * 3. We go from false test to false test, however, there may be one `SwitchCase1 that doesn't 105 | * have a test, the default case. The default case need not be at the end and follow the 106 | * same rules as described above regarding falling through. The false edge from the final 107 | * test will go to the default body, if there is one, or the normal block following the 108 | * `SwitchStatement`, if there is one (otherwise it will eventually work its way up to the 109 | * exit node). 110 | * 111 | * TEST -> true -> body -> break -> out 112 | * -> no break -> next body 113 | * 114 | * -> false -> next test 115 | * 116 | * if last 117 | * 118 | * TEST -> true -> body -> break -> out 119 | * -> no break -> out 120 | * 121 | * -> false -> default OR out 122 | * 123 | * 124 | * 125 | * consequent 126 | * SWITCH │ 127 | * │ v 128 | * └──> TEST ───────────> BODY ─────────┐ break 129 | * │ │ │ 130 | * alt│ no│break │ 131 | * │ no│body │ 132 | * │ │ │ 133 | * V V │ 134 | * TEST ───────────> BODY ─────────┤ break 135 | * │ │ │ 136 | * alt│ no│break │ 137 | * │ no│body │ 138 | * │ │ │ 139 | * │ V │ 140 | * │ DEFAULT ───> BODY ─────────┤ break (or not) 141 | * │ ^ │ │ 142 | * │ │ no│break │ 143 | * │ if│false no│body │ 144 | * │ and│default │ │ 145 | * V │ V │ 146 | * TEST ───────────> BODY ─────────┤ break (or not) 147 | * if│false │ 148 | * and no│default │ 149 | * v │ 150 | * CONT <──────────────────────────────────┘ 151 | * 152 | * 153 | * @author julian on 12/22/17 154 | * @version 1.0.0 155 | *******************************************************************************************************/ 156 | 'use strict'; 157 | 158 | import assert from 'assert'; 159 | import { Block, Edge } from './types'; 160 | import list from 'yallist'; 161 | 162 | /** 163 | * @param {BlockStatement} node 164 | * @param {VisitorHelper} visitorHelper 165 | * @return {?CFGBlock} 166 | * @private 167 | */ 168 | export function BlockStatement( node, visitorHelper ) 169 | { 170 | return visitorHelper.flatWalk( visitorHelper.newBlock().from( visitorHelper.block ), node.body, visitorHelper ); 171 | } 172 | 173 | /** 174 | * @param {LabeledStatement|AnnotatedNode} node 175 | * @param {VisitorHelper} visitorHelper 176 | * @return {?CFGBlock} 177 | * @private 178 | */ 179 | export function LabeledStatement( node, visitorHelper ) 180 | { 181 | const 182 | statement = visitorHelper.newBlock().from( visitorHelper.block ); 183 | 184 | node.cfg = statement; 185 | 186 | return visitorHelper.flatWalk( statement, node.body, visitorHelper ); 187 | } 188 | 189 | /** 190 | * @param {AnnotatedNode} node 191 | * @param {VisitorHelper} vh 192 | * @param {Block} type 193 | * @param {function} targets 194 | * @return {null} 195 | * @private 196 | */ 197 | function to_label( node, vh, type, targets ) 198 | { 199 | if ( node.label ) 200 | { 201 | const 202 | self = vh.newBlock().from( vh.block ).add( node ).edge_as( type ).by( type ), 203 | block = vh.ast.find_label( node, node.label.name ); 204 | 205 | if ( !block ) 206 | throw new SyntaxError( `Labeled "${type}" statement has no label "${node.label.name}" in scope: ${node}` ); 207 | 208 | block.from( self ); 209 | 210 | return null; 211 | } 212 | else 213 | { 214 | const block = targets(); 215 | 216 | if ( !block ) 217 | throw new SyntaxError( `Statement '${type}' is not inside a breakable scope` ); 218 | 219 | vh.block.add( node ).to( block ).classify( block, type ); 220 | 221 | 222 | return null; 223 | } 224 | } 225 | 226 | /** 227 | * @param {BreakStatement|AnnotatedNode} node 228 | * @param {VisitorHelper} visitorHelper 229 | * @return {?CFGBlock[]} 230 | * @private 231 | */ 232 | export function BreakStatement( node, visitorHelper ) 233 | { 234 | return to_label( node, visitorHelper, Edge.BREAK, visitorHelper.getBreakTarget ); 235 | } 236 | 237 | /** 238 | * @param {CatchClause|AnnotatedNode} node 239 | * @param {VisitorHelper} visitorHelper 240 | * @return {?CFGBlock} 241 | * @private 242 | */ 243 | export function CatchClause( node, visitorHelper ) 244 | { 245 | return visitorHelper.flatWalk( visitorHelper.newBlock().from( visitorHelper.block ).as( Block.CATCH ).add( node ), node.body, visitorHelper ); 246 | } 247 | 248 | /** 249 | * @param {ContinueStatement|AnnotatedNode} node 250 | * @param {VisitorHelper} visitorHelper 251 | * @return {?CFGBlock} 252 | * @private 253 | */ 254 | export function ContinueStatement( node, visitorHelper ) 255 | { 256 | return to_label( node, visitorHelper, Edge.CONTINUE, visitorHelper.getLoopTarget ); 257 | } 258 | 259 | /** 260 | * @param {DoWhileStatement|AnnotatedNode} node 261 | * @param {VisitorHelper} visitorHelper 262 | * @return {?CFGBlock} 263 | * @private 264 | */ 265 | export function DoWhileStatement( node, visitorHelper ) 266 | { 267 | const 268 | { newBlock, block } = visitorHelper; 269 | 270 | let 271 | body = newBlock().from( block ).by( 'DoWhile.body' ).as( Block.LOOP ), 272 | cont = newBlock().as( Block.CONVERGE ).by( 'DoWhile.conv' ), 273 | test = newBlock().as( Block.TEST ).by( 'DoWhile.test' ).add( node.test ).whenTrue( body ).whenFalse( cont ); 274 | 275 | visitorHelper.addLoopTarget( test, cont ); 276 | body = visitorHelper.flatWalk( body, node, visitorHelper ); 277 | visitorHelper.popTarget(); 278 | 279 | test.from( body ); 280 | 281 | return cont; 282 | } 283 | 284 | /** 285 | * The `ForStatement` works like shown in the following. Note that the init, test, and update 286 | * may be absent, leaving only a body, in which case it is an infirnite loop, barring a `break` 287 | * or `continue`. 288 | * 289 | * PREV -> 290 | * ┌───────────────┐ 291 | * v │ 292 | * INIT -> TEST -> BODY -> UPDATE <───── `ContinueStatement` target 293 | * │ 294 | * └────> REST OF CODE PAST LOOP <───── `BreakStatement` target 295 | * 296 | * [INIT] -> [TEST] 297 | * 298 | * BODY -> [UPDATE] 299 | * 300 | * REST 301 | * 302 | * 303 | * 304 | * 305 | * @param {ForStatement|AnnotatedNode} node 306 | * @param {VisitorHelper} visitorHelper 307 | * @return {?CFGBlock} 308 | * @private 309 | */ 310 | export function ForStatement( node, visitorHelper ) 311 | { 312 | const 313 | { newBlock, block } = visitorHelper; 314 | 315 | let init = node.init && newBlock().by( 'For.init' ).add( node.init ), 316 | test = node.test && newBlock().as( Block.TEST ).by( 'For.test' ).add( node.test ), 317 | body = newBlock().by( 'For.body' ).as( Block.LOOP ), 318 | update = node.update && newBlock().by( 'For.update' ).add( node.update ), 319 | cont = newBlock().as( Block.CONVERGE ).by( 'For.conv' ); 320 | 321 | // *************** ENTRY 322 | // entry block wants: init -> test -> body (there is always a body) 323 | 324 | if ( init ) 325 | block.to( init ); 326 | else if ( test ) 327 | block.to( test ); 328 | else 329 | block.to( body ); 330 | 331 | // entry block resolved 332 | 333 | if ( init ) 334 | { 335 | if ( test ) 336 | init.to( test ); 337 | else 338 | init.to( body ); 339 | } 340 | 341 | // init block resolved 342 | 343 | if ( test ) 344 | { 345 | test.whenTrue( body ); 346 | test.whenFalse( cont ); 347 | } 348 | 349 | visitorHelper.addLoopTarget( update || test || body, cont ); 350 | let endBody = visitorHelper.flatWalk( body, node.body, visitorHelper ); 351 | visitorHelper.popTarget(); 352 | 353 | if ( endBody ) 354 | { 355 | if ( update ) 356 | endBody.to( update ); 357 | else if ( test ) 358 | endBody.to( test ); 359 | else 360 | endBody.to( body ); 361 | } 362 | 363 | if ( update ) 364 | { 365 | if ( test ) 366 | update.to( test ); 367 | else 368 | update.to( body ); 369 | } 370 | 371 | return cont; 372 | } 373 | 374 | /** 375 | * @param {ForInStatement|AnnotatedNode} node 376 | * @param {VisitorHelper} visitorHelper 377 | * @return {?CFGBlock} 378 | * @private 379 | */ 380 | export function ForInStatement( node, visitorHelper ) 381 | { 382 | const { newBlock, block } = visitorHelper; 383 | 384 | let update = newBlock().from( block ).add( node ).by( 'ForInOf.update' ), 385 | body = newBlock().from( update ).by( 'ForInOf.body' ).as( Block.LOOP ), 386 | cont = newBlock().from( update ).as( Block.CONVERGE ).by( 'ForInOf.conv' ); 387 | 388 | visitorHelper.addLoopTarget( update, cont ); 389 | body = visitorHelper.flatWalk( body, node.body, visitorHelper ); 390 | visitorHelper.popTarget(); 391 | 392 | if ( body ) body.to( update ); 393 | 394 | return cont; 395 | } 396 | 397 | /** 398 | * @param {ForInStatement|AnnotatedNode} node 399 | * @param {VisitorHelper} visitorHelper 400 | * @return {?CFGBlock} 401 | * @private 402 | */ 403 | export function ForOfStatement( node, visitorHelper ) 404 | { 405 | return ForInStatement( node, visitorHelper ); 406 | } 407 | 408 | /** 409 | * 410 | * │ 411 | * │ 412 | * V 413 | * ┌────────────┐ ┌─────────────┐ 414 | * │ test │ ──> │ (alternate) │ 415 | * └────────────┘ └─────────────┘ 416 | * │ │ 417 | * │ │ 418 | * V │ 419 | * ┌────────────┐ │ 420 | * │ consequent │ │ 421 | * └────────────┘ │ 422 | * │ │ 423 | * │ │ 424 | * V │ 425 | * ┌────────────┐ │ 426 | * │ block │<───────────┘ 427 | * └────────────┘ 428 | * 429 | * @param {IfStatement|AnnotatedNode} node 430 | * @param {VisitorHelper} visitorHelper 431 | * @return {?CFGBlock[]} 432 | * @private 433 | */ 434 | export function IfStatement( node, visitorHelper ) 435 | { 436 | let test = visitorHelper.newBlock() 437 | .as( Block.TEST ) 438 | .from( visitorHelper.block ) 439 | .by( 'If.test' ) 440 | .add( node.test ), 441 | 442 | consequent = visitorHelper.newBlock().by( 'If.cons' ), 443 | alternate; 444 | 445 | test.whenTrue( consequent ); 446 | consequent = visitorHelper.flatWalk( consequent, node.consequent, visitorHelper ); 447 | 448 | alternate = visitorHelper.newBlock().by( 'If.alt' ); 449 | test.whenFalse( alternate ); 450 | 451 | if ( node.alternate ) 452 | alternate = visitorHelper.flatWalk( alternate, node.alternate, visitorHelper ); 453 | 454 | if ( !node.alternate && consequent ) consequent.to( alternate ); 455 | 456 | return [ consequent, alternate ]; 457 | } 458 | 459 | /** 460 | * @param {ReturnStatement|AnnotatedNode} node 461 | * @param {VisitorHelper} visitorHelper 462 | * @return {?CFGBlock} 463 | * @private 464 | */ 465 | export function ReturnStatement( node, visitorHelper ) 466 | { 467 | visitorHelper.block.add( node ).defer_edge_type( Edge.RETURN ); 468 | visitorHelper.toExit.push( visitorHelper.block ); 469 | 470 | return null; 471 | } 472 | 473 | /** 474 | * Originally, I treated the Switc/SwitchCase structure like shown in the 475 | * first diagram but ended up changing it to something more leaborate but 476 | * closer to how it would actually be treated by a compiler (sub-dividing 477 | * test cases and so on). 478 | * 479 | * ┌──────────────┐ 480 | * │ │ 481 | * │ switch │ 482 | * │ │ 483 | * └──┬─┬─┬─┬─┬─┬─┘ 484 | * │ │ │ │ │ │ 485 | * │ │ │ │ │ │ ┌─────────────┐ 486 | * │ │ │ │ │ │ │ │ 487 | * │ │ │ │ │ └────────>│ case1 │ 488 | * │ │ │ │ │ │ │ 489 | * │ │ │ │ │ └─────────────┘ 490 | * │ │ │ │ │ 491 | * │ │ │ │ │ ┌─────────────┐ 492 | * │ │ │ │ │ │ │ 493 | * │ │ │ │ └──────────>│ case2 │ 494 | * │ │ │ │ │ │ 495 | * │ │ │ │ └─────────────┘ 496 | * │ │ │ │ 497 | * │ │ │ │ ┌─────────────┐ 498 | * │ │ │ │ │ │ [ fall through succ. is next case ] 499 | * │ │ │ └────────────>│ case3 │ 500 | * │ │ │ │ │ 501 | * │ │ │ └──────┬──────┘ 502 | * │ │ │ │ 503 | * │ │ │ Falls through 504 | * │ │ │ │ 505 | * │ │ │ ┌──────┴──────┐ 506 | * │ │ │ │ │ [ previous falls through, preds are switch and previous case ] 507 | * │ │ └──────────────>│ case4 │ 508 | * │ │ │ │ 509 | * │ │ └─────────────┘ 510 | * │ │ 511 | * │ │ ┌─────────────┐ 512 | * │ │ │ │ 513 | * │ └────────────────>│ default │ 514 | * │ │ │ 515 | * Pred if no default └──────┬──────┘ 516 | * │ │ 517 | * v Pred if default 518 | * ┌─────────────┐ │ 519 | * │ │ │ 520 | * │ next │<───────────┘ 521 | * │ │ 522 | * └─────────────┘ 523 | * 524 | * Now, I deal with it as shown below. We go from test to test, based on the discriminant 525 | * in the `SwitchStatement` itself. A `false` result moves on to the next test while a 526 | * true result transfers control to the body of the `SwitchCase`. Some caveats are: 527 | * 528 | * 1. Without a `BreakStatement` at the end that post-dmoniates the block, it will fall 529 | * through to the next `SwitchCase` body. 530 | * 2. There is no requirement that a `SwitchCase` will have a body block, in which case, we 531 | * keep falling to the next one, until we reach a body or exit the `SwitchStatement` entirely. 532 | * 3. We go from false test to false test, however, there may be one `SwitchCase1 that doesn't 533 | * have a test, the default case. The default case need not be at the end and follow the 534 | * same rules as described above regarding falling through. The false edge from the final 535 | * test will go to the default body, if there is one, or the normal block following the 536 | * `SwitchStatement`, if there is one (otherwise it will eventually work its way up to the 537 | * exit node). 538 | * 539 | * TEST -> true -> body -> break -> out 540 | * -> no break -> next body 541 | * 542 | * -> false -> next test 543 | * 544 | * if last 545 | * 546 | * TEST -> true -> body -> break -> out 547 | * -> no break -> out 548 | * 549 | * -> false -> default OR out 550 | * 551 | * 552 | * 553 | * consequent 554 | * SWITCH │ 555 | * │ v 556 | * └──> TEST ───────────> BODY ─────────┐ break 557 | * │ │ │ 558 | * alt│ no│break │ 559 | * │ no│body │ 560 | * │ │ │ 561 | * V V │ 562 | * TEST ───────────> BODY ─────────┤ break 563 | * │ │ │ 564 | * alt│ no│break │ 565 | * │ no│body │ 566 | * │ │ │ 567 | * │ V │ 568 | * │ DEFAULT ───> BODY ─────────┤ break (or not) 569 | * │ ^ │ │ 570 | * │ │ no│break │ 571 | * │ if│false no│body │ 572 | * │ and│default │ │ 573 | * V │ V │ 574 | * TEST ───────────> BODY ─────────┤ break (or not) 575 | * if│false │ 576 | * and no│default │ 577 | * v │ 578 | * CONT <──────────────────────────────────┘ 579 | * 580 | * @param {SwitchStatement|AnnotatedNode} node 581 | * @param {VisitorHelper} visitorHelper 582 | * @private 583 | */ 584 | export function SwitchStatement( node, visitorHelper ) 585 | { 586 | const { newBlock, block } = visitorHelper; 587 | 588 | let _switch = newBlock().from( block ).add( node.discriminant ), 589 | cont = newBlock().as( Block.CONVERGE ), 590 | 591 | /** 592 | * @type {CaseInfo} 593 | * @private 594 | */ 595 | _default, 596 | 597 | /** 598 | * @type {List} 599 | * @private 600 | */ 601 | caseList = list.create( node.cases.map( n => { 602 | 603 | let test = n.test && newBlock().as( Block.TEST ).add( n.test ), 604 | body = n.consequent && n.consequent.length && newBlock(), 605 | /** 606 | * @type {CaseInfo} 607 | * @private 608 | */ 609 | caseInfo = { 610 | body, 611 | test, 612 | switchTest: n.test, 613 | consequent: n.consequent 614 | }; 615 | 616 | if ( !test ) 617 | _default = caseInfo; 618 | 619 | return caseInfo; 620 | } ) ); 621 | 622 | visitorHelper.addBreakTarget( cont ); 623 | 624 | let prevCi = _switch, 625 | needBody = [], 626 | lastTest = null, 627 | fallsThrough = null; 628 | 629 | caseList.forEach( ci => { 630 | if ( ci.test ) 631 | { 632 | lastTest = ci.test; 633 | if ( prevCi !== _switch ) 634 | prevCi.test.whenFalse( ci.test ); 635 | else 636 | _switch.to( ci.test ); 637 | prevCi = ci; 638 | } 639 | 640 | if ( ci.body ) 641 | { 642 | if ( fallsThrough ) 643 | { 644 | fallsThrough.to( ci.body ); 645 | fallsThrough = null; 646 | } 647 | 648 | for ( const pci of needBody ) 649 | { 650 | if ( pci.test ) 651 | pci.test.whenTrue( ci.body ); 652 | else if ( !pci.body ) 653 | pci.body = ci.body; 654 | } 655 | 656 | needBody.length = 0; 657 | 658 | if ( ci.test ) ci.test.whenTrue( ci.body ); 659 | 660 | fallsThrough = visitorHelper.flatWalk( ci.body, ci.consequent, visitorHelper ); 661 | } 662 | else 663 | needBody.push( ci ); 664 | } ); 665 | 666 | if ( fallsThrough ) fallsThrough.to( cont ); 667 | 668 | if ( _default ) 669 | { 670 | if ( !_default.body ) _default.body = cont; 671 | 672 | if ( lastTest ) 673 | lastTest.whenFalse( _default.body ); 674 | } 675 | 676 | 677 | if ( !caseList.size ) 678 | _switch.to( cont ); 679 | 680 | visitorHelper.popTarget(); 681 | 682 | return cont; 683 | } 684 | 685 | /** 686 | * @param {SwitchCase|AnnotatedNode} node 687 | * @return {?CFGBlock} 688 | * @private 689 | */ 690 | export function SwitchCase( node ) 691 | { 692 | throw new Error( `We hit a switch case which shouldn't happen, node: ${node}` ); 693 | } 694 | 695 | /** 696 | * @param {ThrowStatement|AnnotatedNode} node 697 | * @param {VisitorHelper} visitorHelper 698 | * @return {?CFGBlock} 699 | * @private 700 | */ 701 | export function ThrowStatement( node, visitorHelper ) 702 | { 703 | visitorHelper.block.add( node ).as( Block.THROW ).edge_as( Edge.EXCEPTION ); 704 | return null; 705 | } 706 | 707 | /** 708 | * TRY -> CATCH -> FINALIZER -> OUT 709 | * TRY -> FINALIZER -> OUT 710 | * -> CATCH -> OUT 711 | * -> OUT 712 | * 713 | * IF FINALIZER 714 | * FINALIZER => OUT 715 | * ELSE 716 | * CATCH, TRY => OUT 717 | * 718 | * @param {TryStatement|AnnotatedNode} node 719 | * @param {VisitorHelper} visitorHelper 720 | * @return {?CFGBlock|CFGBlock[]} 721 | * @private 722 | */ 723 | export function TryStatement( node, visitorHelper ) 724 | { 725 | let { newBlock, block } = visitorHelper, 726 | finalizer, 727 | tryBlock = newBlock().add( node ).from( block ); 728 | 729 | let tb = block; 730 | visitorHelper.block = tryBlock; 731 | let catchBlock = CatchClause( node.handler, visitorHelper ); 732 | visitorHelper.block = tb; 733 | 734 | if ( node.finalizer ) 735 | { 736 | finalizer = newBlock().from( [ tryBlock, catchBlock ] ); 737 | finalizer = visitorHelper.flatWalk( finalizer, node.finalizer, visitorHelper ); 738 | } 739 | 740 | return finalizer || [ tryBlock, catchBlock ]; 741 | } 742 | 743 | /** 744 | * @param {WhileStatement|AnnotatedNode} node 745 | * @param {VisitorHelper} visitorHelper 746 | * @return {?CFGBlock} 747 | * @private 748 | */ 749 | export function WhileStatement( node, visitorHelper ) 750 | { 751 | const { newBlock, block } = visitorHelper; 752 | 753 | let 754 | body = newBlock().as( Block.LOOP ), 755 | alt = newBlock().as( Block.CONVERGE ), 756 | test = newBlock().as( Block.TEST ).add( node.test ).whenFalse( alt ).whenTrue( body ).from( block ); 757 | 758 | visitorHelper.addLoopTarget( test, alt ); 759 | body = visitorHelper.flatWalk( body, node.body, visitorHelper ); 760 | visitorHelper.popTarget(); 761 | 762 | assert( body, "Where yo body at?" ); 763 | if ( body ) 764 | body.to( test ); 765 | 766 | return alt; 767 | } 768 | 769 | /** 770 | * @param {ConditionalExpression|AnnotatedNode} node 771 | * @param {VisitorHelper} visitorHelper 772 | * @return {?CFGBlock[]} 773 | * @private 774 | */ 775 | export function ConditionalExpression( node, visitorHelper ) 776 | { 777 | const { newBlock, block } = visitorHelper; 778 | 779 | let cons = newBlock(), 780 | alternate, 781 | test = newBlock() 782 | .from( block ) 783 | .add( node.test ) 784 | .as( Block.TEST ) 785 | .whenTrue( cons ); 786 | 787 | cons = visitorHelper.flatWalk( cons, node.consequent, visitorHelper ); 788 | 789 | if ( node.alternate ) 790 | { 791 | alternate = newBlock(); 792 | test.whenFalse( alternate ); 793 | alternate = visitorHelper.flatWalk( alternate, node.alternate, visitorHelper ); 794 | } 795 | 796 | const output = []; 797 | 798 | if ( !cons && !alternate ) return null; 799 | 800 | if ( cons ) output.push( cons ); 801 | if ( alternate ) output.push( alternate ); 802 | 803 | return output; 804 | } 805 | 806 | /** 807 | * @param {BaseNode|AnnotatedNode} node 808 | * @param {VisitorHelper} visitorHelper 809 | * @return {?CFGBlock} 810 | * @private 811 | */ 812 | export function syntax_default( node, visitorHelper ) 813 | { 814 | return visitorHelper.block.add( node ); 815 | } 816 | -------------------------------------------------------------------------------- /test-data/cfg-test-01.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Tries to confuse the CFG generator. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 27-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | 10 | function a02() 11 | { 12 | for ( let a = 0; ; ++a ) 13 | {} 14 | } 15 | 16 | function a03( a ) 17 | { 18 | for ( ; a; ) a >>= 1; 19 | } 20 | 21 | function a01() 22 | { 23 | for ( ; ; ) ; 24 | } 25 | 26 | function b01() 27 | { 28 | while ( b01() ) 29 | { 30 | if ( b01 ) 31 | { continue; } 32 | if ( !b01 ) 33 | { 34 | 35 | } 36 | } 37 | } 38 | 39 | function b02() 40 | { 41 | while ( b01() ) 42 | { 43 | if ( b01 ) 44 | { continue; } 45 | if ( !b01 ) 46 | { 47 | break; 48 | } 49 | } 50 | } 51 | 52 | function b03() 53 | { 54 | let a = 1; 55 | goto: 56 | while ( b01() ) 57 | { 58 | if ( b01 ) 59 | { 60 | continue goto; 61 | } 62 | 63 | if ( !b01 ) 64 | { 65 | a <<= 1; 66 | break; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test-data/cfg-test-02.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what cfg-test-01 does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 27-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | class A 10 | { 11 | constructor( a, b, c ) 12 | { 13 | let { x, y: { z } } = a, 14 | [ m, n, ...r ] = b, 15 | u, v; 16 | 17 | 18 | u = someBase.eater; 19 | 20 | 21 | if ( x > z ) 22 | u = x + z; 23 | else 24 | { 25 | let dead = u, 26 | c = 10; 27 | dead += m - n; 28 | u = dead + n; 29 | v = u + c; 30 | } 31 | 32 | z = z + u; 33 | u = v + c; 34 | 35 | return z + u; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-data/cfg-test-03.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what cfg-test-03 does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 30-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | function pp() {} 10 | const pp$1 = pp.prototype; 11 | const types = {}; 12 | 13 | pp$1.parseClass = function( node, isStatement ) { 14 | var this$1 = this; 15 | 16 | this.next(); 17 | 18 | this.parseClassId( node, isStatement ); 19 | this.parseClassSuper( node ); 20 | var classBody = this.startNode(); 21 | var hadConstructor = false; 22 | classBody.body = []; 23 | this.expect( types.braceL ); 24 | while ( !this.eat( types.braceR ) ) 25 | { 26 | if ( this$1.eat( types.semi ) ) 27 | { continue } 28 | var method = this$1.startNode(); 29 | var isGenerator = this$1.eat( types.star ); 30 | var isAsync = false; 31 | var isMaybeStatic = this$1.types === types.name && this$1.value === "static"; 32 | this$1.parsePropertyName( method ); 33 | method.static = isMaybeStatic && this$1.types !== types.parenL; 34 | if ( method.static ) 35 | { 36 | if ( isGenerator ) 37 | { this$1.unexpected(); } 38 | isGenerator = this$1.eat( types.star ); 39 | this$1.parsePropertyName( method ); 40 | } 41 | if ( this$1.options.ecmaVersion >= 8 && !isGenerator && !method.computed && 42 | method.key.types === "Identifier" && method.key.name === "async" && this$1.types !== types.parenL && 43 | !this$1.canInsertSemicolon() ) 44 | { 45 | isAsync = true; 46 | this$1.parsePropertyName( method ); 47 | } 48 | method.kind = "method"; 49 | var isGetSet = false; 50 | if ( !method.computed ) 51 | { 52 | var key = method.key; 53 | if ( !isGenerator && !isAsync && key.types === "Identifier" && this$1.types !== types.parenL && ( key.name === "get" || key.name === "set" ) ) 54 | { 55 | isGetSet = true; 56 | method.kind = key.name; 57 | key = this$1.parsePropertyName( method ); 58 | } 59 | if ( !method.static && ( key.types === "Identifier" && key.name === "constructor" || key.types === "Literal" && key.value === "constructor" ) ) 60 | { 61 | if ( hadConstructor ) 62 | { this$1.raise( key.start, "Duplicate constructor in the same class" ); } 63 | if ( isGetSet ) 64 | { this$1.raise( key.start, "Constructor can't have get/set modifier" ); } 65 | if ( isGenerator ) 66 | { this$1.raise( key.start, "Constructor can't be a generator" ); } 67 | if ( isAsync ) 68 | { this$1.raise( key.start, "Constructor can't be an async method" ); } 69 | method.kind = "constructor"; 70 | hadConstructor = true; 71 | } 72 | } 73 | this$1.parseClassMethod( classBody, method, isGenerator, isAsync ); 74 | if ( isGetSet ) 75 | { 76 | var paramCount = method.kind === "get" ? 0 : 1; 77 | if ( method.value.params.length !== paramCount ) 78 | { 79 | var start = method.value.start; 80 | if ( method.kind === "get" ) 81 | { this$1.raiseRecoverable( start, "getter should have no params" ); } 82 | else 83 | { this$1.raiseRecoverable( start, "setter should have exactly one param" ); } 84 | } 85 | else 86 | { 87 | if ( method.kind === "set" && method.value.params[ 0 ].types === "RestElement" ) 88 | { this$1.raiseRecoverable( method.value.params[ 0 ].start, "Setter cannot use rest params" ); } 89 | } 90 | } 91 | } 92 | node.body = this.finishNode( classBody, "ClassBody" ); 93 | return this.finishNode( node, isStatement ? "ClassDeclaration" : "ClassExpression" ) 94 | }; 95 | -------------------------------------------------------------------------------- /test-data/cfg-test-04.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what cfg-test-03 does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 30-Dec-2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | function pp() {} 10 | const pp$1 = pp.prototype; 11 | const types = {}; 12 | 13 | pp$1.parseClass = function( node, isStatement ) { 14 | var this$1 = this; 15 | 16 | this.next(); 17 | 18 | while ( !this.eat( types.braceR ) ) 19 | { 20 | var method = this$1.startNode(); 21 | method.static = isMaybeStatic && this$1.types !== types.parenL; 22 | method.kind = "method"; 23 | let testVar = 123; 24 | if ( !method.computed ) 25 | { 26 | testVar = 234; 27 | var key = method.key; 28 | if ( !isGenerator && !isAsync && key.types === "Identifier" ) 29 | method.kind = key.name; 30 | if ( !method.static ) 31 | method.kind = "constructor"; 32 | } 33 | if ( isGetSet ) 34 | { 35 | var paramCount = method.kind === "get" ? 0 : testVar; 36 | if ( method.value.params.length !== paramCount ) 37 | { 38 | var start = method.value.start; 39 | if ( method.kind === "get" ) 40 | { this$1.raiseRecoverable( start, "getter should have no params" ); } 41 | } 42 | else 43 | { 44 | if ( method.kind === "set" && method.value.params[ 0 ].types === "RestElement" ) 45 | { this$1.raiseRecoverable( method.value.params[ 0 ].start, "Setter cannot use rest params" ); } 46 | } 47 | } 48 | } 49 | node.body = this.finishNode( classBody, "ClassBody" ); 50 | }; 51 | -------------------------------------------------------------------------------- /test-data/cfg-test-05.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what cfg-test-05.js does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 11-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | const a = 10; 10 | let b; 11 | 12 | function blah() 13 | { 14 | b++; 15 | 16 | for ( const x of a ) 17 | { 18 | b++; 19 | if ( b & 1 ) continue; 20 | } 21 | 22 | if ( a < 5 ) 23 | b = 1; 24 | else if ( a < 9 ) 25 | b = 2; 26 | 27 | b += a; 28 | } 29 | -------------------------------------------------------------------------------- /test-data/cfg-test-06.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what cfg-test-06 does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 17-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | function code_coverage( a, b, c ) 10 | { 11 | const cond = a < 10 ? b : c; 12 | 13 | try 14 | { 15 | do { 16 | a += b; 17 | } while ( a < 100 ); 18 | 19 | for( let a = 0; a > 10; --a ) 20 | a -= c; 21 | } 22 | catch ( e ) 23 | { 24 | console.error( e ); 25 | process.exit( 1 ); 26 | } 27 | finally 28 | { 29 | a = b**c + ( a < 10 ? b : c ); 30 | } 31 | 32 | switch ( a ) 33 | { 34 | case 1: b += c; 35 | break; 36 | 37 | case 9: 38 | case 10: 39 | case 2: c += b; 40 | 41 | case 3: c += c; 42 | break; 43 | 44 | default: ++a; 45 | 46 | case 4: a = b + c; 47 | break; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/ast-test.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Describe what ast-test does. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date 17-Jan-2018 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import { expect } from 'chai'; 10 | import fs from 'fs'; 11 | import AST from '../src/ast'; 12 | import { traverse, VisitorOption, Syntax } from 'estraverse'; 13 | const 14 | src = fs.readFileSync( './test-data/cfg-test-01.js', 'utf8' ), 15 | src2 = fs.readFileSync( './test-data/cfg-test-02.js', 'utf8' ); 16 | 17 | let ast, ast2, root, root2; 18 | 19 | const 20 | findType = ( t, r ) => { 21 | let found; 22 | 23 | traverse( r || root, { 24 | enter( node ) { 25 | if ( node.type === t ) 26 | { 27 | found = node; 28 | return VisitorOption.Break; 29 | } 30 | } 31 | } ); 32 | 33 | return found; 34 | }; 35 | 36 | describe( 'AST', function() { 37 | 38 | it( 'should compile a source module', () => { 39 | ast = new AST( src, { 40 | loc: true, 41 | range: true, 42 | comment: true, 43 | tokens: true, 44 | ecmaVersion: 9, 45 | sourceType: 'module', 46 | ecmaFeatures: { 47 | impliedStrict: true, 48 | experimentalObjectRestSpread: true 49 | } 50 | } ); 51 | 52 | root = ast.ast; 53 | 54 | ast2 = new AST( src2, { 55 | loc: true, 56 | range: true, 57 | comment: true, 58 | tokens: true, 59 | ecmaVersion: 9, 60 | sourceType: 'module', 61 | ecmaFeatures: { 62 | impliedStrict: true, 63 | experimentalObjectRestSpread: true 64 | } 65 | } ); 66 | 67 | root2 = ast2.ast; 68 | 69 | expect( ast ).to.be.instanceof( AST ); 70 | expect( ast.ast ).to.be.an( 'object' ); 71 | expect( ast.ast ).to.have.property( 'type', Syntax.Program ); 72 | } ); 73 | 74 | it( 'should display a node as a string', () => { 75 | const str = `${root}`; 76 | 77 | expect( str ).to.be.a( 'string' ); 78 | expect( str ).to.eql( 'Program, lvl: 0, line 7: [body(7)]' ); 79 | } ); 80 | 81 | it( 'should get function information', () => { 82 | 83 | const 84 | fnames = ast.functions.map( f => ( { name: f.name, node: f.node } ) ), 85 | a02 = fnames.find( n => n.name === 'a02' ), 86 | a03 = fnames.find( n => n.name === 'a03' ); 87 | 88 | expect( fnames ).to.be.an( 'array' ); 89 | expect( fnames ).to.have.length( 7 ); 90 | 91 | expect( ast.get_from_function( a02.node, 'name' ) ).to.eql( 'a02' ); 92 | expect( ast.get_from_function( a03.node, 'params' ) ).to.be.an( 'array' ); 93 | expect( ast.get_from_function( a03.node, 'body' ) ).to.be.an( 'array' ); 94 | expect( ast.get_from_function( a03.node, 'lines' ) ).to.eql( [ 16, 19 ] ); 95 | } ); 96 | 97 | it( 'should do various utility functions', () => { 98 | const 99 | fe = findType( Syntax.AssignmentExpression, root2 ), 100 | className = findType( Syntax.ClassDeclaration, root2 ), 101 | lr = {}; 102 | // console.log( Object.keys( node ).map( k => `${k} => ${typeof node[ k ] === 'string' ? `"${node[ k ]}"` : ( node[ k ] + '' )}` + ( k === 'loc' ? JSON.stringify( node[ k ] ) : '' ) ) ); 103 | 104 | ast.call_visitors( fe, node => { 105 | expect( node ).to.have.property( 'parent', fe ); 106 | if ( fe.left === node ) 107 | { 108 | lr.left = node.type; 109 | expect( node ).to.have.property( 'type', Syntax.Identifier ); 110 | expect( node ).to.have.property( 'name', 'u' ); 111 | } 112 | else if ( fe.right === node ) 113 | { 114 | lr.right = node.type; 115 | expect( node ).to.have.property( 'type', Syntax.MemberExpression ); 116 | expect( node ).to.have.property( 'object' ); 117 | expect( node.object ).to.have.property( 'type', Syntax.Identifier ); 118 | expect( node.object ).to.have.property( 'name', 'someBase' ); 119 | 120 | } 121 | else 122 | expect( true ).to.equal( false, "Bad node field" ); 123 | } ); 124 | 125 | ast2.rename( className.id, 'B' ); 126 | ast2.add_line( 8, 'export default' ); 127 | const astSrc = ast2.as_source(); 128 | expect( astSrc ).to.be.a( 'string' ); 129 | expect( /class B/.test( astSrc ) ).to.be.true; 130 | expect( /export default\n\s\s9.\sclass B/.test( astSrc ) ).to.be.true; 131 | 132 | const func = findType( Syntax.FunctionExpression, root2 ); 133 | expect( ast.get_from_function( func.parent, 'name' ) ).to.eql( 'constructor' ); 134 | 135 | } ); 136 | } ); 137 | -------------------------------------------------------------------------------- /test/dot3.txt: -------------------------------------------------------------------------------- 1 | // pp$1.parseClass:13-94 2 | digraph "pp$1.parseClass:13-94" { 3 | default = "#0D3B66"; 4 | bgcolor = "white"; 5 | color = "#0D3B66"; 6 | fontcolor = "#0D3B66"; 7 | fontname = "arial"; 8 | shape = "ellipse"; 9 | nodesep = "1.5"; 10 | margin = "0.5, 0.2"; 11 | labelloc="t"; 12 | label="pp$1.parseClass:13-94"; 13 | fontsize=30 14 | node [color = "#0D3B66", fontcolor = "#0D3B66", fontname = "arial", style = "rounded"]; 15 | 0 [label = "entry:0", color = "#C6AC4D", fontcolor = "#0D3B66", fontname = "arial", style = "rounded", shape = "box"]; 16 | 29 [label = "exit:29", color = "#C6AC4D", fontcolor = "#0D3B66", fontname = "arial", style = "rounded", shape = "box"]; 17 | 1 [label = "NORMAL:1@14-93"]; 18 | 2 [label = "TEST:2@24"]; 19 | 3 [label = "TEST|LOOP:3@26"]; 20 | 4 [label = "TEST|LOOP:4@28-34"]; 21 | 5 [label = "LOOP:5@37-27"]; 22 | 6 [label = "LOOP:6@38-39"]; 23 | 7 [label = "TEST|LOOP:7@41-43"]; 24 | 8 [label = "LOOP:8@45-46"]; 25 | 9 [label = "TEST|LOOP:9@48-50"]; 26 | 10 [label = "TEST|LOOP:10@52-53"]; 27 | 11 [label = "TEST|LOOP:11@55-36"]; 28 | 12 [label = "TEST|LOOP:12@59"]; 29 | 13 [label = "TEST|LOOP:13@61"]; 30 | 14 [label = "LOOP:14@62"]; 31 | 15 [label = "TEST|LOOP:15@63"]; 32 | 16 [label = "LOOP:16@64"]; 33 | 17 [label = "TEST|LOOP:17@65"]; 34 | 18 [label = "LOOP:18@66"]; 35 | 19 [label = "TEST|LOOP:19@67"]; 36 | 20 [label = "LOOP:20@68"]; 37 | 21 [label = "LOOP:21@69-70"]; 38 | 22 [label = "TEST|LOOP:22@73-74"]; 39 | 23 [label = "TEST|LOOP:23@76-77"]; 40 | 24 [label = "TEST|LOOP:24@79-80"]; 41 | 25 [label = "LOOP:25@81"]; 42 | 26 [label = "LOOP:26@83"]; 43 | 27 [label = "TEST|LOOP:27@87"]; 44 | 28 [label = "LOOP:28@88"]; 45 | 46 | // Unconditional edges 47 | edge [color = "#0D3B65", fontcolor = "#0D3B66", fontname = "arial"]; 48 | 0 -> 1 49 | 1 -> 2 50 | 4 -> 7 51 | 4 -> 6 52 | 4 -> 5 53 | 5 -> 6 54 | 6 -> 7 55 | 8 -> 9 56 | 9 -> 22 57 | 9 -> 10 58 | 10 -> 12 59 | 10 -> 11 60 | 11 -> 12 61 | 14 -> 15 62 | 16 -> 17 63 | 18 -> 19 64 | 20 -> 21 65 | 21 -> 22 66 | 22 -> 2 67 | 22 -> 23 68 | 23 -> 24 69 | 23 -> 27 70 | 24 -> 25 71 | 24 -> 26 72 | 25 -> 2 73 | 26 -> 2 74 | 28 -> 2 75 | 76 | // Conditional edges 77 | edge [color = "#F95738", fontcolor = "#F95738", fontname = "arial italic", style = "dashed"]; 78 | 2 -> 3 [label = "TRUE"] 79 | 2 -> 29 [label = "FALSE"] 80 | 3 -> 4 [label = "FALSE"] 81 | 3 -> 2 [label = "TRUE"] 82 | 7 -> 9 [label = "FALSE"] 83 | 7 -> 8 [label = "TRUE"] 84 | 12 -> 22 [label = "FALSE"] 85 | 12 -> 13 [label = "TRUE"] 86 | 13 -> 15 [label = "FALSE"] 87 | 13 -> 14 [label = "TRUE"] 88 | 15 -> 17 [label = "FALSE"] 89 | 15 -> 16 [label = "TRUE"] 90 | 17 -> 19 [label = "FALSE"] 91 | 17 -> 18 [label = "TRUE"] 92 | 19 -> 21 [label = "FALSE"] 93 | 19 -> 20 [label = "TRUE"] 94 | 27 -> 2 [label = "FALSE"] 95 | 27 -> 28 [label = "TRUE"] 96 | } -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | /** ****************************************************************************************************************** 2 | * @file Standard unit test. 3 | * @author Julian Jensen 4 | * @since 1.0.0 5 | * @date Sat Dec 16 2017 6 | *********************************************************************************************************************/ 7 | "use strict"; 8 | 9 | import { expect } from 'chai'; 10 | import fs from 'fs'; 11 | 12 | import { 13 | load_plugins 14 | } from '../src/utils'; 15 | 16 | load_plugins(); 17 | import CFG from '../src/cfg'; 18 | import AST from '../src/ast'; 19 | 20 | const 21 | testFiles = [ 22 | fs.readFileSync( './test-data/cfg-test-01.js', 'utf8' ), 23 | fs.readFileSync( './test-data/cfg-test-02.js', 'utf8' ), 24 | fs.readFileSync( './test-data/cfg-test-03.js', 'utf8' ), 25 | fs.readFileSync( './test-data/cfg-test-04.js', 'utf8' ), 26 | fs.readFileSync( './test-data/cfg-test-05.js', 'utf8' ), 27 | fs.readFileSync( './test-data/cfg-test-06.js', 'utf8' ) 28 | ], 29 | a01 = [ 30 | { id: 0, nodes: [], types: 1, createdBy: '' }, 31 | { 32 | id: 1, 33 | nodes: [ 'EmptyStatement' ], 34 | types: 16, 35 | createdBy: 'For.body' 36 | }, 37 | { id: 2, nodes: [], types: 0, createdBy: 'For.conv' }, 38 | { id: 3, nodes: [], types: 4, createdBy: '' }, 39 | { id: 4, nodes: [], types: 2, createdBy: '' } 40 | ], 41 | a02 = [ 42 | { id: 0, nodes: [], types: 1, createdBy: '' }, 43 | { 44 | id: 1, 45 | nodes: [ 'VariableDeclaration' ], 46 | types: 4, 47 | createdBy: 'For.init' 48 | }, 49 | { id: 2, nodes: [], types: 0, createdBy: 'For.conv' }, 50 | { 51 | id: 3, 52 | nodes: [ 'EmptyStatement', 'UpdateExpression' ], 53 | types: 16, 54 | createdBy: 'CFG: BlockStatement' 55 | }, 56 | { id: 4, nodes: [], types: 4, createdBy: '' }, 57 | { id: 5, nodes: [], types: 2, createdBy: '' } 58 | ], 59 | a03 = [ 60 | { id: 0, nodes: [], types: 1, createdBy: '' }, 61 | { 62 | id: 1, 63 | nodes: [ 'Identifier' ], 64 | types: 8, 65 | createdBy: 'For.test' 66 | }, 67 | { 68 | id: 2, 69 | nodes: [ 'ExpressionStatement' ], 70 | types: 16, 71 | createdBy: 'For.body' 72 | }, 73 | { id: 3, nodes: [], types: 4, createdBy: '' }, 74 | { id: 4, nodes: [], types: 2, createdBy: '' } 75 | ], 76 | b01 = 77 | [ 78 | { id: 0, nodes: [], types: 1, createdBy: '' }, 79 | { id: 1, nodes: [ 'CallExpression' ], types: 8, createdBy: '' }, 80 | { 81 | id: 2, 82 | nodes: [ 'Identifier' ], 83 | types: 24, 84 | createdBy: 'If.test' 85 | }, 86 | { 87 | id: 3, 88 | nodes: [ 'ContinueStatement' ], 89 | types: 16, 90 | createdBy: '' 91 | }, 92 | { 93 | id: 4, 94 | nodes: [ 'UnaryExpression' ], 95 | types: 24, 96 | createdBy: 'If.test' 97 | }, 98 | { 99 | id: 5, 100 | nodes: [ 'EmptyStatement' ], 101 | types: 16, 102 | createdBy: 'CFG: BlockStatement' 103 | }, 104 | { id: 6, nodes: [], types: 4, createdBy: '' }, 105 | { id: 7, nodes: [], types: 2, createdBy: '' } 106 | ], 107 | b02 = 108 | [ 109 | { id: 0, nodes: [], types: 1, createdBy: '' }, 110 | { id: 1, nodes: [ 'CallExpression' ], types: 8, createdBy: '' }, 111 | { 112 | id: 2, 113 | nodes: [ 'Identifier' ], 114 | types: 24, 115 | createdBy: 'If.test' 116 | }, 117 | { 118 | id: 3, 119 | nodes: [ 'ContinueStatement' ], 120 | types: 16, 121 | createdBy: '' 122 | }, 123 | { 124 | id: 4, 125 | nodes: [ 'UnaryExpression' ], 126 | types: 24, 127 | createdBy: 'If.test' 128 | }, 129 | { id: 5, nodes: [ 'BreakStatement' ], types: 16, createdBy: '' }, 130 | { id: 6, nodes: [], types: 4, createdBy: '' }, 131 | { id: 7, nodes: [], types: 2, createdBy: '' } 132 | ], 133 | b03 = 134 | [ 135 | { id: 0, nodes: [], types: 1, createdBy: '' }, 136 | { 137 | id: 1, 138 | nodes: [ 'VariableDeclaration' ], 139 | types: 4, 140 | createdBy: '' 141 | }, 142 | { id: 2, nodes: [ 'CallExpression' ], types: 8, createdBy: '' }, 143 | { 144 | id: 3, 145 | nodes: [ 'Identifier' ], 146 | types: 24, 147 | createdBy: 'If.test' 148 | }, 149 | { 150 | id: 4, 151 | nodes: [ 'ContinueStatement' ], 152 | types: 16, 153 | createdBy: 'label' 154 | }, 155 | { 156 | id: 5, 157 | nodes: [ 'UnaryExpression' ], 158 | types: 24, 159 | createdBy: 'If.test' 160 | }, 161 | { 162 | id: 6, 163 | nodes: [ 'ExpressionStatement', 'BreakStatement' ], 164 | types: 16, 165 | createdBy: '' 166 | }, 167 | { id: 7, nodes: [], types: 4, createdBy: '' }, 168 | { id: 8, nodes: [], types: 2, createdBy: '' } 169 | ], 170 | _constructor = 171 | [ 172 | { id: 0, nodes: [], types: 1, createdBy: '' }, 173 | { 174 | id: 1, 175 | nodes: 176 | [ 177 | 'VariableDeclaration', 178 | 'ExpressionStatement', 179 | 'BinaryExpression' 180 | ], 181 | types: 8, 182 | createdBy: '' 183 | }, 184 | { 185 | id: 2, 186 | nodes: [ 'ExpressionStatement' ], 187 | types: 4, 188 | createdBy: 'If.cons' 189 | }, 190 | { 191 | id: 3, 192 | nodes: 193 | [ 194 | 'ExpressionStatement', 195 | 'ExpressionStatement', 196 | 'ReturnStatement', 197 | 'VariableDeclaration', 198 | 'ExpressionStatement', 199 | 'ExpressionStatement', 200 | 'ExpressionStatement' 201 | ], 202 | types: 4, 203 | createdBy: '' 204 | }, 205 | { id: 4, nodes: [], types: 2, createdBy: '' } 206 | ], 207 | pp$1parseClass3 = 208 | [ 209 | { id: 0, nodes: [], types: 1, createdBy: '' }, 210 | { 211 | id: 1, 212 | nodes: 213 | [ 214 | 'VariableDeclaration', 215 | 'ExpressionStatement', 216 | 'ExpressionStatement', 217 | 'ExpressionStatement', 218 | 'VariableDeclaration', 219 | 'VariableDeclaration', 220 | 'ExpressionStatement', 221 | 'ExpressionStatement', 222 | 'ExpressionStatement', 223 | 'ReturnStatement' 224 | ], 225 | types: 4, 226 | createdBy: '' 227 | }, 228 | { id: 2, nodes: [ 'UnaryExpression' ], types: 8, createdBy: '' }, 229 | { 230 | id: 3, 231 | nodes: [ 'CallExpression' ], 232 | types: 24, 233 | createdBy: 'If.test' 234 | }, 235 | { 236 | id: 4, 237 | nodes: 238 | [ 239 | 'VariableDeclaration', 240 | 'VariableDeclaration', 241 | 'VariableDeclaration', 242 | 'VariableDeclaration', 243 | 'ExpressionStatement', 244 | 'ExpressionStatement', 245 | 'MemberExpression' 246 | ], 247 | types: 24, 248 | createdBy: '' 249 | }, 250 | { 251 | id: 5, 252 | nodes: [ 'ExpressionStatement', 'ContinueStatement' ], 253 | types: 16, 254 | createdBy: 'CFG: BlockStatement' 255 | }, 256 | { 257 | id: 6, 258 | nodes: [ 'ExpressionStatement', 'ExpressionStatement' ], 259 | types: 16, 260 | createdBy: 'CFG: BlockStatement' 261 | }, 262 | { 263 | id: 7, 264 | nodes: [ 'LogicalExpression' ], 265 | types: 24, 266 | createdBy: 'If.test' 267 | }, 268 | { 269 | id: 8, 270 | nodes: [ 'ExpressionStatement', 'ExpressionStatement' ], 271 | types: 16, 272 | createdBy: 'CFG: BlockStatement' 273 | }, 274 | { 275 | id: 9, 276 | nodes: 277 | [ 278 | 'ExpressionStatement', 279 | 'VariableDeclaration', 280 | 'UnaryExpression' 281 | ], 282 | types: 24, 283 | createdBy: '' 284 | }, 285 | { 286 | id: 10, 287 | nodes: [ 'VariableDeclaration', 'LogicalExpression' ], 288 | types: 24, 289 | createdBy: '' 290 | }, 291 | { 292 | id: 11, 293 | nodes: 294 | [ 295 | 'ExpressionStatement', 296 | 'ExpressionStatement', 297 | 'ExpressionStatement', 298 | 'Identifier' 299 | ], 300 | types: 24, 301 | createdBy: 'CFG: BlockStatement' 302 | }, 303 | { 304 | id: 12, 305 | nodes: [ 'LogicalExpression' ], 306 | types: 24, 307 | createdBy: 'If.test' 308 | }, 309 | { 310 | id: 13, 311 | nodes: [ 'Identifier' ], 312 | types: 24, 313 | createdBy: 'If.test' 314 | }, 315 | { 316 | id: 14, 317 | nodes: [ 'ExpressionStatement' ], 318 | types: 16, 319 | createdBy: 'CFG: BlockStatement' 320 | }, 321 | { 322 | id: 15, 323 | nodes: [ 'Identifier' ], 324 | types: 24, 325 | createdBy: 'If.test' 326 | }, 327 | { 328 | id: 16, 329 | nodes: [ 'ExpressionStatement' ], 330 | types: 16, 331 | createdBy: 'CFG: BlockStatement' 332 | }, 333 | { 334 | id: 17, 335 | nodes: [ 'Identifier' ], 336 | types: 24, 337 | createdBy: 'If.test' 338 | }, 339 | { 340 | id: 18, 341 | nodes: [ 'ExpressionStatement' ], 342 | types: 16, 343 | createdBy: 'CFG: BlockStatement' 344 | }, 345 | { 346 | id: 19, 347 | nodes: [ 'Identifier' ], 348 | types: 24, 349 | createdBy: 'If.test' 350 | }, 351 | { 352 | id: 20, 353 | nodes: [ 'ExpressionStatement' ], 354 | types: 16, 355 | createdBy: 'CFG: BlockStatement' 356 | }, 357 | { 358 | id: 21, 359 | nodes: [ 'ExpressionStatement', 'ExpressionStatement' ], 360 | types: 16, 361 | createdBy: 'CFG: BlockStatement' 362 | }, 363 | { 364 | id: 22, 365 | nodes: [ 'ExpressionStatement', 'Identifier' ], 366 | types: 24, 367 | createdBy: '' 368 | }, 369 | { 370 | id: 23, 371 | nodes: [ 'VariableDeclaration', 'BinaryExpression' ], 372 | types: 24, 373 | createdBy: '' 374 | }, 375 | { 376 | id: 24, 377 | nodes: [ 'VariableDeclaration', 'BinaryExpression' ], 378 | types: 24, 379 | createdBy: '' 380 | }, 381 | { 382 | id: 25, 383 | nodes: [ 'ExpressionStatement' ], 384 | types: 16, 385 | createdBy: 'CFG: BlockStatement' 386 | }, 387 | { 388 | id: 26, 389 | nodes: [ 'ExpressionStatement' ], 390 | types: 16, 391 | createdBy: 'CFG: BlockStatement' 392 | }, 393 | { 394 | id: 27, 395 | nodes: [ 'LogicalExpression' ], 396 | types: 24, 397 | createdBy: 'If.test' 398 | }, 399 | { 400 | id: 28, 401 | nodes: [ 'ExpressionStatement' ], 402 | types: 16, 403 | createdBy: 'CFG: BlockStatement' 404 | }, 405 | { id: 29, nodes: [], types: 2, createdBy: '' } 406 | ], 407 | pp$1parseClass4 = 408 | [ 409 | { id: 0, nodes: [], types: 1, createdBy: '' }, 410 | { 411 | id: 1, 412 | nodes: 413 | [ 414 | 'VariableDeclaration', 415 | 'ExpressionStatement', 416 | 'ExpressionStatement' 417 | ], 418 | types: 4, 419 | createdBy: '' 420 | }, 421 | { id: 2, nodes: [ 'UnaryExpression' ], types: 8, createdBy: '' }, 422 | { 423 | id: 3, 424 | nodes: 425 | [ 426 | 'VariableDeclaration', 427 | 'ExpressionStatement', 428 | 'ExpressionStatement', 429 | 'VariableDeclaration', 430 | 'UnaryExpression' 431 | ], 432 | types: 24, 433 | createdBy: '' 434 | }, 435 | { 436 | id: 4, 437 | nodes: [ 'ExpressionStatement' ], 438 | types: 16, 439 | createdBy: 'If.cons' 440 | }, 441 | { 442 | id: 5, 443 | nodes: [ 'UnaryExpression' ], 444 | types: 24, 445 | createdBy: 'If.test' 446 | }, 447 | { 448 | id: 6, 449 | nodes: 450 | [ 451 | 'ExpressionStatement', 452 | 'ExpressionStatement', 453 | 'VariableDeclaration', 454 | 'LogicalExpression' 455 | ], 456 | types: 24, 457 | createdBy: 'If.cons' 458 | }, 459 | { 460 | id: 7, 461 | nodes: [ 'Identifier' ], 462 | types: 24, 463 | createdBy: 'If.test' 464 | }, 465 | { 466 | id: 8, 467 | nodes: [ 'VariableDeclaration', 'BinaryExpression' ], 468 | types: 24, 469 | createdBy: '' 470 | }, 471 | { 472 | id: 9, 473 | nodes: [ 'VariableDeclaration', 'BinaryExpression' ], 474 | types: 24, 475 | createdBy: '' 476 | }, 477 | { 478 | id: 10, 479 | nodes: [ 'ExpressionStatement' ], 480 | types: 16, 481 | createdBy: 'CFG: BlockStatement' 482 | }, 483 | { 484 | id: 11, 485 | nodes: [ 'LogicalExpression' ], 486 | types: 24, 487 | createdBy: 'If.test' 488 | }, 489 | { 490 | id: 12, 491 | nodes: [ 'ExpressionStatement' ], 492 | types: 16, 493 | createdBy: 'CFG: BlockStatement' 494 | }, 495 | { id: 13, nodes: [], types: 2, createdBy: '' } 496 | ], 497 | blah = 498 | [ 499 | { id: 0, nodes: [], types: 1, createdBy: '' }, 500 | { 501 | id: 1, 502 | nodes: [ 'ExpressionStatement' ], 503 | types: 4, 504 | createdBy: '' 505 | }, 506 | { 507 | id: 2, 508 | nodes: [ 'ForOfStatement' ], 509 | types: 4, 510 | createdBy: 'ForInOf.update' 511 | }, 512 | { 513 | id: 3, 514 | nodes: [ 'ExpressionStatement', 'BinaryExpression' ], 515 | types: 24, 516 | createdBy: '' 517 | }, 518 | { 519 | id: 4, 520 | nodes: [ 'ContinueStatement' ], 521 | types: 16, 522 | createdBy: 'If.cons' 523 | }, 524 | { 525 | id: 5, 526 | nodes: [ 'BinaryExpression' ], 527 | types: 8, 528 | createdBy: 'If.test' 529 | }, 530 | { 531 | id: 6, 532 | nodes: [ 'ExpressionStatement' ], 533 | types: 4, 534 | createdBy: 'If.cons' 535 | }, 536 | { 537 | id: 7, 538 | nodes: [ 'BinaryExpression' ], 539 | types: 8, 540 | createdBy: 'If.test' 541 | }, 542 | { 543 | id: 8, 544 | nodes: [ 'ExpressionStatement' ], 545 | types: 4, 546 | createdBy: 'If.cons' 547 | }, 548 | { 549 | id: 9, 550 | nodes: [ 'ExpressionStatement' ], 551 | types: 4, 552 | createdBy: 'CFG: BlockStatement' 553 | }, 554 | { id: 10, nodes: [], types: 4, createdBy: '' }, 555 | { id: 11, nodes: [], types: 2, createdBy: '' } 556 | ], 557 | code_coverage = [ 558 | { id: 0, nodes: [], types: 1, createdBy: '' }, 559 | { 560 | id: 1, 561 | nodes: [ 'VariableDeclaration', 'TryStatement' ], 562 | types: 4, 563 | createdBy: '' 564 | }, 565 | { 566 | id: 2, 567 | nodes: [ 'CatchClause', 'ExpressionStatement', 'ExpressionStatement' ], 568 | types: 512, 569 | createdBy: '' 570 | }, 571 | { 572 | id: 3, 573 | nodes: [ 'ExpressionStatement', 'Identifier' ], 574 | types: 4, 575 | createdBy: 'CFG: BlockStatement' 576 | }, 577 | { id: 4, nodes: [ 'Literal' ], types: 8, createdBy: '' }, 578 | { 579 | id: 5, 580 | nodes: [ 'ExpressionStatement', 'BreakStatement' ], 581 | types: 4, 582 | createdBy: '' 583 | }, 584 | { id: 6, nodes: [ 'Literal' ], types: 8, createdBy: '' }, 585 | { id: 7, nodes: [ 'Literal' ], types: 8, createdBy: '' }, 586 | { id: 8, nodes: [ 'Literal' ], types: 8, createdBy: '' }, 587 | { 588 | id: 9, 589 | nodes: [ 'ExpressionStatement' ], 590 | types: 4, 591 | createdBy: '' 592 | }, 593 | { id: 10, nodes: [ 'Literal' ], types: 8, createdBy: '' }, 594 | { 595 | id: 11, 596 | nodes: [ 'ExpressionStatement', 'BreakStatement' ], 597 | types: 4, 598 | createdBy: '' 599 | }, 600 | { 601 | id: 12, 602 | nodes: [ 'ExpressionStatement' ], 603 | types: 4, 604 | createdBy: '' 605 | }, 606 | { id: 13, nodes: [ 'Literal' ], types: 8, createdBy: '' }, 607 | { 608 | id: 14, 609 | nodes: [ 'ExpressionStatement', 'BreakStatement' ], 610 | types: 4, 611 | createdBy: '' 612 | }, 613 | { id: 15, nodes: [], types: 4, createdBy: '' }, 614 | { id: 16, nodes: [], types: 2, createdBy: '' } 615 | ], 616 | sourceToTests = [ 617 | { a01, a02, a03, b01, b02, b03 }, 618 | { 'constructor': _constructor }, 619 | { 'pp$1.parseClass': pp$1parseClass3 }, 620 | { 'pp$1.parseClass': pp$1parseClass4 }, 621 | { blah }, 622 | { code_coverage } 623 | ], 624 | toTable6 = fs.readFileSync( './test/table6.txt', 'utf8' ).split( /\r?\n/ ).map( l => l.trim() ).join( '\n' ), 625 | toText6 = fs.readFileSync( './test/text6.txt', 'utf8' ).split( /\r?\n/ ).map( l => l.trim() ).join( '\n' ), 626 | dotFile = fs.readFileSync( './test/dot3.txt', 'utf8' ).split( /\r?\n/ ).map( l => l.trim() ).join( '\n' ), 627 | extract = bm => bm.blocks.map( b => ( { 628 | id: b.id, 629 | nodes: b.nodes.map( n => n.type ), 630 | types: b.types, 631 | createdBy: b.createdBy 632 | } ) ); 633 | 634 | describe( 'cfg', function() { 635 | 636 | it( 'should initialize a source module', () => { 637 | const cfg = new CFG( testFiles[ 2 ] ); 638 | 639 | expect( cfg ).to.be.instanceof( CFG ); 640 | expect( cfg.ast ).to.be.instanceof( AST ); 641 | 642 | } ); 643 | 644 | it( 'should generate some graphs', () => { 645 | const cfg = new CFG( testFiles[ 2 ] ); 646 | 647 | cfg.generate(); 648 | expect( cfg.cfgs ).to.be.an( 'array' ); 649 | expect( cfg.cfgs ).to.have.length( 3 ); 650 | } ); 651 | 652 | it( 'should generate a graph by name', () => { 653 | const cfg = new CFG( testFiles[ 5 ] ); 654 | 655 | expect( cfg.generate( 'kersploink' ) ).to.be.null; 656 | expect( extract( cfg.generate( 'code_coverage' ).bm ) ).to.eql( code_coverage ); 657 | } ); 658 | 659 | // it( 'should create pretty tables', () => { 660 | // const 661 | // cfg = new CFG( testFiles[ 5 ] ).generate(), 662 | // tables = cfg.toTable(); 663 | // 664 | // expect( tables.split( /\r?\n/ ).map( l => l.trim() ).join( '\n' ) ).to.eql( toTable6 ); 665 | // // expect( text.split( /\r?\n/ ).map( l => l.trim() ).join( '\n' ) ).to.eql( toText6 ); 666 | // } ); 667 | 668 | it( 'should iterate over graphs', () => { 669 | const 670 | cfg = new CFG( testFiles[ 0 ] ).generate(), 671 | seen = [], 672 | altSeen = []; 673 | 674 | for ( const cgraph of cfg ) 675 | if ( cgraph.name !== 'main' ) seen.push( cgraph.name ); 676 | 677 | cfg.forEach( cgraph => cgraph.name !== 'main' && altSeen.push( cgraph.name ) ); 678 | 679 | expect( seen ).to.have.members( Object.keys( sourceToTests[ 0 ] ) ); 680 | expect( altSeen ).to.have.members( Object.keys( sourceToTests[ 0 ] ) ); 681 | } ); 682 | 683 | it( 'should generate a graph-viz dot file', () => { 684 | const 685 | cfg = new CFG( testFiles[ 2 ] ).generate(), 686 | fn = cfg.by_name( 'pp$1.parseClass' ), 687 | df = cfg.create_dot( fn ).split( /\r?\n/ ).map( l => l.trim() ).join( '\n' ); 688 | 689 | expect( df ).to.eql( dotFile ); 690 | } ); 691 | 692 | it( 'graph troublesome code', () => { 693 | const 694 | cfgs = testFiles.map( src => new CFG( src ) ); 695 | 696 | cfgs.forEach( cfg => cfg.generate() ); 697 | 698 | sourceToTests.forEach( ( testFns, i ) => { 699 | const cfg = cfgs[ i ]; 700 | 701 | [ ...Object.entries( testFns ) ].forEach( ( [ name, expt ] ) => expect( expt ).to.eql( extract( cfg.by_name( name ).bm ) ) ); 702 | } ); 703 | } ); 704 | 705 | } ); 706 | -------------------------------------------------------------------------------- /test/table6.txt: -------------------------------------------------------------------------------- 1 | ┌───────────────────────────────────────────────────────────────────────────┐ 2 | │ main:7-49 │ 3 | ├───────┬───────┬────────────┬──────┬─────────────┬────────────┬────────────┤ 4 | │ TYPE │ LINES │ LEFT EDGES │ NODE │ RIGHT EDGES │ CREATED BY │ AST │ 5 | ├───────┼───────┼────────────┼──────┼─────────────┼────────────┼────────────┤ 6 | │ START │ 7-49 │ │  →0 │  1 │ │ Program(0) │ 7 | ├───────┼───────┼────────────┼──────┼─────────────┼────────────┼────────────┤ 8 | │ EXIT │ │  0 │  1⛔ │ │ │ │ 9 | └───────┴───────┴────────────┴──────┴─────────────┴────────────┴────────────┘ 10 | 11 | ┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 12 | │ code_coverage:9-49 │ 13 | ├────────┬───────┬─────────────────┬──────┬─────────────┬─────────────────────┬──────────────────────────┤ 14 | │ TYPE │ LINES │ LEFT EDGES │ NODE │ RIGHT EDGES │ CREATED BY │ AST │ 15 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 16 | │ START │ │ │  →0 │  1 │ │ │ 17 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 18 | │ NORMAL │ 11-30 │  0 │  1 │  2 3 │ │ VariableDeclaration(9) │ 19 | │ │ │ │ │ │ │ TryStatement(18) │ 20 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 21 | │ CATCH │ 22-25 │  1 │  2 │  3 │ │ CatchClause(43) │ 22 | │ │ │ │ │ │ │ ExpressionStatement(46) │ 23 | │ │ │ │ │ │ │ ExpressionStatement(52) │ 24 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 25 | │ NORMAL │ 29-32 │  1 2 │  3 │  4 15 │ CFG: BlockStatement │ ExpressionStatement(59) │ 26 | │ │ │ │ │ │ │ Identifier(73) │ 27 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 28 | │ TEST │ 34 │  3 │  4 │  5✔ 6✖ │ │ Literal(75) │ 29 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 30 | │ NORMAL │ 34-35 │  ✔4 │  5 │  15* │ │ ExpressionStatement(76) │ 31 | │ │ │ │ │ │ │ BreakStatement(80) │ 32 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 33 | │ TEST │ 37 │  ✖4 │  6 │  7✖ 9✔ │ │ Literal(82) │ 34 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 35 | │ TEST │ 38 │  ✖6 │  7 │  8✖ 9✔ │ │ Literal(84) │ 36 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 37 | │ TEST │ 39 │  ✖7 │  8 │  9✔ 10✖ │ │ Literal(86) │ 38 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 39 | │ NORMAL │ 39 │  ✔6 ✔7 ✔8 │  9 │  11 │ │ ExpressionStatement(87) │ 40 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 41 | │ TEST │ 41 │  ✖8 │  10 │  11✔ 13✖ │ │ Literal(92) │ 42 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 43 | │ NORMAL │ 41-42 │  9 ✔10 │  11 │  15* │ │ ExpressionStatement(93) │ 44 | │ │ │ │ │ │ │ BreakStatement(97) │ 45 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 46 | │ NORMAL │ 44 │  ✖13 │  12 │  14 │ │ ExpressionStatement(99) │ 47 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 48 | │ TEST │ 46 │  ✖10 │  13 │  14✔ 12✖ │ │ Literal(103) │ 49 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 50 | │ NORMAL │ 46-47 │  12 ✔13 │  14 │  15* │ │ ExpressionStatement(104) │ 51 | │ │ │ │ │ │ │ BreakStatement(110) │ 52 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 53 | │ NORMAL │ │  3 *5 *11 *14 │  15 │  16 │ │ │ 54 | ├────────┼───────┼─────────────────┼──────┼─────────────┼─────────────────────┼──────────────────────────┤ 55 | │ EXIT │ │  15 │  16⛔ │ │ │ │ 56 | └────────┴───────┴─────────────────┴──────┴─────────────┴─────────────────────┴──────────────────────────┘ -------------------------------------------------------------------------------- /test/text6.txt: -------------------------------------------------------------------------------- 1 | main:7-49 2 | START 7-49 →0 ──→ 1 Program(0) 3 | EXIT 0 ←── 1⛔ 4 | 5 | code_coverage:9-49 6 | START →0 ──→ 1 7 | NORMAL 11-30 0 ←── 1 ──→ 2 3 VariableDeclaration(9), TryStatement(18) 8 | CATCH 22-25 1 ←── 2 ──→ 3 CatchClause(43), ExpressionStatement(46), ExpressionStatement(52) 9 | NORMAL 29-32 1 2 ←── 3 ──→ 4 15 CFG: BlockStatement ExpressionStatement(59), Identifier(73) 10 | TEST 34 3 ←── 4 ──→ 5✔ 6✖ Literal(75) 11 | NORMAL 34-35 ✔4 ←── 5 ──→ 15* ExpressionStatement(76), BreakStatement(80) 12 | TEST 37 ✖4 ←── 6 ──→ 7✖ 9✔ Literal(82) 13 | TEST 38 ✖6 ←── 7 ──→ 8✖ 9✔ Literal(84) 14 | TEST 39 ✖7 ←── 8 ──→ 9✔ 10✖ Literal(86) 15 | NORMAL 39 ✔6 ✔7 ✔8 ←── 9 ──→ 11 ExpressionStatement(87) 16 | TEST 41 ✖8 ←── 10 ──→ 11✔ 13✖ Literal(92) 17 | NORMAL 41-42 9 ✔10 ←── 11 ──→ 15* ExpressionStatement(93), BreakStatement(97) 18 | NORMAL 44 ✖13 ←── 12 ──→ 14 ExpressionStatement(99) 19 | TEST 46 ✖10 ←── 13 ──→ 14✔ 12✖ Literal(103) 20 | NORMAL 46-47 12 ✔13 ←── 14 ──→ 15* ExpressionStatement(104), BreakStatement(110) 21 | NORMAL 3 *5 *11 *14 ←── 15 ──→ 16 22 | EXIT 15 ←── 16⛔ --------------------------------------------------------------------------------