├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── api.js ├── appveyor.yml ├── cli.js ├── fixtures ├── component.js ├── default-anonymous.js ├── es6.js └── parsing-error.js ├── lib ├── ast-helpers.js ├── common.js ├── dep-registry.js ├── importer.js ├── parser.js └── walker.js ├── logo.png ├── package.json ├── patches ├── escope.js └── progress.js ├── test ├── api.js ├── cli.js ├── importer.js ├── importer.yml ├── parser.js └── parser.yml ├── visits ├── find-exports.js ├── find-imports.js ├── find-style.js └── resolve-jsx.js └── yarn.lock /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 6 3 | sourceType: module 4 | ecmaFeatures: 5 | jsx: true 6 | env: 7 | node: true 8 | es6: true 9 | root: true 10 | extends: 11 | - eslint:recommended 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | fixtures/ 5 | test/ 6 | yarn.lock 7 | logo.png 8 | appveyor.yml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: [6, 7] 3 | sudo: false 4 | cache: 5 | directories: [node_modules] 6 | script: npm test 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Karthik Viswanathan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | This software includes some code from ESLint. ESLint is under the following 25 | license: 26 | 27 | ESLint 28 | Copyright JS Foundation and other contributors, https://js.foundation 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy of 31 | this software and associated documentation files (the "Software"), to deal in 32 | the Software without restriction, including without limitation the rights to 33 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 34 | the Software, and to permit persons to whom the Software is furnished to do so, 35 | subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in 38 | all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | THE SOFTWARE. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![tradeship][logo-image]][tradeship-url] 2 | 3 | [![Linux Build][travis-image]][travis-url] 4 | [![Windows Build][appveyor-image]][appveyor-url] 5 | [![NPM Version][npm-image]][npm-url] 6 | 7 | tradeship statically analyzes your JavaScript code for identifiers that aren't 8 | defined and finds the appropriate dependencies to import. It also removes 9 | imports that aren't used. tradeship is meant to be used as an editor plugin that 10 | manages your imports. 11 | 12 | ![tradeship](http://g.recordit.co/OlDKhJc9LI.gif) 13 | 14 | ## Features 15 | - Imports dependencies from node.js standard libraries, npm packages listed in 16 | package.json, and other files within your project directory. 17 | - Infers properties that are exported by dependencies and imports them using 18 | destructuring syntax (e.g. `const { Component } = require("react");`). 19 | - Knows aliases for dependencies (e.g. `$` for jQuery, `_` for 20 | lodash/underscore, `test` for ava, etc.) by analyzing more than 100 GB of 21 | public JS code on GitHub. 22 | - Automatically identifies the style of your code and makes the imports match 23 | (e.g. single vs. double quotes, semicolons vs. no semicolons, var vs. let vs. 24 | const, etc.) 25 | - Statically analyzes files within your project directory to determine their 26 | exports for use in other files. 27 | - Supports both CommonJS and ES6 modules. Can output `require()` or `import` 28 | syntax. 29 | - Supports JSX identifiers and ensures React is in scope when using JSX. 30 | 31 | ## Installation 32 | Install tradeship using npm or yarn. It's recommended to either install 33 | tradeship globally or make the local installation available on your path (e.g. 34 | by adding `export PATH=node_modules/.bin:$PATH` to your shell configuration), 35 | since editor plugins make use of the `tradeship` executable. 36 | 37 | ```sh 38 | $ npm install -g tradeship 39 | # or use yarn: 40 | $ yarn global add tradeship 41 | ``` 42 | 43 | Then, install the editor plugin of your choice: 44 | 45 | - Vim: [`tradeship-vim`](https://github.com/karthikv/tradeship-vim) 46 | - Atom: [`tradeship-atom`](https://github.com/karthikv/tradeship-atom) 47 | - Sublime: [`tradeship-sublime`](https://github.com/karthikv/tradeship-sublime) 48 | - Emacs: [`tradeship-emacs`](https://github.com/karthikv/tradeship-emacs) 49 | - VS Code: [vscode-tradeship](https://marketplace.visualstudio.com/items?itemName=taichi.vscode-tradeship) 50 | 51 | Each editor plugin has instructions on how to run tradeship on the current file 52 | being edited. You can also configure each plugin to run tradeship on save. See 53 | the respective links above for more information. 54 | 55 | The first time tradeship runs in a project directory with many JavaScript files, 56 | it'll take some time to parse and cache dependencies. Future runs will be much 57 | faster. If you'd like to use tradeship in a large project, it's recommended to 58 | run it once on the command-line, as shown below, so you can see the progress as 59 | it populates the cache: 60 | 61 | ```sh 62 | tradeship [path] 63 | ``` 64 | 65 | Replace `[path]` with any path to a JavaScript file in your project directory. 66 | Note that this won't modify the file in any way; it'll just print the new code 67 | to stdout. See the [Command Line Interface](#command-line-interface-cli) section 68 | for details. 69 | 70 | ## Configuration 71 | tradeship doesn't need any configuration to work out of the box. By design, 72 | almost nothing is configurable, as tradeship automatically infers style from 73 | your code. There is, however, one configuration option that you may want to 74 | tweak: globals. 75 | 76 | To correctly find identifiers that aren't defined, tradeship needs to know what 77 | global variables are available. It does this primarily through environments, 78 | where each environment defines a set of global variables that come with it. 79 | tradeship piggybacks on [eslint's configuration 80 | system](http://eslint.org/docs/user-guide/configuring) to avoid introducing 81 | another configuration file and format. 82 | 83 | In eslint, you specify the environments you want in your configuration file, 84 | generally at the root of your project directory. tradeship searches for an 85 | eslint configuration file (either `.eslintrc.js`, `.eslintrc.yaml`, 86 | `.eslintrc.yml`, `.eslintrc.json`, `.eslintrc`, or an `eslintConfig` object in 87 | `package.json`) within the code's directory or successive parent directories. 88 | 89 | When it finds one, it looks at the `env` object to determine the active 90 | environments. An example configuration object is: 91 | 92 | ```js 93 | { 94 | "env": { 95 | "browser": true, 96 | "es6": true 97 | } 98 | } 99 | ``` 100 | 101 | Each key is an environment name, and the corresponding value is whether that 102 | environment is active. See eslint's guide to [specifying 103 | environments](http://eslint.org/docs/user-guide/configuring#specifying-environments) 104 | for more details about the available environments. 105 | 106 | If there's no configuration file, `tradeship` assumes the environments 107 | `browser`, `node`, and `es6`, bringing in globals from the browser (e.g. 108 | `window` and `document`), from node.js (e.g. `process` and `__dirname`), and 109 | from ES6 (e.g. `Set` and `Map`). 110 | 111 | Note that tradeship makes all the node.js standard libraries available for 112 | import if and only if the `node` environment is active. 113 | 114 | In addition to environment-based globals, you can also specify any globals 115 | specific to your project or build process using the `globals` object. Each key 116 | is the name of a global, and the value is whether that global is writable 117 | (unused by tradeship, but used by eslint). For instance, the following registers 118 | `React` and `$` as globals, so tradeship won't attempt to import them: 119 | 120 | ```js 121 | { 122 | "globals": { 123 | "React": false, 124 | "$": false 125 | } 126 | } 127 | ``` 128 | 129 | ## Command Line Interface (CLI) 130 | Using an editor plugin (see section above) is the easiest way to get started 131 | with tradeship, but you can also use the command line interface (which all 132 | editor plugins use internally). 133 | 134 | ```sh 135 | tradeship [options] [path] 136 | ``` 137 | 138 | Reads the code given at `[path]`. Outputs new code to stdout, adding missing 139 | dependencies and removing unused ones. The `[options]` are as follows: 140 | 141 | - `-s` or `--stdin`: Read contents from stdin as opposed to `[path]`. `[path]` 142 | is still required so that tradeship can resolve relative imports and find 143 | available npm packages, but it need not exist as a file; you can even just 144 | provide a directory. 145 | 146 | - `-w` or `--write`: Write output back to `[path]` (be careful!). 147 | 148 | - `-h` or `--help`: Print help. 149 | 150 | - `-v` or `--version`: Print version. 151 | 152 | The full help text is below: 153 | 154 | ``` 155 | Usage: tradeship [options] [path] 156 | Automatically imports missing JS dependencies and removes unused ones. 157 | 158 | Options: 159 | -s, --stdin read contents from stdin 160 | -w, --write write output to source file (careful!) 161 | -h, --help print help 162 | -v, --version print version 163 | 164 | Arguments: 165 | [path] Relative imports and available npm packages will be determined 166 | from this path. If not --stdin, input code will be read from this 167 | path. If --write, new code will be written to this path. 168 | ``` 169 | 170 | ## Node.js Interface 171 | tradeship exposes a simple node.js API if you'd like to use it programmatically: 172 | 173 | ```js 174 | const tradeship = require("tradeship"); 175 | tradeship.import(dir, code).then(newCode => { 176 | // do something with newCode 177 | }); 178 | ``` 179 | 180 | `dir` is the directory used to resolve relative imports and find available npm 181 | packages (generally the directory where the `code` comes from). `code` is the 182 | actual JavaScript code. `tradeship.import()` returns a promise that, when 183 | resolved, gives the resulting new code. 184 | 185 | ## How it works 186 | `tradeship` analyzes dependencies from three sources: node.js standard 187 | libraries, package.json dependencies, and other files within your project 188 | directory. 189 | 190 | For each dependency it finds, tradeship: 191 | 192 | - **Determines potential import names**: 193 | 194 | An import name is a variable name you might see in code that refers to the 195 | dependency. For instance, the import name `fs` would refer to the `fs` node.js 196 | standard library. The import name `React` would refer to the `react` npm 197 | package. 198 | 199 | For node.js standard libraries and package.json packages, if the 200 | library/package name is itself a valid JavaScript identifier, it and its 201 | capitalized version are potential import names (e.g. `react` and `React` for 202 | the `react` npm package). If the library/package name has non-word characters 203 | or underscores, it is split on `[\W_]+` and the parts are joined, both in 204 | camel and class case, to get two more import names (`childProcess` and 205 | `ChildProcess` for the `child_process` node.js standard library). 206 | 207 | There are various package.json packages that are imported under common aliases 208 | known by the community (e.g. `$` for jQuery, `_` for lodash/underscore, `test` 209 | for ava, etc.). By analyzing more than 100 GB of public JS code on GitHub, 210 | tradeship knows many such aliases, and will import the associated package. 211 | 212 | For project files, the code is statically analyzed to find an import name. For 213 | instance, if you write `module.exports = SomeExport;`, `SomeExport` will be an 214 | import name. This is one simple case; there are many others that tradeship 215 | parses, including those with ES6 `export` syntax. In addition to the analyzed 216 | import name, tradeship also uses the file path's base name as an import name, 217 | using the same logic as defined for node.js standard library names above. 218 | 219 | - **Determines properties**: 220 | 221 | These are JavaScript object properties that are exported by the dependency. 222 | For instance, `readFile` is a property of the `fs` node.js standard library. 223 | `Component` is a property of the `react` npm package. The import name of 224 | properties is equivalent to the property name. 225 | 226 | For node.js standard libraries and package.json packages, the library is 227 | loaded within a separate node.js process, and properties are extracted using 228 | `Object.keys()`. 229 | 230 | For project files, the code is statically analyzed to find properties. For 231 | instance, if you write `exports.someProperty = ...;`, `someProperty` will be 232 | a parsed property. This is one simple case; there are many others that 233 | tradeship parses, including those with ES6 `export` syntax. 234 | 235 | Then, tradeship analyzes your code for identifiers that aren't defined. Each 236 | identifier is a potential import name, and tradeship searches for the 237 | corresponding dependency or property. If it finds a match, it adds the 238 | appropriate import to the code. If multiple dependencies or properties match 239 | a given import name, tradeship prioritizes them as follows: 240 | 241 | 1. Project file dependency (highest priority) 242 | 1. Project file property 243 | 1. package.json dependency 244 | 1. package.json property 245 | 1. Node.js standard library dependency 246 | 1. Node.js standard library property (lowest priority) 247 | 248 | tradeship groups all node.js and package.json dependencies together, sorted 249 | lexicographically. It then adds a blank line and all project file dependencies, 250 | also sorted lexicographically. 251 | 252 | tradeship finds all imports that aren't used and removes them. 253 | 254 | ## License 255 | [MIT](https://github.com/karthikv/tradeship/blob/master/LICENSE.md) 256 | 257 | ship vector icon by [Vecteezy](https://www.vecteezy.com/) 258 | 259 | [tradeship-url]: https://github.com/karthikv/tradeship 260 | [logo-image]: https://raw.githubusercontent.com/karthikv/tradeship/master/logo.png 261 | [travis-image]: https://img.shields.io/travis/karthikv/tradeship/master.svg?label=linux 262 | [travis-url]: https://travis-ci.org/karthikv/tradeship 263 | [appveyor-image]: https://img.shields.io/appveyor/ci/karthikv/tradeship/master.svg?label=windows 264 | [appveyor-url]: https://ci.appveyor.com/project/karthikv/tradeship 265 | [npm-image]: https://img.shields.io/npm/v/tradeship.svg 266 | [npm-url]: https://npmjs.org/package/tradeship 267 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const importer = require("./lib/importer"); 4 | 5 | exports.import = function(dir, code) { 6 | return importer.run(dir, code); 7 | }; 8 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "6" 4 | - nodejs_version: "7" 5 | cache: [node_modules] 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm install 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm test 13 | build: off 14 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint no-console: ["error", { allow: ["error"] }] */ 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | const { readFile, stat, writeFile } = require("./lib/common"); 8 | const importer = require("./lib/importer"); 9 | 10 | const args = process.argv.slice(2); 11 | const options = { 12 | stdin: false, 13 | write: false, 14 | help: false, 15 | version: false, 16 | path: null 17 | }; 18 | 19 | args.forEach(arg => { 20 | if (arg === "-s" || arg === "--stdin") { 21 | options.stdin = true; 22 | } else if (arg === "-w" || arg === "--write") { 23 | options.write = true; 24 | } else if (arg === "-h" || arg === "--help") { 25 | options.help = true; 26 | } else if (arg === "-v" || arg === "--version") { 27 | options.version = true; 28 | } else if (arg[0] !== "-") { 29 | if (options.path) { 30 | console.error("Can only specify a single path"); 31 | process.exit(1); 32 | } 33 | options.path = arg; 34 | } else { 35 | console.error("Unexpected option", arg); 36 | process.exit(1); 37 | } 38 | }); 39 | 40 | if (options.help || args.length === 0) { 41 | const lines = [ 42 | "Usage: tradeship [options] [path]", 43 | "Automatically imports missing JS dependencies and removes unused ones.", 44 | "", 45 | "Options:", 46 | "-s, --stdin read contents from stdin", 47 | "-w, --write write output to source file (careful!)", 48 | "-h, --help print help", 49 | "-v, --version print version", 50 | "", 51 | "Arguments:", 52 | "[path] Relative imports and available npm packages will be determined", 53 | " from this path. If not --stdin, input code will be read from this", 54 | " path. If --write, new code will be written to this path." 55 | ]; 56 | console.error(lines.join("\n")); 57 | process.exit(0); 58 | } 59 | 60 | if (options.version) { 61 | const pkg = require("./package.json"); 62 | console.error(pkg.version); 63 | process.exit(0); 64 | } 65 | 66 | if (!options.path) { 67 | let message = "Must specify a path "; 68 | if (options.stdin) { 69 | message += "to resolve relative imports and find available npm packages."; 70 | } else { 71 | message += "that contains the input source code."; 72 | } 73 | console.error(message); 74 | process.exit(1); 75 | } 76 | 77 | let codePromise; 78 | if (options.stdin) { 79 | codePromise = new Promise((resolve, reject) => { 80 | let contents = ""; 81 | process.stdin.on("data", chunk => contents += chunk); 82 | process.stdin.on("end", () => resolve(contents)); 83 | process.stdin.on("error", reject); 84 | }); 85 | } else { 86 | codePromise = readFile(options.path, "utf8"); 87 | } 88 | 89 | let code; 90 | codePromise 91 | .then(c => { 92 | code = c; 93 | if (options.stdin) { 94 | return stat(options.path); 95 | } 96 | return null; 97 | }) 98 | .catch(err => { 99 | if (err.code === "ENOENT" && options.write) { 100 | return null; 101 | } 102 | throw err; 103 | }) 104 | .then(s => { 105 | if (s && s.isDirectory()) { 106 | return importer.run(options.path, code); 107 | } else { 108 | return importer.run(path.dirname(options.path), code); 109 | } 110 | }) 111 | .then(newCode => { 112 | if (options.write) { 113 | return writeFile(options.path, newCode); 114 | } else { 115 | process.stdout.write(newCode); 116 | } 117 | }) 118 | .catch(err => { 119 | if (err instanceof SyntaxError && err.loc) { 120 | const codeFrame = require("babel-code-frame"); 121 | const { line, column } = err.loc; 122 | 123 | const frame = codeFrame(code, line, column + 1, { highlightCode: true }); 124 | console.error(`${err.message}\n${frame}`); 125 | process.exit(1); 126 | } 127 | 128 | console.error(err); 129 | process.exit(1); 130 | }); 131 | -------------------------------------------------------------------------------- /fixtures/component.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | 3 | module.exports = class Component extends React.Component { 4 | render() { 5 | return

Paragraph

; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /fixtures/default-anonymous.js: -------------------------------------------------------------------------------- 1 | export default 3; 2 | -------------------------------------------------------------------------------- /fixtures/es6.js: -------------------------------------------------------------------------------- 1 | const es6 = {}; 2 | export default es6; 3 | export const prop = 3; 4 | -------------------------------------------------------------------------------- /fixtures/parsing-error.js: -------------------------------------------------------------------------------- 1 | class A extends {} 2 | -------------------------------------------------------------------------------- /lib/ast-helpers.js: -------------------------------------------------------------------------------- 1 | const flowNodes = new Set([ 2 | "DeclaredPredicate", 3 | "InferredPredicate", 4 | "DeclareClass", 5 | "FunctionTypeAnnotation", 6 | "TypeAnnotation", 7 | "DeclareFunction", 8 | "DeclareVariable", 9 | "DeclareModule", 10 | "DeclareModuleExports", 11 | "DeclareTypeAlias", 12 | "DeclareInterface", 13 | "InterfaceExtends", 14 | "InterfaceDeclaration", 15 | "TypeAlias", 16 | "TypeParameter", 17 | "TypeParameterDeclaration", 18 | "TypeParameterInstantiation", 19 | "ObjectTypeIndexer", 20 | "ObjectTypeProperty", 21 | "ObjectTypeCallProperty", 22 | "ObjectTypeAnnotation", 23 | "QualifiedTypeIdentifier", 24 | "GenericTypeAnnotation", 25 | "TypeofTypeAnnotation", 26 | "TupleTypeAnnotation", 27 | "FunctionTypeParam", 28 | "AnyTypeAnnotation", 29 | "VoidTypeAnnotation", 30 | "BooleanTypeAnnotation", 31 | "MixedTypeAnnotation", 32 | "EmptyTypeAnnotation", 33 | "NumberTypeAnnotation", 34 | "StringTypeAnnotation", 35 | "BooleanLiteralTypeAnnotation", 36 | "NullLiteralTypeAnnotation", 37 | "ThisTypeAnnotation", 38 | "ExistsTypeAnnotation", 39 | "ArrayTypeAnnotation", 40 | "NullableTypeAnnotation", 41 | "IntersectionTypeAnnotation", 42 | "UnionTypeAnnotation", 43 | "Variance", 44 | "TypeCastExpression", 45 | "ClassImplements" 46 | ]); 47 | 48 | exports.isFlowNode = function(node) { 49 | return flowNodes.has(node.type); 50 | }; 51 | 52 | exports.isFlowImport = function(node) { 53 | return node.importKind && node.importKind !== "value"; 54 | }; 55 | 56 | exports.isFlowExport = function(node) { 57 | return node.exportKind && node.exportKind !== "value"; 58 | }; 59 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const yaml = require("js-yaml"); 5 | const path = require("path"); 6 | 7 | exports.debug = require("debug")("tradeship"); 8 | exports.pkgRegex = /^(@[\w\.\-]+\/)?[\w\.\-]+$/; 9 | exports.fileRegex = /^\.?\.?\//; 10 | exports.whiteRegex = /^\s*$/; 11 | 12 | exports.readFile = promisify(fs.readFile, fs); 13 | exports.writeFile = promisify(fs.writeFile, fs); 14 | exports.stat = promisify(fs.stat, fs); 15 | exports.readdir = promisify(fs.readdir, fs); 16 | 17 | const metaPromises = {}; 18 | exports.findPkgMeta = function(dir) { 19 | if (!path.isAbsolute(dir)) { 20 | dir = path.resolve(dir); 21 | } 22 | 23 | if (!metaPromises[dir]) { 24 | metaPromises[dir] = findPkgJSON(dir).then(({ root, contents }) => { 25 | const meta = contents ? exports.tryJSONParse(contents, {}) : {}; 26 | meta.root = root; 27 | 28 | return getEslintConfig(meta, dir).then(config => { 29 | meta.env = config.env || 30 | { 31 | node: true, 32 | browser: true, 33 | es6: true 34 | }; 35 | meta.globals = config.globals || {}; 36 | return meta; 37 | }); 38 | }); 39 | } 40 | 41 | return metaPromises[dir]; 42 | }; 43 | 44 | exports.tryJSONParse = function(contents, defaultValue) { 45 | try { 46 | return JSON.parse(contents) || defaultValue; 47 | } catch (err) { 48 | if (err instanceof SyntaxError && err.message.indexOf("JSON") !== -1) { 49 | return defaultValue; 50 | } 51 | throw err; 52 | } 53 | }; 54 | 55 | function findPkgJSON(dir) { 56 | if (!path.isAbsolute(dir)) { 57 | dir = path.resolve(dir); 58 | } 59 | 60 | return exports 61 | .readFile(path.join(dir, "package.json"), "utf8") 62 | .then(contents => ({ root: dir, contents })) 63 | .catch(err => { 64 | if (err.code === "ENOENT") { 65 | const parent = path.join(dir, ".."); 66 | if (parent === dir) { 67 | // at the root; couldn't find anything 68 | return { root: null, contents: null }; 69 | } 70 | return findPkgJSON(parent); 71 | } else { 72 | throw err; 73 | } 74 | }); 75 | } 76 | 77 | const eslintPriority = { 78 | ".eslintrc.js": 5, 79 | ".eslintrc.yaml": 4, 80 | ".eslintrc.yml": 3, 81 | ".eslintrc.json": 2, 82 | ".eslintrc": 1, 83 | "package.json": 0 84 | }; 85 | const eslintFiles = new Set(Object.keys(eslintPriority)); 86 | 87 | function getEslintConfig(meta, dir) { 88 | if (!path.isAbsolute(dir)) { 89 | dir = path.resolve(dir); 90 | } 91 | 92 | return exports 93 | .readdir(dir) 94 | .then(files => { 95 | const configFiles = files.filter(f => eslintFiles.has(f)); 96 | if (configFiles.length === 0) { 97 | return null; 98 | } 99 | 100 | const bestFile = configFiles.reduce( 101 | (best, file) => 102 | eslintPriority[file] > eslintPriority[best] ? file : best, 103 | configFiles[0] 104 | ); 105 | const bestFilePath = path.join(dir, bestFile); 106 | 107 | return exports.readFile(bestFilePath, "utf8").then( 108 | contents => parseConfig(bestFilePath, contents), 109 | // ignore errors if we can't read the file 110 | () => null 111 | ); 112 | }) 113 | .then(config => { 114 | if (config) { 115 | return config; 116 | } 117 | 118 | const parent = path.join(dir, ".."); 119 | if (parent === dir) { 120 | return { 121 | env: null, 122 | globals: null 123 | }; 124 | } 125 | return getEslintConfig(meta, parent); 126 | }); 127 | } 128 | 129 | function parseConfig(filePath, contents) { 130 | let config; 131 | switch (path.basename(filePath)) { 132 | case ".eslintrc.js": 133 | return require(filePath); 134 | 135 | case ".eslintrc.yaml": 136 | case ".eslintrc.yml": 137 | return tryYAMLParse(contents, null); 138 | 139 | case ".eslintrc.json": 140 | return exports.tryJSONParse(contents, null); 141 | 142 | case ".eslintrc": 143 | config = exports.tryJSONParse(contents, null); 144 | if (config) { 145 | return config; 146 | } 147 | return tryYAMLParse(contents, null); 148 | 149 | case "package.json": 150 | return exports.tryJSONParse(contents, {}).eslintConfig || null; 151 | 152 | default: 153 | throw new Error(`bad eslint file: ${filePath}`); 154 | } 155 | } 156 | 157 | function tryYAMLParse(contents, defaultValue) { 158 | try { 159 | return yaml.safeLoad(contents) || defaultValue; 160 | } catch (err) { 161 | if (err instanceof yaml.YAMLException) { 162 | return defaultValue; 163 | } 164 | throw err; 165 | } 166 | } 167 | 168 | function promisify(fn, context) { 169 | return function(...args) { 170 | return new Promise((resolve, reject) => { 171 | args.push(function(err, ...callbackArgs) { 172 | if (err) { 173 | reject(err); 174 | } else { 175 | if (callbackArgs.length === 0) { 176 | resolve(); 177 | } else if (callbackArgs.length === 1) { 178 | resolve(callbackArgs[0]); 179 | } else { 180 | resolve(callbackArgs); 181 | } 182 | } 183 | }); 184 | 185 | fn.apply(context, args); 186 | }); 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /lib/dep-registry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { spawn } = require("child_process"); 4 | const crypto = require("crypto"); 5 | const https = require("https"); 6 | const os = require("os"); 7 | const path = require("path"); 8 | const qs = require("querystring"); 9 | const repl = require("repl"); 10 | const util = require("util"); 11 | 12 | const { 13 | debug, 14 | pkgRegex, 15 | readFile, 16 | readdir, 17 | stat, 18 | tryJSONParse, 19 | writeFile 20 | } = require("./common"); 21 | const parser = require("./parser"); 22 | const ProgressBar = require("../patches/progress.js"); 23 | 24 | const identRegex = /^[$a-z_][0-9a-z_$]*$/i; 25 | const nonWordRegex = /[\W_]+/g; 26 | const propsScript = [ 27 | 'const object = require("%s");', 28 | "const json = JSON.stringify({", 29 | " props: Object.keys(object),", 30 | " hasDefault: Boolean(object.__esModule) && Boolean(object.default)", 31 | "});", 32 | "console.log(json);" 33 | ].join(""); 34 | 35 | const isNodeID = {}; 36 | repl._builtinLibs.forEach(id => isNodeID[id] = true); 37 | 38 | const cacheVersion = "v2"; 39 | 40 | class DepRegistry { 41 | /* public interface */ 42 | 43 | static populate(dir, meta) { 44 | if (this.promises && this.promises[dir]) { 45 | return this.promises[dir].then(instance => instance.copy(meta.env)); 46 | } 47 | 48 | const instance = new DepRegistry(meta.env); 49 | this.promises = this.promises || {}; 50 | this.promises[dir] = instance.populate(meta).then(() => instance); 51 | return this.promises[dir]; 52 | } 53 | 54 | constructor(env = null, registry = null, deps = null) { 55 | this.env = env; 56 | 57 | // registry[id] is an entry that has the given id 58 | this.registry = registry || {}; 59 | 60 | // deps[name] is a dep that corresponds to the identifier name 61 | this.deps = deps || {}; 62 | } 63 | 64 | search(name) { 65 | const dep = this.deps[name]; 66 | if (dep && !this.env.node && isNodeID[dep.id]) { 67 | return null; 68 | } 69 | return dep; 70 | } 71 | 72 | /* private interface */ 73 | 74 | copy(env) { 75 | return new DepRegistry(env, this.registry, this.deps); 76 | } 77 | 78 | populate(meta) { 79 | const hash = crypto.createHash("sha256"); 80 | hash.update(meta.root || "-"); 81 | const cachePath = path.join( 82 | os.tmpdir(), 83 | `tradeship-dir-${hash.digest("hex")}` 84 | ); 85 | 86 | return Promise 87 | .all([this.readCache(cachePath), this.findSourceFiles(meta.root)]) 88 | .then(([cache, sourceFiles]) => { 89 | this.registry._version = cache._version; 90 | sourceFiles = sourceFiles || []; 91 | 92 | const dependencies = Object.assign( 93 | {}, 94 | meta.devDependencies, 95 | meta.dependencies 96 | ); 97 | const total = repl._builtinLibs.length + 98 | Object.keys(dependencies).length + 99 | sourceFiles.length; 100 | const progress = new ProgressBar( 101 | "Populating dependencies [:bar] :percent :current/:total", 102 | { 103 | total, 104 | incomplete: " ", 105 | width: Math.min(total, 40), 106 | clear: true 107 | } 108 | ); 109 | 110 | const promises = []; 111 | const tick = () => progress.tick(); 112 | const entriesByID = {}; 113 | 114 | repl._builtinLibs.forEach(id => { 115 | const entry = this.register(cache, id, process.version); 116 | if (entry) { 117 | this.populateIdents(entry, id); 118 | promises.push(this.populatePropsDefaults(entry, id).then(tick)); 119 | entriesByID[id] = entry; 120 | } else { 121 | progress.tick(); 122 | } 123 | }); 124 | 125 | for (const id in dependencies) { 126 | const entry = this.register(cache, id, dependencies[id]); 127 | if (entry) { 128 | this.populateIdents(entry, id); 129 | const absPath = path.join(meta.root, "node_modules", id); 130 | 131 | promises.push( 132 | this.populatePropsDefaults(entry, absPath).then(tick) 133 | ); 134 | entriesByID[id] = entry; 135 | } else { 136 | progress.tick(); 137 | } 138 | } 139 | 140 | promises.push(this.populateDockIdents(entriesByID)); 141 | 142 | sourceFiles.forEach(sf => { 143 | const entry = this.register(cache, sf.path, sf.version); 144 | if (entry) { 145 | promises.push(this.populateFile(entry, sf.path).then(tick)); 146 | } else { 147 | progress.tick(); 148 | } 149 | }); 150 | 151 | return Promise.all(promises); 152 | }) 153 | .then(() => writeFile(cachePath, JSON.stringify(this.registry))) 154 | .then(() => this.computeDeps()); 155 | } 156 | 157 | readCache(cachePath) { 158 | return readFile(cachePath, "utf8") 159 | .catch(err => { 160 | if (err.code === "ENOENT") { 161 | return null; 162 | } 163 | throw err; 164 | }) 165 | .then(contents => { 166 | const cache = tryJSONParse(contents, {}); 167 | if (cache._version !== cacheVersion) { 168 | // reset cache, setting the new version 169 | return { _version: cacheVersion }; 170 | } 171 | return cache; 172 | }); 173 | } 174 | 175 | populateIdents(entry, id) { 176 | const ext = path.extname(id); 177 | let base; 178 | if (ext === ".js" || ext === ".jsx") { 179 | base = path.basename(id, ext); 180 | } else { 181 | base = path.basename(id); 182 | } 183 | 184 | if (identRegex.test(base)) { 185 | entry.idents.push(base); 186 | } 187 | 188 | const camelCase = base 189 | .split(nonWordRegex) 190 | .filter(p => p !== "") 191 | .map((p, i) => i === 0 ? p : p[0].toUpperCase() + p.slice(1)) 192 | .join(""); 193 | 194 | if (camelCase.length > 0) { 195 | if (camelCase !== base) { 196 | entry.idents.push(camelCase); 197 | } 198 | 199 | const classCase = camelCase[0].toUpperCase() + camelCase.slice(1); 200 | entry.idents.push(classCase); 201 | } 202 | } 203 | 204 | populatePropsDefaults(entry, id) { 205 | const escapedID = id.replace(/"/g, '\\"'); 206 | const cmd = spawn("node", ["-e", util.format(propsScript, escapedID)]); 207 | 208 | const promise = new Promise((resolve, reject) => { 209 | let stdout = ""; 210 | let stderr = ""; 211 | 212 | cmd.stdout.on("data", chunk => stdout += chunk); 213 | cmd.stderr.on("data", chunk => stderr += chunk); 214 | 215 | cmd.on("close", code => { 216 | if (code === 0) { 217 | resolve(stdout); 218 | } else { 219 | reject(new Error( 220 | `bad code: ${code}; stderr: ${stderr}`, 221 | code, 222 | stderr 223 | )); 224 | } 225 | }); 226 | cmd.on("error", err => reject(err)); 227 | }); 228 | 229 | return promise.then( 230 | json => { 231 | const { props, hasDefault } = tryJSONParse(json, {}); 232 | 233 | if (props) { 234 | props.forEach(p => entry.props.push(p)); 235 | } 236 | if (hasDefault) { 237 | entry.useDefault = true; 238 | } 239 | }, 240 | // ignore errors from requiring this dep; we just won't populate props 241 | err => debug("Couldn't populate props for %s: %O", path.basename(id), err) 242 | ); 243 | } 244 | 245 | populateDockIdents(entriesByID) { 246 | const ids = Object.keys(entriesByID); 247 | if (ids.length === 0) { 248 | return Promise.resolve(); 249 | } 250 | 251 | const options = { 252 | hostname: "dock.karthikv.net", 253 | path: `/load?${qs.stringify({ ids: ids.join(",") })}` 254 | }; 255 | 256 | return new Promise((resolve, reject) => { 257 | const request = https.request(options, response => { 258 | let body = ""; 259 | response.setEncoding("utf8"); 260 | response.on("data", chunk => body += chunk); 261 | response.on("end", () => resolve(body)); 262 | }); 263 | 264 | request.on("error", reject); 265 | request.end(); 266 | }).then(body => { 267 | const identMap = tryJSONParse(body, {}); 268 | for (const pkg in identMap) { 269 | const entry = entriesByID[pkg]; 270 | // take the top three most-frequent idents 271 | const idents = identMap[pkg].slice(0, 3).concat(entry.idents); 272 | entry.idents = Array.from(new Set(idents)); 273 | } 274 | }); 275 | } 276 | 277 | findSourceFiles(dir) { 278 | if (!dir) { 279 | return []; 280 | } 281 | 282 | return readdir(dir).then(files => { 283 | const sourceFiles = []; 284 | const promises = files.map(file => { 285 | if (file[0] === ".") { 286 | return Promise.resolve(); 287 | } 288 | 289 | const filePath = path.join(dir, file); 290 | return stat(filePath).then(s => { 291 | const ext = path.extname(file); 292 | 293 | if (s.isFile() && (ext === ".js" || ext === ".jsx")) { 294 | sourceFiles.push({ 295 | path: filePath, 296 | version: s.mtime.getTime() 297 | }); 298 | } else if ( 299 | s.isDirectory() && 300 | file !== "node_modules" && 301 | file !== "bower_components" 302 | ) { 303 | return this 304 | .findSourceFiles(filePath) 305 | .then(s => sourceFiles.push(...s)); 306 | } 307 | }); 308 | }); 309 | 310 | return Promise.all(promises).then(() => sourceFiles); 311 | }); 312 | } 313 | 314 | populateFile(entry, filePath) { 315 | return readFile(filePath, "utf8") 316 | .then(code => parser.run(path.dirname(filePath), code).catch(err => { 317 | debug("Parsing failed for %s: %O", filePath, err); 318 | return null; 319 | })) 320 | .then(exported => { 321 | if (!exported || !exported.hasExports) { 322 | return; 323 | } 324 | 325 | this.populateIdents(entry, filePath); 326 | exported.idents.forEach(i => entry.idents.push(i)); 327 | exported.props.forEach(p => entry.props.push(p)); 328 | 329 | if (exported.hasDefault) { 330 | entry.useDefault = true; 331 | } 332 | }); 333 | } 334 | 335 | register(cache, id, version) { 336 | if (!cache[id] || cache[id].version !== version) { 337 | const entry = { 338 | version, 339 | idents: [], 340 | props: [], 341 | useDefault: false 342 | }; 343 | this.registry[id] = entry; 344 | return entry; 345 | } else { 346 | this.registry[id] = cache[id]; 347 | // don't return entry; the cached data is valid 348 | return null; 349 | } 350 | } 351 | 352 | computeDeps() { 353 | for (const id in this.registry) { 354 | if (id === "_version") { 355 | continue; 356 | } 357 | 358 | let priority; 359 | if (isNodeID[id]) { 360 | // node library; lowest priority 361 | priority = 1; 362 | } else if (pkgRegex.test(id)) { 363 | // package.json dependency; middle priority 364 | priority = 2; 365 | } else { 366 | // project file; highest priority 367 | priority = 3; 368 | } 369 | 370 | const { idents, props, useDefault } = this.registry[id]; 371 | idents.forEach(name => this.associate({ 372 | name, 373 | id, 374 | priority, 375 | type: useDefault ? types.default : types.ident 376 | })); 377 | props.forEach(name => this.associate({ 378 | name, 379 | id, 380 | priority, 381 | type: types.prop 382 | })); 383 | } 384 | } 385 | 386 | associate({ name, id, priority, type }) { 387 | const dep = this.deps[name]; 388 | if ( 389 | !dep || 390 | dep.priority < priority || 391 | // idents and defaults are prioritized over props 392 | dep.priority === priority && 393 | dep.type === types.prop && 394 | type !== types.prop 395 | ) { 396 | this.deps[name] = { id, priority, type }; 397 | } 398 | } 399 | } 400 | 401 | DepRegistry.types = { 402 | ident: 1, 403 | default: 2, 404 | prop: 3 405 | }; 406 | const types = DepRegistry.types; 407 | 408 | module.exports = DepRegistry; 409 | -------------------------------------------------------------------------------- /lib/importer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | const { fileRegex, findPkgMeta, whiteRegex } = require("./common"); 6 | const DepRegistry = require("./dep-registry"); 7 | const findImports = require("../visits/find-imports.js"); 8 | const findStyle = require("../visits/find-style.js"); 9 | const resolveJSX = require("../visits/resolve-jsx.js"); 10 | const walker = require("./walker.js"); 11 | 12 | const backslashRegex = /\\/g; 13 | 14 | exports.run = function(dir, code, override) { 15 | return findPkgMeta(dir).then(meta => { 16 | if (override) { 17 | meta = Object.assign({}, meta, override); 18 | } 19 | 20 | const context = walker.run(meta, code, [ 21 | findImports, 22 | findStyle, 23 | resolveJSX 24 | ]); 25 | 26 | if (context.error) { 27 | throw context.error; 28 | } 29 | 30 | // resolve all relative dependency paths 31 | context.reqs.forEach(req => { 32 | if (fileRegex.test(req.depID)) { 33 | // must split on forward slash so resolving works correctly on Windows 34 | req.depID = path.resolve(dir, ...req.depID.split("/")); 35 | } 36 | }); 37 | 38 | return DepRegistry.populate(dir, meta) 39 | .then(depRegistry => rewriteCode(context, depRegistry, dir)); 40 | }); 41 | }; 42 | 43 | function rewriteCode(context, depRegistry, dir) { 44 | const { linesToRemove, libsToAdd } = resolveIdents(context, depRegistry); 45 | let requiresText = composeRequires(context.style, dir, libsToAdd); 46 | 47 | let newCode = ""; 48 | let targetLine = null; 49 | 50 | if (context.reqs.length > 0) { 51 | targetLine = context.reqs[0].node.loc.start.line; 52 | } else if (requiresText.length > 0) { 53 | const directive = context.getUseStrict(); 54 | 55 | if (directive) { 56 | if (context.endsLine(directive)) { 57 | requiresText = "\n" + requiresText + "\n"; 58 | targetLine = directive.loc.end.line; 59 | } else { 60 | const { loc: { end } } = directive; 61 | const endLineText = context.getLineText(end.line); 62 | 63 | // use strict is on the same line as some other text. Unfortunately, 64 | // with the current architecture, it's not easy to add the requires 65 | // between use strict and the other text, as we operate on a 66 | // line-by-line basis. Consequently, we resort to directly modifying 67 | // the line. This is ugly, but it's not worth changing the architecture 68 | // for a quite rare edge case like this one. Note that we can't add 69 | // a line to textLines because that would change line numbers and 70 | // break our logic below. 71 | context.textLines[end.line - 1] = endLineText.slice(0, end.column) + 72 | "\n\n" + 73 | requiresText + 74 | "\n\n" + 75 | endLineText.slice(end.column + 1); 76 | } 77 | } else { 78 | requiresText = requiresText + "\n"; 79 | targetLine = 0; 80 | } 81 | } 82 | 83 | // start at non-existent line 0 to allow requiresText to be prepended 84 | linesToRemove.add(0); 85 | for (let line = 0; line <= context.textLines.length; line++) { 86 | if (!linesToRemove.has(line)) { 87 | newCode += context.getLineText(line) + "\n"; 88 | } 89 | if (line === targetLine && requiresText.length > 0) { 90 | newCode += requiresText + "\n"; 91 | } 92 | } 93 | 94 | if (newCode.slice(-1) !== "\n") { 95 | newCode = newCode + "\n"; 96 | } else if (newCode.slice(-2) === "\n\n") { 97 | newCode = newCode.slice(0, -1); 98 | } 99 | return newCode; 100 | } 101 | 102 | function resolveIdents(context, depRegistry) { 103 | const missingIdents = findMissingIdents(context); 104 | const fixableIdents = missingIdents.filter(i => depRegistry.search(i)); 105 | 106 | const deps = fixableIdents.map(i => depRegistry.search(i)); 107 | const depIDs = context.reqs.map(req => req.depID).concat(deps.map(d => d.id)); 108 | 109 | const libsToAdd = {}; 110 | depIDs.forEach( 111 | id => libsToAdd[id] = { 112 | idents: [], 113 | defaults: [], 114 | props: [] 115 | } 116 | ); 117 | 118 | const { types } = DepRegistry; 119 | fixableIdents.forEach((ident, i) => { 120 | const { id, type } = deps[i]; 121 | const lib = libsToAdd[id]; 122 | 123 | switch (type) { 124 | case types.ident: 125 | lib.idents.push(ident); 126 | break; 127 | case types.default: 128 | lib.defaults.push(ident); 129 | break; 130 | case types.prop: 131 | lib.props.push(ident); 132 | break; 133 | default: 134 | throw new Error("unexpected type " + type); 135 | } 136 | }); 137 | 138 | const nodesToRemove = []; 139 | context.reqs.forEach(({ node, depID, idents, defaults, props }) => { 140 | const lib = libsToAdd[depID]; 141 | 142 | if (node) { 143 | nodesToRemove.push(node); 144 | } 145 | if (idents) { 146 | lib.idents.push(...idents); 147 | } 148 | if (defaults) { 149 | lib.defaults.push(...defaults); 150 | } 151 | if (props) { 152 | lib.props.push(...props); 153 | } 154 | }); 155 | 156 | const linesToRemove = new Set(); 157 | nodesToRemove.forEach(({ loc: { start, end } }) => { 158 | for (let line = start.line; line <= end.line; line++) { 159 | linesToRemove.add(line); 160 | } 161 | }); 162 | 163 | removeExtraLines(context, libsToAdd, linesToRemove); 164 | return { libsToAdd, linesToRemove }; 165 | } 166 | 167 | function findMissingIdents(context) { 168 | const globalScope = context.getGlobalScope(); 169 | const missingIdents = globalScope.through 170 | .filter(ref => { 171 | // ignore: 172 | // - identifiers prefixed with typeof 173 | // - writes to undeclared variables 174 | const parent = ref.identifier.parent; 175 | const isTypeOf = parent && 176 | parent.type === "UnaryExpression" && 177 | parent.operator === "typeof"; 178 | 179 | return !isTypeOf && !ref.writeExpr; 180 | }) 181 | .map(ref => ref.identifier.name) 182 | .filter(name => !globalScope.set.get(name)); 183 | return Array.from(new Set(missingIdents)); 184 | } 185 | 186 | function removeExtraLines(context, libsToAdd, linesToRemove) { 187 | const sortedLinesToRemove = Array.from(linesToRemove) 188 | .sort((l1, l2) => l1 - l2); 189 | let prevLine = sortedLinesToRemove[0]; 190 | 191 | // If the intermediate lines between two subsequent lines to remove are all 192 | // blank, remove the intermediate lines as well. 193 | sortedLinesToRemove.slice(1).forEach(line => { 194 | let allBlank = true; 195 | for (let j = prevLine + 1; j < line; j++) { 196 | allBlank = allBlank && whiteRegex.test(context.getLineText(j)); 197 | } 198 | 199 | if (allBlank) { 200 | for (let j = prevLine + 1; j < line; j++) { 201 | linesToRemove.add(j); 202 | } 203 | } 204 | prevLine = line; 205 | }); 206 | 207 | const hasNoImports = Object.keys(libsToAdd).every(id => { 208 | const { idents, defaults, props } = libsToAdd[id]; 209 | return idents.length === 0 && defaults.length === 0 && props.length === 0; 210 | }); 211 | 212 | // If we're removing all imports, remove the blank line after them. 213 | if ( 214 | hasNoImports && 215 | sortedLinesToRemove.length > 0 && 216 | whiteRegex.test(context.getLineText(prevLine + 1)) 217 | ) { 218 | linesToRemove.add(prevLine + 1); 219 | } 220 | } 221 | 222 | function composeRequires(style, dir, libs) { 223 | // turn absolute dep ids into relative ones 224 | Object.keys(libs).forEach(id => { 225 | if (path.isAbsolute(id)) { 226 | // node module ids always have unix-style separators 227 | let newID = path.relative(dir, id).replace(backslashRegex, "/"); 228 | if (newID[0] !== ".") { 229 | newID = `./${newID}`; 230 | } 231 | libs[newID] = libs[id]; 232 | delete libs[id]; 233 | } 234 | }); 235 | 236 | const ids = Object.keys(libs); 237 | const externalIDs = ids 238 | .filter(i => !fileRegex.test(i)) 239 | .sort(compareByBasename); 240 | const localIDs = ids.filter(i => fileRegex.test(i)).sort(compareByBasename); 241 | 242 | const externalStatements = []; 243 | const localStatements = []; 244 | 245 | externalIDs.forEach(id => 246 | externalStatements.push(...composeStatements(style, libs[id], id))); 247 | localIDs.forEach(id => 248 | localStatements.push(...composeStatements(style, libs[id], id))); 249 | 250 | const statements = externalStatements; 251 | if (externalStatements.length > 0 && localStatements.length > 0) { 252 | // add blank line between external and local imports 253 | statements.push(""); 254 | } 255 | statements.push(...localStatements); 256 | 257 | return statements.join("\n"); 258 | } 259 | 260 | function compareByBasename(id1, id2) { 261 | const base1 = path.basename(id1); 262 | const base2 = path.basename(id2); 263 | 264 | if (base1 !== base2) { 265 | return base1 < base2 ? -1 : 1; 266 | } 267 | return id1 < id2 ? -1 : 1; 268 | } 269 | 270 | function composeStatements(style, lib, id) { 271 | const statements = []; 272 | const { idents, defaults, props } = lib; 273 | 274 | if (idents.length === 0 && defaults.length === 0 && props.length === 0) { 275 | // nothing to require 276 | return statements; 277 | } 278 | 279 | idents.sort(); 280 | defaults.sort(); 281 | props.sort(); 282 | 283 | if (style.requireKeyword === "require") { 284 | statements.push( 285 | ...idents.map(ident => composeRequireStatement({ style, id, ident })), 286 | ...defaults.map(def => composeRequireStatement({ style, id, def })) 287 | ); 288 | 289 | if (props.length > 0) { 290 | statements.push(composeRequireStatement({ style, id, props })); 291 | } 292 | } else { 293 | let leftDefaults = defaults; 294 | if (props && props.length > 0) { 295 | statements.push( 296 | composeImportStatement({ style, id, props, def: defaults[0] }) 297 | ); 298 | leftDefaults = defaults.slice(1); 299 | } 300 | 301 | statements.push( 302 | ...leftDefaults.map((def, i) => 303 | composeImportStatement({ style, id, ident: idents[i], def })), 304 | ...idents 305 | .slice(leftDefaults.length) 306 | .map(ident => composeImportStatement({ style, id, ident })) 307 | ); 308 | } 309 | 310 | return statements; 311 | } 312 | 313 | function composeRequireStatement({ style, id, ident, def, props, multiline }) { 314 | if (ident && def || ident && props || def && props) { 315 | throw new Error("only one of ident, default, and props must be specified"); 316 | } 317 | 318 | const { kind, quote, semi } = style; 319 | const requireText = `require(${quote}${id}${quote})`; 320 | 321 | if (ident) { 322 | return `${kind} ${ident} = ${requireText}${semi}`; 323 | } else if (def) { 324 | return `${kind} ${def} = ${requireText}.default${semi}`; 325 | } else { 326 | const destructure = composeDestructure(style, props, multiline); 327 | const statement = `${kind} ${destructure} = ${requireText}${semi}`; 328 | 329 | if (!multiline && statement.length > 80) { 330 | return composeRequireStatement({ 331 | style, 332 | id, 333 | props, 334 | multiline: true 335 | }); 336 | } 337 | return statement; 338 | } 339 | } 340 | 341 | function composeImportStatement({ style, id, ident, def, props, multiline }) { 342 | if (ident && props && props.length > 0) { 343 | throw new Error("ident and props cannot both be specified"); 344 | } 345 | 346 | const parts = []; 347 | if (def) { 348 | parts.push(def); 349 | } 350 | if (ident) { 351 | parts.push(`* as ${ident}`); 352 | } 353 | if (props && props.length > 0) { 354 | parts.push(composeDestructure(style, props, multiline)); 355 | } 356 | 357 | const { quote, semi } = style; 358 | const names = parts.join(", "); 359 | const statement = `import ${names} from ${quote}${id}${quote}${semi}`; 360 | 361 | if (props && !multiline && statement.length > 80) { 362 | return composeImportStatement({ 363 | style, 364 | id, 365 | ident, 366 | def, 367 | props, 368 | multiline: true 369 | }); 370 | } 371 | return statement; 372 | } 373 | 374 | function composeDestructure(style, props, multiline) { 375 | if (multiline) { 376 | const { tab, trailingComma } = style; 377 | const propsText = tab + props.join(`,\n${tab}`) + trailingComma; 378 | return `{\n${propsText}\n}`; 379 | } else { 380 | return `{ ${props.join(", ")} }`; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { findPkgMeta } = require("./common"); 4 | const findExports = require("../visits/find-exports.js"); 5 | const walker = require("./walker.js"); 6 | 7 | exports.run = function(dir, code) { 8 | return findPkgMeta(dir).then(meta => { 9 | const context = walker.run(meta, code, [findExports]); 10 | if (context.error) { 11 | throw context.error; 12 | } 13 | 14 | return context.exported; 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/walker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const babylon = require("babylon"); 4 | const estraverse = require("estraverse"); 5 | const globals = require("globals"); 6 | 7 | const astHelpers = require("./ast-helpers.js"); 8 | const { whiteRegex } = require("./common.js"); 9 | const escope = require("../patches/escope.js"); 10 | 11 | const lineRegex = /\r\n|[\r\n\u2028\u2029]/g; 12 | 13 | exports.run = function(meta, code, visits) { 14 | let ast, scopeManager, fallback; 15 | try { 16 | ({ ast, scopeManager, fallback } = parse(code)); 17 | } catch (err) { 18 | if (err instanceof SyntaxError) { 19 | return { error: err }; 20 | } 21 | throw err; 22 | } 23 | 24 | const context = createContext(ast, scopeManager, code); 25 | populateGlobals(meta, scopeManager); 26 | 27 | const callbacks = {}; 28 | visits.forEach(visit => { 29 | const map = visit.init(context); 30 | for (const type in map) { 31 | callbacks[type] = callbacks[type] || []; 32 | callbacks[type].push(map[type]); 33 | } 34 | }); 35 | 36 | estraverse.traverse(ast.program, { 37 | fallback, 38 | enter(node, parent) { 39 | node.parent = parent; 40 | context.curNode = node; 41 | if (callbacks[node.type]) { 42 | callbacks[node.type].forEach(fn => fn(node)); 43 | } 44 | } 45 | }); 46 | 47 | if (callbacks.finish) { 48 | callbacks.finish.forEach(fn => fn()); 49 | } 50 | return context; 51 | }; 52 | 53 | function parse(code) { 54 | const ast = babylon.parse(code, { 55 | sourceType: "module", 56 | allowImportExportEverywhere: false, 57 | allowReturnOutsideFunction: true, 58 | plugins: [ 59 | "estree", 60 | "jsx", 61 | "flow", 62 | "doExpressions", 63 | "objectRestSpread", 64 | "decorators", 65 | "classProperties", 66 | "exportExtensions", 67 | "asyncGenerators", 68 | "functionBind", 69 | "functionSent", 70 | "dynamicImport" 71 | ] 72 | }); 73 | 74 | const fallback = function(node) { 75 | if (astHelpers.isFlowNode(node)) { 76 | return []; 77 | } 78 | return Object.keys(node).filter(k => k !== "parent"); 79 | }; 80 | 81 | const scopeManager = escope.analyze(ast, { 82 | fallback, 83 | ecmaVersion: 6, 84 | nodejsScope: false, 85 | impliedStrict: false, 86 | ignoreEval: true, 87 | sourceType: "module" 88 | }); 89 | 90 | return { ast, scopeManager, fallback }; 91 | } 92 | 93 | function createContext(ast, scopeManager, code) { 94 | return { 95 | curNode: null, 96 | textLines: code.split(lineRegex), 97 | 98 | getLineText(line) { 99 | // line numbers start from 1 100 | return this.textLines[line - 1]; 101 | }, 102 | 103 | getLastToken(node, skip = 0) { 104 | const index = ast.tokens.findIndex( 105 | t => t.start < node.end && t.end >= node.end 106 | ); 107 | return ast.tokens[index - skip]; 108 | }, 109 | 110 | getTokenAfter(node) { 111 | return ast.tokens.find(t => t.start > node.end); 112 | }, 113 | 114 | getGlobalScope() { 115 | return scopeManager.globalScope; 116 | }, 117 | 118 | getKey(node) { 119 | switch (node.type) { 120 | case "Identifier": 121 | return node.name; 122 | 123 | case "StringLiteral": 124 | return node.value; 125 | 126 | default: 127 | return null; 128 | } 129 | }, 130 | 131 | getScope() { 132 | let scope = null; 133 | let node = this.curNode; 134 | 135 | do { 136 | scope = scopeManager.acquire(node); 137 | node = node.parent; 138 | } while (!scope); 139 | 140 | // top-most scope should be module scope, not global scope 141 | if (scope.type === "global" && scope.childScopes.length === 1) { 142 | scope = scope.childScopes[0]; 143 | } 144 | return scope; 145 | }, 146 | 147 | findVariable(name) { 148 | if (name.type === "Identifier" || name.type === "JSXIdentifier") { 149 | name = name.name; 150 | } 151 | 152 | let scope = this.getScope(); 153 | do { 154 | const variable = scope.set.get(name); 155 | if (variable) { 156 | return variable; 157 | } 158 | scope = scope.upper; 159 | } while (scope); 160 | 161 | return null; 162 | }, 163 | 164 | isGlobal(node, name) { 165 | if (node.type === "Identifier" && node.name === name) { 166 | const variable = this.findVariable(node); 167 | return variable && variable.scope.type === "global"; 168 | } 169 | return false; 170 | }, 171 | 172 | startsLine(node) { 173 | const { loc: { start } } = node; 174 | const startLineText = this.getLineText(start.line); 175 | return whiteRegex.test(startLineText.slice(0, start.column)); 176 | }, 177 | 178 | endsLine(node) { 179 | const { loc: { end } } = node; 180 | const endLineText = this.getLineText(end.line); 181 | return whiteRegex.test(endLineText.slice(end.column)); 182 | }, 183 | 184 | getUseStrict() { 185 | const node = ast.program.body[0]; 186 | if ( 187 | node && 188 | node.type === "ExpressionStatement" && 189 | node.directive === "use strict" 190 | ) { 191 | return node; 192 | } 193 | return null; 194 | } 195 | }; 196 | } 197 | 198 | function populateGlobals(meta, scopeManager) { 199 | const globalNames = new Set(); 200 | Object.keys(meta.globals).forEach(g => globalNames.add(g)); 201 | 202 | const envNames = ["builtin"]; 203 | for (const name in meta.env) { 204 | if (meta.env[name]) { 205 | envNames.push(name); 206 | } 207 | } 208 | 209 | envNames.forEach(e => 210 | Object.keys(globals[e]).forEach(g => globalNames.add(g))); 211 | 212 | // to identify imports and exports, we must have these globals 213 | ["require", "module", "exports"].forEach(g => globalNames.add(g)); 214 | 215 | const globalScope = scopeManager.globalScope; 216 | globalNames.forEach(name => { 217 | const variable = globalScope.set.get(name); 218 | 219 | if (!variable) { 220 | const newVariable = new escope.Variable(name, globalScope); 221 | globalScope.variables.push(newVariable); 222 | globalScope.set.set(name, newVariable); 223 | } 224 | }); 225 | } 226 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karthikv/tradeship/f91740b5d35cf1ee144b073e17b1a5fa2919583e/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tradeship", 3 | "description": "Automatically imports missing JS dependencies and removes unused ones.", 4 | "author": "Karthik Viswanathan ", 5 | "homepage": "https://github.com/karthikv/tradeship", 6 | "version": "0.0.13", 7 | "bin": { 8 | "tradeship": "./cli.js" 9 | }, 10 | "main": "api.js", 11 | "dependencies": { 12 | "babel-code-frame": "^6.22.0", 13 | "babylon": "^6.16.1", 14 | "debug": "^2.6.2", 15 | "escope": "^3.6.0", 16 | "estraverse": "^4.2.0", 17 | "globals": "^9.16.0", 18 | "js-yaml": "^3.8.1", 19 | "progress": "^1.1.8" 20 | }, 21 | "devDependencies": { 22 | "@exponent/json-file": "^5.3.0", 23 | "ava": "^0.18.2", 24 | "eslint": "^3.18.0", 25 | "prettier": "^0.17.1", 26 | "react": "^15.4.2" 27 | }, 28 | "scripts": { 29 | "test": "ava" 30 | }, 31 | "ava": { 32 | "source": [ 33 | "**/*.js", 34 | "!node_modules/*", 35 | "test/*.yml" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /patches/escope.js: -------------------------------------------------------------------------------- 1 | const referencer = require("escope/lib/referencer").default; 2 | 3 | const { visitClass, visitProperty } = referencer.prototype; 4 | 5 | // visit decorators on classes/properties to resolve their identifiers 6 | referencer.prototype.visitClass = function(node) { 7 | visitDecorators.call(this, node); 8 | visitClass.call(this, node); 9 | }; 10 | 11 | referencer.prototype.visitProperty = function(node) { 12 | visitDecorators.call(this, node); 13 | visitProperty.call(this, node); 14 | }; 15 | 16 | function visitDecorators(node) { 17 | if (!node.decorators) { 18 | return; 19 | } 20 | node.decorators.forEach(d => this.visit(d)); 21 | } 22 | 23 | // register class properties by visiting them as regular properties 24 | referencer.prototype.ClassProperty = function(node) { 25 | this.visitProperty(node); 26 | }; 27 | 28 | module.exports = require("escope"); 29 | -------------------------------------------------------------------------------- /patches/progress.js: -------------------------------------------------------------------------------- 1 | const ProgressBar = require("progress"); 2 | 3 | // Monkey patch terminate() so it doesn't throw an exception when the stream 4 | // isn't a TTY. See: https://github.com/visionmedia/node-progress/pull/138 5 | const terminate = ProgressBar.prototype.terminate; 6 | ProgressBar.prototype.terminate = function() { 7 | if (!this.stream.isTTY) { 8 | return; 9 | } 10 | terminate.apply(this, arguments); 11 | }; 12 | 13 | module.exports = ProgressBar; 14 | -------------------------------------------------------------------------------- /test/api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const test = require("ava").default; 4 | const os = require("os"); 5 | 6 | const api = require("../api"); 7 | 8 | const input = "fs.readFile();\n"; 9 | const expected = 'const fs = require("fs");\n\n' + input; 10 | 11 | test("api", t => 12 | api.import(os.tmpdir(), input).then(actual => t.is(actual, expected))); 13 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const test = require("ava").default; 4 | const { spawn } = require("child_process"); 5 | const fs = require("fs"); 6 | const yaml = require("js-yaml"); 7 | const os = require("os"); 8 | const path = require("path"); 9 | 10 | const { readFile, writeFile } = require("../lib/common"); 11 | 12 | const importerYAML = fs.readFileSync( 13 | path.join(__dirname, "importer.yml"), 14 | "utf8" 15 | ); 16 | const importerTests = yaml.safeLoad(importerYAML); 17 | 18 | test("cli-help", t => { 19 | return cli(["-h"]).then(({ stderr, code }) => { 20 | t.regex(stderr, /Usage:/); 21 | t.is(code, 0); 22 | }); 23 | }); 24 | 25 | test("cli-version", t => { 26 | return cli(["-v"]).then(({ stderr, code }) => { 27 | t.regex(stderr, /\d+\.\d+\.\d+/); 28 | t.is(code, 0); 29 | }); 30 | }); 31 | 32 | test("cli-path", t => { 33 | const { input, expected } = importerTests.find( 34 | t => t.name === "prop-wrap-trailing-comma" 35 | ); 36 | const inputPath = path.join(os.tmpdir(), "test-cli-path.js"); 37 | 38 | return writeFile(inputPath, input).then(() => cli([inputPath])).then(( 39 | { stdout, code } 40 | ) => { 41 | t.is(stdout, expected); 42 | t.is(code, 0); 43 | }); 44 | }); 45 | 46 | test("cli-write", t => { 47 | const { input, expected } = importerTests.find(t => t.name === "append-all"); 48 | const inputPath = path.join(os.tmpdir(), "test-cli-write.js"); 49 | 50 | return writeFile(inputPath, input) 51 | .then(() => cli(["-w", inputPath])) 52 | .then(({ stdout, code }) => { 53 | t.is(stdout, ""); 54 | t.is(code, 0); 55 | return readFile(inputPath, "utf8"); 56 | }) 57 | .then(actual => t.is(actual, expected)); 58 | }); 59 | 60 | test("cli-stdin", t => { 61 | const { input, expected } = importerTests.find( 62 | t => t.name === "import-default-prop" 63 | ); 64 | return cli(["-s", __dirname], input).then(({ stdout, code }) => { 65 | t.is(stdout, expected); 66 | t.is(code, 0); 67 | }); 68 | }); 69 | 70 | test("cli-write-stdin", t => { 71 | const { input, expected } = importerTests.find( 72 | t => t.name === "import-multi-ident" 73 | ); 74 | const outputPath = path.join(os.tmpdir(), "test-cli-write-stdin.js"); 75 | 76 | return cli(["-s", "-w", outputPath], input) 77 | .then(({ stdout, code }) => { 78 | t.is(stdout, ""); 79 | t.is(code, 0); 80 | return readFile(outputPath, "utf8"); 81 | }) 82 | .then(actual => t.is(actual, expected)); 83 | }); 84 | 85 | test("cli-parsing-error", t => { 86 | const inputPath = path.join(__dirname, "..", "fixtures", "parsing-error.js"); 87 | return cli([inputPath]).then(({ stderr, code }) => { 88 | t.regex(stderr, /class A extends/); 89 | t.is(code, 1); 90 | }); 91 | }); 92 | 93 | function cli(args, stdin = null) { 94 | const cli = path.resolve(__dirname, "..", "cli.js"); 95 | return new Promise((resolve, reject) => { 96 | let stdout = ""; 97 | let stderr = ""; 98 | 99 | const cmd = spawn("node", [cli].concat(args)); 100 | cmd.stdout.on("data", chunk => stdout += chunk); 101 | cmd.stderr.on("data", chunk => stderr += chunk); 102 | 103 | if (stdin) { 104 | cmd.stdin.end(stdin); 105 | } 106 | 107 | cmd.on("close", code => resolve({ stdout, stderr, code })); 108 | cmd.on("error", err => reject(err)); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /test/importer.js: -------------------------------------------------------------------------------- 1 | const test = require("ava").default; 2 | const fs = require("fs"); 3 | const yaml = require("js-yaml"); 4 | const path = require("path"); 5 | 6 | const importer = require("../lib/importer"); 7 | 8 | const importerYAML = fs.readFileSync( 9 | path.join(__dirname, "importer.yml"), 10 | "utf8" 11 | ); 12 | const importerTests = yaml.safeLoad(importerYAML); 13 | 14 | importerTests.forEach(({ name, input, expected, node }) => { 15 | test(`importer-node-${name}`, t => { 16 | return importer 17 | .run(__dirname, input) 18 | .then(actual => t.is(actual, expected)); 19 | }); 20 | 21 | if (!node) { 22 | test(`importer-${name}`, t => { 23 | return importer 24 | .run(__dirname, input, { env: { node: false } }) 25 | .then(actual => t.is(actual, expected)); 26 | }); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /test/importer.yml: -------------------------------------------------------------------------------- 1 | - name: empty 2 | node: false 3 | input: "" 4 | expected: "\n" 5 | 6 | - name: prepend 7 | node: true 8 | input: | 9 | fs.readFile(); 10 | expected: | 11 | const fs = require("fs"); 12 | 13 | fs.readFile(); 14 | 15 | - name: prepend-quote 16 | node: true 17 | input: | 18 | fs.readFile('example.txt'); 19 | expected: | 20 | const fs = require('fs'); 21 | 22 | fs.readFile('example.txt'); 23 | 24 | - name: prepend-no-semicolon 25 | node: true 26 | input: | 27 | fs.readFile() 28 | expected: | 29 | const fs = require("fs") 30 | 31 | fs.readFile() 32 | 33 | - name: prepend-kind 34 | node: true 35 | input: | 36 | let filename = "example.txt"; 37 | fs.readFile(filename); 38 | expected: | 39 | let fs = require("fs"); 40 | 41 | let filename = "example.txt"; 42 | fs.readFile(filename); 43 | 44 | - name: prepend-all 45 | node: true 46 | input: | 47 | var path = '/tmp/example.txt' 48 | fs.readFile(path) 49 | 50 | expected: | 51 | var fs = require('fs') 52 | 53 | var path = '/tmp/example.txt' 54 | fs.readFile(path) 55 | 56 | - name: prepend-use-strict 57 | node: true 58 | input: | 59 | "use strict"; 60 | fs.readFile(); 61 | expected: | 62 | "use strict"; 63 | 64 | const fs = require("fs"); 65 | 66 | fs.readFile(); 67 | 68 | - name: prepend-use-strict-new-line 69 | node: true 70 | input: | 71 | "use strict"; fs.readFile(); 72 | expected: | 73 | "use strict"; 74 | 75 | const fs = require("fs"); 76 | 77 | fs.readFile(); 78 | 79 | - name: append 80 | node: true 81 | input: | 82 | const fs = require("fs"); 83 | 84 | fs.readFile(); 85 | util.inspect({}); 86 | expected: | 87 | const fs = require("fs"); 88 | const util = require("util"); 89 | 90 | fs.readFile(); 91 | util.inspect({}); 92 | 93 | - name: append-quote 94 | node: true 95 | input: | 96 | const fs = require('fs'); 97 | 98 | fs.readFile(); 99 | util.inspect({}); 100 | expected: | 101 | const fs = require('fs'); 102 | const util = require('util'); 103 | 104 | fs.readFile(); 105 | util.inspect({}); 106 | 107 | - name: append-no-semicolon 108 | node: true 109 | input: | 110 | const fs = require("fs") 111 | 112 | fs.readFile() 113 | util.inspect({}) 114 | expected: | 115 | const fs = require("fs") 116 | const util = require("util") 117 | 118 | fs.readFile() 119 | util.inspect({}) 120 | 121 | - name: append-kind 122 | node: true 123 | input: | 124 | let fs = require("fs"); 125 | 126 | fs.readFile(); 127 | util.inspect({}); 128 | expected: | 129 | let fs = require("fs"); 130 | let util = require("util"); 131 | 132 | fs.readFile(); 133 | util.inspect({}); 134 | 135 | - name: append-all 136 | node: true 137 | input: | 138 | var fs = require('fs') 139 | 140 | fs.readFile() 141 | util.inspect({}) 142 | expected: | 143 | var fs = require('fs') 144 | var util = require('util') 145 | 146 | fs.readFile() 147 | util.inspect({}) 148 | 149 | - name: remove 150 | node: false 151 | input: | 152 | const fs = require("fs"); 153 | console.log("Hello"); 154 | expected: | 155 | console.log("Hello"); 156 | 157 | - name: remove-empty 158 | node: false 159 | input: | 160 | const fs = require("fs"); 161 | expected: "\n" 162 | 163 | - name: remove-only-requires 164 | node: false 165 | input: | 166 | const fs = require("fs"); 167 | const x = 3; 168 | expected: | 169 | const x = 3; 170 | 171 | - name: remove-line-after 172 | node: false 173 | input: | 174 | const fs = require("fs"); 175 | 176 | console.log("Hello"); 177 | expected: | 178 | console.log("Hello"); 179 | 180 | - name: remove-multiple 181 | node: false 182 | input: | 183 | const fs = require("fs"); 184 | const util = require("util"); 185 | const http = require("http"); 186 | 187 | util.inspect({}); 188 | expected: | 189 | const util = require("util"); 190 | 191 | util.inspect({}); 192 | 193 | - name: remove-and-append 194 | node: true 195 | input: | 196 | const fs = require("fs"); 197 | const util = require("util"); 198 | 199 | util.inspect({}); 200 | http.createServer(); 201 | expected: | 202 | const http = require("http"); 203 | const util = require("util"); 204 | 205 | util.inspect({}); 206 | http.createServer(); 207 | 208 | - name: remove-namespaced 209 | node: false 210 | input: | 211 | const JsonFile = require("@exponent/json-file"); 212 | expected: "\n" 213 | 214 | - name: remove-default 215 | node: false 216 | input: | 217 | const es6 = require("../fixtures/es6.js").default; 218 | expected: "\n" 219 | 220 | - name: keep 221 | node: false 222 | input: | 223 | console.log("This is a test"); 224 | expected: | 225 | console.log("This is a test"); 226 | 227 | - name: keep-local-variable 228 | node: true 229 | input: | 230 | let fs = { readFile: () => {} }; 231 | fs.readFile(); 232 | expected: | 233 | let fs = { readFile: () => {} }; 234 | fs.readFile(); 235 | 236 | - name: keep-existing-require 237 | node: true 238 | input: | 239 | const fs = require("fs"); 240 | fs.readFile(); 241 | expected: | 242 | const fs = require("fs"); 243 | fs.readFile(); 244 | 245 | - name: keep-rewrite 246 | node: true 247 | input: | 248 | let fs = require("fs"); 249 | fs = {}; 250 | expected: | 251 | let fs = require("fs"); 252 | fs = {}; 253 | 254 | - name: keep-multiple-declarators 255 | node: true 256 | input: | 257 | const fs = require("fs"), util = require("util"); 258 | 259 | fs.readFile(); 260 | expected: | 261 | const fs = require("fs"), util = require("util"); 262 | 263 | fs.readFile(); 264 | 265 | - name: keep-same-line-code-before 266 | node: true 267 | input: | 268 | const x = 3; const fs = require("fs"); 269 | /* some comment */ const util = require("util"); 270 | expected: | 271 | const x = 3; const fs = require("fs"); 272 | /* some comment */ const util = require("util"); 273 | 274 | - name: keep-same-line-code-after 275 | node: true 276 | input: | 277 | const fs = require("fs"); const x = 3; 278 | const util = require("util"); // some comment 279 | const http = require("http");; 280 | ;const babylon = require("babylon"); 281 | expected: | 282 | const fs = require("fs"); const x = 3; 283 | const util = require("util"); // some comment 284 | const http = require("http");; 285 | ;const babylon = require("babylon"); 286 | 287 | - name: keep-undeclared-assign 288 | node: true 289 | input: | 290 | fs = {}; 291 | expected: | 292 | fs = {}; 293 | 294 | - name: keep-namespaced 295 | node: false 296 | input: | 297 | const JsonFile = require("@exponent/json-file"); 298 | new JsonFile(); 299 | expected: | 300 | const JsonFile = require("@exponent/json-file"); 301 | new JsonFile(); 302 | 303 | - name: keep-relative 304 | node: false 305 | input: | 306 | const DepRegistry = require("../lib/dep-registry.js"); 307 | const runner = require("./runner.js"); 308 | const stub = require("./dir/stub.js"); 309 | 310 | new DepRegistry(); 311 | runner.run(); 312 | console.log(stub); 313 | expected: | 314 | const DepRegistry = require("../lib/dep-registry.js"); 315 | const runner = require("./runner.js"); 316 | const stub = require("./dir/stub.js"); 317 | 318 | new DepRegistry(); 319 | runner.run(); 320 | console.log(stub); 321 | 322 | - name: keep-default 323 | node: false 324 | input: | 325 | const stub = require("stub").default; 326 | console.log(stub); 327 | expected: | 328 | const stub = require("stub").default; 329 | console.log(stub); 330 | 331 | - name: keep-react-for-jsx 332 | node: false 333 | input: | 334 | const React = require("react"); 335 |

Paragraph

; 336 | expected: | 337 | const React = require("react"); 338 |

Paragraph

; 339 | 340 | - name: keep-jsx-ident 341 | node: false 342 | input: | 343 | const React = require("react"); 344 | 345 | const Component = require("../fixtures/component.js"); 346 | 347 | ; 348 | expected: | 349 | const React = require("react"); 350 | 351 | const Component = require("../fixtures/component.js"); 352 | 353 | ; 354 | 355 | - name: keep-inner-scope 356 | node: true 357 | input: | 358 | const util = require("util"); 359 | 360 | function foo() { 361 | const fs = require("fs"); 362 | fs.readFile(); 363 | } 364 | 365 | util.inspect(); 366 | expected: | 367 | const util = require("util"); 368 | 369 | function foo() { 370 | const fs = require("fs"); 371 | fs.readFile(); 372 | } 373 | 374 | util.inspect(); 375 | 376 | - name: keep-order 377 | node: false 378 | input: | 379 | const fs = require("fs"); 380 | const JsonFile = require("@exponent/json-file"); 381 | const util = require("util"); 382 | 383 | const findImports = require("../visits/find-imports.js"); 384 | const parser = require("../lib/parser.js"); 385 | 386 | util.inspect({}); 387 | fs.readFile(); 388 | parser.run(); 389 | findImports.retrieve(); 390 | new JsonFile(); 391 | expected: | 392 | const fs = require("fs"); 393 | const JsonFile = require("@exponent/json-file"); 394 | const util = require("util"); 395 | 396 | const findImports = require("../visits/find-imports.js"); 397 | const parser = require("../lib/parser.js"); 398 | 399 | util.inspect({}); 400 | fs.readFile(); 401 | parser.run(); 402 | findImports.retrieve(); 403 | new JsonFile(); 404 | 405 | - name: keep-global-declaration 406 | node: false 407 | input: | 408 | let x 409 | expected: | 410 | let x 411 | 412 | - name: keep-pkg-path 413 | node: false 414 | input: | 415 | const stub = require("babylon/some/nested/file"); 416 | console.log(stub); 417 | expected: | 418 | const stub = require("babylon/some/nested/file"); 419 | console.log(stub); 420 | 421 | - name: keep-pkg-path-jsx 422 | node: false 423 | input: | 424 | const Stub = require("babylon/some/nested/file"); 425 | const React = require("react"); 426 | ; 427 | expected: | 428 | const Stub = require("babylon/some/nested/file"); 429 | const React = require("react"); 430 | ; 431 | 432 | - name: keep-class-property 433 | node: true 434 | input: | 435 | class C { 436 | static fs = 3; 437 | } 438 | expected: | 439 | class C { 440 | static fs = 3; 441 | } 442 | 443 | - name: keep-typeof-undef 444 | node: true 445 | input: | 446 | if (typeof fs === "undefined") { 447 | console.log("no fs"); 448 | } 449 | expected: | 450 | if (typeof fs === "undefined") { 451 | console.log("no fs"); 452 | } 453 | 454 | - name: keep-object-rest-spread 455 | node: true 456 | input: | 457 | const { x, y, ...fs } = { x: 1, y: 2, z: 3 }; 458 | fs.readFile(); 459 | expected: | 460 | const { x, y, ...fs } = { x: 1, y: 2, z: 3 }; 461 | fs.readFile(); 462 | 463 | - name: keep-dynamic-import 464 | node: true 465 | input: | 466 | const mod = "fs"; 467 | import(mod).then(fs => { fs.readFile() }); 468 | expected: | 469 | const mod = "fs"; 470 | import(mod).then(fs => { fs.readFile() }); 471 | 472 | - name: multi-ident 473 | node: true 474 | input: | 475 | child_process.exec(); 476 | childProcess.spawn(); 477 | expected: | 478 | const childProcess = require("child_process"); 479 | const child_process = require("child_process"); 480 | 481 | child_process.exec(); 482 | childProcess.spawn(); 483 | 484 | - name: default 485 | node: false 486 | input: | 487 | console.log(es6); 488 | expected: | 489 | const es6 = require("../fixtures/es6.js").default; 490 | 491 | console.log(es6); 492 | 493 | - name: default-anonymous 494 | node: false 495 | input: | 496 | console.log(defaultAnonymous); 497 | expected: | 498 | const defaultAnonymous = require("../fixtures/default-anonymous.js").default; 499 | 500 | console.log(defaultAnonymous); 501 | 502 | - name: react-for-jsx 503 | node: false 504 | input: | 505 |

Paragraph

; 506 | expected: | 507 | const React = require("react"); 508 | 509 |

Paragraph

; 510 | 511 | - name: jsx-ident 512 | node: false 513 | input: | 514 | ; 515 | expected: | 516 | const React = require("react"); 517 | 518 | const Component = require("../fixtures/component.js"); 519 | 520 | ; 521 | 522 | - name: no-dupes 523 | node: true 524 | input: | 525 | fs.readFile(); 526 | fs.createReadStream(); 527 | fs.createWriteStream(); 528 | expected: | 529 | const fs = require("fs"); 530 | 531 | fs.readFile(); 532 | fs.createReadStream(); 533 | fs.createWriteStream(); 534 | 535 | - name: global-return 536 | node: false 537 | input: | 538 | readFile(); 539 | return 3; 540 | expected: | 541 | const { readFile } = require("../lib/common.js"); 542 | 543 | readFile(); 544 | return 3; 545 | 546 | - name: read-env 547 | node: true 548 | # crypto is a global in the browser; will work if *only* node env is set 549 | input: | 550 | crypto.randomBytes(); 551 | expected: | 552 | const crypto = require("crypto"); 553 | 554 | crypto.randomBytes(); 555 | 556 | - name: class-property 557 | node: true 558 | input: | 559 | class C { 560 | prop = () => { 561 | fs.readFile(); 562 | }; 563 | } 564 | expected: | 565 | const fs = require("fs"); 566 | 567 | class C { 568 | prop = () => { 569 | fs.readFile(); 570 | }; 571 | } 572 | 573 | - name: do-expressions 574 | node: true 575 | input: | 576 | const result = do { 577 | fs.readFile(); 578 | }; 579 | expected: | 580 | const fs = require("fs"); 581 | 582 | const result = do { 583 | fs.readFile(); 584 | }; 585 | 586 | - name: async-generators 587 | node: true 588 | input: | 589 | async function* foo() { 590 | await fs.readFile(); 591 | } 592 | expected: | 593 | const fs = require("fs"); 594 | 595 | async function* foo() { 596 | await fs.readFile(); 597 | } 598 | 599 | - name: function-bind 600 | node: true 601 | input: | 602 | fs::readFile(); 603 | expected: | 604 | const fs = require("fs"); 605 | 606 | const { readFile } = require("../lib/common.js"); 607 | 608 | fs::readFile(); 609 | 610 | - name: decorators 611 | node: true 612 | input: | 613 | @fs 614 | class C { 615 | @util 616 | foo() { 617 | http.createServer(); 618 | } 619 | } 620 | 621 | const foo = { 622 | @babylon 623 | bar() {} 624 | }; 625 | expected: | 626 | const babylon = require("babylon"); 627 | const fs = require("fs"); 628 | const http = require("http"); 629 | const util = require("util"); 630 | 631 | @fs 632 | class C { 633 | @util 634 | foo() { 635 | http.createServer(); 636 | } 637 | } 638 | 639 | const foo = { 640 | @babylon 641 | bar() {} 642 | }; 643 | 644 | - name: prop 645 | node: false 646 | input: | 647 | readFile(); 648 | expected: | 649 | const { readFile } = require("../lib/common.js"); 650 | 651 | readFile(); 652 | 653 | - name: prop-multiple 654 | node: false 655 | input: | 656 | readFile(); 657 | readdir(); 658 | expected: | 659 | const { readFile, readdir } = require("../lib/common.js"); 660 | 661 | readFile(); 662 | readdir(); 663 | 664 | - name: prop-wrap 665 | node: true 666 | input: | 667 | readFileSync(); 668 | readdirSync(); 669 | createReadStream(); 670 | createWriteStream(); 671 | expected: | 672 | const { 673 | createReadStream, 674 | createWriteStream, 675 | readFileSync, 676 | readdirSync 677 | } = require("fs"); 678 | 679 | readFileSync(); 680 | readdirSync(); 681 | createReadStream(); 682 | createWriteStream(); 683 | 684 | - name: prop-wrap-four-spaces 685 | node: true 686 | input: | 687 | readFileSync(); 688 | readdirSync(); 689 | createReadStream(); 690 | 691 | if (true) { 692 | createWriteStream(); 693 | } 694 | expected: | 695 | const { 696 | createReadStream, 697 | createWriteStream, 698 | readFileSync, 699 | readdirSync 700 | } = require("fs"); 701 | 702 | readFileSync(); 703 | readdirSync(); 704 | createReadStream(); 705 | 706 | if (true) { 707 | createWriteStream(); 708 | } 709 | 710 | - name: prop-wrap-hard-tab 711 | node: true 712 | input: | 713 | readFileSync(); 714 | readdirSync(); 715 | createReadStream(); 716 | 717 | if (true) { 718 | createWriteStream(); 719 | } 720 | expected: | 721 | const { 722 | createReadStream, 723 | createWriteStream, 724 | readFileSync, 725 | readdirSync 726 | } = require("fs"); 727 | 728 | readFileSync(); 729 | readdirSync(); 730 | createReadStream(); 731 | 732 | if (true) { 733 | createWriteStream(); 734 | } 735 | 736 | - name: prop-wrap-trailing-comma 737 | node: true 738 | input: | 739 | readFileSync(); 740 | readdirSync(); 741 | createReadStream(); 742 | createWriteStream(); 743 | 744 | console.log({ 745 | a: 3, 746 | b: 4, 747 | }) 748 | expected: | 749 | const { 750 | createReadStream, 751 | createWriteStream, 752 | readFileSync, 753 | readdirSync, 754 | } = require("fs"); 755 | 756 | readFileSync(); 757 | readdirSync(); 758 | createReadStream(); 759 | createWriteStream(); 760 | 761 | console.log({ 762 | a: 3, 763 | b: 4, 764 | }) 765 | 766 | - name: prop-existing 767 | node: false 768 | input: | 769 | const { readFile, readdir } = require("../lib/common.js"); 770 | 771 | readFile(); 772 | readdir(); 773 | expected: | 774 | const { readFile, readdir } = require("../lib/common.js"); 775 | 776 | readFile(); 777 | readdir(); 778 | 779 | - name: prop-remove 780 | node: false 781 | input: | 782 | const { readFile } = require("fs"); 783 | console.log("Hello"); 784 | expected: | 785 | console.log("Hello"); 786 | 787 | - name: prop-remove-existing 788 | node: false 789 | input: | 790 | const { readFile, readdir } = require("fs"); 791 | readFile(); 792 | expected: | 793 | const { readFile } = require("fs"); 794 | readFile(); 795 | 796 | - name: pkg 797 | node: false 798 | input: | 799 | babylon.parse(); 800 | expected: | 801 | const babylon = require("babylon"); 802 | 803 | babylon.parse(); 804 | 805 | - name: pkg-dev 806 | node: false 807 | input: | 808 | ava.test(); 809 | expected: | 810 | const ava = require("ava"); 811 | 812 | ava.test(); 813 | 814 | - name: pkg-capital 815 | node: false 816 | input: | 817 | new Babylon(); 818 | expected: | 819 | const Babylon = require("babylon"); 820 | 821 | new Babylon(); 822 | 823 | - name: pkg-camel 824 | node: false 825 | input: | 826 | jsYaml.load(); 827 | expected: | 828 | const jsYaml = require("js-yaml"); 829 | 830 | jsYaml.load(); 831 | 832 | - name: pkg-namespaced 833 | node: false 834 | input: | 835 | new JsonFile(); 836 | expected: | 837 | const JsonFile = require("@exponent/json-file"); 838 | 839 | new JsonFile(); 840 | 841 | - name: pkg-dock 842 | node: false 843 | input: | 844 | yaml.safeLoad(); 845 | expected: | 846 | const yaml = require("js-yaml"); 847 | 848 | yaml.safeLoad(); 849 | 850 | - name: order 851 | node: false 852 | input: | 853 | const util = require("util"); 854 | const fs = require("fs"); 855 | 856 | util.inspect({}); 857 | fs.readFile(); 858 | parser.run(); 859 | findImports.retrieve(); 860 | new JsonFile(); 861 | expected: | 862 | const fs = require("fs"); 863 | const JsonFile = require("@exponent/json-file"); 864 | const util = require("util"); 865 | 866 | const findImports = require("../visits/find-imports.js"); 867 | const parser = require("../lib/parser.js"); 868 | 869 | util.inspect({}); 870 | fs.readFile(); 871 | parser.run(); 872 | findImports.retrieve(); 873 | new JsonFile(); 874 | 875 | - name: relative 876 | node: false 877 | input: | 878 | new DepRegistry(); 879 | expected: | 880 | const DepRegistry = require("../lib/dep-registry.js"); 881 | 882 | new DepRegistry(); 883 | 884 | - name: relative-prop 885 | node: false 886 | input: | 887 | tryJSONParse(); 888 | expected: | 889 | const { tryJSONParse } = require("../lib/common.js"); 890 | 891 | tryJSONParse(); 892 | 893 | - name: relative-base 894 | node: false 895 | input: | 896 | parser.run() 897 | expected: | 898 | const parser = require("../lib/parser.js") 899 | 900 | parser.run() 901 | 902 | - name: relative-base-recursive 903 | node: false 904 | input: | 905 | findExports.retrieve(); 906 | expected: | 907 | const findExports = require("../visits/find-exports.js"); 908 | 909 | findExports.retrieve(); 910 | 911 | - name: import-ident 912 | node: true 913 | input: | 914 | import * as fs from "fs"; 915 | 916 | fs.readFile(); 917 | util.inspect({}); 918 | expected: | 919 | import * as fs from "fs"; 920 | import * as util from "util"; 921 | 922 | fs.readFile(); 923 | util.inspect({}); 924 | 925 | - name: import-default 926 | node: false 927 | input: | 928 | import * as fs from "fs"; 929 | 930 | fs.readFile(); 931 | console.log(es6); 932 | expected: | 933 | import * as fs from "fs"; 934 | 935 | import es6 from "../fixtures/es6.js"; 936 | 937 | fs.readFile(); 938 | console.log(es6); 939 | 940 | - name: import-prop 941 | node: true 942 | input: | 943 | import * as fs from "fs"; 944 | 945 | fs.readFile(); 946 | inspect({}); 947 | expected: | 948 | import * as fs from "fs"; 949 | import { inspect } from "util"; 950 | 951 | fs.readFile(); 952 | inspect({}); 953 | 954 | - name: import-ident-prop 955 | node: true 956 | input: | 957 | import * as fs from "fs"; 958 | 959 | fs.readFile(); 960 | util.inspect(); 961 | inherits(); 962 | expected: | 963 | import * as fs from "fs"; 964 | import { inherits } from "util"; 965 | import * as util from "util"; 966 | 967 | fs.readFile(); 968 | util.inspect(); 969 | inherits(); 970 | 971 | - name: import-default-prop 972 | node: false 973 | input: | 974 | import * as fs from "fs"; 975 | 976 | fs.readFile(); 977 | console.log(es6); 978 | prop(); 979 | expected: | 980 | import * as fs from "fs"; 981 | 982 | import es6, { prop } from "../fixtures/es6.js"; 983 | 984 | fs.readFile(); 985 | console.log(es6); 986 | prop(); 987 | 988 | - name: import-remove 989 | node: false 990 | input: | 991 | import * as fs from "fs"; 992 | import { inspect, inherits } from "util"; 993 | import es6 from "../fixtures/es6.js"; 994 | 995 | inspect({}); 996 | expected: | 997 | import { inspect } from "util"; 998 | 999 | inspect({}); 1000 | 1001 | - name: import-multi-ident 1002 | node: true 1003 | input: | 1004 | import * as fs from "fs"; 1005 | 1006 | fs.readFile(); 1007 | child_process.exec(); 1008 | childProcess.spawn(); 1009 | expected: | 1010 | import * as childProcess from "child_process"; 1011 | import * as child_process from "child_process"; 1012 | import * as fs from "fs"; 1013 | 1014 | fs.readFile(); 1015 | child_process.exec(); 1016 | childProcess.spawn(); 1017 | 1018 | - name: import-namespaced 1019 | node: false 1020 | input: | 1021 | import * as fs from "fs"; 1022 | 1023 | fs.readFile(); 1024 | new JsonFile(); 1025 | expected: | 1026 | import * as fs from "fs"; 1027 | import * as JsonFile from "@exponent/json-file"; 1028 | 1029 | fs.readFile(); 1030 | new JsonFile(); 1031 | 1032 | - name: ignore-flow 1033 | node: false 1034 | input: | 1035 | let x 1036 | (x:JsonFile) 1037 | expected: | 1038 | let x 1039 | (x:JsonFile) 1040 | 1041 | - name: ignore-import-type 1042 | node: false 1043 | input: | 1044 | import { a, type b } from 'foo'; 1045 | import type { c, d } from 'bar'; 1046 | import { typeof e } from 'baz'; 1047 | import type * as f from 'foo-bar'; 1048 | expected: | 1049 | import { a, type b } from 'foo'; 1050 | import type { c, d } from 'bar'; 1051 | import { typeof e } from 'baz'; 1052 | import type * as f from 'foo-bar'; 1053 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | const test = require("ava").default; 2 | const fs = require("fs"); 3 | const yaml = require("js-yaml"); 4 | const path = require("path"); 5 | 6 | const parser = require("../lib/parser"); 7 | 8 | const parserYAML = fs.readFileSync(path.join(__dirname, "parser.yml"), "utf8"); 9 | const parserTests = yaml.safeLoad(parserYAML); 10 | 11 | parserTests.forEach(({ name, input, idents, hasDefault, props }) => { 12 | test(`parser-${name}`, t => { 13 | return parser.run(__dirname, input).then(actual => { 14 | t.deepEqual(actual.idents, new Set(idents || [])); 15 | t.deepEqual(actual.props, new Set(props || [])); 16 | t.is(actual.hasDefault, hasDefault || false); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/parser.yml: -------------------------------------------------------------------------------- 1 | - name: assign-ident 2 | input: | 3 | const x = 3; 4 | module.exports = x; 5 | idents: [x] 6 | 7 | - name: assign-assign 8 | input: | 9 | let x, y; 10 | x = module.exports = y = 4; 11 | idents: [x, y] 12 | 13 | - name: assign-member 14 | input: | 15 | const x = { y: 4 }; 16 | module.exports = x.y; 17 | idents: [y] 18 | 19 | - name: assign-fn 20 | input: | 21 | module.exports = function foo() {}; 22 | idents: [foo] 23 | 24 | - name: assign-new 25 | input: | 26 | module.exports = new Bar(); 27 | idents: [Bar] 28 | 29 | - name: assign-other 30 | input: | 31 | module.exports = function() {}; 32 | module.exports = () => {}; 33 | module.exports = {}; 34 | module.exports = someFn(); 35 | module.exports = 3; 36 | idents: [] 37 | 38 | - name: assign-object 39 | input: | 40 | module.exports = { 41 | a: 3, 42 | b() {}, 43 | c: true 44 | }; 45 | props: [a, b, c] 46 | 47 | - name: assign-exports-props 48 | input: | 49 | exports.a = 3; 50 | exports.b = function() {}; 51 | module.exports.c = true; 52 | props: [a, b, c] 53 | 54 | - name: assign-ident-object 55 | input: | 56 | const x = { 57 | a: 3, 58 | b() {}, 59 | c: true 60 | }; 61 | module.exports = x; 62 | idents: [x] 63 | props: [a, b, c] 64 | 65 | - name: assign-ident-object-individual 66 | input: | 67 | const x = {}; 68 | x.a = 3; 69 | x.b = () => {}; 70 | x.c = true; 71 | module.exports = x; 72 | idents: [x] 73 | props: [a, b, c] 74 | 75 | - name: assign-ident-object-recursive 76 | input: | 77 | const x = { a: 3 }; 78 | const y = x; 79 | const z = y; 80 | module.exports = z; 81 | idents: [z] 82 | props: [a] 83 | 84 | - name: assign-ident-object-overwrite 85 | input: | 86 | const x = {}; 87 | x.a = 3; 88 | x.b = () => {}; 89 | x.c = true 90 | x = {d: "hi"}; 91 | const y = x; 92 | module.exports = y; 93 | idents: [y] 94 | props: [d] 95 | 96 | - name: assign-ident-multiple-declarations 97 | input: | 98 | var x = { a: 3 }; 99 | x = { b: 3 }; 100 | x = { c: 3 }; 101 | var x = { d: 3 }; 102 | module.exports = x; 103 | idents: [x] 104 | props: [d] 105 | 106 | - name: assign-default 107 | input: | 108 | const x = 3 109 | exports.default = x 110 | module.exports.default = function foo() {} 111 | idents: [x, foo] 112 | hasDefault: true 113 | 114 | - name: export-default 115 | input: | 116 | const x = 3; 117 | export default x; 118 | idents: [x] 119 | hasDefault: true 120 | 121 | - name: export-default-literal 122 | input: | 123 | export default "str"; 124 | hasDefault: true 125 | 126 | - name: export-default-fn 127 | input: | 128 | export default function foo() {} 129 | idents: [foo] 130 | hasDefault: true 131 | 132 | - name: export-default-class 133 | input: | 134 | export default class Foo {} 135 | idents: [Foo] 136 | hasDefault: true 137 | 138 | - name: export-as-default 139 | input: | 140 | const x = 5 141 | export { x as default } 142 | idents: [x] 143 | hasDefault: true 144 | 145 | - name: export-named 146 | input: | 147 | export const a = 3, b = 4 148 | export function c() {} 149 | export let d, e = true 150 | 151 | const f = true 152 | const str = "str" 153 | const h = 3.1 154 | export { f, str as g, h } 155 | 156 | export class I {} 157 | props: [a, b, c, d, e, f, g, h, I] 158 | 159 | - name: export-from 160 | input: | 161 | export {a, foo as b} from "mod"; 162 | export * from "other-mod"; 163 | props: [a, b] 164 | 165 | - name: export-extensions 166 | input: | 167 | export * as a, { b } from "mod"; 168 | export c from "other-mod"; 169 | idents: [c] 170 | props: [a, b] 171 | hasDefault: true 172 | 173 | - name: ignore-export-type 174 | input: | 175 | export type foo = number; 176 | export type { bar }; 177 | export type { baz } from "foobar"; 178 | -------------------------------------------------------------------------------- /visits/find-exports.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: ["error", { allow: ["error"] }] */ 2 | "use strict"; 3 | 4 | const astHelpers = require("../lib/ast-helpers.js"); 5 | const { debug } = require("../lib/common.js"); 6 | 7 | exports.init = function(context) { 8 | context.exported = { 9 | idents: new Set(), 10 | defaults: new Set(), 11 | props: new Set(), 12 | hasExports: false, 13 | hasDefault: false 14 | }; 15 | const { exported } = context; 16 | 17 | return { 18 | AssignmentExpression(node) { 19 | if (node.left.type === "MemberExpression") { 20 | const { object, property } = node.left; 21 | const right = node.right; 22 | 23 | if ( 24 | context.isGlobal(object, "module") && 25 | context.getKey(property) === "exports" 26 | ) { 27 | addIdents(exported, parseNames(right)); 28 | parsePropsDefaults(context, right); 29 | 30 | let parent = node.parent; 31 | while (parent.type === "AssignmentExpression") { 32 | addIdents(exported, parseNames(parent.left)); 33 | parent = parent.parent; 34 | } 35 | } 36 | 37 | if ( 38 | object.type === "MemberExpression" && 39 | context.isGlobal(object.object, "module") && 40 | context.getKey(object.property) === "exports" || 41 | context.isGlobal(object, "exports") 42 | ) { 43 | const key = context.getKey(property); 44 | if (key === "default") { 45 | addIdents(exported, parseNames(right), true); 46 | } else { 47 | addProps(exported, [key]); 48 | } 49 | } 50 | } 51 | }, 52 | 53 | Identifier(node) { 54 | if ( 55 | context.isGlobal(node, "module") || context.isGlobal(node, "exports") 56 | ) { 57 | exported.hasExports = true; 58 | } 59 | }, 60 | 61 | ExportNamedDeclaration(node) { 62 | if (astHelpers.isFlowExport(node)) { 63 | return; 64 | } 65 | if (node.declaration) { 66 | addProps(exported, parseNames(node.declaration)); 67 | } 68 | if (node.specifiers) { 69 | node.specifiers.forEach(s => { 70 | if (s.exported.name === "default") { 71 | addIdents(exported, [s.local.name], true); 72 | } else if (s.type === "ExportDefaultSpecifier") { 73 | addIdents(exported, [s.exported.name], true); 74 | } else { 75 | addProps(exported, [s.exported.name]); 76 | } 77 | }); 78 | } 79 | }, 80 | 81 | ExportDefaultDeclaration(node) { 82 | addIdents(exported, parseNames(node.declaration), true); 83 | }, 84 | 85 | ExportAllDeclaration() { 86 | exported.hasExports = true; 87 | } 88 | }; 89 | }; 90 | 91 | function parseNames(node) { 92 | switch (node.type) { 93 | case "Identifier": 94 | return [node.name]; 95 | 96 | case "AssignmentExpression": 97 | return parseNames(node.left).concat(parseNames(node.right)); 98 | 99 | case "MemberExpression": 100 | return parseNames(node.property); 101 | 102 | case "FunctionExpression": 103 | case "FunctionDeclaration": 104 | case "ClassExpression": 105 | case "ClassDeclaration": 106 | return node.id ? parseNames(node.id) : []; 107 | 108 | case "NewExpression": 109 | return parseNames(node.callee); 110 | 111 | case "VariableDeclaration": 112 | return node.declarations.reduce( 113 | (names, d) => names.concat(parseNames(d.id)), 114 | [] 115 | ); 116 | 117 | // can't deduce a name from these nodes 118 | case "ArrowFunctionExpression": 119 | case "ObjectExpression": 120 | case "CallExpression": 121 | case "Literal": 122 | return []; 123 | 124 | default: 125 | debug("Didn't consider parsing name from %s", node.type); 126 | return []; 127 | } 128 | } 129 | 130 | function parsePropsDefaults(context, node) { 131 | const { exported } = context; 132 | 133 | if (node.type === "ObjectExpression") { 134 | node.properties.forEach(p => { 135 | if (p.key.type === "Identifier") { 136 | if (p.key.name === "default") { 137 | addIdents(exported, parseNames(p.value), true); 138 | } else { 139 | addProps(exported, [p.key.name]); 140 | } 141 | } 142 | }); 143 | } else if (node.type === "Identifier") { 144 | const variable = context.findVariable(node); 145 | 146 | if (variable) { 147 | let lastWriteIndex = 0; 148 | variable.references.forEach((ref, i) => { 149 | if (ref.writeExpr) { 150 | lastWriteIndex = i; 151 | } 152 | }); 153 | 154 | variable.references.slice(lastWriteIndex).forEach(ref => { 155 | if (ref.writeExpr) { 156 | parsePropsDefaults(context, ref.writeExpr); 157 | } else { 158 | const ident = ref.identifier; 159 | if ( 160 | ident.parent && 161 | ident.parent.type === "MemberExpression" && 162 | ident.parent.object === ident && 163 | ident.parent.parent && 164 | ident.parent.parent.type === "AssignmentExpression" && 165 | ident.parent.parent.left === ident.parent 166 | ) { 167 | const key = context.getKey(ident.parent.property); 168 | if (key === "default") { 169 | addIdents(exported, parseNames(ident.parent.right), true); 170 | } else { 171 | addProps(exported, [key]); 172 | } 173 | } 174 | } 175 | }); 176 | } 177 | } 178 | } 179 | 180 | function addIdents(exported, idents, hasDefault = false) { 181 | if (hasDefault) { 182 | exported.hasDefault = true; 183 | } 184 | exported.hasExports = true; 185 | idents.forEach(i => exported.idents.add(i)); 186 | } 187 | 188 | function addProps(exported, props) { 189 | exported.hasExports = true; 190 | props.forEach(p => exported.props.add(p)); 191 | } 192 | -------------------------------------------------------------------------------- /visits/find-imports.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const astHelpers = require("../lib/ast-helpers.js"); 4 | const { whiteRegex } = require("../lib/common"); 5 | 6 | exports.init = function(context) { 7 | const reqs = []; 8 | 9 | return { 10 | VariableDeclaration(node) { 11 | // For simplicity, we only process require()s we have added. All 12 | // require()s that we add are single variable declarations in the 13 | // top-most scope that occupy entire lines. 14 | if ( 15 | node.declarations.length !== 1 || 16 | node.parent.type !== "Program" || 17 | !context.startsLine(node) || 18 | !context.endsLine(node) 19 | ) { 20 | return; 21 | } 22 | 23 | const { init, id } = node.declarations[0]; 24 | 25 | let call = init; 26 | let isDefault = false; 27 | 28 | // might be require().default 29 | if ( 30 | init && 31 | init.type === "MemberExpression" && 32 | context.getKey(init.property) === "default" 33 | ) { 34 | isDefault = true; 35 | call = init.object; 36 | } 37 | 38 | if ( 39 | !call || 40 | call.type !== "CallExpression" || 41 | !context.isGlobal(call.callee, "require") || 42 | call.arguments.length !== 1 || 43 | call.arguments[0].type !== "Literal" || 44 | typeof call.arguments[0].value !== "string" 45 | ) { 46 | return; 47 | } 48 | 49 | const depID = call.arguments[0].value; 50 | if (id.type === "Identifier") { 51 | const varsKey = isDefault ? "defaultVars" : "identVars"; 52 | reqs.push({ node, depID, [varsKey]: [context.findVariable(id)] }); 53 | } else if ( 54 | id.type === "ObjectPattern" && 55 | id.properties.every( 56 | p => 57 | p.value.type === "Identifier" && 58 | context.getKey(p.key) === p.value.name 59 | ) && 60 | // we don't support destructuring the default 61 | !isDefault 62 | ) { 63 | const propVars = id.properties.map(p => context.findVariable(p.value)); 64 | reqs.push({ node, depID, propVars }); 65 | } 66 | }, 67 | 68 | ImportDeclaration(node) { 69 | if (astHelpers.isFlowImport(node)) { 70 | return; 71 | } 72 | 73 | const depID = node.source.value; 74 | const req = { 75 | node, 76 | depID, 77 | identVars: [], 78 | defaultVars: [], 79 | propVars: [] 80 | }; 81 | let valid = true; 82 | 83 | node.specifiers.forEach(s => { 84 | switch (s.type) { 85 | case "ImportSpecifier": 86 | if ( 87 | s.imported.name === s.local.name && 88 | s.imported.start === s.local.start && 89 | s.imported.end === s.local.end && 90 | !astHelpers.isFlowImport(s) 91 | ) { 92 | req.propVars.push(context.findVariable(s.local)); 93 | } else { 94 | valid = false; 95 | } 96 | break; 97 | 98 | case "ImportDefaultSpecifier": 99 | req.defaultVars.push(context.findVariable(s.local)); 100 | break; 101 | 102 | case "ImportNamespaceSpecifier": 103 | req.identVars.push(context.findVariable(s.local)); 104 | break; 105 | } 106 | }); 107 | 108 | if (valid) { 109 | reqs.push(req); 110 | } 111 | }, 112 | 113 | finish() { 114 | context.reqs = reqs.map(( 115 | { node, depID, identVars, defaultVars, propVars } 116 | ) => ({ 117 | node, 118 | depID, 119 | // remove unused names 120 | idents: getUsedNames(identVars || []), 121 | defaults: getUsedNames(defaultVars || []), 122 | props: getUsedNames(propVars || []) 123 | })); 124 | } 125 | }; 126 | }; 127 | 128 | function getUsedNames(vars) { 129 | return vars 130 | .filter(v => { 131 | return v.used || 132 | v.references.some(r => v.defs.every(d => d.name !== r.identifier)); 133 | }) 134 | .map(v => v.name); 135 | } 136 | -------------------------------------------------------------------------------- /visits/find-style.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { tokTypes } = require("babylon"); 4 | 5 | const leadingWhiteRegex = /^\s*/; 6 | 7 | exports.init = function(context) { 8 | const kindFreqs = {}; 9 | const quoteFreqs = {}; 10 | const semiFreqs = {}; 11 | const tabFreqs = {}; 12 | const trailingCommaFreqs = {}; 13 | const requireKeywordFreqs = {}; 14 | 15 | const countSemisNode = countSemis.bind(null, context, semiFreqs); 16 | const countTrailingCommasNode = countTrailingCommas.bind( 17 | null, 18 | context, 19 | trailingCommaFreqs 20 | ); 21 | countTabs(context, tabFreqs); 22 | 23 | return { 24 | VariableDeclaration(node) { 25 | const kind = node.kind; 26 | inc(kindFreqs, kind); 27 | 28 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/semi.js#L197-L205 29 | const parent = node.parent; 30 | if ( 31 | (parent.type !== "ForStatement" || parent.init !== node) && 32 | (!/^For(?:In|Of)Statement/.test(parent.type) || parent.left !== node) 33 | ) { 34 | countSemisNode(node); 35 | } 36 | }, 37 | 38 | Literal(node) { 39 | if (typeof node.value === "string") { 40 | const quote = node.raw[0]; 41 | if (quote[0] === "'" || quote[0] === '"') { 42 | inc(quoteFreqs, quote); 43 | } 44 | } 45 | }, 46 | 47 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/semi.js#L213-L232 48 | ExpressionStatement: countSemisNode, 49 | ReturnStatement: countSemisNode, 50 | ThrowStatement: countSemisNode, 51 | DoWhileStatement: countSemisNode, 52 | DebuggerStatement: countSemisNode, 53 | BreakStatement: countSemisNode, 54 | ContinueStatement: countSemisNode, 55 | ExportAllDeclaration: countSemisNode, 56 | ExportDefaultDeclaration(node) { 57 | if (!/(?:Class|Function)Declaration/.test(node.declaration.type)) { 58 | countSemisNode(node); 59 | } 60 | }, 61 | 62 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/comma-dangle.js#L319-333 63 | // Excluding function/call 64 | ObjectExpression: countTrailingCommasNode, 65 | ObjectPattern: countTrailingCommasNode, 66 | ArrayExpression: countTrailingCommasNode, 67 | ArrayPattern: countTrailingCommasNode, 68 | 69 | ImportDeclaration(node) { 70 | countSemisNode(node); 71 | countTrailingCommasNode(node); 72 | inc(requireKeywordFreqs, "import"); 73 | }, 74 | ExportNamedDeclaration(node) { 75 | if (!node.declaration) { 76 | countSemisNode(node); 77 | } 78 | countTrailingCommasNode(node); 79 | }, 80 | 81 | CallExpression(node) { 82 | if (context.isGlobal(node.callee, "require")) { 83 | inc(requireKeywordFreqs, "require"); 84 | } 85 | }, 86 | 87 | finish() { 88 | context.style = { 89 | kind: maxKey(kindFreqs, "const"), 90 | quote: maxKey(quoteFreqs, '"'), 91 | tab: maxKey(tabFreqs, " "), 92 | requireKeyword: maxKey(requireKeywordFreqs, "require"), 93 | semi: maxKey(semiFreqs, ";"), 94 | trailingComma: maxKey(trailingCommaFreqs, "") 95 | }; 96 | } 97 | }; 98 | }; 99 | 100 | function countTabs(context, tabFreqs) { 101 | let lastIndent = 0; 102 | context.textLines.forEach(textLine => { 103 | const indent = textLine.match(leadingWhiteRegex)[0]; 104 | // ignore blank lines 105 | if (indent.length === textLine.length) { 106 | return; 107 | } 108 | 109 | if (indent.length > lastIndent.length) { 110 | const tab = indent.slice(lastIndent.length); 111 | inc(tabFreqs, tab); 112 | } else if (indent.length < lastIndent.length) { 113 | const tab = lastIndent.slice(indent.length); 114 | inc(tabFreqs, tab); 115 | } 116 | 117 | lastIndent = indent; 118 | }); 119 | } 120 | 121 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/semi.js#L171-181 122 | function countSemis(context, semiFreqs, node) { 123 | const token = context.getLastToken(node); 124 | const semi = token.type === tokTypes.semi ? ";" : ""; 125 | inc(semiFreqs, semi); 126 | } 127 | 128 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/comma-dangle.js#L251-L274 129 | function countTrailingCommas(context, trailingCommaFreqs, node) { 130 | const lastItem = getLastItem(node); 131 | 132 | if ( 133 | !lastItem || 134 | node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier" 135 | ) { 136 | return; 137 | } 138 | if (!isTrailingCommaAllowed(lastItem) || !isMultiline(context, node)) { 139 | return; 140 | } 141 | 142 | const token = getTrailingToken(context, node, lastItem); 143 | const trailingComma = token.type === tokTypes.comma ? "," : ""; 144 | 145 | inc(trailingCommaFreqs, trailingComma); 146 | } 147 | 148 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/comma-dangle.js#L139-160 149 | function getLastItem(node) { 150 | switch (node.type) { 151 | case "ObjectExpression": 152 | case "ObjectPattern": 153 | return node.properties[node.properties.length - 1]; 154 | case "ArrayExpression": 155 | case "ArrayPattern": 156 | return node.elements[node.elements.length - 1]; 157 | case "ImportDeclaration": 158 | case "ExportNamedDeclaration": 159 | return node.specifiers[node.specifiers.length - 1]; 160 | case "FunctionDeclaration": 161 | case "FunctionExpression": 162 | case "ArrowFunctionExpression": 163 | return node.params[node.params.length - 1]; 164 | case "CallExpression": 165 | case "NewExpression": 166 | return node.arguments[node.arguments.length - 1]; 167 | default: 168 | return null; 169 | } 170 | } 171 | 172 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/comma-dangle.js#L33-39 173 | function isTrailingCommaAllowed(lastItem) { 174 | return lastItem.type !== "RestElement" && 175 | lastItem.type !== "RestProperty" && 176 | lastItem.type !== "ExperimentalRestProperty"; 177 | } 178 | 179 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/comma-dangle.js#L197-208 180 | function isMultiline(context, node) { 181 | const lastItem = getLastItem(node); 182 | 183 | if (!lastItem) { 184 | return false; 185 | } 186 | 187 | const penultimateToken = getTrailingToken(context, node, lastItem); 188 | const lastToken = context.getTokenAfter(penultimateToken); 189 | 190 | return lastToken.loc.end.line !== penultimateToken.loc.end.line; 191 | } 192 | 193 | // Taken from: https://github.com/eslint/eslint/blob/a30eb8d19f407643d35f5af8e270c9a150b9d015/lib/rules/comma-dangle.js#L171-187 194 | function getTrailingToken(context, node, lastItem) { 195 | switch (node.type) { 196 | case "ObjectExpression": 197 | case "ArrayExpression": 198 | case "CallExpression": 199 | case "NewExpression": 200 | return context.getLastToken(node, 1); 201 | default: { 202 | const nextToken = context.getTokenAfter(lastItem); 203 | 204 | if (nextToken.type === tokTypes.comma) { 205 | return nextToken; 206 | } 207 | return context.getLastToken(lastItem); 208 | } 209 | } 210 | } 211 | 212 | function inc(freqs, key) { 213 | if (!freqs[key]) { 214 | freqs[key] = 0; 215 | } 216 | freqs[key]++; 217 | } 218 | 219 | function maxKey(freqs, defaultKey) { 220 | const keys = Object.keys(freqs); 221 | if (keys.length === 0) { 222 | return defaultKey; 223 | } 224 | 225 | return keys.reduce( 226 | (maxKey, key) => freqs[key] > freqs[maxKey] ? key : maxKey, 227 | keys[0] 228 | ); 229 | } 230 | -------------------------------------------------------------------------------- /visits/resolve-jsx.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tagNameRegex = /^[a-z]|\-/; 4 | 5 | exports.init = function(context) { 6 | return { 7 | JSXOpeningElement(node) { 8 | const react = context.findVariable("React"); 9 | if (react) { 10 | react.used = true; 11 | } else { 12 | const ref = { identifier: { name: "React" } }; 13 | context.getGlobalScope().through.push(ref); 14 | } 15 | 16 | let identNode; 17 | // Taken from: https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/jsx-uses-vars.js#L24-L44 18 | if (node.name.namespace && node.name.namespace.name) { 19 | // 20 | identNode = node.name.namespace; 21 | } else if (node.name.name) { 22 | // 23 | identNode = node.name; 24 | if (tagNameRegex.test(identNode.name)) { 25 | return; 26 | } 27 | } else if (node.name.object) { 28 | // 29 | let parent = node.name.object; 30 | while (parent.object) { 31 | parent = parent.object; 32 | } 33 | identNode = parent; 34 | } else { 35 | return; 36 | } 37 | 38 | if (identNode.name === "this") { 39 | return; 40 | } 41 | 42 | const variable = context.findVariable(identNode); 43 | if (variable) { 44 | variable.used = true; 45 | } else { 46 | context.getGlobalScope().through.push({ identifier: identNode }); 47 | } 48 | } 49 | }; 50 | }; 51 | --------------------------------------------------------------------------------